[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug report\ndescription: Create a bug report to help us improve\nlabels: [bug]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ## IMPORTANT WARNING\n\n        **Duplicate or incomplete issues will be closed immediately without any response from developers.**\n\n        - Check existing issues before opening a new one\n        - All required checkboxes below must be checked\n        - Duplicate issues will be closed instantly with no response\n\n  - type: checkboxes\n    id: checklist\n    attributes:\n      label: \"Checklist - Required\"\n      description: |\n        **READ CAREFULLY:** You must complete these checks before submitting. Failing to do so will result in your issue being closed immediately without any response from developers.\n\n        **Duplicate issues = Instant closure with no response**\n      options:\n        - label: \" I am able to reproduce the bug with the [latest debug version](https://github.com/MetrolistGroup/Metrolist/actions/workflows/build.yml?query=branch%3Amain).\"\n          required: true\n        - label: \" I've checked that there is NO open or closed issue about this bug. (Duplicate issues will be closed immediately without response)\"\n          required: true\n        - label: \" This issue contains only ONE bug. PLEASE check pinned issues before opening a new one.\"\n          required: true\n        - label: \" The title of this issue accurately describes the bug.\"\n          required: true\n        - label: \" The entire issue report is written/translated in English.\"\n          required: true\n\n  - type: markdown\n    attributes:\n      value: |\n        ---\n        ## Bug Details\n\n  - type: textarea\n    id: reproduce-steps\n    attributes:\n      label: Steps to reproduce the bug\n      description: What did you do for the bug to show up?\n      placeholder: |\n        Example:\n          1. Go to '...'\n          2. Click on '....'\n          3. Scroll down to '....'\n          4. See error\n    validations:\n      required: true\n\n  - type: textarea\n    id: expected-behavior\n    attributes:\n      label: Expected behavior\n      placeholder: |\n        Example:\n          \"This should happen...\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: actual-behavior\n    attributes:\n      label: Actual behavior\n      placeholder: |\n        Example:\n          \"This happened instead...\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: sreen-media\n    attributes:\n      label: Screenshots/Screen recordings\n      description: |\n        A picture or video helps us understand the bug more.\n\n        You can upload them directly in the text box.\n\n  - type: markdown\n    attributes:\n      value: |\n        ## How to create a logcat recording\n\n        ### Firstly, download and install these apps\n        - [F0x1d/LogFox](https://github.com/F0x1d/LogFox/releases/download/v2.1.9-78/LogFox-2.1.9-release.apk)\n        - [thedjchi/Shizuku](https://github.com/thedjchi/Shizuku/releases/download/v13.6.0.r1318-thedjchi/shizuku-v13.6.0.r1318-thedjchi.apk)\n\n        ### Then follow these instructions\n        1. Open **Shizuku**, and click on the \"View command\" button\n        2. Copy the command and run it on your computer (see ADB setup below if needed)\n        3. Once Shizuku is running, open **LogFox** and start a recording\n        4. Open the app and reproduce the bug you're experiencing\n        5. When finished, press **Export** in LogFox and attach the file to your comment\n        6. Stop the process by pressing **Exit** in the LogFox notification\n        7. Stop Shizuku (unless you're using it for other apps)\n\n        ### Or the video guide\n        https://github.com/user-attachments/assets/30659a03-39d5-4400-b681-4238c608aebd\n\n        > [!IMPORTANT]\n        > **If you're reporting an immediate crash** issue, you should allow LogFox notification permissions on launch. Then open the app and look for a crash report notification from LogFox. \n\n        > [!TIP]\n        > If you export the recording as a zip file, upload it to https://litterbox.catbox.moe (set expiration to 3 days).\n\n  - type: textarea\n    id: logs\n    attributes:\n      label: Logs\n      description: |\n        Please read the instructions above on how to create a logcat recording. Logs are crucial for us to understand and fix the bug. Issues without logs might be closed immediately.\n    validations:\n      required: true\n\n  - type: input\n    id: app-version\n    attributes:\n      label: Metrolist version\n      description: |\n        You can find your Metrolist version in **Settings**.\n      placeholder: |\n        Example: \"13.1.1\"\n    validations:\n      required: true\n\n  - type: input\n    id: android-version\n    attributes:\n      label: Android version\n      description: |\n        You can find this in Android Settings > About Phone.\n      placeholder: |\n        Example: \"Android 12\"\n    validations:\n      required: true\n\n  - type: textarea\n    id: additional-information\n    attributes:\n      label: Additional information\n      placeholder: |\n        Additional details and attachments.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature request\ndescription: Suggest an idea for Metrolist\nlabels: [enhancement]\nbody:\n  - type: checkboxes\n    id: checklist\n    attributes:\n      label: Checklist\n      description: You should ensure the completion of the task before proceeding to check it off the checklist. Neglecting to do so may impede the efficiency of the issue resolution process. The developer has the right to delete the issue directly if you check the list blindly.\n      options:\n        - label: I've checked that there is no other issue about this feature request.\n          required: true\n        - label: This issue contains only one feature request.\n          required: true\n        - label: The title of this issue accurately describes the feature request.\n          required: true\n\n  - type: textarea\n    id: feature-description\n    attributes:\n      label: Feature description\n      description: What feature you want the app to have? Provide detailed description about what it should look like or where it should be added.\n    validations:\n      required: true\n\n  - type: textarea\n    id: why-is-the-feature-requested\n    attributes:\n      label: Why do you want this feature?\n      description: Describe the problem or limitation that motivates you to want this feature to be added.\n    validations:\n      required: true\n\n  - type: textarea\n    id: additional-information\n    attributes:\n      label: Additional information\n      description: Add any other context or screenshots about the feature request here.\n      placeholder: |\n        Additional details and attachments.\n"
  },
  {
    "path": ".github/actions/setup-protobuf/action.yml",
    "content": "name: Setup and Generate Protobuf\ndescription: Install protoc and generate protobuf files\n\nruns:\n  using: composite\n  steps:\n    - name: Install protoc\n      run: |\n        sudo apt-get update\n        sudo apt-get install -y protobuf-compiler\n      shell: bash\n\n    - name: Generate protobuf files\n      run: |\n        cd app\n        bash generate_proto.sh\n      shell: bash\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "## Problem\n<!-- Describe the issue or limitation this PR addresses -->\n\n## Cause\n<!-- Explain the root cause of the problem -->\n\n## Solution\n<!-- List the changes made to fix the issue -->\n- \n\n## Testing\n<!-- Describe how the changes were tested -->\n\n## Related Issues\n<!-- List any related issues or PRs -->\n- Closes #\n- Related to #"
  },
  {
    "path": ".github/scripts/parse_changelog.sh",
    "content": "#!/usr/bin/env bash\n# parse_changelog.sh — Extract the changelog entry for a specific version\n#\n# Reads changelog.md (or a custom file) and outputs the content block\n# associated with the given version, as delimited by ---vX.Y.Z separators.\n#\n# Usage:\n#   ./parse_changelog.sh <version> [changelog_file]\n#\n# Examples:\n#   ./parse_changelog.sh 13.2.1\n#   ./parse_changelog.sh v13.2.1 changelog.md\n#   ./parse_changelog.sh 13.2.1 /path/to/changelog.md\n#\n# Exit codes:\n#   0 — Version found; content written to stdout\n#   1 — Error (missing args, file not found, version not found)\n\nset -euo pipefail\n\nVERSION=\"${1:-}\"\nCHANGELOG_FILE=\"${2:-changelog.md}\"\n\nif [ -z \"$VERSION\" ]; then\n    echo \"Error: version argument required\" >&2\n    echo \"Usage: $0 <version> [changelog_file]\" >&2\n    echo \"Example: $0 13.2.1\" >&2\n    exit 1\nfi\n\nif [ ! -f \"$CHANGELOG_FILE\" ]; then\n    echo \"Error: changelog file not found: '$CHANGELOG_FILE'\" >&2\n    exit 1\nfi\n\nVERSION=\"${VERSION#v}\"\n\nif ! grep -q \"^---v${VERSION}$\" \"$CHANGELOG_FILE\"; then\n    echo \"Error: version '$VERSION' not found in '$CHANGELOG_FILE'\" >&2\n    exit 1\nfi\n\nawk -v ver=\"$VERSION\" '\n    /^---v/ {\n        if (found) exit\n        if ($0 == \"---v\" ver) { found=1; next }\n        next\n    }\n    found { print }\n' \"$CHANGELOG_FILE\"\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build APKs\n\non:\n  workflow_dispatch:\n  push:\n    branches: [\"**\"]\n    paths-ignore:\n      - \"README.md\"\n      - \"fastlane/**\"\n      - \"assets/**\"\n      - \".github/**/*.md\"\n      - \".github/FUNDING.yml\"\n      - \".github/ISSUE_TEMPLATE/**\"\n\npermissions:\n  contents: write\n  discussions: write\n\njobs:\n  build_release:\n    name: Build Release (${{ matrix.variant }})\n    if: github.actor != 'dependabot[bot]' && github.actor != 'renovate[bot]'\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        include:\n          - variant: foss\n            variantName: Foss\n          - variant: gms\n            variantName: Gms\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          submodules: recursive\n\n      - name: Set up JDK 21\n        uses: actions/setup-java@v5\n        with:\n          java-version: \"21\"\n          distribution: \"temurin\"\n\n      - name: Set Up Gradle\n        uses: gradle/actions/setup-gradle@v5\n        with:\n          cache-encryption-key: ${{ secrets.GRADLE_CACHE_KEY }}\n          cache-cleanup: on-success\n\n      - name: Setup and Generate Protobuf\n        uses: ./.github/actions/setup-protobuf\n\n      - name: Grant execute permission for gradlew\n        run: chmod +x gradlew\n\n      - name: Build Release APK and Run Lint\n        run: ./gradlew --console=plain assemble${{ matrix.variantName }}Release :app:lint${{ matrix.variantName }}Release --warning-mode summary\n        env:\n          METROLIST_APPLICATION_ID: com.metrolist.music\n          METROLIST_APP_NAME: Metrolist Nightly\n          PULL_REQUEST: \"false\"\n          GITHUB_EVENT_NAME: ${{ github.event_name }}\n          LASTFM_API_KEY: ${{ secrets.LASTFM_API_KEY }}\n          LASTFM_SECRET: ${{ secrets.LASTFM_SECRET }}\n\n      - name: Sign APK\n        uses: ilharp/sign-android-release@v2.0.0\n        with:\n          releaseDir: app/build/outputs/apk/${{ matrix.variant }}/release/\n          signingKey: ${{ secrets.KEYSTORE }}\n          keyAlias: ${{ secrets.KEY_ALIAS }}\n          keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}\n          keyPassword: ${{ secrets.KEY_PASSWORD }}\n          buildToolsVersion: 35.0.0\n\n      - name: Move signed APK\n        run: |\n          OUTPUT_DIR=\"app/build/outputs/apk/${{ matrix.variant }}/release\"\n          mkdir -p \"$OUTPUT_DIR/out\"\n          if [ \"${{ matrix.variant }}\" = \"gms\" ]; then\n            TARGET_NAME=\"app-universal-with-Google-Cast.apk\"\n          else\n            TARGET_NAME=\"app-universal-release.apk\"\n          fi\n          find \"$OUTPUT_DIR\" -name \"*-signed.apk\" -o -name \"*-unsigned-signed.apk\" | xargs -I{} mv {} \"$OUTPUT_DIR/out/$TARGET_NAME\"\n\n      - name: Upload Signed APK\n        uses: actions/upload-artifact@v6\n        with:\n          name: ${{ matrix.variant == 'gms' && 'app-with-Google-Cast' || 'app-release' }}\n          path: app/build/outputs/apk/${{ matrix.variant }}/release/out/*\n\n  build_debug:\n    name: Build Debug (${{ matrix.variant }})\n    if: github.actor != 'dependabot[bot]' && github.actor != 'renovate[bot]'\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        include:\n          - variant: foss\n            variantName: Foss\n          - variant: gms\n            variantName: Gms\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          submodules: recursive\n\n      - name: Set up JDK 21\n        uses: actions/setup-java@v5\n        with:\n          java-version: \"21\"\n          distribution: \"temurin\"\n\n      - name: Set Up Gradle\n        uses: gradle/actions/setup-gradle@v5\n        with:\n          cache-encryption-key: ${{ secrets.GRADLE_CACHE_KEY }}\n          cache-cleanup: on-success\n\n      - name: Setup and Generate Protobuf\n        uses: ./.github/actions/setup-protobuf\n\n      - name: Grant execute permission for gradlew\n        run: chmod +x gradlew\n\n      - name: Restore Persistent Keystore\n        run: |\n          echo \"${{ secrets.DEBUG_KEYSTORE }}\" | base64 -d > ./app/persistent-debug.keystore\n\n      - name: Build Debug APK and Run Lint\n        run: ./gradlew --console=plain assemble${{ matrix.variantName }}Debug :app:lint${{ matrix.variantName }}Debug --warning-mode summary\n        env:\n          METROLIST_APPLICATION_ID: com.metrolist.music\n          METROLIST_APP_NAME: Metrolist Nightly\n          PULL_REQUEST: \"false\"\n          GITHUB_EVENT_NAME: ${{ github.event_name }}\n          LASTFM_API_KEY: ${{ secrets.LASTFM_API_KEY }}\n          LASTFM_SECRET: ${{ secrets.LASTFM_SECRET }}\n\n      - name: Upload Debug APK\n        uses: actions/upload-artifact@v6\n        with:\n          name: ${{ matrix.variant == 'gms' && 'app-debug-gms' || 'app-debug' }}\n          path: app/build/outputs/apk/${{ matrix.variant }}/debug/*.apk\n"
  },
  {
    "path": ".github/workflows/build_pr.yml",
    "content": "name: Build PR\n\non:\n  pull_request:\n    branches:\n      - \"**\"\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          submodules: recursive\n\n      - name: Set up JDK 21\n        uses: actions/setup-java@v5\n        with:\n          java-version: 21\n          distribution: \"temurin\"\n\n      - name: Set Up Gradle\n        uses: gradle/actions/setup-gradle@v5\n        with:\n          cache-read-only: true\n          cache-cleanup: on-success\n\n      - name: Setup and Generate Protobuf\n        uses: ./.github/actions/setup-protobuf\n\n      - name: Grant execute permission for gradlew\n        run: chmod +x gradlew\n\n      - name: Restore PR debug keystore\n        id: pr_keystore\n        uses: actions/cache/restore@v4\n        with:\n          path: app/pr-debug.keystore\n          key: pr-debug-keystore-${{ github.event.number }}\n\n      - name: Generate PR debug keystore if not cached\n        if: steps.pr_keystore.outputs.cache-hit != 'true'\n        run: |\n          keytool -genkeypair -v \\\n            -keystore app/pr-debug.keystore \\\n            -storepass android \\\n            -alias androiddebugkey \\\n            -keypass android \\\n            -keyalg RSA \\\n            -keysize 2048 \\\n            -validity 10000 \\\n            -dname \"CN=Metrolist PR ${{ github.event.number }},O=Metrolist,C=US\"\n\n      - name: Save PR debug keystore\n        if: steps.pr_keystore.outputs.cache-hit != 'true'\n        uses: actions/cache/save@v4\n        with:\n          path: app/pr-debug.keystore\n          key: ${{ steps.pr_keystore.outputs.cache-primary-key }}\n\n      - name: Build and Lint FOSS Debug APK\n        run: ./gradlew --console=plain assembleFossDebug :app:lintFossDebug --warning-mode summary\n        env:\n          GITHUB_EVENT_NAME: ${{ github.event_name }}\n          METROLIST_APPLICATION_ID: com.metrolist.music.pr.p${{ github.event.number }}\n          METROLIST_APP_NAME: Metrolist PR ${{ github.event.number }}\n          METROLIST_DEBUG_KEYSTORE_PATH: pr-debug.keystore\n          PULL_REQUEST: \"true\"\n          LASTFM_API_KEY: ${{ secrets.LASTFM_API_KEY }}\n          LASTFM_SECRET: ${{ secrets.LASTFM_SECRET }}\n\n      - name: Upload APK\n        uses: actions/upload-artifact@v6\n        with:\n          name: app-universal-debug-pr-${{ github.event.number }}\n          path: app/build/outputs/apk/foss/debug/*.apk\n"
  },
  {
    "path": ".github/workflows/build_quick.yml",
    "content": "name: Quick Test Build\n\non:\n  workflow_dispatch:\n  push:\n    branches: [\"**\"]\n    paths-ignore:\n      - \"README.md\"\n      - \"fastlane/**\"\n      - \"assets/**\"\n      - \".github/**/*.md\"\n      - \".github/FUNDING.yml\"\n      - \".github/ISSUE_TEMPLATE/**\"\n\npermissions:\n  contents: write\n\njobs:\n  build_quick:\n    name: Quick Universal Release Build\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          submodules: recursive\n\n      - name: Set up JDK 21\n        uses: actions/setup-java@v5\n        with:\n          java-version: \"21\"\n          distribution: \"temurin\"\n\n      - name: Set Up Gradle\n        uses: gradle/actions/setup-gradle@v5\n        with:\n          cache-encryption-key: ${{ secrets.GRADLE_CACHE_KEY }}\n          cache-cleanup: on-success\n\n      - name: Setup and Generate Protobuf\n        uses: ./.github/actions/setup-protobuf\n\n      - name: Grant execute permission for gradlew\n        run: chmod +x gradlew\n\n      - name: Build Release APK (No Lint)\n        run: ./gradlew --console=plain assembleFossRelease --warning-mode summary -x lint -x lintFossRelease\n        env:\n          METROLIST_APPLICATION_ID: com.metrolist.music.test\n          METROLIST_APP_NAME: Metrolist Testing\n          PULL_REQUEST: \"false\"\n          GITHUB_EVENT_NAME: ${{ github.event_name }}\n          LASTFM_API_KEY: ${{ secrets.LASTFM_API_KEY }}\n          LASTFM_SECRET: ${{ secrets.LASTFM_SECRET }}\n\n      - name: Sign APK\n        uses: ilharp/sign-android-release@v2.0.0\n        with:\n          releaseDir: app/build/outputs/apk/foss/release/\n          signingKey: ${{ secrets.KEYSTORE }}\n          keyAlias: ${{ secrets.KEY_ALIAS }}\n          keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}\n          keyPassword: ${{ secrets.KEY_PASSWORD }}\n          buildToolsVersion: 35.0.0\n\n      - name: Move signed APK\n        run: |\n          OUTPUT_DIR=\"app/build/outputs/apk/foss/release\"\n          mkdir -p \"$OUTPUT_DIR/out\"\n          find \"$OUTPUT_DIR\" -name \"*-signed.apk\" -o -name \"*-unsigned-signed.apk\" | xargs -I{} mv {} \"$OUTPUT_DIR/out/app-universal-test-release.apk\"\n\n      - name: Upload Signed APK\n        uses: actions/upload-artifact@v6\n        with:\n          name: app-universal-test-release\n          path: app/build/outputs/apk/foss/release/out/*\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Bump to new version\non:\n  push:\n    branches:\n      - \"main\"\n    paths:\n      - \"app/build.gradle.kts\"\n  workflow_dispatch:\n\njobs:\n  check-version:\n    if: \"!contains(github.ref, 'refs/tags')\"\n    runs-on: ubuntu-latest\n    outputs:\n      version_changed: ${{ steps.check_version.outputs.version_changed }}\n      new_version: ${{ steps.check_version.outputs.new_version }}\n      version_code: ${{ steps.check_version.outputs.version_code }}\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n          submodules: recursive\n\n      - name: Check if version changed\n        id: check_version\n        run: |\n          if [ ! -f app/build.gradle.kts ]; then\n            echo \"File app/build.gradle.kts does not exist\"\n            echo \"version_changed=false\" >> $GITHUB_OUTPUT\n            exit 0\n          fi\n\n          NEW_VERSION=$(grep -oP 'versionName\\s*=\\s*\"\\K[^\"]+' app/build.gradle.kts || echo \"\")\n          VERSION_CODE=$(grep -oP 'versionCode\\s*=\\s*\\K\\d+' app/build.gradle.kts || echo \"\")\n\n          if [ -z \"$NEW_VERSION\" ]; then\n            echo \"Could not find versionName in app/build.gradle.kts\"\n            echo \"version_changed=false\" >> $GITHUB_OUTPUT\n            exit 0\n          fi\n\n          echo \"Current version: $NEW_VERSION (code: $VERSION_CODE)\"\n\n          if git show HEAD^:app/build.gradle.kts > /dev/null 2>&1; then\n            OLD_VERSION=$(git show HEAD^:app/build.gradle.kts | grep -oP 'versionName\\s*=\\s*\"\\K[^\"]+' || echo \"\")\n            echo \"Previous version: $OLD_VERSION\"\n            \n            if [ \"$OLD_VERSION\" != \"$NEW_VERSION\" ]; then\n              echo \"Version changed from $OLD_VERSION to $NEW_VERSION\"\n              echo \"version_changed=true\" >> $GITHUB_OUTPUT\n              echo \"new_version=$NEW_VERSION\" >> $GITHUB_OUTPUT\n              echo \"version_code=$VERSION_CODE\" >> $GITHUB_OUTPUT\n            else\n              echo \"Version unchanged: $NEW_VERSION\"\n              echo \"version_changed=false\" >> $GITHUB_OUTPUT\n            fi\n          else\n            echo \"First version detected: $NEW_VERSION\"\n            echo \"version_changed=true\" >> $GITHUB_OUTPUT\n            echo \"new_version=$NEW_VERSION\" >> $GITHUB_OUTPUT\n            echo \"version_code=$VERSION_CODE\" >> $GITHUB_OUTPUT\n          fi\n\n      - name: Debug output\n        run: |\n          echo \"Debug Information:\"\n          echo \"  - version_changed: ${{ steps.check_version.outputs.version_changed }}\"\n          echo \"  - new_version: ${{ steps.check_version.outputs.new_version }}\"\n          echo \"  - version_code: ${{ steps.check_version.outputs.version_code }}\"\n          echo \"  - event_name: ${{ github.event_name }}\"\n\n  build:\n    needs: check-version\n    if: needs.check-version.outputs.version_changed == 'true' || github.event_name == 'workflow_dispatch'\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        include:\n          - variant: foss\n            variantName: Foss\n          - variant: gms\n            variantName: Gms\n          - variant: izzy\n            variantName: Izzy\n\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          submodules: recursive\n\n      - name: Set up JDK 21\n        uses: actions/setup-java@v5\n        with:\n          java-version: \"21\"\n          distribution: \"temurin\"\n\n      - name: Set Up Gradle\n        uses: gradle/actions/setup-gradle@v5\n        with:\n          cache-disabled: true\n          cache-cleanup: on-success\n\n      - name: Setup and Generate Protobuf\n        uses: ./.github/actions/setup-protobuf\n\n      - name: Grant execute permission for gradlew\n        run: chmod +x gradlew\n\n      - name: Build and Lint Release APK\n        run: ./gradlew --no-configuration-cache --console=plain clean assemble${{ matrix.variantName }}Release :app:lint${{ matrix.variantName }}Release --warning-mode summary\n        env:\n          METROLIST_APPLICATION_ID: com.metrolist.music\n          METROLIST_APP_NAME: Metrolist\n          PULL_REQUEST: \"false\"\n          GITHUB_EVENT_NAME: ${{ github.event_name }}\n          LASTFM_API_KEY: ${{ secrets.LASTFM_API_KEY }}\n          LASTFM_SECRET: ${{ secrets.LASTFM_SECRET }}\n\n      - name: Sign APK\n        uses: ilharp/sign-android-release@v2.0.0\n        with:\n          releaseDir: app/build/outputs/apk/${{ matrix.variant }}/release/\n          signingKey: ${{ secrets.KEYSTORE }}\n          keyAlias: ${{ secrets.KEY_ALIAS }}\n          keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}\n          keyPassword: ${{ secrets.KEY_PASSWORD }}\n          buildToolsVersion: 35.0.0\n\n      - name: Move and rename signed APKs\n        run: |\n          OUTPUT_DIR=\"app/build/outputs/apk/${{ matrix.variant }}/release\"\n          mkdir -p \"$OUTPUT_DIR/out\"\n\n          SIGNED_APK=$(find \"$OUTPUT_DIR\" -name \"*-signed.apk\" -o -name \"*-unsigned-signed.apk\" | head -1)\n\n          if [ -z \"$SIGNED_APK\" ]; then\n            echo \"No signed APK found for ${{ matrix.variant }}\"\n            ls -la \"$OUTPUT_DIR\"\n            exit 1\n          fi\n\n          if [ \"${{ matrix.variant }}\" = \"gms\" ]; then\n            TARGET_NAME=\"Metrolist-with-Google-Cast.apk\"\n          elif [ \"${{ matrix.variant }}\" = \"izzy\" ]; then\n            TARGET_NAME=\"Metrolist-izzy.apk\"\n          else\n            TARGET_NAME=\"Metrolist.apk\"\n          fi\n\n          mv \"$SIGNED_APK\" \"$OUTPUT_DIR/out/$TARGET_NAME\"\n\n          echo \"APK renamed to: $TARGET_NAME\"\n          ls -la \"$OUTPUT_DIR/out/\"\n\n      - name: Upload Signed APKs\n        uses: actions/upload-artifact@v6\n        with:\n          name: ${{ matrix.variant == 'gms' && 'Metrolist-with-Google-Cast' || matrix.variant == 'izzy' && 'Metrolist-izzy' || 'Metrolist' }}\n          path: app/build/outputs/apk/${{ matrix.variant }}/release/out/*.apk\n\n  create-release:\n    needs: [check-version, build]\n    if: needs.check-version.outputs.version_changed == 'true' || github.event_name == 'workflow_dispatch'\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Download all APKs\n        uses: actions/download-artifact@v7\n        with:\n          path: downloaded_artifacts/\n\n      - name: Organize APKs for release\n        run: |\n          echo \"Organizing APK files...\"\n          mkdir -p release_files\n\n          find downloaded_artifacts -name \"*.apk\" -exec cp {} release_files/ \\;\n\n          echo \"APKs ready for release:\"\n          ls -la release_files/\n\n      - name: Parse release notes from changelog\n        run: |\n          VERSION=\"${{ needs.check-version.outputs.new_version }}\"\n          chmod +x .github/scripts/parse_changelog.sh\n\n          if bash .github/scripts/parse_changelog.sh \"$VERSION\" changelog.md > release_notes.md; then\n            echo \"Release notes sourced from changelog.md for v$VERSION\"\n            echo \"--- Preview (first 20 lines) ---\"\n            head -20 release_notes.md\n          else\n            echo \"::warning::v$VERSION not found in changelog.md — falling back to git log\"\n            {\n              echo \"Release of version $VERSION\"\n              echo \"\"\n              git log \"$(git describe --tags --abbrev=0 2>/dev/null || echo \"HEAD~10\")..HEAD\" \\\n                --pretty=format:\"- %s\" --no-merges | head -10 || echo \"- Initial release\"\n            } > release_notes.md\n            echo \"--- Fallback notes ---\"\n            cat release_notes.md\n          fi\n\n      - name: Create Release\n        env:\n          GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}\n        run: |\n          echo \"Creating release v${{ needs.check-version.outputs.new_version }}\"\n\n          gh release create \"v${{ needs.check-version.outputs.new_version }}\" \\\n            --title \"${{ needs.check-version.outputs.new_version }}\" \\\n            --notes-file release_notes.md \\\n            --latest \\\n            release_files/*.apk\n\n          echo \"Release created successfully!\"\n\n      - name: Update release summary\n        run: |\n          echo \"## Release Summary\" >> $GITHUB_STEP_SUMMARY\n          echo \"- **Version**: v${{ needs.check-version.outputs.new_version }}\" >> $GITHUB_STEP_SUMMARY\n          echo \"- **Version Code**: ${{ needs.check-version.outputs.version_code }}\" >> $GITHUB_STEP_SUMMARY\n          echo \"- **APKs Built**: $(ls release_files/*.apk | wc -l)\" >> $GITHUB_STEP_SUMMARY\n          echo \"- **Release URL**: https://github.com/${{ github.repository }}/releases/tag/v${{ needs.check-version.outputs.new_version }}\" >> $GITHUB_STEP_SUMMARY\n"
  },
  {
    "path": ".gitignore",
    "content": "# Built application files\n*.apk\n*.aar\n*.ap_\n*.aab\n\n# Files for the ART/Dalvik VM\n*.dex\n\n# Java class files\n*.class\n\n# Generated files\nbin/\ngen/\nout/\n#  Uncomment the following line in case you need and you don't have the release build type files in your app\n# release/\n\n# Gradle files\n.gradle/\nbuild/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Proguard folder generated by Eclipse\nproguard/\n\n# Log Files\n*.log\n\n# Android Studio Navigation editor temp files\n.navigation/\n\n# Android Studio captures folder\ncaptures/\n\n# IntelliJ\n*.iml\n.idea\n.idea/workspace.xml\n.idea/tasks.xml\n.idea/gradle.xml\n.idea/assetWizardSettings.xml\n.idea/dictionaries\n.idea/libraries\n# Android Studio 3 in .gitignore file.\n.idea/caches\n.idea/modules.xml\n# Comment next line if keeping position of elements in Navigation Editor is relevant for you\n.idea/navEditor.xml\n\n# Keystore files\n# Uncomment the following lines if you do not want to check your keystore files in.\n#*.jks\n#*.keystore\napp/persistent-debug.keystore\n# ^^^ think twice before removing this gitignore, it became so FUCKING\n# annoying to unstage this file every time i want to commit stuff\n\n# External native build folder generated in Android Studio 2.2 and later\n.externalNativeBuild\n.cxx/\n\n# Freeline\nfreeline.py\nfreeline/\nfreeline_project_description.json\n\n# fastlane\nfastlane/report.xml\nfastlane/Preview.html\nfastlane/screenshots\nfastlane/test_output\nfastlane/readme.md\n\n# Version control\nvcs.xml\n\n# lint\nlint/intermediates/\nlint/generated/\nlint/outputs/\nlint/tmp/\n# lint/reports/\n\n.DS_Store\n/app/release/output-metadata.json\n\n.kotlin\napp/release\noutput-metadata.json\n\n# VS Code\n.vscode/\n\n# Binary files and core dumps\ncore\ncore.*\n*.so\n*.bin\n.env\napp/src/main/java/com/metrolist/music/listentogether/proto/*\n\n# FFTW third-party library build artifacts\n.build-fftw\napp/src/main/cpp/coverart/\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"metroproto\"]\n\tpath = metroproto\n\turl = https://github.com/MetrolistGroup/metroproto\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# Working with Metrolist as an AI agent\n\nMetrolist is a 3rd party YouTube Music client written in Kotlin. It follows material 3 design guidelines closely.\n\n## Rules for working on the project\n\n1. Always create a new branch for your feature work. Follow these naming conventions:\n   - Bug fixes: `fix/short-description`\n   - New features: `feat/short-description`\n   - Refactoring: `ref/short-description`\n   - Documentation: `docs/short-description`\n   - Chores: `chore/short-description`\n2. Branch descriptions should be concise yet descriptive enough to understand the purpose of the branch at a glance.\n3. Always pull the latest changes from `main` before starting your work to minimize merge conflicts.\n4. While working on your feature you should rebase your branch on top of the latest `main` at least once a day to ensure compatibility.\n5. Commit names should be clear and follow the format: `type(scope): short description`. For example: `feat(ui): add dark mode support`. Including the scope is optional.\n6. All string edits should be made to the `Metrolist/app/src/main/res/values/metrolist_strings.xml` file, NOT `Metrolist/app/src/main/res/values/strings.xml`. Do not touch other `strings.xml` or `metrolist_strings.xml` files in the project.\n7. You are to follow best practices for Kotlin and Android development.\n\n## AI-only guidelines\n\n1. You are strictly prohibited from making ANY changes to the readme/markdown files, including this one. This is to ensure that the documentation remains accurate and consistent for all contributors.\n2. You are NOT allowed to use the following commands:\n   - You are not to commit, push, or merge any changes to any branch.\n   - You should absolutely NOT use any commands that would modify the git history, do force pushes (except for rebases on your own branch), or delete branches without explicit instructions from a human.\n3. Always follow the guidelines and instructions provided by human contributors.\n4. Ensure the absolutely highest code quality in all contributions, including proper formatting, clear variable naming, and comprehensive comments where necessary.\n5. Comments should be added only for complex logic or non-obvious code. Avoid redundant comments that simply restate what the code does.\n6. Prioritize performance, battery efficiency, and maintainability in all code contributions. Always consider the impact of your changes on the overall user experience and app performance.\n7. If you have any doubts ask a human contributor. Never make assumptions about the requirements or implementation details without clarification.\n8. If you do not test your changes using the instructions in the next section, you will be faced with reprimands from human contributors and may be asked to redo your work. Always ensure that you test your changes thoroughly before asking for a final review.\n9. You are absolutely **not allowed to bump the version** of the app in ANY way. Version bumps are only done by the core development team after manual review.\n\n## Building and testing your changes\n\n1. After making changes to the code, you should build the app to ensure that there are no compilation errors. Use the following command from the root directory of the project:\n\n```bash\n./gradlew :app:assembleFossDebug\n```\n\n2. If the build is not successful, review the error messages, fix the issues in your code, and try building again.\n3. Once the build is successful, you can test your changes on an emulator or a physical device. Install the generated APK located at `app/build/outputs/apk/universalFoss/debug/app-universal-foss-debug.apk` and ask a human for help testing the specific features you worked on.\n"
  },
  {
    "path": "LICENSE",
    "content": "                     GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n<img src=\"https://github.com/MetrolistGroup/Metrolist/blob/main/fastlane/metadata/android/en-US/images/icon.png\" width=\"160\" height=\"160\" style=\"display: block; margin: 0 auto\"/>\n<h1>Metrolist</h1>\n<p>YouTube Music client for Android</p>\n\n<div style=\"padding: 16px; margin: 16px 0; background-color: #FFFBE5; border-left: 6px solid #FFC107; border-radius: 4px;\">\n<h2 style=\"margin: 0;\"><strong>⚠Warning</strong></h2>\nIf you're in a region where YouTube Music is not supported, you won't be able to use this app <strong>unless</strong> you have a proxy or VPN to connect to a YTM-supported region.\n</div>\n\n<h1>Screenshots</h1>\n\n<img src=\"https://github.com/MetrolistGroup/Metrolist/blob/main/fastlane/metadata/android/en-US/images/screenshots/screenshot_1.png\" width=\"30%\" />\n<img src=\"https://github.com/MetrolistGroup/Metrolist/blob/main/fastlane/metadata/android/en-US/images/screenshots/screenshot_2.png\" width=\"30%\" />\n<img src=\"https://github.com/MetrolistGroup/Metrolist/blob/main/fastlane/metadata/android/en-US/images/screenshots/screenshot_3.png\" width=\"30%\" />\n\n<img src=\"https://github.com/MetrolistGroup/Metrolist/blob/main/fastlane/metadata/android/en-US/images/screenshots/screenshot_4.png\" width=\"30%\" />\n<img src=\"https://github.com/MetrolistGroup/Metrolist/blob/main/fastlane/metadata/android/en-US/images/screenshots/screenshot_5.png\" width=\"30%\" />\n<img src=\"https://github.com/MetrolistGroup/Metrolist/blob/main/fastlane/metadata/android/en-US/images/screenshots/screenshot_6.png\" width=\"30%\" />\n\n<div align=\"center\">\n<h1>Release numbers</h1>\n</div>\n\n[![Latest release](https://img.shields.io/github/v/release/MetrolistGroup/Metrolist?style=for-the-badge)](https://github.com/MetrolistGroup/Metrolist/releases)\n[![GitHub license](https://img.shields.io/github/license/MetrolistGroup/metrolist?style=for-the-badge)](https://github.com/MetrolistGroup/Metrolist/blob/main/LICENSE)\n[![Downloads](https://img.shields.io/github/downloads/MetrolistGroup/Metrolist/total?style=for-the-badge)](https://github.com/MetrolistGroup/Metrolist/releases)\n\n</div>\n\n<div align=\"center\">\n<h1>Download Now</h1>\n\n<table>\n<tr>\n<td align=\"center\">\n<a href=\"https://github.com/MetrolistGroup/Metrolist/releases/latest/download/Metrolist.apk\"><img src=\"https://github.com/machiav3lli/oandbackupx/blob/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png\" alt=\"Get it on GitHub\" height=\"82\"></a><br/>\n<a href=\"https://www.openapk.net/metrolist/com.metrolist.music/\"><img src=\"https://www.openapk.net/images/openapk-badge.png\" alt=\"Get it on OpenAPK\" height=\"80\"></a>\n</td>\n<td align=\"center\">\n<a href=\"https://apps.obtainium.imranr.dev/redirect?r=obtainium://add/https://github.com/MetrolistGroup/Metrolist/\"><img src=\"https://github.com/ImranR98/Obtainium/blob/main/assets/graphics/badge_obtainium.png\" alt=\"Get it on Obtainium\" height=\"50\"></a>\n</td>\n<td align=\"center\">\n<a href=\"https://apt.izzysoft.de/fdroid/index/apk/com.metrolist.music\"><img src=\"https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png\" alt=\"Get it on IzzyOnDroid\" height=\"80\"></a><br/>\n<a href=\"https://belberi.com/metrolist/?fbclid=PAY2xjawJP5dlleHRuA2FlbQIxMAABpjSk1oBp4e8aSV4nfX2dfunQObTlMWIkN-aVA9CSq36pnmkHsvfoYTjhHg_aem_9o9OGbQuZ2PjJTArq21UDA\"><img src=\"https://github.com/MetrolistGroup/Metrolist/blob/main/fastlane/metadata/android/en-US/images/belberi_github.png\" alt=\"Get it on Belberi\" height=\"82\"></a>\n</td>\n</tr>\n</table>\n\n</div>\n\n<div align=\"center\">\n<h1>Nightly Build</h1>\n\n<a href=\"https://nightly.link/MetrolistGroup/Metrolist/workflows/build/main/app-universal-with-Google-Cast.zip\">\n  <img src=\"https://github.com/machiav3lli/oandbackupx/blob/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png\" alt=\"Get it on GitHub\" height=\"82\">\n</a>\n\n</div>\n\n<div align=\"center\">\n<h1>Table of Contents</h1>\n</div>\n\n- [Features](#features)\n- [FAQ](#faq)\n- [Development Setup](./development_guide.md)\n- [Translations](#translations)\n- [Support Me](#support-me)\n- [Join our community](#join-our-community)\n- [Contributors](#thanks-to-all-contributors)\n\n<div align=\"center\">\n<h1>Features</h1>\n</div>\n\n- Play any song or video from YT Music\n- Background playback\n- Personalized quick picks\n- Library management\n- Listen together with friends\n- Download and cache songs for offline playback\n- Search for songs, albums, artists, videos and playlists\n- Live lyrics\n- YouTube Music account login support\n- Syncing of songs, artists, albums and playlists, from and to your account\n- Skip silence\n- Import playlists\n- Audio normalization\n- Adjust tempo/pitch\n- Local playlist management\n- Reorder songs in playlist or queue\n- Home screen widget with playback controls\n- Light - Dark - black - Dynamic theme\n- Sleep timer\n- Material 3\n- etc.\n\n<div align=\"center\">\n<h1>Translations</h1>\n\n[![Translation status](https://img.shields.io/weblate/progress/metrolist?style=for-the-badge)](https://hosted.weblate.org/engage/metrolist/)\n\nWe use Weblate to translate Metrolist. For more details or to get started, visit our [Weblate page](https://hosted.weblate.org/projects/Metrolist/).\n\n<a href=\"https://hosted.weblate.org/projects/Metrolist/\">\n<img src=\"https://hosted.weblate.org/widget/Metrolist/horizontal-auto.svg\" alt=\"Translation status\" />\n</a>\n\nThank you very much for helping to make Metrolist accessible to many people worldwide.\n\n</div>\n\n<div align=\"center\">\n<h1>FAQ</h1>\n</div>\n\n### Q: Why Metrolist isn't showing in Android Auto?\n\n1. Go to Android Auto's settings and tap multiple times on the version in the bottom to enable\n   developer settings\n2. In the three dots menu at the top-right of the screen, click \"Developer settings\"\n3. Enable \"Unknown sources\"\n\n<div align=\"center\">\n<h1>Support Me</h1>\n\nIf you'd like to support my work, send a Monero (XMR) donation to this address:\n\n44XjSELSWcgJTZiCKzjpCQWyXhokrH9RqH3rpp35FkSKi57T25hniHWHQNhLeXyFn3DDYqufmfRB1iEtENerZpJc7xJCcqt\n\nOr scan this QR code:\n\n<img src=\"https://github.com/MetrolistGroup/Metrolist/blob/main/assets/XMR.png\" alt=\"QR Code\" width=\"200\" height=\"200\" />\n\nOr other\n\n<a href=\"https://www.buymeacoffee.com/mostafaalagamy\">\n<img src=\"https://github.com/MetrolistGroup/Metrolist/blob/main/assets/buymeacoffee.png?raw=true\" alt=\"Buy Me a Coffee\" width=\"150\" height=\"150\" />\n</a>\n\n<div align=\"center\">\n<h1>Join our community</h1>\n\n[![Discord](https://img.shields.io/badge/Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white&labelColor=1c1917)](https://dsc.gg/metrolist)\n[![Telegram](https://img.shields.io/badge/Telegram-2CA5E0?style=for-the-badge&logo=telegram&logoColor=white&labelColor=1c1917)](https://t.me/metrolistapp)\n\n</div>\n\n<div align=\"center\">\n<h1>Special thanks</h1>\n\n**InnerTune**\n[Zion Huang](https://github.com/z-huang) • [Malopieds](https://github.com/Malopieds)\n\n**OuterTune**\n[Davide Garberi](https://github.com/DD3Boh) • [Michael Zh](https://github.com/mikooomich)\n\nCredits:\n\n[**Kizzy**](https://github.com/dead8309/Kizzy) – for the Discord Rich Presence implementation and inspiration.\n\n[**Better Lyrics**](https://better-lyrics.boidu.dev) – for beautiful time-synced lyrics with word-by-word highlighting, and seamless YouTube Music integration.\n\n[**SimpMusic Lyrics**](https://github.com/maxrave-dev/SimpMusic) – for providing lyrics data through the SimpMusic Lyrics API.\n\n[**metroserver**](https://github.com/MetrolistGroup/metroserver) – for providing us with the listen together implementation.\n\n[**MusicRecognizer**](https://github.com/aleksey-saenko/MusicRecognizer) – for the music recognition feature implementation and Shazam API integration.\n\nThe open-source community for tools, libraries, and APIs that make this project possible.\n\n<sub>Thank you to all the amazing developers who made this project possible!</sub>\n\n</div>\n\n<div align=\"center\">\n<h1>Thanks to all contributors</h1>\n\n<a href = \"https://github.com/MetrolistGroup/Metrolist/graphs/contributors\">\n<img src = \"https://contrib.rocks/image?repo=MetrolistGroup/Metrolist\" width=\"600\"/>\n</a>\n\n</div>\n\n<div align=\"center\">\n<h1>Disclaimer</h1>\n</div>\n\nThis project and its contents are not affiliated with, funded, authorized, endorsed by, or in any way associated with YouTube, Google LLC, Metrolist Group LLC or any of its affiliates and subsidiaries.\n\nAny trademark, service mark, trade name, or other intellectual property rights used in this project are owned by the respective owners.\n\n**Made with ❤️ by [Mo Agamy](https://github.com/mostafaalagamy)**\n\n**This project stands with Palestine 🇵🇸**\n"
  },
  {
    "path": "app/.gitignore",
    "content": "/build\r\n*.keystore\nsrc/main/cpp/vibrafp/third_party/fftw-android/\n"
  },
  {
    "path": "app/build.gradle.kts",
    "content": "import org.jetbrains.kotlin.gradle.dsl.JvmTarget\nimport java.util.Properties\n\nval localProperties = Properties()\nval localPropertiesFile = rootProject.file(\"local.properties\")\nif (localPropertiesFile.exists()) {\n    localProperties.load(localPropertiesFile.inputStream())\n}\n\nval baseApplicationId = \"com.metrolist.music\"\nval applicationIdOverride = System.getenv(\"METROLIST_APPLICATION_ID\")?.takeIf { it.isNotBlank() }\nval appNameOverride = System.getenv(\"METROLIST_APP_NAME\")?.takeIf { it.isNotBlank() }\nval debugKeystorePathOverride = System.getenv(\"METROLIST_DEBUG_KEYSTORE_PATH\")?.takeIf { it.isNotBlank() }\nval debugKeystorePassword = System.getenv(\"METROLIST_DEBUG_KEYSTORE_PASSWORD\")?.takeIf { it.isNotBlank() } ?: \"android\"\nval debugKeyAlias = System.getenv(\"METROLIST_DEBUG_KEY_ALIAS\")?.takeIf { it.isNotBlank() } ?: \"androiddebugkey\"\nval debugKeyPassword = System.getenv(\"METROLIST_DEBUG_KEY_PASSWORD\")?.takeIf { it.isNotBlank() } ?: \"android\"\nval persistentDebugKeystoreFile = file(\"persistent-debug.keystore\")\nval workflowDebugKeystoreFile = debugKeystorePathOverride?.let(::file)\n\nplugins {\n    id(\"com.android.application\")\n    alias(libs.plugins.hilt)\n    alias(libs.plugins.kotlin.ksp)\n    alias(libs.plugins.compose.compiler)\n    alias(libs.plugins.kotlin.serialization)\n}\n\nandroid {\n    namespace = \"com.metrolist.music\"\n    compileSdk = 36\n\n    defaultConfig {\n        applicationId = applicationIdOverride ?: baseApplicationId\n        minSdk = 26\n        targetSdk = 36\n        versionCode = 143\n        versionName = \"13.3.0\"\n        resValue(\"string\", \"app_name\", appNameOverride ?: \"Metrolist\")\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n        vectorDrawables.useSupportLibrary = true\n\n        // LastFM API keys from GitHub Secrets\n        val lastFmKey = localProperties.getProperty(\"LASTFM_API_KEY\") ?: System.getenv(\"LASTFM_API_KEY\") ?: \"\"\n        val lastFmSecret = localProperties.getProperty(\"LASTFM_SECRET\") ?: System.getenv(\"LASTFM_SECRET\") ?: \"\"\n\n        buildConfigField(\"String\", \"LASTFM_API_KEY\", \"\\\"$lastFmKey\\\"\")\n        buildConfigField(\"String\", \"LASTFM_SECRET\", \"\\\"$lastFmSecret\\\"\")\n        buildConfigField(\"String\", \"ARCHITECTURE\", \"\\\"universal\\\"\")\n    }\n\n    flavorDimensions += listOf(\"variant\")\n    productFlavors {\n        // FOSS variant (default) - F-Droid compatible, no Google Play Services\n        create(\"foss\") {\n            dimension = \"variant\"\n            isDefault = true\n            buildConfigField(\"Boolean\", \"CAST_AVAILABLE\", \"false\")\n            buildConfigField(\"Boolean\", \"UPDATER_AVAILABLE\", \"true\")\n        }\n\n        // GMS variant - with Google Cast support (requires Google Play Services)\n        create(\"gms\") {\n            dimension = \"variant\"\n            buildConfigField(\"Boolean\", \"CAST_AVAILABLE\", \"true\")\n            buildConfigField(\"Boolean\", \"UPDATER_AVAILABLE\", \"true\")\n        }\n\n        // IzzyOnDroid variant - no Google Cast, no built-in updater (store handles updates)\n        create(\"izzy\") {\n            dimension = \"variant\"\n            buildConfigField(\"Boolean\", \"CAST_AVAILABLE\", \"false\")\n            buildConfigField(\"Boolean\", \"UPDATER_AVAILABLE\", \"false\")\n        }\n    }\n\n    signingConfigs {\n        create(\"persistentDebug\") {\n            storeFile = persistentDebugKeystoreFile\n            storePassword = \"android\"\n            keyAlias = \"androiddebugkey\"\n            keyPassword = \"android\"\n        }\n        create(\"workflowDebug\") {\n            storeFile = workflowDebugKeystoreFile ?: persistentDebugKeystoreFile\n            storePassword = debugKeystorePassword\n            keyAlias = debugKeyAlias\n            keyPassword = debugKeyPassword\n        }\n        create(\"release\") {\n            storeFile = file(\"keystore/release.keystore\")\n            storePassword = System.getenv(\"STORE_PASSWORD\")\n            keyAlias = System.getenv(\"KEY_ALIAS\")\n            keyPassword = System.getenv(\"KEY_PASSWORD\")\n        }\n        getByName(\"debug\") {\n            keyAlias = \"androiddebugkey\"\n            keyPassword = \"android\"\n            storePassword = \"android\"\n            storeFile = file(\"${System.getProperty(\"user.home\")}/.android/debug.keystore\")\n        }\n    }\n\n    buildTypes {\n        release {\n            isMinifyEnabled = true\n            isShrinkResources = true\n            isCrunchPngs = false\n            isDebuggable = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\",\n            )\n        }\n        debug {\n            if (applicationIdOverride == null) {\n                applicationIdSuffix = \".debug\"\n            }\n            isDebuggable = true\n            if (appNameOverride == null) {\n                resValue(\"string\", \"app_name\", \"Metrolist Debug\")\n            }\n            signingConfig =\n                if (workflowDebugKeystoreFile != null) {\n                    signingConfigs.getByName(\"workflowDebug\")\n                } else if (persistentDebugKeystoreFile.exists()) {\n                    signingConfigs.getByName(\"persistentDebug\")\n                } else {\n                    signingConfigs.getByName(\"debug\")\n                }\n        }\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_21\n        targetCompatibility = JavaVersion.VERSION_21\n    }\n\n    kotlin {\n        jvmToolchain(21)\n        compilerOptions {\n            freeCompilerArgs.add(\"-Xannotation-default-target=param-property\")\n            jvmTarget.set(JvmTarget.JVM_21)\n        }\n    }\n\n    buildFeatures {\n        compose = true\n        buildConfig = true\n        resValues = true\n    }\n\n    dependenciesInfo {\n        includeInApk = false\n        includeInBundle = false\n    }\n\n    lint {\n        lintConfig = file(\"lint.xml\")\n        warningsAsErrors = false\n        abortOnError = false\n        checkDependencies = false\n    }\n\n    androidResources {\n        generateLocaleConfig = true\n    }\n\n    packaging {\n        jniLibs {\n            useLegacyPackaging = false\n            keepDebugSymbols +=\n                listOf(\n                    \"**/libandroidx.graphics.path.so\",\n                    \"**/libdatastore_shared_counter.so\",\n                )\n        }\n        resources {\n            excludes += \"/META-INF/{AL2.0,LGPL2.1}\"\n            excludes += \"META-INF/NOTICE.md\"\n            excludes += \"META-INF/CONTRIBUTORS.md\"\n            excludes += \"META-INF/LICENSE.md\"\n            excludes += \"META-INF/INDEX.LIST\"\n            excludes += \"META-INF/io.netty.versions.properties\"\n        }\n    }\n}\n\nksp {\n    arg(\"room.schemaLocation\", \"$projectDir/schemas\")\n}\n\ntasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {\n    compilerOptions {\n        freeCompilerArgs.addAll(\n            \"-opt-in=kotlin.RequiresOptIn\",\n        )\n        suppressWarnings.set(false)\n    }\n}\n\n// Android provides org.json as a platform API (/apex/com.android.art/javalib/core-libart.jar).\n// The standalone org.json:json artefact bundles an older Apache Harmony copy of JSONArray that\n// contains an internal `myArrayList` field absent from the platform class.  Without obfuscation\n// R8 inlines against this internal field; at runtime the platform class is resolved instead,\n// producing a NoSuchFieldError.  Excluding the artefact globally ensures only the platform\n// class is ever referenced.\nconfigurations.configureEach {\n    exclude(group = \"org.json\", module = \"json\")\n}\n\ndependencies {\n    implementation(libs.guava)\n    implementation(libs.coroutines.guava)\n    implementation(libs.concurrent.futures)\n\n    implementation(libs.activity)\n    implementation(libs.hilt.navigation)\n    implementation(libs.datastore)\n\n    implementation(libs.compose.runtime)\n    implementation(libs.compose.foundation)\n    implementation(libs.compose.ui)\n    implementation(libs.compose.ui.util)\n    implementation(libs.compose.ui.tooling)\n    implementation(libs.compose.animation)\n    implementation(libs.compose.reorderable)\n\n    implementation(libs.viewmodel)\n    implementation(libs.viewmodel.compose)\n\n    implementation(libs.material3)\n    implementation(libs.palette)\n    implementation(libs.materialKolor)\n\n    implementation(libs.appcompat)\n\n    implementation(libs.coil)\n    implementation(libs.coil.network.okhttp)\n\n    implementation(libs.ucrop)\n\n    implementation(libs.shimmer)\n\n    implementation(libs.media3)\n    implementation(libs.media3.session)\n    implementation(libs.media3.okhttp)\n\n    // Google Cast - only included in GMS flavor (not available in F-Droid/FOSS builds)\n    \"gmsImplementation\"(libs.media3.cast)\n    \"gmsImplementation\"(libs.mediarouter)\n    \"gmsImplementation\"(libs.cast.framework)\n\n    implementation(libs.room.runtime)\n    implementation(libs.kuromoji.ipadic)\n    implementation(libs.tinypinyin)\n    ksp(libs.room.compiler)\n    implementation(libs.room.ktx)\n\n    implementation(libs.apache.lang3)\n\n    implementation(libs.hilt)\n    implementation(libs.jsoup)\n    ksp(libs.hilt.compiler)\n\n    implementation(project(\":innertube\"))\n    implementation(project(\":kugou\"))\n    implementation(project(\":lrclib\"))\n    implementation(project(\":kizzy\"))\n    implementation(project(\":lastfm\"))\n    implementation(project(\":betterlyrics\"))\n    implementation(project(\":simpmusic\"))\n    implementation(project(\":shazamkit\"))\n\n    implementation(libs.ktor.client.core)\n    implementation(libs.ktor.client.cio)\n    implementation(libs.ktor.client.content.negotiation)\n    implementation(libs.ktor.serialization.json)\n\n    // Protobuf for message serialization (lite version for Android)\n    implementation(libs.protobuf.javalite)\n    implementation(libs.protobuf.kotlin.lite)\n\n    coreLibraryDesugaring(libs.desugaring)\n\n    implementation(libs.timber)\n}\n"
  },
  {
    "path": "app/generate_proto.sh",
    "content": "#!/bin/bash\n# Generate Kotlin protobuf files for Android\n\nset -e\n\nPROTO_DIR=\"../metroproto\"\nOUT_DIR=\"src/main/java\"\n\nif [ ! -f \"$PROTO_DIR/listentogether.proto\" ]; then\n    echo \"Missing proto file at $PROTO_DIR/listentogether.proto\"\n    echo \"Did you initialize submodules? Try: git submodule update --init --recursive\"\n    exit 1\nfi\n\n# Create output directory if it doesn't exist\nmkdir -p \"$OUT_DIR\"\n\n# Generate Java and Kotlin code (lite version for Android)\nprotoc --java_out=lite:\"$OUT_DIR\" --kotlin_out=\"$OUT_DIR\" \\\n    -I=\"$PROTO_DIR\" \\\n    \"$PROTO_DIR/listentogether.proto\"\n\necho \"Protobuf files (lite) generated successfully in $OUT_DIR\""
  },
  {
    "path": "app/lint.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<lint>\n    <!-- Translations are crowdsourced, we can't have 100% coverage at all times -->\n    <issue id=\"MissingTranslation\" severity=\"ignore\" />\n\n    <!-- Weblate doesn't handle these yet: https://github.com/WeblateOrg/weblate/issues/7520 -->\n    <issue id=\"MissingQuantity\" severity=\"error\">\n        <ignore path=\"src/main/res/values-cs\" />\n        <ignore path=\"src/main/res/values-lt\" />\n        <ignore path=\"src/main/res/values-sk\" />\n    </issue>\n</lint>\n"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.kts.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n## Reproducible Build Support\n# Disable obfuscation to ensure deterministic R8 output across different build environments.\n# Without this, R8 assigns short names (e.g. `j`, `k`) to renamed classes in a non-deterministic\n# order, causing byte-for-byte differences between builds. This is required for F-Droid / IzzyOnDroid\n# Reproducible Build verification. Code shrinking (dead code removal) remains fully enabled.\n# Since Metrolist is fully open-source, obfuscation provides no meaningful security benefit.\n-dontobfuscate\n\n# WEB_REMIX Streaming - WebView JavaScript interfaces\n-keepclassmembers class com.metrolist.music.utils.sabr.EjsNTransformSolver$SolverWebView {\n    @android.webkit.JavascriptInterface public *;\n}\n-keepclassmembers class com.metrolist.music.utils.cipher.CipherWebView {\n    @android.webkit.JavascriptInterface public *;\n}\n-keepclassmembers class com.metrolist.music.utils.potoken.PoTokenWebView {\n    @android.webkit.JavascriptInterface public *;\n}\n\n# Keep streaming utility classes\n-keep class com.metrolist.music.utils.cipher.** { *; }\n-keep class com.metrolist.music.utils.sabr.** { *; }\n-keep class com.metrolist.music.utils.potoken.** { *; }\n\n# Keep coroutine continuation for WebView callbacks\n-keepclassmembers class * {\n    void resume(...);\n    void resumeWithException(...);\n}\n\n## Kotlin Coroutines — Reproducible Build Rules\n# Keep volatile fields in coroutine classes to prevent AtomicFieldUpdater optimisation issues\n# and ensure R8 does not reorder or merge these across builds.\n# Source: https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro\n-keepclassmembers class kotlinx.coroutines.** {\n    volatile <fields>;\n}\n-keepclassmembers class kotlin.coroutines.SafeContinuation {\n    volatile <fields>;\n}\n\n# Eliminate coroutines debug-only code paths so R8 sees a single, consistent\n# control-flow graph regardless of build machine or JVM configuration.\n# Source: https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/r8-from-1.6.0/coroutines.pro\n-assumenosideeffects class kotlinx.coroutines.internal.MainDispatcherLoader {\n    boolean FAST_SERVICE_LOADER_ENABLED return false;\n}\n-assumenosideeffects class kotlinx.coroutines.internal.FastServiceLoaderKt {\n    boolean ANDROID_DETECTED return true;\n}\n-assumenosideeffects class kotlinx.coroutines.DebugKt {\n    boolean getASSERTIONS_ENABLED() return false;\n    boolean getDEBUG() return false;\n    boolean getRECOVER_STACK_TRACES() return false;\n}\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Preserve line number information for readable crash stack traces.\n-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n\n## Kotlin Serialization\n# Keep `Companion` object fields of serializable classes.\n# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.\n-if @kotlinx.serialization.Serializable class **\n-keepclasseswithmembers class <1> {\n    static <1>$Companion Companion;\n}\n\n# Keep `serializer()` on companion objects (both default and named) of serializable classes.\n-if @kotlinx.serialization.Serializable class ** {\n    static **$* *;\n}\n-keepclasseswithmembers class <2>$<3> {\n    kotlinx.serialization.KSerializer serializer(...);\n}\n\n# Keep `INSTANCE.serializer()` of serializable objects.\n-if @kotlinx.serialization.Serializable class ** {\n    public static ** INSTANCE;\n}\n-keepclasseswithmembers class <1> {\n    public static <1> INSTANCE;\n    kotlinx.serialization.KSerializer serializer(...);\n}\n\n# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.\n-keepattributes RuntimeVisibleAnnotations,AnnotationDefault\n\n-dontwarn javax.servlet.ServletContainerInitializer\n-dontwarn org.bouncycastle.jsse.BCSSLParameters\n-dontwarn org.bouncycastle.jsse.BCSSLSocket\n-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider\n-dontwarn org.conscrypt.Conscrypt$Version\n-dontwarn org.conscrypt.Conscrypt\n-dontwarn org.conscrypt.ConscryptHostnameVerifier\n-dontwarn org.openjsse.javax.net.ssl.SSLParameters\n-dontwarn org.openjsse.javax.net.ssl.SSLSocket\n-dontwarn org.openjsse.net.ssl.OpenJSSE\n-dontwarn org.slf4j.impl.StaticLoggerBinder\n\n## Rules for NewPipeExtractor\n-keep class org.schabi.newpipe.extractor.services.youtube.protos.** { *; }\n-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }\n-keep class org.mozilla.javascript.** { *; }\n-keep class org.mozilla.javascript.engine.** { *; }\n-dontwarn org.mozilla.javascript.JavaToJSONConverters\n-dontwarn org.mozilla.javascript.tools.**\n-keep class javax.script.** { *; }\n-dontwarn javax.script.**\n-keep class jdk.dynalink.** { *; }\n-dontwarn jdk.dynalink.**\n\n## Logging (does not affect Timber)\n-assumenosideeffects class android.util.Log {\n    public static boolean isLoggable(java.lang.String, int);\n    public static int v(...);\n    public static int d(...);\n    ## Leave in release builds\n    #public static int i(...);\n    #public static int w(...);\n    #public static int e(...);\n}\n\n# Generated automatically by the Android Gradle plugin.\n-dontwarn java.beans.BeanDescriptor\n-dontwarn java.beans.BeanInfo\n-dontwarn java.beans.IntrospectionException\n-dontwarn java.beans.Introspector\n-dontwarn java.beans.PropertyDescriptor\n\n# Keep all classes within the kuromoji package\n-keep class com.atilika.kuromoji.** { *; }\n\n## Queue Persistence Rules\n# Keep queue-related classes to prevent serialization issues in release builds\n-keep class com.metrolist.music.models.PersistQueue { *; }\n-keep class com.metrolist.music.models.PersistPlayerState { *; }\n-keep class com.metrolist.music.models.QueueData { *; }\n-keep class com.metrolist.music.models.QueueType { *; }\n-keep class com.metrolist.music.playback.queues.** { *; }\n\n# Keep serialization methods for queue persistence\n-keepclassmembers class * implements java.io.Serializable {\n    private void writeObject(java.io.ObjectOutputStream);\n    private void readObject(java.io.ObjectInputStream);\n}\n\n## UCrop Rules\n-dontwarn com.yalantis.ucrop**\n-keep class com.yalantis.ucrop** { *; }\n-keep interface com.yalantis.ucrop** { *; }\n\n## Google Cast Rules\n-keep class com.metrolist.music.cast.** { *; }\n-keep class com.google.android.gms.cast.** { *; }\n-keep class androidx.mediarouter.** { *; }\n\n## JSoup re2j optional dependency\n-dontwarn com.google.re2j.**\n\n# Vibra fingerprint library\n-keep class com.metrolist.music.recognition.VibraSignature { *; }\n-keepclassmembers class com.metrolist.music.recognition.VibraSignature {\n    native <methods>;\n}\n\n## Kotlin Reflection Fix\n-keep class kotlin.Metadata { *; }\n-keep class kotlin.reflect.** { *; }\n-dontwarn kotlin.reflect.**\n\n## Ktor Serialization\n-keep class io.ktor.** { *; }\n-keepclassmembers class io.ktor.** { *; }\n-dontwarn io.ktor.**\n\n## Listen Together Protobuf\n-keep class com.metrolist.music.listentogether.proto.** { *; }\n-keepclassmembers class com.metrolist.music.listentogether.proto.** { *; }\n\n## Shazam Models\n-keep class com.metrolist.shazamkit.models.** { *; }\n-keepclassmembers class com.metrolist.shazamkit.models.** {\n    *;\n}\n\n## Kotlinx Serialization\n-keepattributes *Annotation*\n-keepclassmembers class com.metrolist.shazamkit.models.** {\n    *** Companion;\n}\n-keepclasseswithmembers class com.metrolist.shazamkit.models.** {\n    kotlinx.serialization.KSerializer serializer(...);\n}\n"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/1.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 1,\n    \"identityHash\": \"38686a738e9e794eca8e1f635cf072b0\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistId` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `liked` INTEGER NOT NULL, `artworkType` INTEGER NOT NULL, `isTrash` INTEGER NOT NULL, `download_state` INTEGER NOT NULL, `create_date` INTEGER NOT NULL, `modify_date` INTEGER NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artworkType\",\n            \"columnName\": \"artworkType\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isTrash\",\n            \"columnName\": \"isTrash\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"downloadState\",\n            \"columnName\": \"download_state\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"create_date\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"modifyDate\",\n            \"columnName\": \"modify_date\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_id\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"id\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_song_id` ON `${TABLE_NAME}` (`id`)\"\n          },\n          {\n            \"name\": \"index_song_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_artist_id\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"id\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_artist_id` ON `${TABLE_NAME}` (`id`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`playlistId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"playlistId\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_playlistId\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_playlist_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist_song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` INTEGER NOT NULL, `songId` TEXT NOT NULL, `idInPlaylist` INTEGER NOT NULL, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`playlistId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"idInPlaylist\",\n            \"columnName\": \"idInPlaylist\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"playlistId\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"download\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `songId` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '38686a738e9e794eca8e1f635cf072b0')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/10.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 10,\n    \"identityHash\": \"465b6d837bb0b1291e375df6f08219cb\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `liked` INTEGER NOT NULL, `totalPlayTime` INTEGER NOT NULL, `downloadState` INTEGER NOT NULL, `inLibrary` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"downloadState\",\n            \"columnName\": \"downloadState\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `bannerUrl` TEXT, `description` TEXT, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"bannerUrl\",\n            \"columnName\": \"bannerUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"description\",\n            \"columnName\": \"description\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '465b6d837bb0b1291e375df6f08219cb')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/11.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 11,\n    \"identityHash\": \"de2e37d1206f721ad51de3a08f66f99c\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `liked` INTEGER NOT NULL, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'de2e37d1206f721ad51de3a08f66f99c')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/12.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 12,\n    \"identityHash\": \"8db3d5731dbcc716a90427d4dde63c66\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `liked` INTEGER NOT NULL, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8db3d5731dbcc716a90427d4dde63c66')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/13.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 13,\n    \"identityHash\": \"8db3d5731dbcc716a90427d4dde63c66\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `liked` INTEGER NOT NULL, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8db3d5731dbcc716a90427d4dde63c66')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/14.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 14,\n    \"identityHash\": \"8d828b8d2d5ddc5730c653d29c853ff0\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `liked` INTEGER NOT NULL, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, `createdAt` INTEGER, `lastUpdateTime` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8d828b8d2d5ddc5730c653d29c853ff0')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/15.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 15,\n    \"identityHash\": \"b2aefbaf97375d551a710d2cbc5e3393\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `localPath` TEXT, `dateDownload` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"date\",\n            \"columnName\": \"date\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"dateModified\",\n            \"columnName\": \"dateModified\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"dateDownload\",\n            \"columnName\": \"dateDownload\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b2aefbaf97375d551a710d2cbc5e3393')\"\n    ]\n  }\n}\n"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/16.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 16,\n    \"identityHash\": \"b78ea238955043c3308d49775d7267a8\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `liked` INTEGER NOT NULL, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `localPath` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"date\",\n            \"columnName\": \"date\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"dateModified\",\n            \"columnName\": \"dateModified\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"localPath\",\n            \"columnName\": \"localPath\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"dateDownload\",\n            \"columnName\": \"dateDownload\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `channelId` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlistId` TEXT, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, `isEditable` INTEGER NOT NULL DEFAULT true, `isLocal` INTEGER NOT NULL DEFAULT false, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"isEditable\",\n            \"columnName\": \"isEditable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `setVideoId` TEXT, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"set_video_id\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `setVideoId` TEXT, PRIMARY KEY(`videoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"videoId\",\n            \"columnName\": \"videoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"videoId\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b78ea238955043c3308d49775d7267a8')\"\n    ]\n  }\n}\n"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/17.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 17,\n    \"identityHash\": \"59f80ce4b59b0c31db6e5895871ae26d\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"date\",\n            \"columnName\": \"date\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"dateModified\",\n            \"columnName\": \"dateModified\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"dateDownload\",\n            \"columnName\": \"dateDownload\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `channelId` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlistId` TEXT, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `likedDate` INTEGER, `inLibrary` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, `createdAt` INTEGER, `lastUpdateTime` INTEGER, `isEditable` INTEGER NOT NULL DEFAULT true, `bookmarkedAt` INTEGER, `remoteSongCount` INTEGER, `playEndpointParams` TEXT, `shuffleEndpointParams` TEXT, `radioEndpointParams` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"isEditable\",\n            \"columnName\": \"isEditable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"remoteSongCount\",\n            \"columnName\": \"remoteSongCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"playEndpointParams\",\n            \"columnName\": \"playEndpointParams\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"shuffleEndpointParams\",\n            \"columnName\": \"shuffleEndpointParams\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"radioEndpointParams\",\n            \"columnName\": \"radioEndpointParams\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `setVideoId` TEXT, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, `playbackUrl` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"playbackUrl\",\n            \"columnName\": \"playbackUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"set_video_id\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `setVideoId` TEXT, PRIMARY KEY(`videoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"videoId\",\n            \"columnName\": \"videoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"videoId\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '59f80ce4b59b0c31db6e5895871ae26d')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/18.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 18,\n    \"identityHash\": \"a92d7d81fc8b49d3b1e9f92f6a1c4b7e\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"date\",\n            \"columnName\": \"date\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"dateModified\",\n            \"columnName\": \"dateModified\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"dateDownload\",\n            \"columnName\": \"dateDownload\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `channelId` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlistId` TEXT, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `likedDate` INTEGER, `inLibrary` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, `createdAt` INTEGER, `lastUpdateTime` INTEGER, `isEditable` INTEGER NOT NULL DEFAULT true, `bookmarkedAt` INTEGER, `remoteSongCount` INTEGER, `playEndpointParams` TEXT, `shuffleEndpointParams` TEXT, `radioEndpointParams` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"isEditable\",\n            \"columnName\": \"isEditable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"remoteSongCount\",\n            \"columnName\": \"remoteSongCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"playEndpointParams\",\n            \"columnName\": \"playEndpointParams\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"shuffleEndpointParams\",\n            \"columnName\": \"shuffleEndpointParams\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"radioEndpointParams\",\n            \"columnName\": \"radioEndpointParams\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `setVideoId` TEXT, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, `playbackUrl` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"playbackUrl\",\n            \"columnName\": \"playbackUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playCount\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`song`, `year`, `month`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"song\",\n            \"columnName\": \"song\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"month\",\n            \"columnName\": \"month\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"count\",\n            \"columnName\": \"count\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"song\",\n            \"year\",\n            \"month\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"set_video_id\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `setVideoId` TEXT, PRIMARY KEY(`videoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"videoId\",\n            \"columnName\": \"videoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"videoId\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a92d7d81fc8b49d3b1e9f92f6a1c4b7e')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/19.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 19,\n    \"identityHash\": \"c8f37a94d4c749f6a6c07a53f7b2e1fc\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"artistName\",\n            \"columnName\": \"artistName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"date\",\n            \"columnName\": \"date\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"dateModified\",\n            \"columnName\": \"dateModified\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"dateDownload\",\n            \"columnName\": \"dateDownload\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `channelId` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlistId` TEXT, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `likedDate` INTEGER, `inLibrary` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, `createdAt` INTEGER, `lastUpdateTime` INTEGER, `isEditable` INTEGER NOT NULL DEFAULT true, `bookmarkedAt` INTEGER, `remoteSongCount` INTEGER, `playEndpointParams` TEXT, `shuffleEndpointParams` TEXT, `radioEndpointParams` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"isEditable\",\n            \"columnName\": \"isEditable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"remoteSongCount\",\n            \"columnName\": \"remoteSongCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"playEndpointParams\",\n            \"columnName\": \"playEndpointParams\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"shuffleEndpointParams\",\n            \"columnName\": \"shuffleEndpointParams\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"radioEndpointParams\",\n            \"columnName\": \"radioEndpointParams\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `setVideoId` TEXT, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, `playbackUrl` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"playbackUrl\",\n            \"columnName\": \"playbackUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playCount\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`song`, `year`, `month`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"song\",\n            \"columnName\": \"song\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"month\",\n            \"columnName\": \"month\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"count\",\n            \"columnName\": \"count\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"song\",\n            \"year\",\n            \"month\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"set_video_id\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `setVideoId` TEXT, PRIMARY KEY(`videoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"videoId\",\n            \"columnName\": \"videoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"videoId\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c8f37a94d4c749f6a6c07a53f7b2e1fc')\"\n    ]\n  }\n}\n"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/2.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 2,\n    \"identityHash\": \"3a7db15c3d60f94f6a7acc75fad88d79\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `liked` INTEGER NOT NULL, `totalPlayTime` INTEGER NOT NULL, `isTrash` INTEGER NOT NULL, `download_state` INTEGER NOT NULL, `create_date` INTEGER NOT NULL, `modify_date` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isTrash\",\n            \"columnName\": \"isTrash\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"downloadState\",\n            \"columnName\": \"download_state\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"create_date\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"modifyDate\",\n            \"columnName\": \"modify_date\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `bannerUrl` TEXT, `description` TEXT, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"bannerUrl\",\n            \"columnName\": \"bannerUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"description\",\n            \"columnName\": \"description\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT, `authorId` TEXT, `year` INTEGER, `thumbnailUrl` TEXT, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"author\",\n            \"columnName\": \"author\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"authorId\",\n            \"columnName\": \"authorId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"download\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `songId` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3a7db15c3d60f94f6a7acc75fad88d79')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/20.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 20,\n    \"identityHash\": \"d9f47b95e5d749f7b7c08a64f8c3f2fd\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `artistName` TEXT, `explicit` INTEGER NOT NULL DEFAULT 0, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"artistName\",\n            \"columnName\": \"artistName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"date\",\n            \"columnName\": \"date\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"dateModified\",\n            \"columnName\": \"dateModified\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"dateDownload\",\n            \"columnName\": \"dateDownload\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `channelId` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlistId` TEXT, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `explicit` INTEGER NOT NULL DEFAULT 0, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `likedDate` INTEGER, `inLibrary` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, `createdAt` INTEGER, `lastUpdateTime` INTEGER, `isEditable` INTEGER NOT NULL DEFAULT true, `bookmarkedAt` INTEGER, `remoteSongCount` INTEGER, `playEndpointParams` TEXT, `shuffleEndpointParams` TEXT, `radioEndpointParams` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"isEditable\",\n            \"columnName\": \"isEditable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"remoteSongCount\",\n            \"columnName\": \"remoteSongCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"playEndpointParams\",\n            \"columnName\": \"playEndpointParams\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"shuffleEndpointParams\",\n            \"columnName\": \"shuffleEndpointParams\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"radioEndpointParams\",\n            \"columnName\": \"radioEndpointParams\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `setVideoId` TEXT, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, `playbackUrl` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"playbackUrl\",\n            \"columnName\": \"playbackUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playCount\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`song`, `year`, `month`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"song\",\n            \"columnName\": \"song\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"month\",\n            \"columnName\": \"month\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"count\",\n            \"columnName\": \"count\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"song\",\n            \"year\",\n            \"month\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"set_video_id\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `setVideoId` TEXT, PRIMARY KEY(`videoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"videoId\",\n            \"columnName\": \"videoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"videoId\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd9f47b95e5d749f7b7c08a64f8c3f2fd')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/21.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 21,\n    \"identityHash\": \"e0f58c96f6e849f8c8d09b75f9d4f3fe\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `explicit` INTEGER NOT NULL DEFAULT 0, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"date\",\n            \"columnName\": \"date\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"dateModified\",\n            \"columnName\": \"dateModified\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"dateDownload\",\n            \"columnName\": \"dateDownload\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `channelId` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlistId` TEXT, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `explicit` INTEGER NOT NULL DEFAULT 0, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `likedDate` INTEGER, `inLibrary` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, `createdAt` INTEGER, `lastUpdateTime` INTEGER, `isEditable` INTEGER NOT NULL DEFAULT true, `bookmarkedAt` INTEGER, `remoteSongCount` INTEGER, `playEndpointParams` TEXT, `shuffleEndpointParams` TEXT, `radioEndpointParams` TEXT, `isLocal` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"isEditable\",\n            \"columnName\": \"isEditable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"remoteSongCount\",\n            \"columnName\": \"remoteSongCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"playEndpointParams\",\n            \"columnName\": \"playEndpointParams\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"shuffleEndpointParams\",\n            \"columnName\": \"shuffleEndpointParams\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"radioEndpointParams\",\n            \"columnName\": \"radioEndpointParams\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `setVideoId` TEXT, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, `playbackUrl` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"playbackUrl\",\n            \"columnName\": \"playbackUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playCount\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`song`, `year`, `month`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"song\",\n            \"columnName\": \"song\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"month\",\n            \"columnName\": \"month\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"count\",\n            \"columnName\": \"count\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"song\",\n            \"year\",\n            \"month\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"set_video_id\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `setVideoId` TEXT, PRIMARY KEY(`videoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"videoId\",\n            \"columnName\": \"videoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"videoId\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e0f58c96f6e849f8c8d09b75f9d4f3fe')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/22.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 22,\n    \"identityHash\": \"e0f58c96f6e849f8c8d09b75f9d4f3fe\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `explicit` INTEGER NOT NULL DEFAULT 0, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"date\",\n            \"columnName\": \"date\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"dateModified\",\n            \"columnName\": \"dateModified\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"dateDownload\",\n            \"columnName\": \"dateDownload\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `channelId` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlistId` TEXT, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `explicit` INTEGER NOT NULL DEFAULT 0, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `likedDate` INTEGER, `inLibrary` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, `createdAt` INTEGER, `lastUpdateTime` INTEGER, `isEditable` INTEGER NOT NULL DEFAULT true, `bookmarkedAt` INTEGER, `remoteSongCount` INTEGER, `playEndpointParams` TEXT, `thumbnailUrl` TEXT, `shuffleEndpointParams` TEXT, `radioEndpointParams` TEXT, `isLocal` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"isEditable\",\n            \"columnName\": \"isEditable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"remoteSongCount\",\n            \"columnName\": \"remoteSongCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"playEndpointParams\",\n            \"columnName\": \"playEndpointParams\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"shuffleEndpointParams\",\n            \"columnName\": \"shuffleEndpointParams\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"radioEndpointParams\",\n            \"columnName\": \"radioEndpointParams\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `setVideoId` TEXT, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, `playbackUrl` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"playbackUrl\",\n            \"columnName\": \"playbackUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playCount\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`song`, `year`, `month`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"song\",\n            \"columnName\": \"song\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"month\",\n            \"columnName\": \"month\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"count\",\n            \"columnName\": \"count\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"song\",\n            \"year\",\n            \"month\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"set_video_id\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `setVideoId` TEXT, PRIMARY KEY(`videoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"videoId\",\n            \"columnName\": \"videoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"videoId\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e0f58c96f6e849f8c8d09b75f9d4f3fe')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/23.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 23,\n    \"identityHash\": \"163997ad95cd0d0fe167198a705f7012\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `explicit` INTEGER NOT NULL DEFAULT 0, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `libraryAddToken` TEXT, `libraryRemoveToken` TEXT, `romanizeLyrics` INTEGER NOT NULL DEFAULT true, `isDownloaded` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"date\",\n            \"columnName\": \"date\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateModified\",\n            \"columnName\": \"dateModified\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateDownload\",\n            \"columnName\": \"dateDownload\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"libraryAddToken\",\n            \"columnName\": \"libraryAddToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"libraryRemoveToken\",\n            \"columnName\": \"libraryRemoveToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"romanizeLyrics\",\n            \"columnName\": \"romanizeLyrics\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"isDownloaded\",\n            \"columnName\": \"isDownloaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `channelId` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlistId` TEXT, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `explicit` INTEGER NOT NULL DEFAULT 0, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `likedDate` INTEGER, `inLibrary` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, `createdAt` INTEGER, `lastUpdateTime` INTEGER, `isEditable` INTEGER NOT NULL DEFAULT true, `bookmarkedAt` INTEGER, `remoteSongCount` INTEGER, `playEndpointParams` TEXT, `thumbnailUrl` TEXT, `shuffleEndpointParams` TEXT, `radioEndpointParams` TEXT, `isLocal` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isEditable\",\n            \"columnName\": \"isEditable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"remoteSongCount\",\n            \"columnName\": \"remoteSongCount\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"playEndpointParams\",\n            \"columnName\": \"playEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"shuffleEndpointParams\",\n            \"columnName\": \"shuffleEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"radioEndpointParams\",\n            \"columnName\": \"radioEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `setVideoId` TEXT, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, `playbackUrl` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"playbackUrl\",\n            \"columnName\": \"playbackUrl\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"set_video_id\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `setVideoId` TEXT, PRIMARY KEY(`videoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"videoId\",\n            \"columnName\": \"videoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"videoId\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playCount\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`song`, `year`, `month`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"song\",\n            \"columnName\": \"song\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"month\",\n            \"columnName\": \"month\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"count\",\n            \"columnName\": \"count\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"song\",\n            \"year\",\n            \"month\"\n          ]\n        }\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '163997ad95cd0d0fe167198a705f7012')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/24.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 24,\n    \"identityHash\": \"163997ad95cd0d0fe167198a705f7012\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `explicit` INTEGER NOT NULL DEFAULT 0, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `libraryAddToken` TEXT, `libraryRemoveToken` TEXT, `romanizeLyrics` INTEGER NOT NULL DEFAULT true, `isDownloaded` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"date\",\n            \"columnName\": \"date\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateModified\",\n            \"columnName\": \"dateModified\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateDownload\",\n            \"columnName\": \"dateDownload\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"libraryAddToken\",\n            \"columnName\": \"libraryAddToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"libraryRemoveToken\",\n            \"columnName\": \"libraryRemoveToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"romanizeLyrics\",\n            \"columnName\": \"romanizeLyrics\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"isDownloaded\",\n            \"columnName\": \"isDownloaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `channelId` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlistId` TEXT, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `explicit` INTEGER NOT NULL DEFAULT 0, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `likedDate` INTEGER, `inLibrary` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `isUploaded` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, `createdAt` INTEGER, `lastUpdateTime` INTEGER, `isEditable` INTEGER NOT NULL DEFAULT true, `bookmarkedAt` INTEGER, `remoteSongCount` INTEGER, `playEndpointParams` TEXT, `thumbnailUrl` TEXT, `shuffleEndpointParams` TEXT, `radioEndpointParams` TEXT, `isLocal` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isEditable\",\n            \"columnName\": \"isEditable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"remoteSongCount\",\n            \"columnName\": \"remoteSongCount\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"playEndpointParams\",\n            \"columnName\": \"playEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"shuffleEndpointParams\",\n            \"columnName\": \"shuffleEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"radioEndpointParams\",\n            \"columnName\": \"radioEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `setVideoId` TEXT, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, `playbackUrl` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"playbackUrl\",\n            \"columnName\": \"playbackUrl\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"set_video_id\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `setVideoId` TEXT, PRIMARY KEY(`videoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"videoId\",\n            \"columnName\": \"videoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"videoId\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playCount\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`song`, `year`, `month`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"song\",\n            \"columnName\": \"song\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"month\",\n            \"columnName\": \"month\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"count\",\n            \"columnName\": \"count\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"song\",\n            \"year\",\n            \"month\"\n          ]\n        }\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '163997ad95cd0d0fe167198a705f7012')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/25.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 25,\n    \"identityHash\": \"163997ad95cd0d0fe167198a705f7012\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `explicit` INTEGER NOT NULL DEFAULT 0, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `libraryAddToken` TEXT, `libraryRemoveToken` TEXT, `romanizeLyrics` INTEGER NOT NULL DEFAULT true, `isDownloaded` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"date\",\n            \"columnName\": \"date\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateModified\",\n            \"columnName\": \"dateModified\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateDownload\",\n            \"columnName\": \"dateDownload\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"libraryAddToken\",\n            \"columnName\": \"libraryAddToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"libraryRemoveToken\",\n            \"columnName\": \"libraryRemoveToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"romanizeLyrics\",\n            \"columnName\": \"romanizeLyrics\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"isDownloaded\",\n            \"columnName\": \"isDownloaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `channelId` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlistId` TEXT, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `explicit` INTEGER NOT NULL DEFAULT 0, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `likedDate` INTEGER, `inLibrary` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `isUploaded` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, `createdAt` INTEGER, `lastUpdateTime` INTEGER, `isEditable` INTEGER NOT NULL DEFAULT true, `bookmarkedAt` INTEGER, `remoteSongCount` INTEGER, `playEndpointParams` TEXT, `thumbnailUrl` TEXT, `shuffleEndpointParams` TEXT, `radioEndpointParams` TEXT, `isLocal` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isEditable\",\n            \"columnName\": \"isEditable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"remoteSongCount\",\n            \"columnName\": \"remoteSongCount\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"playEndpointParams\",\n            \"columnName\": \"playEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"shuffleEndpointParams\",\n            \"columnName\": \"shuffleEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"radioEndpointParams\",\n            \"columnName\": \"radioEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `setVideoId` TEXT, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, `perceptualLoudnessDb` REAL, `playbackUrl` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"perceptualLoudnessDb\",\n            \"columnName\": \"perceptualLoudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"playbackUrl\",\n            \"columnName\": \"playbackUrl\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"set_video_id\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `setVideoId` TEXT, PRIMARY KEY(`videoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"videoId\",\n            \"columnName\": \"videoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"videoId\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playCount\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`song`, `year`, `month`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"song\",\n            \"columnName\": \"song\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"month\",\n            \"columnName\": \"month\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"count\",\n            \"columnName\": \"count\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"song\",\n            \"year\",\n            \"month\"\n          ]\n        }\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '163997ad95cd0d0fe167198a705f7012')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/26.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 26,\n    \"identityHash\": \"77118ea292614a4db192780d42629896\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `explicit` INTEGER NOT NULL DEFAULT 0, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, `isLocal` INTEGER NOT NULL, `libraryAddToken` TEXT, `libraryRemoveToken` TEXT, `romanizeLyrics` INTEGER NOT NULL DEFAULT true, `isDownloaded` INTEGER NOT NULL DEFAULT 0, `isUploaded` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"date\",\n            \"columnName\": \"date\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateModified\",\n            \"columnName\": \"dateModified\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateDownload\",\n            \"columnName\": \"dateDownload\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"libraryAddToken\",\n            \"columnName\": \"libraryAddToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"libraryRemoveToken\",\n            \"columnName\": \"libraryRemoveToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"romanizeLyrics\",\n            \"columnName\": \"romanizeLyrics\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"isDownloaded\",\n            \"columnName\": \"isDownloaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `channelId` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `isLocal` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlistId` TEXT, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `explicit` INTEGER NOT NULL DEFAULT 0, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `likedDate` INTEGER, `inLibrary` INTEGER, `isLocal` INTEGER NOT NULL, `isUploaded` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, `createdAt` INTEGER, `lastUpdateTime` INTEGER, `isEditable` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `remoteSongCount` INTEGER, `playEndpointParams` TEXT, `thumbnailUrl` TEXT, `shuffleEndpointParams` TEXT, `radioEndpointParams` TEXT, `isLocal` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isEditable\",\n            \"columnName\": \"isEditable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"remoteSongCount\",\n            \"columnName\": \"remoteSongCount\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"playEndpointParams\",\n            \"columnName\": \"playEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"shuffleEndpointParams\",\n            \"columnName\": \"shuffleEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"radioEndpointParams\",\n            \"columnName\": \"radioEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `setVideoId` TEXT, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, `perceptualLoudnessDb` REAL, `playbackUrl` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"perceptualLoudnessDb\",\n            \"columnName\": \"perceptualLoudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"playbackUrl\",\n            \"columnName\": \"playbackUrl\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"set_video_id\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `setVideoId` TEXT, PRIMARY KEY(`videoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"videoId\",\n            \"columnName\": \"videoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"videoId\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playCount\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`song`, `year`, `month`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"song\",\n            \"columnName\": \"song\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"month\",\n            \"columnName\": \"month\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"count\",\n            \"columnName\": \"count\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"song\",\n            \"year\",\n            \"month\"\n          ]\n        }\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '77118ea292614a4db192780d42629896')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/27.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 27,\n    \"identityHash\": \"1d93d14854c13d1e6158b68dcc956fd3\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `explicit` INTEGER NOT NULL DEFAULT 0, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, `isLocal` INTEGER NOT NULL, `libraryAddToken` TEXT, `libraryRemoveToken` TEXT, `lyricsOffset` INTEGER NOT NULL DEFAULT 0, `romanizeLyrics` INTEGER NOT NULL DEFAULT true, `isDownloaded` INTEGER NOT NULL DEFAULT 0, `isUploaded` INTEGER NOT NULL DEFAULT false, `isVideo` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"date\",\n            \"columnName\": \"date\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateModified\",\n            \"columnName\": \"dateModified\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateDownload\",\n            \"columnName\": \"dateDownload\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"libraryAddToken\",\n            \"columnName\": \"libraryAddToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"libraryRemoveToken\",\n            \"columnName\": \"libraryRemoveToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lyricsOffset\",\n            \"columnName\": \"lyricsOffset\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"romanizeLyrics\",\n            \"columnName\": \"romanizeLyrics\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"isDownloaded\",\n            \"columnName\": \"isDownloaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isVideo\",\n            \"columnName\": \"isVideo\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `channelId` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `isLocal` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlistId` TEXT, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `explicit` INTEGER NOT NULL DEFAULT 0, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `likedDate` INTEGER, `inLibrary` INTEGER, `isLocal` INTEGER NOT NULL, `isUploaded` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, `createdAt` INTEGER, `lastUpdateTime` INTEGER, `isEditable` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `remoteSongCount` INTEGER, `playEndpointParams` TEXT, `thumbnailUrl` TEXT, `shuffleEndpointParams` TEXT, `radioEndpointParams` TEXT, `isLocal` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isEditable\",\n            \"columnName\": \"isEditable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"remoteSongCount\",\n            \"columnName\": \"remoteSongCount\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"playEndpointParams\",\n            \"columnName\": \"playEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"shuffleEndpointParams\",\n            \"columnName\": \"shuffleEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"radioEndpointParams\",\n            \"columnName\": \"radioEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `setVideoId` TEXT, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, `perceptualLoudnessDb` REAL, `playbackUrl` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"perceptualLoudnessDb\",\n            \"columnName\": \"perceptualLoudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"playbackUrl\",\n            \"columnName\": \"playbackUrl\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"set_video_id\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `setVideoId` TEXT, PRIMARY KEY(`videoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"videoId\",\n            \"columnName\": \"videoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"videoId\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playCount\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`song`, `year`, `month`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"song\",\n            \"columnName\": \"song\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"month\",\n            \"columnName\": \"month\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"count\",\n            \"columnName\": \"count\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"song\",\n            \"year\",\n            \"month\"\n          ]\n        }\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1d93d14854c13d1e6158b68dcc956fd3')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/28.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 28,\n    \"identityHash\": \"331218677f74a364b5cad847a411999c\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `explicit` INTEGER NOT NULL DEFAULT 0, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `libraryAddToken` TEXT, `libraryRemoveToken` TEXT, `lyricsOffset` INTEGER NOT NULL DEFAULT 0, `romanizeLyrics` INTEGER NOT NULL DEFAULT true, `isDownloaded` INTEGER NOT NULL DEFAULT 0, `isUploaded` INTEGER NOT NULL DEFAULT false, `isVideo` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"date\",\n            \"columnName\": \"date\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateModified\",\n            \"columnName\": \"dateModified\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateDownload\",\n            \"columnName\": \"dateDownload\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"libraryAddToken\",\n            \"columnName\": \"libraryAddToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"libraryRemoveToken\",\n            \"columnName\": \"libraryRemoveToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lyricsOffset\",\n            \"columnName\": \"lyricsOffset\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"romanizeLyrics\",\n            \"columnName\": \"romanizeLyrics\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"isDownloaded\",\n            \"columnName\": \"isDownloaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isVideo\",\n            \"columnName\": \"isVideo\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `channelId` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlistId` TEXT, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `explicit` INTEGER NOT NULL DEFAULT 0, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `likedDate` INTEGER, `inLibrary` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `isUploaded` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, `createdAt` INTEGER, `lastUpdateTime` INTEGER, `isEditable` INTEGER NOT NULL DEFAULT true, `bookmarkedAt` INTEGER, `remoteSongCount` INTEGER, `playEndpointParams` TEXT, `thumbnailUrl` TEXT, `shuffleEndpointParams` TEXT, `radioEndpointParams` TEXT, `isLocal` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isEditable\",\n            \"columnName\": \"isEditable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"remoteSongCount\",\n            \"columnName\": \"remoteSongCount\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"playEndpointParams\",\n            \"columnName\": \"playEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"shuffleEndpointParams\",\n            \"columnName\": \"shuffleEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"radioEndpointParams\",\n            \"columnName\": \"radioEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `setVideoId` TEXT, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, `perceptualLoudnessDb` REAL, `playbackUrl` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"perceptualLoudnessDb\",\n            \"columnName\": \"perceptualLoudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"playbackUrl\",\n            \"columnName\": \"playbackUrl\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"set_video_id\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `setVideoId` TEXT, PRIMARY KEY(`videoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"videoId\",\n            \"columnName\": \"videoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"videoId\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playCount\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`song`, `year`, `month`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"song\",\n            \"columnName\": \"song\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"month\",\n            \"columnName\": \"month\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"count\",\n            \"columnName\": \"count\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"song\",\n            \"year\",\n            \"month\"\n          ]\n        }\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '331218677f74a364b5cad847a411999c')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/29.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 29,\n    \"identityHash\": \"715638298a0d1c2fa6063b1ebbccb1be\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `explicit` INTEGER NOT NULL DEFAULT 0, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `libraryAddToken` TEXT, `libraryRemoveToken` TEXT, `lyricsOffset` INTEGER NOT NULL DEFAULT 0, `romanizeLyrics` INTEGER NOT NULL DEFAULT true, `isDownloaded` INTEGER NOT NULL DEFAULT 0, `isUploaded` INTEGER NOT NULL DEFAULT false, `isVideo` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"date\",\n            \"columnName\": \"date\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateModified\",\n            \"columnName\": \"dateModified\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateDownload\",\n            \"columnName\": \"dateDownload\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"libraryAddToken\",\n            \"columnName\": \"libraryAddToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"libraryRemoveToken\",\n            \"columnName\": \"libraryRemoveToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lyricsOffset\",\n            \"columnName\": \"lyricsOffset\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"romanizeLyrics\",\n            \"columnName\": \"romanizeLyrics\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"isDownloaded\",\n            \"columnName\": \"isDownloaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isVideo\",\n            \"columnName\": \"isVideo\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `channelId` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlistId` TEXT, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `explicit` INTEGER NOT NULL DEFAULT 0, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `likedDate` INTEGER, `inLibrary` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `isUploaded` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, `createdAt` INTEGER, `lastUpdateTime` INTEGER, `isEditable` INTEGER NOT NULL DEFAULT true, `bookmarkedAt` INTEGER, `remoteSongCount` INTEGER, `playEndpointParams` TEXT, `thumbnailUrl` TEXT, `shuffleEndpointParams` TEXT, `radioEndpointParams` TEXT, `isLocal` INTEGER NOT NULL DEFAULT false, `isAutoSync` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isEditable\",\n            \"columnName\": \"isEditable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"remoteSongCount\",\n            \"columnName\": \"remoteSongCount\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"playEndpointParams\",\n            \"columnName\": \"playEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"shuffleEndpointParams\",\n            \"columnName\": \"shuffleEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"radioEndpointParams\",\n            \"columnName\": \"radioEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isAutoSync\",\n            \"columnName\": \"isAutoSync\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `setVideoId` TEXT, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, `perceptualLoudnessDb` REAL, `playbackUrl` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"perceptualLoudnessDb\",\n            \"columnName\": \"perceptualLoudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"playbackUrl\",\n            \"columnName\": \"playbackUrl\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"set_video_id\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `setVideoId` TEXT, PRIMARY KEY(`videoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"videoId\",\n            \"columnName\": \"videoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"videoId\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playCount\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`song`, `year`, `month`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"song\",\n            \"columnName\": \"song\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"month\",\n            \"columnName\": \"month\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"count\",\n            \"columnName\": \"count\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"song\",\n            \"year\",\n            \"month\"\n          ]\n        }\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '715638298a0d1c2fa6063b1ebbccb1be')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/3.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 3,\n    \"identityHash\": \"b0a90e3281fad7803ea9fadbc6aac04f\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `liked` INTEGER NOT NULL, `totalPlayTime` INTEGER NOT NULL, `isTrash` INTEGER NOT NULL, `download_state` INTEGER NOT NULL, `create_date` INTEGER NOT NULL, `modify_date` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isTrash\",\n            \"columnName\": \"isTrash\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"downloadState\",\n            \"columnName\": \"download_state\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"create_date\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"modifyDate\",\n            \"columnName\": \"modify_date\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `bannerUrl` TEXT, `description` TEXT, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"bannerUrl\",\n            \"columnName\": \"bannerUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"description\",\n            \"columnName\": \"description\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT, `authorId` TEXT, `year` INTEGER, `thumbnailUrl` TEXT, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"author\",\n            \"columnName\": \"author\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"authorId\",\n            \"columnName\": \"authorId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"download\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `songId` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b0a90e3281fad7803ea9fadbc6aac04f')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/30.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 30,\n    \"identityHash\": \"8f2089a8689ee426c5e3a5ea6c935041\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `explicit` INTEGER NOT NULL DEFAULT 0, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `libraryAddToken` TEXT, `libraryRemoveToken` TEXT, `lyricsOffset` INTEGER NOT NULL DEFAULT 0, `romanizeLyrics` INTEGER NOT NULL DEFAULT true, `isDownloaded` INTEGER NOT NULL DEFAULT 0, `isUploaded` INTEGER NOT NULL DEFAULT false, `isVideo` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"date\",\n            \"columnName\": \"date\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateModified\",\n            \"columnName\": \"dateModified\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateDownload\",\n            \"columnName\": \"dateDownload\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"libraryAddToken\",\n            \"columnName\": \"libraryAddToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"libraryRemoveToken\",\n            \"columnName\": \"libraryRemoveToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lyricsOffset\",\n            \"columnName\": \"lyricsOffset\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"romanizeLyrics\",\n            \"columnName\": \"romanizeLyrics\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"isDownloaded\",\n            \"columnName\": \"isDownloaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isVideo\",\n            \"columnName\": \"isVideo\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `channelId` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlistId` TEXT, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `explicit` INTEGER NOT NULL DEFAULT 0, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `likedDate` INTEGER, `inLibrary` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `isUploaded` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, `createdAt` INTEGER, `lastUpdateTime` INTEGER, `isEditable` INTEGER NOT NULL DEFAULT true, `bookmarkedAt` INTEGER, `remoteSongCount` INTEGER, `playEndpointParams` TEXT, `thumbnailUrl` TEXT, `shuffleEndpointParams` TEXT, `radioEndpointParams` TEXT, `isLocal` INTEGER NOT NULL DEFAULT false, `isAutoSync` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isEditable\",\n            \"columnName\": \"isEditable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"remoteSongCount\",\n            \"columnName\": \"remoteSongCount\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"playEndpointParams\",\n            \"columnName\": \"playEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"shuffleEndpointParams\",\n            \"columnName\": \"shuffleEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"radioEndpointParams\",\n            \"columnName\": \"radioEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isAutoSync\",\n            \"columnName\": \"isAutoSync\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `setVideoId` TEXT, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, `perceptualLoudnessDb` REAL, `playbackUrl` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"perceptualLoudnessDb\",\n            \"columnName\": \"perceptualLoudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"playbackUrl\",\n            \"columnName\": \"playbackUrl\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, `provider` TEXT NOT NULL DEFAULT 'Unknown', PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"provider\",\n            \"columnName\": \"provider\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"'Unknown'\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"set_video_id\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `setVideoId` TEXT, PRIMARY KEY(`videoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"videoId\",\n            \"columnName\": \"videoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"videoId\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playCount\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`song`, `year`, `month`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"song\",\n            \"columnName\": \"song\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"month\",\n            \"columnName\": \"month\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"count\",\n            \"columnName\": \"count\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"song\",\n            \"year\",\n            \"month\"\n          ]\n        }\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8f2089a8689ee426c5e3a5ea6c935041')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/31.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 31,\n    \"identityHash\": \"e05443bce6fbfd39a4be703c2d6467da\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `explicit` INTEGER NOT NULL DEFAULT 0, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `libraryAddToken` TEXT, `libraryRemoveToken` TEXT, `lyricsOffset` INTEGER NOT NULL DEFAULT 0, `romanizeLyrics` INTEGER NOT NULL DEFAULT true, `isDownloaded` INTEGER NOT NULL DEFAULT 0, `isUploaded` INTEGER NOT NULL DEFAULT false, `isVideo` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"date\",\n            \"columnName\": \"date\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateModified\",\n            \"columnName\": \"dateModified\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateDownload\",\n            \"columnName\": \"dateDownload\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"libraryAddToken\",\n            \"columnName\": \"libraryAddToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"libraryRemoveToken\",\n            \"columnName\": \"libraryRemoveToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lyricsOffset\",\n            \"columnName\": \"lyricsOffset\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"romanizeLyrics\",\n            \"columnName\": \"romanizeLyrics\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"isDownloaded\",\n            \"columnName\": \"isDownloaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isVideo\",\n            \"columnName\": \"isVideo\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `channelId` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlistId` TEXT, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `explicit` INTEGER NOT NULL DEFAULT 0, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `likedDate` INTEGER, `inLibrary` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `isUploaded` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, `createdAt` INTEGER, `lastUpdateTime` INTEGER, `isEditable` INTEGER NOT NULL DEFAULT true, `bookmarkedAt` INTEGER, `remoteSongCount` INTEGER, `playEndpointParams` TEXT, `thumbnailUrl` TEXT, `shuffleEndpointParams` TEXT, `radioEndpointParams` TEXT, `isLocal` INTEGER NOT NULL DEFAULT false, `isAutoSync` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isEditable\",\n            \"columnName\": \"isEditable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"remoteSongCount\",\n            \"columnName\": \"remoteSongCount\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"playEndpointParams\",\n            \"columnName\": \"playEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"shuffleEndpointParams\",\n            \"columnName\": \"shuffleEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"radioEndpointParams\",\n            \"columnName\": \"radioEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isAutoSync\",\n            \"columnName\": \"isAutoSync\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `setVideoId` TEXT, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, `perceptualLoudnessDb` REAL, `playbackUrl` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"perceptualLoudnessDb\",\n            \"columnName\": \"perceptualLoudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"playbackUrl\",\n            \"columnName\": \"playbackUrl\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, `provider` TEXT NOT NULL DEFAULT 'Unknown', PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"provider\",\n            \"columnName\": \"provider\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"'Unknown'\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"set_video_id\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `setVideoId` TEXT, PRIMARY KEY(`videoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"videoId\",\n            \"columnName\": \"videoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"videoId\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playCount\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`song`, `year`, `month`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"song\",\n            \"columnName\": \"song\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"month\",\n            \"columnName\": \"month\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"count\",\n            \"columnName\": \"count\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"song\",\n            \"year\",\n            \"month\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"recognition_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `trackId` TEXT NOT NULL, `title` TEXT NOT NULL, `artist` TEXT NOT NULL, `album` TEXT, `coverArtUrl` TEXT, `coverArtHqUrl` TEXT, `genre` TEXT, `releaseDate` TEXT, `label` TEXT, `shazamUrl` TEXT, `appleMusicUrl` TEXT, `spotifyUrl` TEXT, `isrc` TEXT, `youtubeVideoId` TEXT, `recognizedAt` INTEGER NOT NULL, `liked` INTEGER NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"trackId\",\n            \"columnName\": \"trackId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artist\",\n            \"columnName\": \"artist\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"album\",\n            \"columnName\": \"album\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"coverArtUrl\",\n            \"columnName\": \"coverArtUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"coverArtHqUrl\",\n            \"columnName\": \"coverArtHqUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"genre\",\n            \"columnName\": \"genre\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"releaseDate\",\n            \"columnName\": \"releaseDate\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"label\",\n            \"columnName\": \"label\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"shazamUrl\",\n            \"columnName\": \"shazamUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"appleMusicUrl\",\n            \"columnName\": \"appleMusicUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"spotifyUrl\",\n            \"columnName\": \"spotifyUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"isrc\",\n            \"columnName\": \"isrc\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"youtubeVideoId\",\n            \"columnName\": \"youtubeVideoId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"recognizedAt\",\n            \"columnName\": \"recognizedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_recognition_history_trackId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"trackId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_recognition_history_trackId` ON `${TABLE_NAME}` (`trackId`)\"\n          }\n        ]\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e05443bce6fbfd39a4be703c2d6467da')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/32.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 32,\n    \"identityHash\": \"6c3169c6fab939b089c79314ac12d9b9\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `explicit` INTEGER NOT NULL DEFAULT 0, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `libraryAddToken` TEXT, `libraryRemoveToken` TEXT, `lyricsOffset` INTEGER NOT NULL DEFAULT 0, `romanizeLyrics` INTEGER NOT NULL DEFAULT true, `isDownloaded` INTEGER NOT NULL DEFAULT 0, `isUploaded` INTEGER NOT NULL DEFAULT false, `isVideo` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"date\",\n            \"columnName\": \"date\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateModified\",\n            \"columnName\": \"dateModified\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateDownload\",\n            \"columnName\": \"dateDownload\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"libraryAddToken\",\n            \"columnName\": \"libraryAddToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"libraryRemoveToken\",\n            \"columnName\": \"libraryRemoveToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lyricsOffset\",\n            \"columnName\": \"lyricsOffset\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"romanizeLyrics\",\n            \"columnName\": \"romanizeLyrics\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"isDownloaded\",\n            \"columnName\": \"isDownloaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isVideo\",\n            \"columnName\": \"isVideo\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `channelId` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlistId` TEXT, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `explicit` INTEGER NOT NULL DEFAULT 0, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `likedDate` INTEGER, `inLibrary` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `isUploaded` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, `createdAt` INTEGER, `lastUpdateTime` INTEGER, `isEditable` INTEGER NOT NULL DEFAULT true, `bookmarkedAt` INTEGER, `remoteSongCount` INTEGER, `playEndpointParams` TEXT, `thumbnailUrl` TEXT, `shuffleEndpointParams` TEXT, `radioEndpointParams` TEXT, `isLocal` INTEGER NOT NULL DEFAULT false, `isAutoSync` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isEditable\",\n            \"columnName\": \"isEditable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"remoteSongCount\",\n            \"columnName\": \"remoteSongCount\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"playEndpointParams\",\n            \"columnName\": \"playEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"shuffleEndpointParams\",\n            \"columnName\": \"shuffleEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"radioEndpointParams\",\n            \"columnName\": \"radioEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isAutoSync\",\n            \"columnName\": \"isAutoSync\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `setVideoId` TEXT, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, `perceptualLoudnessDb` REAL, `playbackUrl` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"perceptualLoudnessDb\",\n            \"columnName\": \"perceptualLoudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"playbackUrl\",\n            \"columnName\": \"playbackUrl\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, `provider` TEXT NOT NULL DEFAULT 'Unknown', `translatedLyrics` TEXT NOT NULL DEFAULT '', `translationLanguage` TEXT NOT NULL DEFAULT '', `translationMode` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"provider\",\n            \"columnName\": \"provider\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"'Unknown'\"\n          },\n          {\n            \"fieldPath\": \"translatedLyrics\",\n            \"columnName\": \"translatedLyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          },\n          {\n            \"fieldPath\": \"translationLanguage\",\n            \"columnName\": \"translationLanguage\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          },\n          {\n            \"fieldPath\": \"translationMode\",\n            \"columnName\": \"translationMode\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"set_video_id\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `setVideoId` TEXT, PRIMARY KEY(`videoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"videoId\",\n            \"columnName\": \"videoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"videoId\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playCount\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`song`, `year`, `month`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"song\",\n            \"columnName\": \"song\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"month\",\n            \"columnName\": \"month\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"count\",\n            \"columnName\": \"count\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"song\",\n            \"year\",\n            \"month\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"recognition_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `trackId` TEXT NOT NULL, `title` TEXT NOT NULL, `artist` TEXT NOT NULL, `album` TEXT, `coverArtUrl` TEXT, `coverArtHqUrl` TEXT, `genre` TEXT, `releaseDate` TEXT, `label` TEXT, `shazamUrl` TEXT, `appleMusicUrl` TEXT, `spotifyUrl` TEXT, `isrc` TEXT, `youtubeVideoId` TEXT, `recognizedAt` INTEGER NOT NULL, `liked` INTEGER NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"trackId\",\n            \"columnName\": \"trackId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artist\",\n            \"columnName\": \"artist\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"album\",\n            \"columnName\": \"album\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"coverArtUrl\",\n            \"columnName\": \"coverArtUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"coverArtHqUrl\",\n            \"columnName\": \"coverArtHqUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"genre\",\n            \"columnName\": \"genre\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"releaseDate\",\n            \"columnName\": \"releaseDate\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"label\",\n            \"columnName\": \"label\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"shazamUrl\",\n            \"columnName\": \"shazamUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"appleMusicUrl\",\n            \"columnName\": \"appleMusicUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"spotifyUrl\",\n            \"columnName\": \"spotifyUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"isrc\",\n            \"columnName\": \"isrc\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"youtubeVideoId\",\n            \"columnName\": \"youtubeVideoId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"recognizedAt\",\n            \"columnName\": \"recognizedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_recognition_history_trackId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"trackId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_recognition_history_trackId` ON `${TABLE_NAME}` (`trackId`)\"\n          }\n        ]\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6c3169c6fab939b089c79314ac12d9b9')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/33.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 33,\n    \"identityHash\": \"bd72668b1e47bd29fc4195bfc7b85064\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `explicit` INTEGER NOT NULL DEFAULT 0, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `libraryAddToken` TEXT, `libraryRemoveToken` TEXT, `lyricsOffset` INTEGER NOT NULL DEFAULT 0, `romanizeLyrics` INTEGER NOT NULL DEFAULT true, `isDownloaded` INTEGER NOT NULL DEFAULT 0, `isUploaded` INTEGER NOT NULL DEFAULT false, `isVideo` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"date\",\n            \"columnName\": \"date\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateModified\",\n            \"columnName\": \"dateModified\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateDownload\",\n            \"columnName\": \"dateDownload\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"libraryAddToken\",\n            \"columnName\": \"libraryAddToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"libraryRemoveToken\",\n            \"columnName\": \"libraryRemoveToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lyricsOffset\",\n            \"columnName\": \"lyricsOffset\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"romanizeLyrics\",\n            \"columnName\": \"romanizeLyrics\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"isDownloaded\",\n            \"columnName\": \"isDownloaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isVideo\",\n            \"columnName\": \"isVideo\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `channelId` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlistId` TEXT, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `explicit` INTEGER NOT NULL DEFAULT 0, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `likedDate` INTEGER, `inLibrary` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `isUploaded` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, `createdAt` INTEGER, `lastUpdateTime` INTEGER, `isEditable` INTEGER NOT NULL DEFAULT true, `bookmarkedAt` INTEGER, `remoteSongCount` INTEGER, `playEndpointParams` TEXT, `thumbnailUrl` TEXT, `shuffleEndpointParams` TEXT, `radioEndpointParams` TEXT, `isLocal` INTEGER NOT NULL DEFAULT false, `isAutoSync` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isEditable\",\n            \"columnName\": \"isEditable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"remoteSongCount\",\n            \"columnName\": \"remoteSongCount\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"playEndpointParams\",\n            \"columnName\": \"playEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"shuffleEndpointParams\",\n            \"columnName\": \"shuffleEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"radioEndpointParams\",\n            \"columnName\": \"radioEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isAutoSync\",\n            \"columnName\": \"isAutoSync\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `setVideoId` TEXT, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, `perceptualLoudnessDb` REAL, `playbackUrl` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"perceptualLoudnessDb\",\n            \"columnName\": \"perceptualLoudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"playbackUrl\",\n            \"columnName\": \"playbackUrl\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, `provider` TEXT NOT NULL DEFAULT 'Unknown', `translatedLyrics` TEXT NOT NULL DEFAULT '', `translationLanguage` TEXT NOT NULL DEFAULT '', `translationMode` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"provider\",\n            \"columnName\": \"provider\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"'Unknown'\"\n          },\n          {\n            \"fieldPath\": \"translatedLyrics\",\n            \"columnName\": \"translatedLyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          },\n          {\n            \"fieldPath\": \"translationLanguage\",\n            \"columnName\": \"translationLanguage\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          },\n          {\n            \"fieldPath\": \"translationMode\",\n            \"columnName\": \"translationMode\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"set_video_id\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `setVideoId` TEXT, PRIMARY KEY(`videoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"videoId\",\n            \"columnName\": \"videoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"videoId\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playCount\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`song`, `year`, `month`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"song\",\n            \"columnName\": \"song\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"month\",\n            \"columnName\": \"month\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"count\",\n            \"columnName\": \"count\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"song\",\n            \"year\",\n            \"month\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"recognition_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `trackId` TEXT NOT NULL, `title` TEXT NOT NULL, `artist` TEXT NOT NULL, `album` TEXT, `coverArtUrl` TEXT, `coverArtHqUrl` TEXT, `genre` TEXT, `releaseDate` TEXT, `label` TEXT, `shazamUrl` TEXT, `appleMusicUrl` TEXT, `spotifyUrl` TEXT, `isrc` TEXT, `youtubeVideoId` TEXT, `recognizedAt` INTEGER NOT NULL, `liked` INTEGER NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"trackId\",\n            \"columnName\": \"trackId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artist\",\n            \"columnName\": \"artist\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"album\",\n            \"columnName\": \"album\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"coverArtUrl\",\n            \"columnName\": \"coverArtUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"coverArtHqUrl\",\n            \"columnName\": \"coverArtHqUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"genre\",\n            \"columnName\": \"genre\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"releaseDate\",\n            \"columnName\": \"releaseDate\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"label\",\n            \"columnName\": \"label\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"shazamUrl\",\n            \"columnName\": \"shazamUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"appleMusicUrl\",\n            \"columnName\": \"appleMusicUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"spotifyUrl\",\n            \"columnName\": \"spotifyUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"isrc\",\n            \"columnName\": \"isrc\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"youtubeVideoId\",\n            \"columnName\": \"youtubeVideoId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"recognizedAt\",\n            \"columnName\": \"recognizedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_recognition_history_trackId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"trackId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_recognition_history_trackId` ON `${TABLE_NAME}` (`trackId`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"speed_dial_item\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `secondaryId` TEXT, `title` TEXT NOT NULL, `subtitle` TEXT, `thumbnailUrl` TEXT, `type` TEXT NOT NULL, `explicit` INTEGER NOT NULL, `createDate` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"secondaryId\",\n            \"columnName\": \"secondaryId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subtitle\",\n            \"columnName\": \"subtitle\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bd72668b1e47bd29fc4195bfc7b85064')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/34.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 34,\n    \"identityHash\": \"8e486373672922fea7afc4aa634c077b\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `explicit` INTEGER NOT NULL DEFAULT 0, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `libraryAddToken` TEXT, `libraryRemoveToken` TEXT, `lyricsOffset` INTEGER NOT NULL DEFAULT 0, `romanizeLyrics` INTEGER NOT NULL DEFAULT true, `isDownloaded` INTEGER NOT NULL DEFAULT 0, `isUploaded` INTEGER NOT NULL DEFAULT false, `isVideo` INTEGER NOT NULL DEFAULT false, `isEpisode` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"date\",\n            \"columnName\": \"date\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateModified\",\n            \"columnName\": \"dateModified\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateDownload\",\n            \"columnName\": \"dateDownload\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"libraryAddToken\",\n            \"columnName\": \"libraryAddToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"libraryRemoveToken\",\n            \"columnName\": \"libraryRemoveToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lyricsOffset\",\n            \"columnName\": \"lyricsOffset\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"romanizeLyrics\",\n            \"columnName\": \"romanizeLyrics\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"isDownloaded\",\n            \"columnName\": \"isDownloaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isVideo\",\n            \"columnName\": \"isVideo\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isEpisode\",\n            \"columnName\": \"isEpisode\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `channelId` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlistId` TEXT, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `explicit` INTEGER NOT NULL DEFAULT 0, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `likedDate` INTEGER, `inLibrary` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `isUploaded` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, `createdAt` INTEGER, `lastUpdateTime` INTEGER, `isEditable` INTEGER NOT NULL DEFAULT true, `bookmarkedAt` INTEGER, `remoteSongCount` INTEGER, `playEndpointParams` TEXT, `thumbnailUrl` TEXT, `shuffleEndpointParams` TEXT, `radioEndpointParams` TEXT, `isLocal` INTEGER NOT NULL DEFAULT false, `isAutoSync` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isEditable\",\n            \"columnName\": \"isEditable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"remoteSongCount\",\n            \"columnName\": \"remoteSongCount\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"playEndpointParams\",\n            \"columnName\": \"playEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"shuffleEndpointParams\",\n            \"columnName\": \"shuffleEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"radioEndpointParams\",\n            \"columnName\": \"radioEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isAutoSync\",\n            \"columnName\": \"isAutoSync\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `setVideoId` TEXT, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, `perceptualLoudnessDb` REAL, `playbackUrl` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"perceptualLoudnessDb\",\n            \"columnName\": \"perceptualLoudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"playbackUrl\",\n            \"columnName\": \"playbackUrl\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, `provider` TEXT NOT NULL DEFAULT 'Unknown', `translatedLyrics` TEXT NOT NULL DEFAULT '', `translationLanguage` TEXT NOT NULL DEFAULT '', `translationMode` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"provider\",\n            \"columnName\": \"provider\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"'Unknown'\"\n          },\n          {\n            \"fieldPath\": \"translatedLyrics\",\n            \"columnName\": \"translatedLyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          },\n          {\n            \"fieldPath\": \"translationLanguage\",\n            \"columnName\": \"translationLanguage\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          },\n          {\n            \"fieldPath\": \"translationMode\",\n            \"columnName\": \"translationMode\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"set_video_id\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `setVideoId` TEXT, PRIMARY KEY(`videoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"videoId\",\n            \"columnName\": \"videoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"videoId\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playCount\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`song`, `year`, `month`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"song\",\n            \"columnName\": \"song\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"month\",\n            \"columnName\": \"month\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"count\",\n            \"columnName\": \"count\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"song\",\n            \"year\",\n            \"month\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"recognition_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `trackId` TEXT NOT NULL, `title` TEXT NOT NULL, `artist` TEXT NOT NULL, `album` TEXT, `coverArtUrl` TEXT, `coverArtHqUrl` TEXT, `genre` TEXT, `releaseDate` TEXT, `label` TEXT, `shazamUrl` TEXT, `appleMusicUrl` TEXT, `spotifyUrl` TEXT, `isrc` TEXT, `youtubeVideoId` TEXT, `recognizedAt` INTEGER NOT NULL, `liked` INTEGER NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"trackId\",\n            \"columnName\": \"trackId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artist\",\n            \"columnName\": \"artist\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"album\",\n            \"columnName\": \"album\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"coverArtUrl\",\n            \"columnName\": \"coverArtUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"coverArtHqUrl\",\n            \"columnName\": \"coverArtHqUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"genre\",\n            \"columnName\": \"genre\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"releaseDate\",\n            \"columnName\": \"releaseDate\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"label\",\n            \"columnName\": \"label\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"shazamUrl\",\n            \"columnName\": \"shazamUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"appleMusicUrl\",\n            \"columnName\": \"appleMusicUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"spotifyUrl\",\n            \"columnName\": \"spotifyUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"isrc\",\n            \"columnName\": \"isrc\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"youtubeVideoId\",\n            \"columnName\": \"youtubeVideoId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"recognizedAt\",\n            \"columnName\": \"recognizedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_recognition_history_trackId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"trackId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_recognition_history_trackId` ON `${TABLE_NAME}` (`trackId`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"speed_dial_item\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `secondaryId` TEXT, `title` TEXT NOT NULL, `subtitle` TEXT, `thumbnailUrl` TEXT, `type` TEXT NOT NULL, `explicit` INTEGER NOT NULL, `createDate` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"secondaryId\",\n            \"columnName\": \"secondaryId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subtitle\",\n            \"columnName\": \"subtitle\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"podcast\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT, `thumbnailUrl` TEXT, `channelId` TEXT, `bookmarkedAt` INTEGER, `lastUpdateTime` INTEGER NOT NULL, `libraryAddToken` TEXT, `libraryRemoveToken` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"author\",\n            \"columnName\": \"author\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"libraryAddToken\",\n            \"columnName\": \"libraryAddToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"libraryRemoveToken\",\n            \"columnName\": \"libraryRemoveToken\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8e486373672922fea7afc4aa634c077b')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/35.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 35,\n    \"identityHash\": \"73924a5ef1b9fb713b5e197988a0c633\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `explicit` INTEGER NOT NULL DEFAULT 0, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `libraryAddToken` TEXT, `libraryRemoveToken` TEXT, `lyricsOffset` INTEGER NOT NULL DEFAULT 0, `romanizeLyrics` INTEGER NOT NULL DEFAULT true, `isDownloaded` INTEGER NOT NULL DEFAULT 0, `isUploaded` INTEGER NOT NULL DEFAULT false, `isVideo` INTEGER NOT NULL DEFAULT false, `isEpisode` INTEGER NOT NULL DEFAULT false, `playbackPosition` INTEGER DEFAULT NULL, `uploadEntityId` TEXT DEFAULT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"date\",\n            \"columnName\": \"date\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateModified\",\n            \"columnName\": \"dateModified\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateDownload\",\n            \"columnName\": \"dateDownload\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"libraryAddToken\",\n            \"columnName\": \"libraryAddToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"libraryRemoveToken\",\n            \"columnName\": \"libraryRemoveToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lyricsOffset\",\n            \"columnName\": \"lyricsOffset\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"romanizeLyrics\",\n            \"columnName\": \"romanizeLyrics\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"isDownloaded\",\n            \"columnName\": \"isDownloaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isVideo\",\n            \"columnName\": \"isVideo\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isEpisode\",\n            \"columnName\": \"isEpisode\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"playbackPosition\",\n            \"columnName\": \"playbackPosition\",\n            \"affinity\": \"INTEGER\",\n            \"defaultValue\": \"NULL\"\n          },\n          {\n            \"fieldPath\": \"uploadEntityId\",\n            \"columnName\": \"uploadEntityId\",\n            \"affinity\": \"TEXT\",\n            \"defaultValue\": \"NULL\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `channelId` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `isPodcastChannel` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isPodcastChannel\",\n            \"columnName\": \"isPodcastChannel\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlistId` TEXT, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `explicit` INTEGER NOT NULL DEFAULT 0, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `likedDate` INTEGER, `inLibrary` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `isUploaded` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, `createdAt` INTEGER, `lastUpdateTime` INTEGER, `isEditable` INTEGER NOT NULL DEFAULT true, `bookmarkedAt` INTEGER, `remoteSongCount` INTEGER, `playEndpointParams` TEXT, `thumbnailUrl` TEXT, `shuffleEndpointParams` TEXT, `radioEndpointParams` TEXT, `isLocal` INTEGER NOT NULL DEFAULT false, `isAutoSync` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isEditable\",\n            \"columnName\": \"isEditable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"remoteSongCount\",\n            \"columnName\": \"remoteSongCount\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"playEndpointParams\",\n            \"columnName\": \"playEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"shuffleEndpointParams\",\n            \"columnName\": \"shuffleEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"radioEndpointParams\",\n            \"columnName\": \"radioEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isAutoSync\",\n            \"columnName\": \"isAutoSync\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `setVideoId` TEXT, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, `perceptualLoudnessDb` REAL, `playbackUrl` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"perceptualLoudnessDb\",\n            \"columnName\": \"perceptualLoudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"playbackUrl\",\n            \"columnName\": \"playbackUrl\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, `provider` TEXT NOT NULL DEFAULT 'Unknown', `translatedLyrics` TEXT NOT NULL DEFAULT '', `translationLanguage` TEXT NOT NULL DEFAULT '', `translationMode` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"provider\",\n            \"columnName\": \"provider\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"'Unknown'\"\n          },\n          {\n            \"fieldPath\": \"translatedLyrics\",\n            \"columnName\": \"translatedLyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          },\n          {\n            \"fieldPath\": \"translationLanguage\",\n            \"columnName\": \"translationLanguage\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          },\n          {\n            \"fieldPath\": \"translationMode\",\n            \"columnName\": \"translationMode\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"set_video_id\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `setVideoId` TEXT, PRIMARY KEY(`videoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"videoId\",\n            \"columnName\": \"videoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"videoId\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playCount\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`song`, `year`, `month`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"song\",\n            \"columnName\": \"song\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"month\",\n            \"columnName\": \"month\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"count\",\n            \"columnName\": \"count\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"song\",\n            \"year\",\n            \"month\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"recognition_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `trackId` TEXT NOT NULL, `title` TEXT NOT NULL, `artist` TEXT NOT NULL, `album` TEXT, `coverArtUrl` TEXT, `coverArtHqUrl` TEXT, `genre` TEXT, `releaseDate` TEXT, `label` TEXT, `shazamUrl` TEXT, `appleMusicUrl` TEXT, `spotifyUrl` TEXT, `isrc` TEXT, `youtubeVideoId` TEXT, `recognizedAt` INTEGER NOT NULL, `liked` INTEGER NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"trackId\",\n            \"columnName\": \"trackId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artist\",\n            \"columnName\": \"artist\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"album\",\n            \"columnName\": \"album\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"coverArtUrl\",\n            \"columnName\": \"coverArtUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"coverArtHqUrl\",\n            \"columnName\": \"coverArtHqUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"genre\",\n            \"columnName\": \"genre\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"releaseDate\",\n            \"columnName\": \"releaseDate\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"label\",\n            \"columnName\": \"label\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"shazamUrl\",\n            \"columnName\": \"shazamUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"appleMusicUrl\",\n            \"columnName\": \"appleMusicUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"spotifyUrl\",\n            \"columnName\": \"spotifyUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"isrc\",\n            \"columnName\": \"isrc\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"youtubeVideoId\",\n            \"columnName\": \"youtubeVideoId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"recognizedAt\",\n            \"columnName\": \"recognizedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_recognition_history_trackId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"trackId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_recognition_history_trackId` ON `${TABLE_NAME}` (`trackId`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"speed_dial_item\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `secondaryId` TEXT, `title` TEXT NOT NULL, `subtitle` TEXT, `thumbnailUrl` TEXT, `type` TEXT NOT NULL, `explicit` INTEGER NOT NULL, `createDate` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"secondaryId\",\n            \"columnName\": \"secondaryId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subtitle\",\n            \"columnName\": \"subtitle\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"podcast\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT, `thumbnailUrl` TEXT, `channelId` TEXT, `bookmarkedAt` INTEGER, `lastUpdateTime` INTEGER NOT NULL, `libraryAddToken` TEXT, `libraryRemoveToken` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"author\",\n            \"columnName\": \"author\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"libraryAddToken\",\n            \"columnName\": \"libraryAddToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"libraryRemoveToken\",\n            \"columnName\": \"libraryRemoveToken\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '73924a5ef1b9fb713b5e197988a0c633')\"\n    ]\n  }\n}\n"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/36.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 36,\n    \"identityHash\": \"afcd734f45bc50034a6692f5255e7b92\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `explicit` INTEGER NOT NULL DEFAULT 0, `year` INTEGER, `date` INTEGER, `dateModified` INTEGER, `liked` INTEGER NOT NULL, `likedDate` INTEGER, `totalPlayTime` INTEGER NOT NULL, `inLibrary` INTEGER, `dateDownload` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `libraryAddToken` TEXT, `libraryRemoveToken` TEXT, `lyricsOffset` INTEGER NOT NULL DEFAULT 0, `romanizeLyrics` INTEGER NOT NULL DEFAULT true, `isDownloaded` INTEGER NOT NULL DEFAULT 0, `isUploaded` INTEGER NOT NULL DEFAULT false, `isVideo` INTEGER NOT NULL DEFAULT false, `isEpisode` INTEGER NOT NULL DEFAULT false, `playbackPosition` INTEGER DEFAULT NULL, `uploadEntityId` TEXT DEFAULT NULL, `isCached` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"date\",\n            \"columnName\": \"date\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateModified\",\n            \"columnName\": \"dateModified\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"dateDownload\",\n            \"columnName\": \"dateDownload\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"libraryAddToken\",\n            \"columnName\": \"libraryAddToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"libraryRemoveToken\",\n            \"columnName\": \"libraryRemoveToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lyricsOffset\",\n            \"columnName\": \"lyricsOffset\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"romanizeLyrics\",\n            \"columnName\": \"romanizeLyrics\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"isDownloaded\",\n            \"columnName\": \"isDownloaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isVideo\",\n            \"columnName\": \"isVideo\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isEpisode\",\n            \"columnName\": \"isEpisode\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"playbackPosition\",\n            \"columnName\": \"playbackPosition\",\n            \"affinity\": \"INTEGER\",\n            \"defaultValue\": \"NULL\"\n          },\n          {\n            \"fieldPath\": \"uploadEntityId\",\n            \"columnName\": \"uploadEntityId\",\n            \"affinity\": \"TEXT\",\n            \"defaultValue\": \"NULL\"\n          },\n          {\n            \"fieldPath\": \"isCached\",\n            \"columnName\": \"isCached\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `channelId` TEXT, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `isPodcastChannel` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isPodcastChannel\",\n            \"columnName\": \"isPodcastChannel\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `playlistId` TEXT, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `themeColor` INTEGER, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `explicit` INTEGER NOT NULL DEFAULT 0, `lastUpdateTime` INTEGER NOT NULL, `bookmarkedAt` INTEGER, `likedDate` INTEGER, `inLibrary` INTEGER, `isLocal` INTEGER NOT NULL DEFAULT false, `isUploaded` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"themeColor\",\n            \"columnName\": \"themeColor\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"0\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"likedDate\",\n            \"columnName\": \"likedDate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isUploaded\",\n            \"columnName\": \"isUploaded\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, `createdAt` INTEGER, `lastUpdateTime` INTEGER, `isEditable` INTEGER NOT NULL DEFAULT true, `bookmarkedAt` INTEGER, `remoteSongCount` INTEGER, `playEndpointParams` TEXT, `thumbnailUrl` TEXT, `shuffleEndpointParams` TEXT, `radioEndpointParams` TEXT, `isLocal` INTEGER NOT NULL DEFAULT false, `isAutoSync` INTEGER NOT NULL DEFAULT false, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"createdAt\",\n            \"columnName\": \"createdAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"isEditable\",\n            \"columnName\": \"isEditable\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"true\"\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"remoteSongCount\",\n            \"columnName\": \"remoteSongCount\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"playEndpointParams\",\n            \"columnName\": \"playEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"shuffleEndpointParams\",\n            \"columnName\": \"shuffleEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"radioEndpointParams\",\n            \"columnName\": \"radioEndpointParams\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"isLocal\",\n            \"columnName\": \"isLocal\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          },\n          {\n            \"fieldPath\": \"isAutoSync\",\n            \"columnName\": \"isAutoSync\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true,\n            \"defaultValue\": \"false\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, `setVideoId` TEXT, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, `perceptualLoudnessDb` REAL, `playbackUrl` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"perceptualLoudnessDb\",\n            \"columnName\": \"perceptualLoudnessDb\",\n            \"affinity\": \"REAL\"\n          },\n          {\n            \"fieldPath\": \"playbackUrl\",\n            \"columnName\": \"playbackUrl\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, `provider` TEXT NOT NULL DEFAULT 'Unknown', `translatedLyrics` TEXT NOT NULL DEFAULT '', `translationLanguage` TEXT NOT NULL DEFAULT '', `translationMode` TEXT NOT NULL DEFAULT '', PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"provider\",\n            \"columnName\": \"provider\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"'Unknown'\"\n          },\n          {\n            \"fieldPath\": \"translatedLyrics\",\n            \"columnName\": \"translatedLyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          },\n          {\n            \"fieldPath\": \"translationLanguage\",\n            \"columnName\": \"translationLanguage\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          },\n          {\n            \"fieldPath\": \"translationMode\",\n            \"columnName\": \"translationMode\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true,\n            \"defaultValue\": \"''\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"set_video_id\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`videoId` TEXT NOT NULL, `setVideoId` TEXT, PRIMARY KEY(`videoId`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"videoId\",\n            \"columnName\": \"videoId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"setVideoId\",\n            \"columnName\": \"setVideoId\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"videoId\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"playCount\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`song` TEXT NOT NULL, `year` INTEGER NOT NULL, `month` INTEGER NOT NULL, `count` INTEGER NOT NULL, PRIMARY KEY(`song`, `year`, `month`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"song\",\n            \"columnName\": \"song\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"month\",\n            \"columnName\": \"month\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"count\",\n            \"columnName\": \"count\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"song\",\n            \"year\",\n            \"month\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"recognition_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `trackId` TEXT NOT NULL, `title` TEXT NOT NULL, `artist` TEXT NOT NULL, `album` TEXT, `coverArtUrl` TEXT, `coverArtHqUrl` TEXT, `genre` TEXT, `releaseDate` TEXT, `label` TEXT, `shazamUrl` TEXT, `appleMusicUrl` TEXT, `spotifyUrl` TEXT, `isrc` TEXT, `youtubeVideoId` TEXT, `recognizedAt` INTEGER NOT NULL, `liked` INTEGER NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"trackId\",\n            \"columnName\": \"trackId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artist\",\n            \"columnName\": \"artist\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"album\",\n            \"columnName\": \"album\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"coverArtUrl\",\n            \"columnName\": \"coverArtUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"coverArtHqUrl\",\n            \"columnName\": \"coverArtHqUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"genre\",\n            \"columnName\": \"genre\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"releaseDate\",\n            \"columnName\": \"releaseDate\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"label\",\n            \"columnName\": \"label\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"shazamUrl\",\n            \"columnName\": \"shazamUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"appleMusicUrl\",\n            \"columnName\": \"appleMusicUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"spotifyUrl\",\n            \"columnName\": \"spotifyUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"isrc\",\n            \"columnName\": \"isrc\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"youtubeVideoId\",\n            \"columnName\": \"youtubeVideoId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"recognizedAt\",\n            \"columnName\": \"recognizedAt\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_recognition_history_trackId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"trackId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_recognition_history_trackId` ON `${TABLE_NAME}` (`trackId`)\"\n          }\n        ]\n      },\n      {\n        \"tableName\": \"speed_dial_item\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `secondaryId` TEXT, `title` TEXT NOT NULL, `subtitle` TEXT, `thumbnailUrl` TEXT, `type` TEXT NOT NULL, `explicit` INTEGER NOT NULL, `createDate` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"secondaryId\",\n            \"columnName\": \"secondaryId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"subtitle\",\n            \"columnName\": \"subtitle\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"type\",\n            \"columnName\": \"type\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"explicit\",\n            \"columnName\": \"explicit\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      },\n      {\n        \"tableName\": \"podcast\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT, `thumbnailUrl` TEXT, `channelId` TEXT, `bookmarkedAt` INTEGER, `lastUpdateTime` INTEGER NOT NULL, `libraryAddToken` TEXT, `libraryRemoveToken` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"author\",\n            \"columnName\": \"author\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"channelId\",\n            \"columnName\": \"channelId\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"bookmarkedAt\",\n            \"columnName\": \"bookmarkedAt\",\n            \"affinity\": \"INTEGER\"\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"libraryAddToken\",\n            \"columnName\": \"libraryAddToken\",\n            \"affinity\": \"TEXT\"\n          },\n          {\n            \"fieldPath\": \"libraryRemoveToken\",\n            \"columnName\": \"libraryRemoveToken\",\n            \"affinity\": \"TEXT\"\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        }\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'afcd734f45bc50034a6692f5255e7b92')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/4.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 4,\n    \"identityHash\": \"fe70b678dc51b8cad5fd1cb4eadbb95d\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `liked` INTEGER NOT NULL, `totalPlayTime` INTEGER NOT NULL, `isTrash` INTEGER NOT NULL, `download_state` INTEGER NOT NULL, `create_date` INTEGER NOT NULL, `modify_date` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isTrash\",\n            \"columnName\": \"isTrash\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"downloadState\",\n            \"columnName\": \"download_state\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"create_date\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"modifyDate\",\n            \"columnName\": \"modify_date\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `bannerUrl` TEXT, `description` TEXT, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"bannerUrl\",\n            \"columnName\": \"bannerUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"description\",\n            \"columnName\": \"description\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT, `authorId` TEXT, `year` INTEGER, `thumbnailUrl` TEXT, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"author\",\n            \"columnName\": \"author\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"authorId\",\n            \"columnName\": \"authorId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"download\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `songId` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fe70b678dc51b8cad5fd1cb4eadbb95d')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/5.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 5,\n    \"identityHash\": \"2ab124580a16b74c86883a1a06edae27\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `liked` INTEGER NOT NULL, `totalPlayTime` INTEGER NOT NULL, `isTrash` INTEGER NOT NULL, `download_state` INTEGER NOT NULL, `create_date` INTEGER NOT NULL, `modify_date` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"isTrash\",\n            \"columnName\": \"isTrash\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"downloadState\",\n            \"columnName\": \"download_state\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"create_date\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"modifyDate\",\n            \"columnName\": \"modify_date\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `bannerUrl` TEXT, `description` TEXT, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"bannerUrl\",\n            \"columnName\": \"bannerUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"description\",\n            \"columnName\": \"description\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT, `authorId` TEXT, `year` INTEGER, `thumbnailUrl` TEXT, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"author\",\n            \"columnName\": \"author\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"authorId\",\n            \"columnName\": \"authorId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"download\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `songId` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": true\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"columnNames\": [\n            \"id\"\n          ],\n          \"autoGenerate\": false\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2ab124580a16b74c86883a1a06edae27')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/6.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 6,\n    \"identityHash\": \"e099eec2e21e2def3fd2dc8b29798a02\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `liked` INTEGER NOT NULL, `totalPlayTime` INTEGER NOT NULL, `downloadState` INTEGER NOT NULL, `createDate` INTEGER NOT NULL, `modifyDate` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"downloadState\",\n            \"columnName\": \"downloadState\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"modifyDate\",\n            \"columnName\": \"modifyDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `bannerUrl` TEXT, `description` TEXT, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"bannerUrl\",\n            \"columnName\": \"bannerUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"description\",\n            \"columnName\": \"description\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"download\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `songId` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e099eec2e21e2def3fd2dc8b29798a02')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/7.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 7,\n    \"identityHash\": \"8badff35bb8509366509650a5b15634a\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `liked` INTEGER NOT NULL, `totalPlayTime` INTEGER NOT NULL, `downloadState` INTEGER NOT NULL, `inLibrary` INTEGER, `createDate` INTEGER NOT NULL, `modifyDate` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"downloadState\",\n            \"columnName\": \"downloadState\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"modifyDate\",\n            \"columnName\": \"modifyDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `bannerUrl` TEXT, `description` TEXT, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"bannerUrl\",\n            \"columnName\": \"bannerUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"description\",\n            \"columnName\": \"description\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"download\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `songId` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8badff35bb8509366509650a5b15634a')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/8.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 8,\n    \"identityHash\": \"8de04c586d6be08319c8fab4240706ff\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `liked` INTEGER NOT NULL, `totalPlayTime` INTEGER NOT NULL, `downloadState` INTEGER NOT NULL, `inLibrary` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"downloadState\",\n            \"columnName\": \"downloadState\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `bannerUrl` TEXT, `description` TEXT, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"bannerUrl\",\n            \"columnName\": \"bannerUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"description\",\n            \"columnName\": \"description\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"download\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `songId` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8de04c586d6be08319c8fab4240706ff')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/schemas/com.metrolist.music.db.InternalDatabase/9.json",
    "content": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 9,\n    \"identityHash\": \"ccad10efd9b5c5ee1dc9b42c6e3715fd\",\n    \"entities\": [\n      {\n        \"tableName\": \"song\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `liked` INTEGER NOT NULL, `totalPlayTime` INTEGER NOT NULL, `downloadState` INTEGER NOT NULL, `inLibrary` INTEGER, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"albumName\",\n            \"columnName\": \"albumName\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"liked\",\n            \"columnName\": \"liked\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"totalPlayTime\",\n            \"columnName\": \"totalPlayTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"downloadState\",\n            \"columnName\": \"downloadState\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"inLibrary\",\n            \"columnName\": \"inLibrary\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"artist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `bannerUrl` TEXT, `description` TEXT, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"bannerUrl\",\n            \"columnName\": \"bannerUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"description\",\n            \"columnName\": \"description\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"album\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"title\",\n            \"columnName\": \"title\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"year\",\n            \"columnName\": \"year\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"thumbnailUrl\",\n            \"columnName\": \"thumbnailUrl\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"songCount\",\n            \"columnName\": \"songCount\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"duration\",\n            \"columnName\": \"duration\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"createDate\",\n            \"columnName\": \"createDate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lastUpdateTime\",\n            \"columnName\": \"lastUpdateTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"playlist\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"name\",\n            \"columnName\": \"name\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"browseId\",\n            \"columnName\": \"browseId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"song_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_artist_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"song_album_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"index\",\n            \"columnName\": \"index\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"songId\",\n            \"albumId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_song_album_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_song_album_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"album_artist_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"albumId\",\n            \"columnName\": \"albumId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"artistId\",\n            \"columnName\": \"artistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"order\",\n            \"columnName\": \"order\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"albumId\",\n            \"artistId\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_album_artist_map_albumId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"albumId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `${TABLE_NAME}` (`albumId`)\"\n          },\n          {\n            \"name\": \"index_album_artist_map_artistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"artistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `${TABLE_NAME}` (`artistId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"album\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"albumId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"artist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"artistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"playlist_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playlistId\",\n            \"columnName\": \"playlistId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"position\",\n            \"columnName\": \"position\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_playlist_song_map_playlistId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"playlistId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `${TABLE_NAME}` (`playlistId`)\"\n          },\n          {\n            \"name\": \"index_playlist_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"playlist\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"playlistId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"download\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `songId` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"search_history\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"query\",\n            \"columnName\": \"query\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_search_history_query\",\n            \"unique\": true,\n            \"columnNames\": [\n              \"query\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `${TABLE_NAME}` (`query`)\"\n          }\n        ],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"format\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `itag` INTEGER NOT NULL, `mimeType` TEXT NOT NULL, `codecs` TEXT NOT NULL, `bitrate` INTEGER NOT NULL, `sampleRate` INTEGER, `contentLength` INTEGER NOT NULL, `loudnessDb` REAL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"itag\",\n            \"columnName\": \"itag\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"mimeType\",\n            \"columnName\": \"mimeType\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"codecs\",\n            \"columnName\": \"codecs\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"bitrate\",\n            \"columnName\": \"bitrate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"sampleRate\",\n            \"columnName\": \"sampleRate\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": false\n          },\n          {\n            \"fieldPath\": \"contentLength\",\n            \"columnName\": \"contentLength\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"loudnessDb\",\n            \"columnName\": \"loudnessDb\",\n            \"affinity\": \"REAL\",\n            \"notNull\": false\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"lyrics\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `lyrics` TEXT NOT NULL, PRIMARY KEY(`id`))\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"lyrics\",\n            \"columnName\": \"lyrics\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": false,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [],\n        \"foreignKeys\": []\n      },\n      {\n        \"tableName\": \"event\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"timestamp\",\n            \"columnName\": \"timestamp\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"playTime\",\n            \"columnName\": \"playTime\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_event_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_event_songId` ON `${TABLE_NAME}` (`songId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      },\n      {\n        \"tableName\": \"related_song_map\",\n        \"createSql\": \"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `relatedSongId` TEXT NOT NULL, FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`relatedSongId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n        \"fields\": [\n          {\n            \"fieldPath\": \"id\",\n            \"columnName\": \"id\",\n            \"affinity\": \"INTEGER\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"songId\",\n            \"columnName\": \"songId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          },\n          {\n            \"fieldPath\": \"relatedSongId\",\n            \"columnName\": \"relatedSongId\",\n            \"affinity\": \"TEXT\",\n            \"notNull\": true\n          }\n        ],\n        \"primaryKey\": {\n          \"autoGenerate\": true,\n          \"columnNames\": [\n            \"id\"\n          ]\n        },\n        \"indices\": [\n          {\n            \"name\": \"index_related_song_map_songId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"songId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_songId` ON `${TABLE_NAME}` (`songId`)\"\n          },\n          {\n            \"name\": \"index_related_song_map_relatedSongId\",\n            \"unique\": false,\n            \"columnNames\": [\n              \"relatedSongId\"\n            ],\n            \"orders\": [],\n            \"createSql\": \"CREATE INDEX IF NOT EXISTS `index_related_song_map_relatedSongId` ON `${TABLE_NAME}` (`relatedSongId`)\"\n          }\n        ],\n        \"foreignKeys\": [\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"songId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          },\n          {\n            \"table\": \"song\",\n            \"onDelete\": \"CASCADE\",\n            \"onUpdate\": \"NO ACTION\",\n            \"columns\": [\n              \"relatedSongId\"\n            ],\n            \"referencedColumns\": [\n              \"id\"\n            ]\n          }\n        ]\n      }\n    ],\n    \"views\": [\n      {\n        \"viewName\": \"sorted_song_artist_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_artist_map ORDER BY position\"\n      },\n      {\n        \"viewName\": \"sorted_song_album_map\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM song_album_map ORDER BY `index`\"\n      },\n      {\n        \"viewName\": \"playlist_song_map_preview\",\n        \"createSql\": \"CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\"\n      }\n    ],\n    \"setupQueries\": [\n      \"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)\",\n      \"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ccad10efd9b5c5ee1dc9b42c6e3715fd')\"\n    ]\n  }\n}"
  },
  {
    "path": "app/src/debug/res/xml/shortcuts.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!--suppress AndroidDomInspection -->\n<shortcuts xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <shortcut\n        android:enabled=\"true\"\n        android:icon=\"@drawable/shortcut_search\"\n        android:shortcutId=\"search\"\n        android:shortcutShortLabel=\"@string/search\">\n        <intent\n            android:action=\"com.metrolist.music.action.SEARCH\"\n            android:targetClass=\"com.metrolist.music.MainActivity\"\n            android:targetPackage=\"com.metrolist.music.debug\" />\n    </shortcut>\n    <shortcut\n        android:enabled=\"true\"\n        android:icon=\"@drawable/shortcut_library\"\n        android:shortcutId=\"library\"\n        android:shortcutShortLabel=\"@string/filter_library\">\n        <intent\n            android:action=\"com.metrolist.music.action.LIBRARY\"\n            android:targetClass=\"com.metrolist.music.MainActivity\"\n            android:targetPackage=\"com.metrolist.music.debug\" />\n    </shortcut>\n</shortcuts>\n"
  },
  {
    "path": "app/src/foss/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n    \n    <application>\n        <!-- Remove Cast provider in FOSS build since GMS is not available -->\n        <meta-data\n            android:name=\"com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME\"\n            tools:node=\"remove\" />\n    </application>\n</manifest>\n"
  },
  {
    "path": "app/src/foss/kotlin/com/metrolist/music/cast/CastOptionsProvider.kt",
    "content": "package com.metrolist.music.cast\n\n/**\n * Stub CastOptionsProvider for F-Droid builds.\n * The AndroidManifest reference is removed via manifest merger.\n */\nclass CastOptionsProvider\n"
  },
  {
    "path": "app/src/foss/kotlin/com/metrolist/music/playback/CastConnectionHandler.kt",
    "content": "package com.metrolist.music.playback\n\nimport android.content.Context\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\n/**\n * Stub CastConnectionHandler for F-Droid builds.\n * Cast functionality is not available without Google Play Services.\n */\nclass CastConnectionHandler(\n    context: Context,\n    scope: CoroutineScope,\n    musicService: MusicService\n) {\n    private val _isCasting = MutableStateFlow(false)\n    val isCasting: StateFlow<Boolean> = _isCasting\n    \n    private val _isConnecting = MutableStateFlow(false)\n    val isConnecting: StateFlow<Boolean> = _isConnecting\n    \n    private val _castDeviceName = MutableStateFlow<String?>(null)\n    val castDeviceName: StateFlow<String?> = _castDeviceName\n    \n    private val _castPosition = MutableStateFlow(0L)\n    val castPosition: StateFlow<Long> = _castPosition\n    \n    private val _castDuration = MutableStateFlow(0L)\n    val castDuration: StateFlow<Long> = _castDuration\n    \n    private val _castIsPlaying = MutableStateFlow(false)\n    val castIsPlaying: StateFlow<Boolean> = _castIsPlaying\n    \n    private val _castIsBuffering = MutableStateFlow(false)\n    val castIsBuffering: StateFlow<Boolean> = _castIsBuffering\n    \n    private val _castVolume = MutableStateFlow(1.0f)\n    val castVolume: StateFlow<Float> = _castVolume\n    \n    var isSyncingFromCast: Boolean = false\n        private set\n    \n    fun initialize(): Boolean = false\n    fun disconnect() {}\n    fun loadCurrentMedia() {}\n    fun loadMedia(metadata: com.metrolist.music.models.MediaMetadata) {}\n    fun play() {}\n    fun pause() {}\n    fun seekTo(position: Long) {}\n    fun setVolume(volume: Float) {}\n    fun skipToNext() {}\n    fun skipToPrevious() {}\n    fun navigateToMediaIfInQueue(mediaId: String): Boolean = false\n    fun release() {}\n}\n"
  },
  {
    "path": "app/src/foss/kotlin/com/metrolist/music/ui/component/CastButton.kt",
    "content": "package com.metrolist.music.ui.component\n\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\n\n/**\n * Stub CastButton for F-Droid builds.\n * Does not render anything - Cast not available without GMS.\n */\n@Composable\nfun CastButton(\n    modifier: Modifier = Modifier,\n    tintColor: Color = MaterialTheme.colorScheme.onSurface,\n) {\n    // No-op: Cast not available in FOSS build\n}\n"
  },
  {
    "path": "app/src/gms/kotlin/com/metrolist/music/cast/CastManager.kt",
    "content": "package com.metrolist.music.cast\n\nimport android.content.Context\nimport androidx.media3.cast.CastPlayer\nimport androidx.media3.cast.SessionAvailabilityListener\nimport androidx.media3.common.MediaItem\nimport androidx.media3.common.Player\nimport com.google.android.gms.cast.framework.CastContext\nimport com.google.android.gms.cast.framework.CastState\nimport com.google.android.gms.cast.framework.CastStateListener\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport timber.log.Timber\n\n/**\n * Manages Google Cast integration for the music player.\n * Handles switching between local ExoPlayer and remote CastPlayer.\n */\nclass CastManager(\n    private val context: Context\n) : SessionAvailabilityListener, CastStateListener {\n\n    private var castContext: CastContext? = null\n    private var castPlayer: CastPlayer? = null\n\n    private val _isCasting = MutableStateFlow(false)\n    val isCasting: StateFlow<Boolean> = _isCasting.asStateFlow()\n\n    private val _castState = MutableStateFlow(CastState.NO_DEVICES_AVAILABLE)\n    val castState: StateFlow<Int> = _castState.asStateFlow()\n\n    private var onCastSessionStarted: ((CastPlayer) -> Unit)? = null\n    private var onCastSessionEnded: (() -> Unit)? = null\n\n    /**\n     * Initialize the Cast context. Should be called when the activity is created.\n     * This is safe to call even if Google Play Services is not available.\n     */\n    @Suppress(\"DEPRECATION\")\n    fun initialize() {\n        try {\n            castContext = CastContext.getSharedInstance(context)\n            castContext?.addCastStateListener(this)\n            \n            // Using deprecated constructor and setSessionAvailabilityListener as the new\n            // CastPlayer.Builder API requires a local player which we don't use in this architecture\n            castPlayer = CastPlayer(castContext!!)\n            castPlayer?.setSessionAvailabilityListener(this)\n            \n            _castState.value = castContext?.castState ?: CastState.NO_DEVICES_AVAILABLE\n            \n            Timber.d(\"CastManager initialized successfully\")\n        } catch (e: Exception) {\n            Timber.e(e, \"Failed to initialize CastManager - Cast may not be available on this device\")\n            castContext = null\n            castPlayer = null\n        }\n    }\n\n    /**\n     * Set callbacks for cast session events.\n     */\n    fun setSessionCallbacks(\n        onStarted: (CastPlayer) -> Unit,\n        onEnded: () -> Unit\n    ) {\n        onCastSessionStarted = onStarted\n        onCastSessionEnded = onEnded\n    }\n\n    /**\n     * Get the CastPlayer instance if available.\n     */\n    fun getCastPlayer(): CastPlayer? = castPlayer\n\n    /**\n     * Check if casting is currently active.\n     */\n    @Suppress(\"DEPRECATION\")\n    fun isCastSessionAvailable(): Boolean = castPlayer?.isCastSessionAvailable == true\n\n    /**\n     * Get the current playback position from the cast player.\n     */\n    fun getCurrentPosition(): Long = castPlayer?.currentPosition ?: 0\n\n    /**\n     * Get whether the cast player is currently playing.\n     */\n    fun isPlaying(): Boolean = castPlayer?.isPlaying == true\n\n    /**\n     * Load media items into the cast player.\n     */\n    fun loadMediaItems(\n        mediaItems: List<MediaItem>,\n        startIndex: Int = 0,\n        startPositionMs: Long = 0\n    ) {\n        castPlayer?.let { player ->\n            player.setMediaItems(mediaItems, startIndex, startPositionMs)\n            player.prepare()\n            player.play()\n        }\n    }\n\n    /**\n     * Add a listener to the cast player.\n     */\n    fun addListener(listener: Player.Listener) {\n        castPlayer?.addListener(listener)\n    }\n\n    /**\n     * Remove a listener from the cast player.\n     */\n    fun removeListener(listener: Player.Listener) {\n        castPlayer?.removeListener(listener)\n    }\n\n    override fun onCastStateChanged(state: Int) {\n        _castState.value = state\n        Timber.d(\"Cast state changed: $state\")\n    }\n\n    override fun onCastSessionAvailable() {\n        _isCasting.value = true\n        castPlayer?.let { player ->\n            onCastSessionStarted?.invoke(player)\n        }\n        Timber.d(\"Cast session available\")\n    }\n\n    override fun onCastSessionUnavailable() {\n        _isCasting.value = false\n        onCastSessionEnded?.invoke()\n        Timber.d(\"Cast session unavailable\")\n    }\n\n    /**\n     * Release resources. Should be called when the service is destroyed.\n     */\n    @Suppress(\"DEPRECATION\")\n    fun release() {\n        castContext?.removeCastStateListener(this)\n        castPlayer?.setSessionAvailabilityListener(null)\n        castPlayer?.release()\n        castPlayer = null\n        castContext = null\n    }\n\n    companion object {\n        /**\n         * Check if Cast is available on this device.\n         */\n        fun isCastAvailable(context: Context): Boolean {\n            return try {\n                CastContext.getSharedInstance(context)\n                true\n            } catch (e: Exception) {\n                Timber.d(\"Cast not available: ${e.message}\")\n                false\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/gms/kotlin/com/metrolist/music/cast/CastOptionsProvider.kt",
    "content": "package com.metrolist.music.cast\n\nimport android.content.Context\nimport com.google.android.gms.cast.CastMediaControlIntent\nimport com.google.android.gms.cast.framework.CastOptions\nimport com.google.android.gms.cast.framework.OptionsProvider\nimport com.google.android.gms.cast.framework.SessionProvider\nimport com.google.android.gms.cast.framework.media.CastMediaOptions\nimport com.google.android.gms.cast.framework.media.MediaIntentReceiver\nimport com.google.android.gms.cast.framework.media.NotificationOptions\n\n/**\n * CastOptionsProvider for Google Cast integration.\n * This class provides the Cast options for the app.\n */\nclass CastOptionsProvider : OptionsProvider {\n\n    override fun getCastOptions(context: Context): CastOptions {\n        val notificationOptions = NotificationOptions.Builder()\n            .setActions(\n                listOf(\n                    MediaIntentReceiver.ACTION_SKIP_PREV,\n                    MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK,\n                    MediaIntentReceiver.ACTION_SKIP_NEXT,\n                    MediaIntentReceiver.ACTION_STOP_CASTING\n                ),\n                intArrayOf(1, 2) // Indices of actions for compact view\n            )\n            .build()\n\n        val mediaOptions = CastMediaOptions.Builder()\n            .setNotificationOptions(notificationOptions)\n            .build()\n\n        return CastOptions.Builder()\n            .setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)\n            .setCastMediaOptions(mediaOptions)\n            .setStopReceiverApplicationWhenEndingSession(true)\n            .build()\n    }\n\n    override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {\n        return null\n    }\n}\n"
  },
  {
    "path": "app/src/gms/kotlin/com/metrolist/music/playback/CastConnectionHandler.kt",
    "content": "package com.metrolist.music.playback\n\nimport android.content.Context\nimport android.net.Uri\nimport androidx.media3.common.Player\nimport androidx.mediarouter.media.MediaRouteSelector\nimport androidx.mediarouter.media.MediaRouter\nimport com.google.android.gms.cast.CastMediaControlIntent\nimport com.google.android.gms.cast.MediaInfo\nimport com.google.android.gms.cast.MediaLoadRequestData\nimport com.google.android.gms.cast.MediaMetadata\nimport com.google.android.gms.cast.MediaQueueItem\nimport com.google.android.gms.cast.MediaSeekOptions\nimport com.google.android.gms.cast.MediaStatus\nimport com.google.android.gms.cast.framework.CastContext\nimport com.google.android.gms.cast.framework.CastSession\nimport com.google.android.gms.cast.framework.SessionManager\nimport com.google.android.gms.cast.framework.SessionManagerListener\nimport com.google.android.gms.cast.framework.media.RemoteMediaClient\nimport com.google.android.gms.common.images.WebImage\nimport com.metrolist.music.extensions.metadata\nimport com.metrolist.music.models.MediaMetadata as AppMediaMetadata\nimport com.metrolist.music.ui.utils.resize\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport timber.log.Timber\n\n/**\n * Manages Google Cast connections and media playback on Cast devices.\n * This class handles the entire Cast lifecycle including:\n * - Device discovery\n * - Session management\n * - Media loading and playback control\n * - Synchronization between local and remote playback\n */\nclass CastConnectionHandler(\n    private val context: Context,\n    private val scope: CoroutineScope,\n    private val musicService: MusicService\n) {\n    private var castContext: CastContext? = null\n    private var sessionManager: SessionManager? = null\n    private var mediaRouter: MediaRouter? = null\n    private var routeSelector: MediaRouteSelector? = null\n    private var remoteMediaClient: RemoteMediaClient? = null\n    private var castSession: CastSession? = null\n    \n    private val _isCasting = MutableStateFlow(false)\n    val isCasting: StateFlow<Boolean> = _isCasting.asStateFlow()\n    \n    private val _isConnecting = MutableStateFlow(false)\n    val isConnecting: StateFlow<Boolean> = _isConnecting.asStateFlow()\n    \n    private val _castDeviceName = MutableStateFlow<String?>(null)\n    val castDeviceName: StateFlow<String?> = _castDeviceName.asStateFlow()\n    \n    private val _castPosition = MutableStateFlow(0L)\n    val castPosition: StateFlow<Long> = _castPosition.asStateFlow()\n    \n    private val _castDuration = MutableStateFlow(0L)\n    val castDuration: StateFlow<Long> = _castDuration.asStateFlow()\n    \n    private val _castIsPlaying = MutableStateFlow(false)\n    val castIsPlaying: StateFlow<Boolean> = _castIsPlaying.asStateFlow()\n    \n    private val _castIsBuffering = MutableStateFlow(false)\n    val castIsBuffering: StateFlow<Boolean> = _castIsBuffering.asStateFlow()\n    \n    private val _castVolume = MutableStateFlow(1.0f)\n    val castVolume: StateFlow<Float> = _castVolume.asStateFlow()\n    \n    private var positionUpdateJob: Job? = null\n    private var currentMediaId: String? = null\n    private var lastCastItemId: Int = -1\n    private var isReloadingQueue: Boolean = false\n    \n    // Flag to prevent reverse sync when Cast triggers local player update\n    var isSyncingFromCast: Boolean = false\n        private set\n    \n    private val remoteMediaClientCallback = object : RemoteMediaClient.Callback() {\n        override fun onStatusUpdated() {\n            remoteMediaClient?.let { client ->\n                val mediaStatus = client.mediaStatus\n                val playerState = mediaStatus?.playerState\n                // Show as \"playing\" when playing OR buffering/loading (so pause icon shows during buffering)\n                _castIsPlaying.value = playerState == MediaStatus.PLAYER_STATE_PLAYING ||\n                                       playerState == MediaStatus.PLAYER_STATE_BUFFERING ||\n                                       playerState == MediaStatus.PLAYER_STATE_LOADING\n                _castIsBuffering.value = playerState == MediaStatus.PLAYER_STATE_BUFFERING || \n                                         playerState == MediaStatus.PLAYER_STATE_LOADING\n                _castDuration.value = client.streamDuration\n                \n                // Check if the current Cast item changed (user skipped on Cast widget)\n                val currentItemId = mediaStatus?.currentItemId ?: -1\n                if (currentItemId != -1 && currentItemId != lastCastItemId && lastCastItemId != -1 && !isReloadingQueue && mediaStatus != null) {\n                    Timber.d(\"Cast item changed: $lastCastItemId -> $currentItemId\")\n                    handleCastItemChanged(mediaStatus)\n                }\n                lastCastItemId = currentItemId\n                \n                Timber.d(\"Cast status updated: playing=${_castIsPlaying.value}, buffering=${_castIsBuffering.value}, itemId=$currentItemId\")\n            }\n        }\n        \n        override fun onMediaError(error: com.google.android.gms.cast.MediaError) {\n            Timber.e(\"Cast media error: ${error.reason}\")\n        }\n        \n        override fun onQueueStatusUpdated() {\n            Timber.d(\"Cast queue status updated\")\n        }\n    }\n    \n    // Job for resetting sync flag\n    private var syncResetJob: Job? = null\n    \n    /**\n     * Handle when Cast changes to a different item (user pressed next/prev on Cast widget)\n     * This syncs the local player - we don't reload the queue since the item is already loaded\n     */\n    private fun handleCastItemChanged(mediaStatus: MediaStatus) {\n        val queueItems = mediaStatus.queueItems\n        if (queueItems.isEmpty()) return\n        val currentItemId = mediaStatus.currentItemId\n        val currentIndex = queueItems.indexOfFirst { it.itemId == currentItemId }\n        \n        if (currentIndex < 0) return\n        \n        // Get the mediaId from the current Cast item's custom data\n        val currentQueueItem = queueItems[currentIndex]\n        val customData = currentQueueItem.media?.customData\n        val castMediaId = customData?.optString(\"mediaId\")\n        \n        Timber.d(\"Cast switched to item: index=$currentIndex, mediaId=$castMediaId, queueSize=${queueItems.size}\")\n        \n        if (castMediaId != null && castMediaId != currentMediaId) {\n            currentMediaId = castMediaId\n            \n            // Cancel any pending sync reset\n            syncResetJob?.cancel()\n            \n            // Set flag immediately to prevent reverse sync\n            isSyncingFromCast = true\n            \n            // Find this song in the local player queue and switch to it\n            val player = musicService.player\n            val playerItemCount = player.mediaItemCount\n            \n            // Find the matching item in local player\n            for (i in 0 until playerItemCount) {\n                val mediaItem = player.getMediaItemAt(i)\n                if (mediaItem.mediaId == castMediaId) {\n                    Timber.d(\"Syncing local player to index $i (mediaId=$castMediaId)\")\n                    \n                    // Ensure local player is paused before seeking\n                    player.pause()\n                    \n                    // Move local player to match Cast (just for metadata sync)\n                    player.seekTo(i, 0)\n                    \n                    // Make absolutely sure local player stays paused\n                    player.pause()\n                    \n                    // Extend queue if needed (in background)\n                    val itemsAhead = queueItems.size - 1 - currentIndex\n                    val itemsBehind = currentIndex\n                    \n                    if (itemsAhead < 2 || itemsBehind < 2) {\n                        scope.launch {\n                            val metadata = mediaItem.metadata\n                            if (metadata != null) {\n                                extendQueueIfNeeded(i, playerItemCount, queueItems)\n                            }\n                        }\n                    }\n                    break\n                }\n            }\n            \n            // Reset flag after a short delay\n            syncResetJob = scope.launch {\n                delay(300)\n                isSyncingFromCast = false\n            }\n        }\n    }\n    \n    /**\n     * Extend the Cast queue by adding more items at the edges if needed\n     * This avoids a full queue reload which causes the widget to refresh\n     */\n    private suspend fun extendQueueIfNeeded(localPlayerIndex: Int, playerItemCount: Int, currentCastQueue: List<MediaQueueItem>) {\n        if (isReloadingQueue) return\n        \n        val client = remoteMediaClient ?: return\n        val currentCastIndex = currentCastQueue.indexOfFirst { \n            it.media?.customData?.optString(\"mediaId\") == currentMediaId \n        }\n        if (currentCastIndex < 0) return\n        \n        isReloadingQueue = true\n        \n        try {\n            // Add more items to the end of queue if needed\n            val itemsAhead = currentCastQueue.size - 1 - currentCastIndex\n            if (itemsAhead < 2) {\n                // Find what songs we need to add\n                val lastCastItem = currentCastQueue.lastOrNull()\n                val lastMediaId = lastCastItem?.media?.customData?.optString(\"mediaId\")\n                \n                // Find the index of the last Cast item in local player\n                var lastLocalIndex = -1\n                for (i in 0 until playerItemCount) {\n                    if (musicService.player.getMediaItemAt(i).mediaId == lastMediaId) {\n                        lastLocalIndex = i\n                        break\n                    }\n                }\n                \n                // Add next items from local player\n                if (lastLocalIndex >= 0 && lastLocalIndex < playerItemCount - 1) {\n                    val itemsToAdd = mutableListOf<MediaQueueItem>()\n                    val addCount = minOf(2, playerItemCount - lastLocalIndex - 1)\n                    \n                    for (i in 1..addCount) {\n                        val nextItem = musicService.player.getMediaItemAt(lastLocalIndex + i)\n                        nextItem.metadata?.let { metadata ->\n                            buildMediaInfo(metadata)?.let { mediaInfo ->\n                                itemsToAdd.add(MediaQueueItem.Builder(mediaInfo).build())\n                            }\n                        }\n                    }\n                    \n                    if (itemsToAdd.isNotEmpty()) {\n                        Timber.d(\"Appending ${itemsToAdd.size} items to Cast queue\")\n                        withContext(Dispatchers.Main) {\n                            client.queueAppendItem(itemsToAdd.first(), null)\n                        }\n                    }\n                }\n            }\n        } catch (e: Exception) {\n            Timber.e(e, \"Failed to extend Cast queue\")\n        } finally {\n            delay(500)\n            isReloadingQueue = false\n        }\n    }\n    \n    /**\n     * Reload the Cast queue centered on the current item\n     * This updates prev/next context after a skip\n     * Respects shuffle mode when determining prev/next items\n     */\n    private fun reloadQueueForCurrentItem(metadata: AppMediaMetadata) {\n        if (!_isCasting.value || isReloadingQueue) return\n        \n        isReloadingQueue = true\n        scope.launch {\n            try {\n                val player = musicService.player\n                val currentIndex = player.currentMediaItemIndex\n                val shuffleEnabled = player.shuffleModeEnabled\n                val timeline = player.currentTimeline\n                \n                // Build new queue items: up to 2 previous, current, and up to 2 next\n                val queueItems = mutableListOf<MediaQueueItem>()\n                \n                // Get previous items respecting shuffle order\n                val prevItems = mutableListOf<androidx.media3.common.MediaItem>()\n                if (!timeline.isEmpty) {\n                    var prevIdx = currentIndex\n                    for (i in 0 until 2) {\n                        prevIdx = timeline.getPreviousWindowIndex(prevIdx, Player.REPEAT_MODE_OFF, shuffleEnabled)\n                        if (prevIdx == androidx.media3.common.C.INDEX_UNSET) break\n                        prevItems.add(0, player.getMediaItemAt(prevIdx))\n                    }\n                }\n                \n                // Add previous items\n                for (prevItem in prevItems) {\n                    prevItem.metadata?.let { prevMetadata ->\n                        buildMediaInfo(prevMetadata)?.let { mediaInfo ->\n                            queueItems.add(MediaQueueItem.Builder(mediaInfo).build())\n                        }\n                    }\n                }\n                val startIndex = queueItems.size // Current item index after previous items\n                \n                // Add current item\n                val currentMediaInfo = buildMediaInfo(metadata)\n                if (currentMediaInfo != null) {\n                    queueItems.add(MediaQueueItem.Builder(currentMediaInfo).build())\n                }\n                \n                // Get next items respecting shuffle order\n                if (!timeline.isEmpty) {\n                    var nextIdx = currentIndex\n                    for (i in 0 until 2) {\n                        nextIdx = timeline.getNextWindowIndex(nextIdx, Player.REPEAT_MODE_OFF, shuffleEnabled)\n                        if (nextIdx == androidx.media3.common.C.INDEX_UNSET) break\n                        val nextItem = player.getMediaItemAt(nextIdx)\n                        nextItem.metadata?.let { nextMetadata ->\n                            buildMediaInfo(nextMetadata)?.let { mediaInfo ->\n                                queueItems.add(MediaQueueItem.Builder(mediaInfo).build())\n                            }\n                        }\n                    }\n                }\n                \n                if (queueItems.isNotEmpty()) {\n                    Timber.d(\"Reloading Cast queue: ${queueItems.size} items, startIndex=$startIndex, shuffle=$shuffleEnabled\")\n                    \n                    withContext(Dispatchers.Main) {\n                        remoteMediaClient?.queueLoad(\n                            queueItems.toTypedArray(),\n                            startIndex,\n                            MediaStatus.REPEAT_MODE_REPEAT_OFF,\n                            0L, // Start from beginning since Cast already has position\n                            org.json.JSONObject()\n                        )\n                    }\n                }\n            } catch (e: Exception) {\n                Timber.e(e, \"Failed to reload Cast queue\")\n            } finally {\n                // Delay before allowing another reload to prevent rapid reloads\n                delay(1000)\n                isReloadingQueue = false\n            }\n        }\n    }\n    \n    private val sessionManagerListener = object : SessionManagerListener<CastSession> {\n        override fun onSessionStarting(session: CastSession) {\n            Timber.d(\"Cast session starting\")\n            _isConnecting.value = true\n        }\n        \n        override fun onSessionStarted(session: CastSession, sessionId: String) {\n            Timber.d(\"Cast session started: $sessionId\")\n            _isCasting.value = true\n            _isConnecting.value = false\n            _castDeviceName.value = session.castDevice?.friendlyName\n            castSession = session\n            remoteMediaClient = session.remoteMediaClient\n            remoteMediaClient?.registerCallback(remoteMediaClientCallback)\n            \n            // Get initial volume\n            _castVolume.value = session.volume.toFloat()\n            \n            // Start position updates\n            startPositionUpdates()\n            \n            // Load current media\n            loadCurrentMedia()\n        }\n        \n        override fun onSessionStartFailed(session: CastSession, error: Int) {\n            Timber.e(\"Cast session start failed: $error\")\n            _isCasting.value = false\n            _isConnecting.value = false\n        }\n        \n        override fun onSessionEnding(session: CastSession) {\n            Timber.d(\"Cast session ending\")\n            // Capture Cast position before session ends\n            val castPosition = remoteMediaClient?.approximateStreamPosition ?: _castPosition.value\n            if (castPosition > 0) {\n                // Seek local player to Cast position so playback can continue from there\n                musicService.player.seekTo(castPosition)\n                Timber.d(\"Saved Cast position: $castPosition\")\n            }\n        }\n        \n        override fun onSessionEnded(session: CastSession, error: Int) {\n            Timber.d(\"Cast session ended: error=$error\")\n            _isCasting.value = false\n            _isConnecting.value = false\n            _castDeviceName.value = null\n            castSession = null\n            \n            remoteMediaClient?.unregisterCallback(remoteMediaClientCallback)\n            remoteMediaClient = null\n            \n            stopPositionUpdates()\n            \n            // Pause local playback when disconnecting from Cast\n            musicService.player.pause()\n        }\n        \n        override fun onSessionResuming(session: CastSession, sessionId: String) {\n            _isConnecting.value = true\n        }\n        \n        override fun onSessionResumed(session: CastSession, wasSuspended: Boolean) {\n            _isCasting.value = true\n            _isConnecting.value = false\n            _castDeviceName.value = session.castDevice?.friendlyName\n            \n            remoteMediaClient = session.remoteMediaClient\n            remoteMediaClient?.registerCallback(remoteMediaClientCallback)\n            \n            startPositionUpdates()\n        }\n        \n        override fun onSessionResumeFailed(session: CastSession, error: Int) {\n            _isConnecting.value = false\n        }\n        \n        override fun onSessionSuspended(session: CastSession, reason: Int) {}\n    }\n    \n    fun initialize(): Boolean {\n        return try {\n            castContext = CastContext.getSharedInstance(context)\n            sessionManager = castContext?.sessionManager\n            mediaRouter = MediaRouter.getInstance(context)\n            routeSelector = MediaRouteSelector.Builder()\n                .addControlCategory(CastMediaControlIntent.categoryForCast(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID))\n                .build()\n            \n            sessionManager?.addSessionManagerListener(sessionManagerListener, CastSession::class.java)\n            \n            // Check if already connected\n            sessionManager?.currentCastSession?.let { session ->\n                _isCasting.value = true\n                _castDeviceName.value = session.castDevice?.friendlyName\n                remoteMediaClient = session.remoteMediaClient\n                remoteMediaClient?.registerCallback(remoteMediaClientCallback)\n                startPositionUpdates()\n            }\n            \n            true\n        } catch (e: Exception) {\n            Timber.e(e, \"Failed to initialize Cast\")\n            false\n        }\n    }\n    \n    fun getAvailableRoutes(): List<MediaRouter.RouteInfo> {\n        val router = mediaRouter ?: return emptyList()\n        val selector = routeSelector ?: return emptyList()\n        \n        return router.routes.filter { route ->\n            route.matchesSelector(selector) && !route.isDefault\n        }\n    }\n    \n    fun connectToRoute(route: MediaRouter.RouteInfo) {\n        // Ensure we're initialized before trying to connect\n        if (mediaRouter == null) {\n            initialize()\n        }\n        _isConnecting.value = true\n        mediaRouter?.selectRoute(route)\n    }\n    \n    fun disconnect() {\n        sessionManager?.endCurrentSession(true)\n    }\n    \n    fun loadCurrentMedia() {\n        val metadata = musicService.currentMediaMetadata.value ?: return\n        loadMediaWithQueue(metadata)\n    }\n    \n    fun loadMedia(metadata: AppMediaMetadata) {\n        loadMediaWithQueue(metadata)\n    }\n    \n    /**\n     * Build MediaInfo for a single track\n     */\n    private suspend fun buildMediaInfo(metadata: AppMediaMetadata): MediaInfo? {\n        val streamUrl = musicService.getStreamUrl(metadata.id) ?: return null\n        \n        val castMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MUSIC_TRACK).apply {\n            putString(MediaMetadata.KEY_TITLE, metadata.title)\n            putString(MediaMetadata.KEY_ARTIST, metadata.artists.joinToString(\", \") { it.name })\n            metadata.album?.title?.let { putString(MediaMetadata.KEY_ALBUM_TITLE, it) }\n            metadata.thumbnailUrl?.let { thumbUrl ->\n                // Use high quality thumbnail (1080x1080) for Cast display\n                val highQualityUrl = thumbUrl.resize(1080, 1080)\n                addImage(WebImage(Uri.parse(highQualityUrl)))\n            }\n        }\n        \n        return MediaInfo.Builder(streamUrl)\n            .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)\n            .setContentType(\"audio/mp4\")\n            .setMetadata(castMetadata)\n            .setCustomData(org.json.JSONObject().put(\"mediaId\", metadata.id))\n            .build()\n    }\n    \n    /**\n     * Load media with queue context to enable skip prev/next buttons on Cast widget\n     * Loads up to 5 items: 2 previous, current, and 2 next for smoother transitions\n     * Respects shuffle mode when determining prev/next items\n     */\n    private fun loadMediaWithQueue(metadata: AppMediaMetadata) {\n        if (!_isCasting.value) return\n        \n        isReloadingQueue = true // Prevent sync logic from triggering during load\n        scope.launch {\n            try {\n                currentMediaId = metadata.id\n                _castIsBuffering.value = true\n                lastCastItemId = -1 // Reset to prevent false change detection\n                \n                val player = musicService.player\n                val currentIndex = player.currentMediaItemIndex\n                val mediaItemCount = player.mediaItemCount\n                val shuffleEnabled = player.shuffleModeEnabled\n                val timeline = player.currentTimeline\n                \n                // Build queue items: up to 2 previous, current, and up to 2 next songs\n                val queueItems = mutableListOf<MediaQueueItem>()\n                \n                // Get previous items respecting shuffle order\n                val prevItems = mutableListOf<androidx.media3.common.MediaItem>()\n                if (!timeline.isEmpty) {\n                    var prevIdx = currentIndex\n                    for (i in 0 until 2) {\n                        prevIdx = timeline.getPreviousWindowIndex(prevIdx, Player.REPEAT_MODE_OFF, shuffleEnabled)\n                        if (prevIdx == androidx.media3.common.C.INDEX_UNSET) break\n                        prevItems.add(0, player.getMediaItemAt(prevIdx)) // Add at beginning to maintain order\n                    }\n                }\n                \n                // Add previous items\n                for (prevItem in prevItems) {\n                    prevItem.metadata?.let { prevMetadata ->\n                        buildMediaInfo(prevMetadata)?.let { mediaInfo ->\n                            queueItems.add(MediaQueueItem.Builder(mediaInfo).build())\n                        }\n                    }\n                }\n                val startIndex = queueItems.size // Current item index after previous items\n                \n                // Add current item\n                val currentMediaInfo = buildMediaInfo(metadata)\n                if (currentMediaInfo == null) {\n                    Timber.e(\"Failed to get stream URL for Cast\")\n                    _castIsBuffering.value = false\n                    return@launch\n                }\n                queueItems.add(MediaQueueItem.Builder(currentMediaInfo).build())\n                \n                // Get next items respecting shuffle order\n                if (!timeline.isEmpty) {\n                    var nextIdx = currentIndex\n                    for (i in 0 until 2) {\n                        nextIdx = timeline.getNextWindowIndex(nextIdx, Player.REPEAT_MODE_OFF, shuffleEnabled)\n                        if (nextIdx == androidx.media3.common.C.INDEX_UNSET) break\n                        val nextItem = player.getMediaItemAt(nextIdx)\n                        nextItem.metadata?.let { nextMetadata ->\n                            buildMediaInfo(nextMetadata)?.let { mediaInfo ->\n                                queueItems.add(MediaQueueItem.Builder(mediaInfo).build())\n                            }\n                        }\n                    }\n                }\n                \n                // Get current position from local player if same song\n                val startPosition = if (player.currentMediaItem?.mediaId == metadata.id) {\n                    player.currentPosition\n                } else {\n                    0L\n                }\n                \n                Timber.d(\"Loading Cast queue: ${queueItems.size} items, startIndex=$startIndex, shuffle=$shuffleEnabled\")\n                \n                withContext(Dispatchers.Main) {\n                    val client = remoteMediaClient ?: return@withContext\n                    \n                    // Load the queue\n                    client.queueLoad(\n                        queueItems.toTypedArray(),\n                        startIndex,\n                        MediaStatus.REPEAT_MODE_REPEAT_OFF,\n                        startPosition,\n                        org.json.JSONObject()\n                    )\n                    \n                    // Pause local playback\n                    musicService.player.pause()\n                }\n                \n                Timber.d(\"Loaded media on Cast: ${metadata.title}\")\n            } catch (e: Exception) {\n                Timber.e(e, \"Failed to load media on Cast\")\n                _castIsBuffering.value = false\n            } finally {\n                // Allow sync logic after a delay\n                delay(1500)\n                isReloadingQueue = false\n            }\n        }\n    }\n    \n    fun play() {\n        remoteMediaClient?.play()\n    }\n    \n    fun pause() {\n        remoteMediaClient?.pause()\n    }\n    \n    fun seekTo(position: Long) {\n        val seekOptions = MediaSeekOptions.Builder()\n            .setPosition(position)\n            .build()\n        remoteMediaClient?.seek(seekOptions)\n    }\n    \n    /**\n     * Set the Cast device volume (0.0 to 1.0)\n     */\n    fun setVolume(volume: Float) {\n        try {\n            val clampedVolume = volume.coerceIn(0f, 1f)\n            castSession?.volume = clampedVolume.toDouble()\n            _castVolume.value = clampedVolume\n            Timber.d(\"Set Cast volume to $clampedVolume\")\n        } catch (e: Exception) {\n            Timber.e(e, \"Failed to set Cast volume\")\n        }\n    }\n    \n    /**\n     * Try to navigate to a media item if it's already in the Cast queue\n     * Returns true if successful, false if the item isn't in the queue\n     */\n    fun navigateToMediaIfInQueue(mediaId: String): Boolean {\n        val client = remoteMediaClient ?: return false\n        val mediaStatus = client.mediaStatus ?: return false\n        val queueItems = mediaStatus.queueItems\n        if (queueItems.isEmpty()) return false\n        \n        // Find the item in Cast queue\n        val targetIndex = queueItems.indexOfFirst { \n            it.media?.customData?.optString(\"mediaId\") == mediaId \n        }\n        \n        if (targetIndex < 0) {\n            Timber.d(\"Media $mediaId not found in Cast queue\")\n            return false\n        }\n        \n        val currentItemId = mediaStatus.currentItemId\n        val currentIndex = queueItems.indexOfFirst { it.itemId == currentItemId }\n        \n        if (targetIndex == currentIndex) {\n            // Already on this item - ensure local player is paused\n            currentMediaId = mediaId\n            musicService.player.pause()\n            return true\n        }\n        \n        // Navigate to the item on Cast\n        val targetItem = queueItems[targetIndex]\n        Timber.d(\"Navigating Cast to item at index $targetIndex (mediaId=$mediaId)\")\n        \n        // Set flag to prevent reverse sync loop\n        isSyncingFromCast = true\n        \n        // Update local player to match (for UI sync) - find the item in local queue\n        val player = musicService.player\n        for (i in 0 until player.mediaItemCount) {\n            if (player.getMediaItemAt(i).mediaId == mediaId) {\n                player.seekTo(i, 0)\n                break\n            }\n        }\n        player.pause()\n        \n        // Navigate Cast\n        client.queueJumpToItem(targetItem.itemId, org.json.JSONObject())\n        currentMediaId = mediaId\n        \n        // Reset sync flag after a short delay\n        scope.launch {\n            delay(300)\n            isSyncingFromCast = false\n        }\n        \n        return true\n    }\n    \n    fun skipToNext() {\n        // First try to use Cast queue\n        val client = remoteMediaClient\n        val mediaStatus = client?.mediaStatus\n        if (mediaStatus != null && mediaStatus.queueItemCount > 0) {\n            // Check if there's a next item in Cast queue\n            val currentItemId = mediaStatus.currentItemId\n            val queueItems = mediaStatus.queueItems\n            val currentIndex = queueItems.indexOfFirst { it.itemId == currentItemId }\n            if (currentIndex >= 0 && currentIndex < queueItems.size - 1) {\n                // There's a next item in Cast queue, use it\n                client.queueNext(org.json.JSONObject())\n                // Ensure local player stays paused\n                musicService.player.pause()\n                return\n            }\n        }\n        \n        // Fall back to loading from MusicService queue\n        val player = musicService.player\n        if (player.hasNextMediaItem()) {\n            // Pause first, then seek\n            player.pause()\n            player.seekToNextMediaItem()\n            // The player listener will handle loading the new media to Cast\n        }\n    }\n    \n    fun skipToPrevious() {\n        // First try to use Cast queue\n        val client = remoteMediaClient\n        val mediaStatus = client?.mediaStatus\n        if (mediaStatus != null && mediaStatus.queueItemCount > 0) {\n            // Check if there's a previous item in Cast queue\n            val currentItemId = mediaStatus.currentItemId\n            val queueItems = mediaStatus.queueItems\n            val currentIndex = queueItems.indexOfFirst { it.itemId == currentItemId }\n            if (currentIndex > 0) {\n                // There's a previous item in Cast queue, use it\n                client.queuePrev(org.json.JSONObject())\n                // Ensure local player stays paused\n                musicService.player.pause()\n                return\n            }\n        }\n        \n        // Fall back to loading from MusicService queue\n        val player = musicService.player\n        if (player.hasPreviousMediaItem()) {\n            // Pause first, then seek\n            player.pause()\n            player.seekToPreviousMediaItem()\n        }\n    }\n    \n    private fun startPositionUpdates() {\n        positionUpdateJob?.cancel()\n        positionUpdateJob = scope.launch {\n            while (isActive && _isCasting.value) {\n                remoteMediaClient?.let { client ->\n                    _castPosition.value = client.approximateStreamPosition\n                }\n                delay(500)\n            }\n        }\n    }\n    \n    private fun stopPositionUpdates() {\n        positionUpdateJob?.cancel()\n        positionUpdateJob = null\n    }\n    \n    fun release() {\n        stopPositionUpdates()\n        remoteMediaClient?.unregisterCallback(remoteMediaClientCallback)\n        sessionManager?.removeSessionManagerListener(sessionManagerListener, CastSession::class.java)\n    }\n}\n"
  },
  {
    "path": "app/src/gms/kotlin/com/metrolist/music/ui/component/CastButton.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport android.widget.Toast\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.ColorFilter\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport androidx.mediarouter.media.MediaRouteSelector\nimport androidx.mediarouter.media.MediaRouter\nimport com.google.android.gms.cast.CastMediaControlIntent\nimport com.google.android.gms.cast.framework.CastContext\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.EnableGoogleCastKey\nimport com.metrolist.music.utils.rememberPreference\nimport timber.log.Timber\n\n/**\n * A Composable Cast button that shows available Cast devices.\n * Uses the app's MenuState to show a styled bottom sheet.\n */\n@Composable\nfun CastButton(\n    modifier: Modifier = Modifier,\n    tintColor: Color = MaterialTheme.colorScheme.onSurface,\n) {\n    val context = LocalContext.current\n    val playerConnection = LocalPlayerConnection.current\n    val menuState = LocalMenuState.current\n    \n    var castAvailable by remember { mutableStateOf(false) }\n    var mediaRouter by remember { mutableStateOf<MediaRouter?>(null) }\n    var routeSelector by remember { mutableStateOf<MediaRouteSelector?>(null) }\n    var availableRoutes by remember { mutableStateOf<List<MediaRouter.RouteInfo>>(emptyList()) }\n    \n    val (enableGoogleCast) = rememberPreference(\n        key = EnableGoogleCastKey,\n        defaultValue = true\n    )\n    \n    // Get cast state from service\n    val castHandler = playerConnection?.service?.castConnectionHandler\n    val isCasting by castHandler?.isCasting?.collectAsState() ?: remember { mutableStateOf(false) }\n    val isConnecting by castHandler?.isConnecting?.collectAsState() ?: remember { mutableStateOf(false) }\n    val castDeviceName by castHandler?.castDeviceName?.collectAsState() ?: remember { mutableStateOf(null) }\n    \n    // Get current media metadata\n    val currentMetadata by playerConnection?.mediaMetadata?.collectAsState() ?: remember { mutableStateOf(null) }\n\n    // Check if Cast is available and disconnect if disabled while casting\n    LaunchedEffect(enableGoogleCast) {\n        if (!enableGoogleCast) {\n            // Disconnect from Cast if currently casting\n            if (isCasting) {\n                playerConnection?.service?.castConnectionHandler?.disconnect()\n            }\n            castAvailable = false\n            mediaRouter = null\n            routeSelector = null\n            availableRoutes = emptyList()\n            return@LaunchedEffect\n        }\n        try {\n            CastContext.getSharedInstance(context)\n            mediaRouter = MediaRouter.getInstance(context)\n            routeSelector = MediaRouteSelector.Builder()\n                .addControlCategory(CastMediaControlIntent.categoryForCast(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID))\n                .build()\n            // Reinitialize the Cast handler to ensure it's ready\n            playerConnection?.service?.castConnectionHandler?.initialize()\n            castAvailable = true\n        } catch (e: Exception) {\n            Timber.d(\"Cast not available: ${e.message}\")\n            castAvailable = false\n        }\n    }\n    \n    // Listen for route changes to discover devices\n    DisposableEffect(mediaRouter, routeSelector) {\n        val callback = object : MediaRouter.Callback() {\n            override fun onRouteAdded(router: MediaRouter, route: MediaRouter.RouteInfo) {\n                updateRoutes(router, routeSelector) { availableRoutes = it }\n            }\n            \n            override fun onRouteRemoved(router: MediaRouter, route: MediaRouter.RouteInfo) {\n                updateRoutes(router, routeSelector) { availableRoutes = it }\n            }\n            \n            override fun onRouteChanged(router: MediaRouter, route: MediaRouter.RouteInfo) {\n                updateRoutes(router, routeSelector) { availableRoutes = it }\n            }\n        }\n        \n        routeSelector?.let { selector ->\n            mediaRouter?.addCallback(selector, callback, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY)\n            // Initial update\n            updateRoutes(mediaRouter, selector) { availableRoutes = it }\n        }\n        \n        onDispose {\n            mediaRouter?.removeCallback(callback)\n        }\n    }\n\n    // Show the button if Cast is enabled and SDK is available\n    if (enableGoogleCast && castAvailable) {\n        Box(\n            modifier = modifier\n        ) {\n            // Shadow background for cast button\n            Box(\n                modifier = Modifier\n                    .size(56.dp)\n                    .align(Alignment.Center)\n                    .background(\n                        brush = Brush.radialGradient(\n                            colors = listOf(\n                                Color.Black.copy(alpha = 0.4f),\n                                Color.Transparent\n                            )\n                        )\n                    )\n            )\n            \n            // Cast button\n            Box(\n                contentAlignment = Alignment.Center,\n                modifier = Modifier\n                    .size(40.dp)\n                    .align(Alignment.Center)\n                    .clip(RoundedCornerShape(20.dp))\n                    .clickable {\n                    if (currentMetadata == null && !isCasting) {\n                        Toast.makeText(context, \"Play a song first to cast\", Toast.LENGTH_SHORT).show()\n                        return@clickable\n                    }\n                    \n                    // Get current connected route if casting\n                    val currentRoute = if (isCasting) {\n                        mediaRouter?.routes?.find { route ->\n                            routeSelector?.let { selector -> \n                                route.matchesSelector(selector) && route.isSelected\n                            } == true\n                        }\n                    } else null\n                    \n                    // Show bottom sheet with cast picker\n                    menuState.show {\n                        CastPickerSheet(\n                            routes = availableRoutes,\n                            isConnecting = isConnecting,\n                            currentlyConnectedRoute = currentRoute,\n                            onRouteSelected = { route ->\n                                castHandler?.connectToRoute(route)\n                                menuState.dismiss()\n                            },\n                            onDisconnect = {\n                                castHandler?.disconnect()\n                                menuState.dismiss()\n                            }\n                        )\n                    }\n                }\n            ) {\n                Image(\n                    painter = painterResource(\n                        if (isCasting) R.drawable.cast_connected else R.drawable.cast\n                    ),\n                    contentDescription = if (isCasting) \"Stop casting\" else \"Cast\",\n                    colorFilter = ColorFilter.tint(\n                        if (isCasting) MaterialTheme.colorScheme.primary else tintColor\n                    ),\n                    modifier = Modifier.size(24.dp)\n                )\n            }\n        }\n    }\n}\n\nprivate fun updateRoutes(\n    router: MediaRouter?,\n    selector: MediaRouteSelector?,\n    onUpdate: (List<MediaRouter.RouteInfo>) -> Unit\n) {\n    if (router == null || selector == null) {\n        onUpdate(emptyList())\n        return\n    }\n    val routes = router.routes.filter { route ->\n        route.matchesSelector(selector) && !route.isDefault\n    }\n    onUpdate(routes)\n}\n"
  },
  {
    "path": "app/src/gms/kotlin/com/metrolist/music/ui/component/CastPickerSheet.kt",
    "content": "package com.metrolist.music.ui.component\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.mediarouter.media.MediaRouter\nimport com.metrolist.music.R\n\n@Composable\nfun CastPickerSheet(\n    routes: List<MediaRouter.RouteInfo>,\n    isConnecting: Boolean,\n    onRouteSelected: (MediaRouter.RouteInfo) -> Unit,\n    onDisconnect: () -> Unit,\n    currentlyConnectedRoute: MediaRouter.RouteInfo?,\n    modifier: Modifier = Modifier\n) {\n    Column(\n        modifier = modifier\n            .fillMaxWidth()\n            .padding(bottom = 24.dp)\n    ) {\n        // Header\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n            modifier = Modifier.padding(bottom = 16.dp)\n        ) {\n            Icon(\n                painter = painterResource(R.drawable.cast),\n                contentDescription = null,\n                modifier = Modifier.size(28.dp),\n                tint = MaterialTheme.colorScheme.primary\n            )\n            Spacer(modifier = Modifier.width(12.dp))\n            Text(\n                text = if (currentlyConnectedRoute != null) \"Casting\" else \"Cast to\",\n                style = MaterialTheme.typography.titleLarge,\n                fontWeight = FontWeight.Bold\n            )\n        }\n        \n        if (isConnecting) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(vertical = 16.dp)\n            ) {\n                CircularProgressIndicator(\n                    modifier = Modifier.size(24.dp),\n                    strokeWidth = 2.dp\n                )\n                Spacer(modifier = Modifier.width(16.dp))\n                Text(\n                    text = \"Connecting...\",\n                    style = MaterialTheme.typography.bodyLarge\n                )\n            }\n        } else if (currentlyConnectedRoute != null) {\n            // Currently connected - show disconnect option\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .clickable { onDisconnect() }\n                    .padding(vertical = 12.dp)\n            ) {\n                Icon(\n                    painter = painterResource(R.drawable.cast_connected),\n                    contentDescription = null,\n                    modifier = Modifier.size(24.dp),\n                    tint = MaterialTheme.colorScheme.primary\n                )\n                Spacer(modifier = Modifier.width(16.dp))\n                Column(modifier = Modifier.weight(1f)) {\n                    Text(\n                        text = currentlyConnectedRoute.name,\n                        style = MaterialTheme.typography.bodyLarge,\n                        fontWeight = FontWeight.Medium\n                    )\n                    Text(\n                        text = \"Connected\",\n                        style = MaterialTheme.typography.bodySmall,\n                        color = MaterialTheme.colorScheme.primary\n                    )\n                }\n                Text(\n                    text = \"Disconnect\",\n                    style = MaterialTheme.typography.labelLarge,\n                    color = MaterialTheme.colorScheme.error\n                )\n            }\n        } else if (routes.isEmpty()) {\n            // No devices found\n            Column(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(vertical = 24.dp),\n                horizontalAlignment = Alignment.CenterHorizontally\n            ) {\n                Icon(\n                    painter = painterResource(R.drawable.cast),\n                    contentDescription = null,\n                    modifier = Modifier.size(48.dp),\n                    tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)\n                )\n                Spacer(modifier = Modifier.height(16.dp))\n                Text(\n                    text = \"No Cast devices found\",\n                    style = MaterialTheme.typography.bodyLarge,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant\n                )\n                Spacer(modifier = Modifier.height(8.dp))\n                Text(\n                    text = \"Make sure your device is on the same Wi-Fi network\",\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)\n                )\n            }\n        } else {\n            // Show available devices\n            LazyColumn {\n                items(routes) { route ->\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .clickable { onRouteSelected(route) }\n                            .padding(vertical = 12.dp)\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.cast),\n                            contentDescription = null,\n                            modifier = Modifier.size(24.dp),\n                            tint = MaterialTheme.colorScheme.onSurface\n                        )\n                        Spacer(modifier = Modifier.width(16.dp))\n                        Text(\n                            text = route.name,\n                            style = MaterialTheme.typography.bodyLarge,\n                            modifier = Modifier.weight(1f)\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/izzy/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <application>\n        <!-- Remove Cast provider in Izzy build since GMS is not available -->\n        <meta-data\n            android:name=\"com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME\"\n            tools:node=\"remove\" />\n    </application>\n</manifest>\n"
  },
  {
    "path": "app/src/izzy/kotlin/com/metrolist/music/cast/CastOptionsProvider.kt",
    "content": "package com.metrolist.music.cast\n\n/**\n * Stub CastOptionsProvider for Izzy builds.\n * The AndroidManifest reference is removed via manifest merger.\n */\nclass CastOptionsProvider\n"
  },
  {
    "path": "app/src/izzy/kotlin/com/metrolist/music/playback/CastConnectionHandler.kt",
    "content": "package com.metrolist.music.playback\n\nimport android.content.Context\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\n\n/**\n * Stub CastConnectionHandler for Izzy builds.\n * Cast functionality is not available without Google Play Services.\n */\nclass CastConnectionHandler(\n    context: Context,\n    scope: CoroutineScope,\n    musicService: MusicService\n) {\n    private val _isCasting = MutableStateFlow(false)\n    val isCasting: StateFlow<Boolean> = _isCasting\n\n    private val _isConnecting = MutableStateFlow(false)\n    val isConnecting: StateFlow<Boolean> = _isConnecting\n\n    private val _castDeviceName = MutableStateFlow<String?>(null)\n    val castDeviceName: StateFlow<String?> = _castDeviceName\n\n    private val _castPosition = MutableStateFlow(0L)\n    val castPosition: StateFlow<Long> = _castPosition\n\n    private val _castDuration = MutableStateFlow(0L)\n    val castDuration: StateFlow<Long> = _castDuration\n\n    private val _castIsPlaying = MutableStateFlow(false)\n    val castIsPlaying: StateFlow<Boolean> = _castIsPlaying\n\n    private val _castIsBuffering = MutableStateFlow(false)\n    val castIsBuffering: StateFlow<Boolean> = _castIsBuffering\n\n    private val _castVolume = MutableStateFlow(1.0f)\n    val castVolume: StateFlow<Float> = _castVolume\n\n    var isSyncingFromCast: Boolean = false\n        private set\n\n    fun initialize(): Boolean = false\n    fun disconnect() {}\n    fun loadCurrentMedia() {}\n    fun loadMedia(metadata: com.metrolist.music.models.MediaMetadata) {}\n    fun play() {}\n    fun pause() {}\n    fun seekTo(position: Long) {}\n    fun setVolume(volume: Float) {}\n    fun skipToNext() {}\n    fun skipToPrevious() {}\n    fun navigateToMediaIfInQueue(mediaId: String): Boolean = false\n    fun release() {}\n}\n"
  },
  {
    "path": "app/src/izzy/kotlin/com/metrolist/music/ui/component/CastButton.kt",
    "content": "package com.metrolist.music.ui.component\n\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\n\n/**\n * Stub CastButton for Izzy builds.\n * Does not render anything - Cast not available without GMS.\n */\n@Composable\nfun CastButton(\n    modifier: Modifier = Modifier,\n    tintColor: Color = MaterialTheme.colorScheme.onSurface,\n) {\n    // No-op: Cast not available in Izzy build\n}\n"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.POST_NOTIFICATIONS\" />\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\" />\n    <uses-permission android:name=\"android.permission.WAKE_LOCK\" />\n    <uses-permission android:name=\"android.permission.SCHEDULE_EXACT_ALARM\" />\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE\" />\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK\" />\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_DATA_SYNC\" />\n    <uses-permission android:name=\"android.permission.FOREGROUND_SERVICE_MICROPHONE\" />\n    <uses-permission android:name=\"android.permission.RECORD_AUDIO\" />\n\n    <!-- Microphone feature for music recognition (not required) -->\n    <uses-feature android:name=\"android.hardware.microphone\" android:required=\"false\" />\n\n    <!-- Android TV -->\n    <uses-feature android:name=\"android.software.leanback\" android:required=\"false\" />\n    <uses-feature android:name=\"android.hardware.touchscreen\" android:required=\"false\" />\n\n    <queries>\n        <intent>\n            <action android:name=\"android.media.action.DISPLAY_AUDIO_EFFECT_CONTROL_PANEL\" />\n        </intent>\n        <package android:name=\"com.google.android.projection.gearhead\" />\n    </queries>\n    <application\n        android:name=\".App\"\n        android:allowBackup=\"true\"\n        android:appCategory=\"audio\"\n        android:banner=\"@mipmap/tv_banner\"\n        android:dataExtractionRules=\"@xml/data_extraction_rules\"\n        android:enableOnBackInvokedCallback=\"true\"\n        android:fullBackupContent=\"@xml/backup_rules\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:networkSecurityConfig=\"@xml/network_security_config\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"false\"\n        android:theme=\"@style/Theme.Metrolist\"\n        tools:targetApi=\"tiramisu\">\n        <uses-library\n            android:name=\"org.apache.http.legacy\"\n            android:required=\"false\" />\n\n        <activity\n            android:name=\".ui.screens.CrashActivity\"\n            android:exported=\"false\"\n            android:theme=\"@style/Theme.Metrolist\"\n            android:excludeFromRecents=\"true\"\n            android:process=\":crash\" />\n\n        <activity\n            android:name=\".MainActivity\"\n            android:exported=\"true\"\n            android:launchMode=\"singleTask\"\n            android:theme=\"@style/Theme.Metrolist\"\n            android:windowSoftInputMode=\"adjustResize\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <action android:name=\"android.intent.action.VIEW\" />\n                <action android:name=\"android.intent.action.MUSIC_PLAYER\" />\n\n                <category android:name=\"android.intent.category.LEANBACK_LAUNCHER\" />\n                <category android:name=\"android.intent.category.APP_MUSIC\" />\n                <category android:name=\"android.intent.category.DEFAULT\" />\n            </intent-filter>\n\n            <!-- Music Recognizer Widget: open recognition / recognition_history screen -->\n            <intent-filter>\n                <action android:name=\"com.metrolist.music.action.RECOGNITION\" />\n                <category android:name=\"android.intent.category.DEFAULT\" />\n            </intent-filter>\n\n            <!-- Listen Together invite link -->\n            <intent-filter android:autoVerify=\"true\">\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data android:scheme=\"https\" />\n                <data android:host=\"metrolist.meowery.eu\" />\n                <data android:pathPrefix=\"/listen\" />\n            </intent-filter>\n\n            <!-- Youtube filter -->\n            <intent-filter android:autoVerify=\"true\">\n                <action android:name=\"android.intent.action.VIEW\" />\n                <action android:name=\"android.media.action.MEDIA_PLAY_FROM_SEARCH\" />\n                <action android:name=\"android.nfc.action.NDEF_DISCOVERED\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data android:scheme=\"http\" />\n                <data android:scheme=\"https\" />\n                <data android:host=\"youtube.com\" />\n                <data android:host=\"m.youtube.com\" />\n                <data android:host=\"www.youtube.com\" />\n                <data android:host=\"music.youtube.com\" />\n                <!-- video prefix -->\n                <data android:pathPrefix=\"/v/\" />\n                <data android:pathPrefix=\"/embed/\" />\n                <data android:pathPrefix=\"/watch\" />\n                <!-- channel prefix -->\n                <data android:pathPrefix=\"/channel/\" />\n                <data android:pathPrefix=\"/user/\" />\n                <data android:pathPrefix=\"/c/\" />\n                <!-- playlist prefix -->\n                <data android:pathPrefix=\"/playlist\" />\n                <!-- browse prefix -->\n                <data android:pathPrefix=\"/browse/\" />\n                <!-- search prefix -->\n                <data android:pathPrefix=\"/search\" />\n            </intent-filter>\n\n            <intent-filter android:autoVerify=\"true\">\n                <action android:name=\"android.intent.action.VIEW\" />\n                <action android:name=\"android.media.action.MEDIA_PLAY_FROM_SEARCH\" />\n                <action android:name=\"android.nfc.action.NDEF_DISCOVERED\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data android:scheme=\"http\" />\n                <data android:scheme=\"https\" />\n                <data android:host=\"youtu.be\" />\n                <data android:pathPrefix=\"/\" />\n            </intent-filter>\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n                <action android:name=\"android.media.action.MEDIA_PLAY_FROM_SEARCH\" />\n                <action android:name=\"android.nfc.action.NDEF_DISCOVERED\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data android:scheme=\"vnd.youtube\" />\n                <data android:scheme=\"vnd.youtube.launch\" />\n            </intent-filter>\n\n            <!-- Share filter -->\n            <intent-filter>\n                <action android:name=\"android.intent.action.SEND\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n\n                <data android:mimeType=\"text/plain\" />\n            </intent-filter>\n\n            <meta-data\n                android:name=\"android.app.shortcuts\"\n                android:resource=\"@xml/shortcuts\" />\n        </activity>\n\n        <activity-alias\n            android:name=\".MainActivityAlias\"\n            android:enabled=\"true\"\n            android:exported=\"true\"\n            android:icon=\"@mipmap/ic_launcher\"\n            android:label=\"@string/app_name\"\n            android:roundIcon=\"@mipmap/ic_launcher_round\"\n            android:targetActivity=\".MainActivity\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity-alias>\n\n        <activity-alias\n            android:name=\".MainActivityStatic\"\n            android:enabled=\"false\"\n            android:exported=\"true\"\n            android:icon=\"@mipmap/ic_launcher_static\"\n            android:label=\"@string/app_name\"\n            android:roundIcon=\"@mipmap/ic_launcher_static_round\"\n            android:targetActivity=\".MainActivity\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity-alias>\n\n        <provider\n            android:name=\"androidx.core.content.FileProvider\"\n            android:authorities=\"${applicationId}.FileProvider\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\">\n            <meta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/provider_paths\" />\n        </provider>\n\n        <provider\n            android:name=\"com.dpi.DensityScaler\"\n            android:authorities=\"${applicationId}.com.dpi.DensityScaler\"\n            android:enabled=\"true\"\n            android:exported=\"false\" />\n\n        <activity\n            android:name=\"com.yalantis.ucrop.UCropActivity\"\n            android:exported=\"false\"\n            android:theme=\"@style/Theme.AppCompat.Light.NoActionBar\" />\n\n        <activity\n            android:name=\".recognition.RecognitionLaunchActivity\"\n            android:excludeFromRecents=\"true\"\n            android:exported=\"false\"\n            android:launchMode=\"singleTask\"\n            android:noHistory=\"true\"\n            android:theme=\"@style/Theme.Metrolist.Transparent\" />\n\n        <service\n            android:name=\".playback.MusicService\"\n            android:exported=\"true\"\n            android:foregroundServiceType=\"mediaPlayback\"\n            tools:ignore=\"ExportedService\">\n            <intent-filter>\n                <action android:name=\"androidx.media3.session.MediaSessionService\" />\n                <action android:name=\"androidx.media3.session.MediaLibraryService\" />\n                <action android:name=\"android.media.browse.MediaBrowserService\" />\n            </intent-filter>\n        </service>\n\n        <service\n            android:name=\".playback.ExoDownloadService\"\n            android:exported=\"false\"\n            android:foregroundServiceType=\"dataSync\">\n            <intent-filter>\n                <action android:name=\"androidx.media3.exoplayer.downloadService.action.RESTART\" />\n                <category android:name=\"android.intent.category.DEFAULT\" />\n            </intent-filter>\n        </service>\n\n        <!-- Receiver for Listen Together notification actions (Approve/Reject) -->\n        <receiver\n            android:name=\".listentogether.ListenTogetherActionReceiver\"\n            android:exported=\"false\" />\n\n        <receiver\n            android:name=\".playback.alarm.MusicAlarmReceiver\"\n            android:enabled=\"true\"\n            android:exported=\"false\" />\n\n        <receiver\n            android:name=\".playback.alarm.MusicAlarmRescheduleReceiver\"\n            android:directBootAware=\"true\"\n            android:enabled=\"true\"\n            android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.LOCKED_BOOT_COMPLETED\" />\n                <action android:name=\"android.intent.action.BOOT_COMPLETED\" />\n                <action android:name=\"android.intent.action.TIME_SET\" />\n                <action android:name=\"android.intent.action.TIMEZONE_CHANGED\" />\n                <action android:name=\"android.intent.action.MY_PACKAGE_REPLACED\" />\n            </intent-filter>\n        </receiver>\n\n        <receiver\n            android:name=\"androidx.media3.session.MediaButtonReceiver\"\n            android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MEDIA_BUTTON\" />\n            </intent-filter>\n        </receiver>\n\n        <meta-data\n            android:name=\"com.google.android.gms.car.application\"\n            android:resource=\"@xml/automotive_app_desc\" />\n        <!-- Music Widget -->\n        <receiver\n            android:name=\".widget.MusicWidgetReceiver\"\n            android:exported=\"true\"\n            android:label=\"@string/widget_music_player\">\n            <intent-filter>\n                <action android:name=\"android.appwidget.action.APPWIDGET_UPDATE\" />\n                <action android:name=\"com.metrolist.music.widget.PLAY_PAUSE\" />\n                <action android:name=\"com.metrolist.music.widget.LIKE\" />\n                <action android:name=\"com.metrolist.music.widget.UPDATE_WIDGET\" />\n            </intent-filter>\n\n            <meta-data\n                android:name=\"android.appwidget.provider\"\n                android:resource=\"@xml/music_widget_info\" />\n        </receiver>\n\n        <!-- Turntable Widget -->\n        <receiver\n            android:name=\".widget.TurntableWidgetReceiver\"\n            android:exported=\"true\"\n            android:label=\"@string/widget_turntable\">\n            <intent-filter>\n                <action android:name=\"android.appwidget.action.APPWIDGET_UPDATE\" />\n                <action android:name=\"com.metrolist.music.widget.TURNTABLE_PLAY_PAUSE\" />\n                <action android:name=\"com.metrolist.music.widget.TURNTABLE_LIKE\" />\n                <action android:name=\"com.metrolist.music.widget.UPDATE_TURNTABLE_WIDGET\" />\n            </intent-filter>\n\n            <meta-data\n                android:name=\"android.appwidget.provider\"\n                android:resource=\"@xml/turntable_widget_info\" />\n        </receiver>\n\n        <!-- Google Cast Options Provider -->\n        <meta-data\n            android:name=\"com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME\"\n            android:value=\"com.metrolist.music.cast.CastOptionsProvider\" />\n\n        <!-- Music Recognizer Widget -->\n        <receiver\n            android:name=\".widget.MusicRecognizerWidgetReceiver\"\n            android:exported=\"true\"\n            android:label=\"@string/widget_recognizer_name\">\n            <intent-filter>\n                <action android:name=\"android.appwidget.action.APPWIDGET_UPDATE\" />\n                <action android:name=\"com.metrolist.music.widget.recognizer.TAP_MIC\" />\n                <action android:name=\"com.metrolist.music.widget.recognizer.UPDATE\" />\n                <action android:name=\"com.metrolist.music.widget.recognizer.RESET\" />\n            </intent-filter>\n\n            <meta-data\n                android:name=\"android.appwidget.provider\"\n                android:resource=\"@xml/recognizer_widget_info\" />\n        </receiver>\n\n        <service\n            android:name=\".widget.MusicRecognizerWidgetService\"\n            android:exported=\"false\"\n            android:foregroundServiceType=\"microphone\" />\n\n        <service\n            android:name=\".recognition.RecognitionForegroundService\"\n            android:exported=\"false\"\n            android:foregroundServiceType=\"microphone\" />\n\n        <!-- Music Recognizer Quick Settings Tile -->\n        <service\n            android:name=\".quicksettings.MusicRecognizerTileService\"\n            android:label=\"@string/qs_tile_music_recognizer\"\n            android:icon=\"@drawable/mic\"\n            android:exported=\"true\"\n            android:permission=\"android.permission.BIND_QUICK_SETTINGS_TILE\">\n            <intent-filter>\n                <action android:name=\"android.service.quicksettings.action.QS_TILE\" />\n            </intent-filter>\n        </service>\n\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "app/src/main/assets/po_token.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\"><head><title></title><script>\n    /**\n     * BotGuard client for generating poTokens.\n     * Updated to fix JavaScript 'this' binding issues and match BgUtils v3.2.0 patterns.\n     */\n\n    // Global state for BotGuard\n    var bgVmFunctions = null;\n    var bgVm = null;\n    var bgProgram = null;\n    var poTokenMinter = null;  // Minter callback - created ONCE during init, reused for all tokens\n\n    function loadBotGuard(challengeData) {\n      bgVm = window[challengeData.globalName];\n      bgProgram = challengeData.program;\n      bgVmFunctions = null;\n\n      if (!bgVm)\n        throw new Error('[BotGuardClient]: VM not found in the global object');\n\n      if (!bgVm.a)\n        throw new Error('[BotGuardClient]: Could not load program');\n\n      // Use explicit variable capture instead of 'this' to avoid binding issues\n      var vmFunctionsCallback = function (\n        asyncSnapshotFunction,\n        shutdownFunction,\n        passEventFunction,\n        checkCameraFunction\n      ) {\n        bgVmFunctions = {\n          asyncSnapshotFunction: asyncSnapshotFunction,\n          shutdownFunction: shutdownFunction,\n          passEventFunction: passEventFunction,\n          checkCameraFunction: checkCameraFunction\n        };\n      };\n\n      // Execute the BotGuard program\n      try {\n        bgVm.a(bgProgram, vmFunctionsCallback, true, undefined, function () {/** no-op */ }, [ [], [] ]);\n      } catch (e) {\n        throw new Error('[BotGuardClient]: Failed to execute program: ' + e.message);\n      }\n\n      // Wait for vmFunctions to be populated (async callback)\n      return new Promise(function (resolve, reject) {\n        var attempts = 0;\n        var maxAttempts = 10000; // 10 seconds at 1ms intervals\n        var checkInterval = setInterval(function () {\n          if (bgVmFunctions && bgVmFunctions.asyncSnapshotFunction) {\n            clearInterval(checkInterval);\n            resolve({\n              vmFunctions: bgVmFunctions,\n              vm: bgVm,\n              program: bgProgram\n            });\n          } else if (attempts >= maxAttempts) {\n            clearInterval(checkInterval);\n            reject(new Error('[BotGuardClient]: Timeout waiting for asyncSnapshotFunction'));\n          }\n          attempts++;\n        }, 1);\n      });\n    }\n\n    /**\n     * Takes a snapshot asynchronously using the loaded BotGuard.\n     */\n    function snapshot(botguard, args) {\n      return new Promise(function (resolve, reject) {\n        if (!botguard.vmFunctions || !botguard.vmFunctions.asyncSnapshotFunction) {\n          return reject(new Error('[BotGuardClient]: Async snapshot function not found'));\n        }\n\n        try {\n          botguard.vmFunctions.asyncSnapshotFunction(\n            function (response) { resolve(response); },\n            [\n              args.contentBinding,\n              args.signedTimestamp,\n              args.webPoSignalOutput,\n              args.skipPrivacyBuffer\n            ]\n          );\n        } catch (e) {\n          reject(new Error('[BotGuardClient]: Snapshot failed: ' + e.message));\n        }\n      });\n    }\n\n    function runBotGuard(challengeData) {\n      var interpreterJavascript = challengeData.interpreterJavascript.privateDoNotAccessOrElseSafeScriptWrappedValue;\n\n      if (interpreterJavascript) {\n        new Function(interpreterJavascript)();\n      } else {\n        throw new Error('[BotGuardClient]: Could not load VM - no interpreter JavaScript');\n      }\n\n      var webPoSignalOutput = [];\n\n      return loadBotGuard({\n        globalName: challengeData.globalName,\n        program: challengeData.program\n      }).then(function (botguard) {\n        return snapshot(botguard, { webPoSignalOutput: webPoSignalOutput });\n      }).then(function (botguardResponse) {\n        return { webPoSignalOutput: webPoSignalOutput, botguardResponse: botguardResponse };\n      });\n    }\n\n    /**\n     * Creates the poToken minter callback. MUST be called exactly ONCE during initialization,\n     * right after runBotGuard completes. The minter is stored globally and reused for all tokens.\n     * NOTE: getMinter() may return a Promise, so this function is async.\n     * @param webPoSignalOutput - Array from BotGuard containing the minter factory function\n     * @param integrityToken - The integrity token (as bytes/Uint8Array)\n     * @returns Promise that resolves when minter is ready\n     */\n    async function createPoTokenMinter(webPoSignalOutput, integrityToken) {\n      console.log('[createPoTokenMinter] ENTER - webPoSignalOutput type: ' + typeof webPoSignalOutput + ', length: ' + (webPoSignalOutput ? webPoSignalOutput.length : 'null'));\n      console.log('[createPoTokenMinter] integrityToken type: ' + typeof integrityToken + ', is array: ' + Array.isArray(integrityToken));\n\n      // Get the minter factory function from webPoSignalOutput\n      var getMinter = webPoSignalOutput[0];\n      console.log('[createPoTokenMinter] getMinter type: ' + typeof getMinter);\n\n      if (!getMinter) {\n        throw new Error('PMD:Undefined - webPoSignalOutput[0] is not defined');\n      }\n\n      if (typeof getMinter !== 'function') {\n        throw new Error('PMD:NotFunction - webPoSignalOutput[0] is not a function, got: ' + typeof getMinter);\n      }\n\n      // Create the mint callback by passing the integrity token - THIS MUST ONLY HAPPEN ONCE!\n      // NOTE: getMinter() may return a Promise, so we await it\n      console.log('[createPoTokenMinter] Calling getMinter(integrityToken)...');\n      var mintCallback;\n      try {\n        var minterResult = getMinter(integrityToken);\n        console.log('[createPoTokenMinter] getMinter returned type: ' + typeof minterResult + ', isPromise: ' + (minterResult && typeof minterResult.then === 'function'));\n        // Handle both sync and async getMinter\n        if (minterResult && typeof minterResult.then === 'function') {\n          console.log('[createPoTokenMinter] getMinter returned a Promise, awaiting...');\n          mintCallback = await minterResult;\n          console.log('[createPoTokenMinter] Promise resolved, mintCallback type: ' + typeof mintCallback);\n        } else {\n          mintCallback = minterResult;\n        }\n      } catch (e) {\n        console.log('[createPoTokenMinter] getMinter EXCEPTION: ' + e.message);\n        throw new Error('GMC:Failed - getMinter() threw: ' + e.message);\n      }\n\n      if (!mintCallback) {\n        throw new Error('APF:Undefined - mintCallback is undefined');\n      }\n\n      if (typeof mintCallback !== 'function') {\n        throw new Error('APF:NotFunction - mintCallback is not a function, got: ' + typeof mintCallback);\n      }\n\n      // Store the minter globally for reuse\n      poTokenMinter = mintCallback;\n      console.log('[createPoTokenMinter] SUCCESS - Minter created and stored globally (type: ' + typeof mintCallback + ')');\n    }\n\n    /**\n     * Mints a poToken using the pre-created minter callback.\n     * The minter MUST have been created during initialization via createPoTokenMinter().\n     * NOTE: mintCallback() may return a Promise, so this function is async.\n     * @param identifier - The identifier to bind the token to (as Uint8Array)\n     * @returns Promise<Uint8Array> containing the poToken\n     */\n    async function obtainPoToken(identifier) {\n      if (!poTokenMinter) {\n        throw new Error('MNT:NotInit - poTokenMinter was not initialized. Call createPoTokenMinter first.');\n      }\n\n      // Mint the token with the identifier\n      // NOTE: mintCallback() may return a Promise, so we await it\n      var result;\n      try {\n        var mintResult = poTokenMinter(identifier);\n        // Handle both sync and async mintCallback\n        if (mintResult && typeof mintResult.then === 'function') {\n          console.log('[obtainPoToken] mintCallback returned a Promise, awaiting...');\n          result = await mintResult;\n        } else {\n          result = mintResult;\n        }\n      } catch (e) {\n        throw new Error('MNT:Failed - mintCallback() threw: ' + e.message);\n      }\n\n      if (!result) {\n        throw new Error('YNJ:Undefined - mint result is undefined');\n      }\n\n      if (!(result instanceof Uint8Array)) {\n        throw new Error('ODM:Invalid - result is not Uint8Array, got: ' + Object.prototype.toString.call(result));\n      }\n\n      // Validate token size (expected 110-128 bytes per BgUtils documentation)\n      if (result.length < 100 || result.length > 140) {\n        console.warn('[obtainPoToken] Token size ' + result.length + ' bytes may be outside expected range (110-128)');\n      }\n\n      return result;\n    }\n</script></head><body></body></html>\n"
  },
  {
    "path": "app/src/main/assets/solver/astring.js",
    "content": "(function(a,b){if(\"function\"==typeof define&&define.amd)define([\"exports\"],b);else if(\"undefined\"!=typeof exports)b(exports);else{var c={exports:{}};b(c.exports),a.astring=c.exports}})(\"undefined\"==typeof globalThis?\"undefined\"==typeof self?this:self:globalThis,function(a){\"use strict\";var b=String.prototype;function c(a,b){if(!(a instanceof b))throw new TypeError(\"Cannot call a class as a function\")}function d(a,b){for(var c,d=0;d<b.length;d++)c=b[d],c.enumerable=c.enumerable||!1,c.configurable=!0,\"value\"in c&&(c.writable=!0),Object.defineProperty(a,c.key,c)}function e(a,b,c){return b&&d(a.prototype,b),c&&d(a,c),a}function f(a,b){var c=a.generator;if(a.write(\"(\"),null!=b&&0<b.length){c[b[0].type](b[0],a);for(var d,e=b.length,f=1;f<e;f++)d=b[f],a.write(\", \"),c[d.type](d,a)}a.write(\")\")}function g(a,b,c,d){var e=a.expressionsPrecedence[b.type];if(e===17)return!0;var f=a.expressionsPrecedence[c.type];return e===f?(13===e||14===e)&&(\"**\"===b.operator&&\"**\"===c.operator?!d:!(13!==e||13!==f||\"??\"!==b.operator&&\"??\"!==c.operator)||(d?o[b.operator]<=o[c.operator]:o[b.operator]<o[c.operator])):!d&&15===e&&14===f&&\"**\"===c.operator||e<f}function h(a,b,c,d){var e=a.generator;g(a,b,c,d)?(a.write(\"(\"),e[b.type](b,a),a.write(\")\")):e[b.type](b,a)}function j(a,b,c,d){var e=b.split(\"\\n\"),f=e.length-1;if(a.write(e[0].trim()),0<f){a.write(d);for(var g=1;g<f;g++)a.write(c+e[g].trim()+d);a.write(c+e[f].trim())}}function k(a,b,c,d){for(var e,f=b.length,g=0;g<f;g++)e=b[g],a.write(c),\"L\"===e.type[0]?a.write(\"// \"+e.value.trim()+\"\\n\",e):(a.write(\"/*\"),j(a,e.value,c,d),a.write(\"*/\"+d))}function l(a){for(var d=a;null!=d;){var b=d,c=b.type;if(\"C\"===c[0]&&\"a\"===c[1])return!0;if(\"M\"===c[0]&&\"e\"===c[1]&&\"m\"===c[2])d=d.object;else return!1}}function m(a,b){var c=a.generator,d=b.declarations;a.write(b.kind+\" \");var e=d.length;if(0<e){c.VariableDeclarator(d[0],a);for(var f=1;f<e;f++)a.write(\", \"),c.VariableDeclarator(d[f],a)}}Object.defineProperty(a,\"__esModule\",{value:!0}),a.generate=function(a,b){var c=new z(b);return c.generator[a.type](a,c),c.output},a.baseGenerator=a.GENERATOR=a.EXPRESSIONS_PRECEDENCE=a.NEEDS_PARENTHESES=void 0;var n=JSON.stringify;if(!b.repeat)throw new Error(\"String.prototype.repeat is undefined, see https://github.com/davidbonnet/astring#installation\");if(!b.endsWith)throw new Error(\"String.prototype.endsWith is undefined, see https://github.com/davidbonnet/astring#installation\");var o={\"||\":2,\"??\":3,\"&&\":4,\"|\":5,\"^\":6,\"&\":7,\"==\":8,\"!=\":8,\"===\":8,\"!==\":8,\"<\":9,\">\":9,\"<=\":9,\">=\":9,in:9,instanceof:9,\"<<\":10,\">>\":10,\">>>\":10,\"+\":11,\"-\":11,\"*\":12,\"%\":12,\"/\":12,\"**\":13},p=17;a.NEEDS_PARENTHESES=p;var q={ArrayExpression:20,TaggedTemplateExpression:20,ThisExpression:20,Identifier:20,PrivateIdentifier:20,Literal:18,TemplateLiteral:20,Super:20,SequenceExpression:20,MemberExpression:19,ChainExpression:19,CallExpression:19,NewExpression:19,ArrowFunctionExpression:p,ClassExpression:p,FunctionExpression:p,ObjectExpression:p,UpdateExpression:16,UnaryExpression:15,AwaitExpression:15,BinaryExpression:14,LogicalExpression:13,ConditionalExpression:4,AssignmentExpression:3,YieldExpression:2,RestElement:1};a.EXPRESSIONS_PRECEDENCE=q;var r,s,t,u,v,w,x={Program:function Program(a,b){var c=b.indent.repeat(b.indentLevel),d=b.lineEnd,e=b.writeComments;e&&null!=a.comments&&k(b,a.comments,c,d);for(var f,g=a.body,h=g.length,j=0;j<h;j++)f=g[j],e&&null!=f.comments&&k(b,f.comments,c,d),b.write(c),this[f.type](f,b),b.write(d);e&&null!=a.trailingComments&&k(b,a.trailingComments,c,d)},BlockStatement:w=function(a,b){var c=b.indent.repeat(b.indentLevel++),d=b.lineEnd,e=b.writeComments,f=c+b.indent;b.write(\"{\");var g=a.body;if(null!=g&&0<g.length){b.write(d),e&&null!=a.comments&&k(b,a.comments,f,d);for(var h,j=g.length,l=0;l<j;l++)h=g[l],e&&null!=h.comments&&k(b,h.comments,f,d),b.write(f),this[h.type](h,b),b.write(d);b.write(c)}else e&&null!=a.comments&&(b.write(d),k(b,a.comments,f,d),b.write(c));e&&null!=a.trailingComments&&k(b,a.trailingComments,f,d),b.write(\"}\"),b.indentLevel--},ClassBody:w,StaticBlock:function StaticBlock(a,b){b.write(\"static \"),this.BlockStatement(a,b)},EmptyStatement:function EmptyStatement(a,b){b.write(\";\")},ExpressionStatement:function ExpressionStatement(a,b){var c=b.expressionsPrecedence[a.expression.type];c===p||3===c&&\"O\"===a.expression.left.type[0]?(b.write(\"(\"),this[a.expression.type](a.expression,b),b.write(\")\")):this[a.expression.type](a.expression,b),b.write(\";\")},IfStatement:function IfStatement(a,b){b.write(\"if (\"),this[a.test.type](a.test,b),b.write(\") \"),this[a.consequent.type](a.consequent,b),null!=a.alternate&&(b.write(\" else \"),this[a.alternate.type](a.alternate,b))},LabeledStatement:function LabeledStatement(a,b){this[a.label.type](a.label,b),b.write(\": \"),this[a.body.type](a.body,b)},BreakStatement:function BreakStatement(a,b){b.write(\"break\"),null!=a.label&&(b.write(\" \"),this[a.label.type](a.label,b)),b.write(\";\")},ContinueStatement:function ContinueStatement(a,b){b.write(\"continue\"),null!=a.label&&(b.write(\" \"),this[a.label.type](a.label,b)),b.write(\";\")},WithStatement:function WithStatement(a,b){b.write(\"with (\"),this[a.object.type](a.object,b),b.write(\") \"),this[a.body.type](a.body,b)},SwitchStatement:function SwitchStatement(a,b){var c=b.indent.repeat(b.indentLevel++),d=b.lineEnd,e=b.writeComments;b.indentLevel++;var f=c+b.indent,g=f+b.indent;b.write(\"switch (\"),this[a.discriminant.type](a.discriminant,b),b.write(\") {\"+d);for(var h,j=a.cases,l=j.length,m=0;m<l;m++){h=j[m],e&&null!=h.comments&&k(b,h.comments,f,d),h.test?(b.write(f+\"case \"),this[h.test.type](h.test,b),b.write(\":\"+d)):b.write(f+\"default:\"+d);for(var n,o=h.consequent,p=o.length,q=0;q<p;q++)n=o[q],e&&null!=n.comments&&k(b,n.comments,g,d),b.write(g),this[n.type](n,b),b.write(d)}b.indentLevel-=2,b.write(c+\"}\")},ReturnStatement:function ReturnStatement(a,b){b.write(\"return\"),a.argument&&(b.write(\" \"),this[a.argument.type](a.argument,b)),b.write(\";\")},ThrowStatement:function ThrowStatement(a,b){b.write(\"throw \"),this[a.argument.type](a.argument,b),b.write(\";\")},TryStatement:function TryStatement(a,b){if(b.write(\"try \"),this[a.block.type](a.block,b),a.handler){var c=a.handler;null==c.param?b.write(\" catch \"):(b.write(\" catch (\"),this[c.param.type](c.param,b),b.write(\") \")),this[c.body.type](c.body,b)}a.finalizer&&(b.write(\" finally \"),this[a.finalizer.type](a.finalizer,b))},WhileStatement:function WhileStatement(a,b){b.write(\"while (\"),this[a.test.type](a.test,b),b.write(\") \"),this[a.body.type](a.body,b)},DoWhileStatement:function DoWhileStatement(a,b){b.write(\"do \"),this[a.body.type](a.body,b),b.write(\" while (\"),this[a.test.type](a.test,b),b.write(\");\")},ForStatement:function ForStatement(a,b){if(b.write(\"for (\"),null!=a.init){var c=a.init;\"V\"===c.type[0]?m(b,c):this[c.type](c,b)}b.write(\"; \"),a.test&&this[a.test.type](a.test,b),b.write(\"; \"),a.update&&this[a.update.type](a.update,b),b.write(\") \"),this[a.body.type](a.body,b)},ForInStatement:r=function(a,b){b.write(\"for \".concat(a[\"await\"]?\"await \":\"\",\"(\"));var c=a.left;\"V\"===c.type[0]?m(b,c):this[c.type](c,b),b.write(\"I\"===a.type[3]?\" in \":\" of \"),this[a.right.type](a.right,b),b.write(\") \"),this[a.body.type](a.body,b)},ForOfStatement:r,DebuggerStatement:function DebuggerStatement(a,b){b.write(\"debugger;\",a)},FunctionDeclaration:s=function(a,b){b.write((a.async?\"async \":\"\")+(a.generator?\"function* \":\"function \")+(a.id?a.id.name:\"\"),a),f(b,a.params),b.write(\" \"),this[a.body.type](a.body,b)},FunctionExpression:s,VariableDeclaration:function VariableDeclaration(a,b){m(b,a),b.write(\";\")},VariableDeclarator:function VariableDeclarator(a,b){this[a.id.type](a.id,b),null!=a.init&&(b.write(\" = \"),this[a.init.type](a.init,b))},ClassDeclaration:function ClassDeclaration(a,b){if(b.write(\"class \"+(a.id?\"\".concat(a.id.name,\" \"):\"\"),a),a.superClass){b.write(\"extends \");var c=a.superClass,d=c.type,e=b.expressionsPrecedence[d];(\"C\"!==d[0]||\"l\"!==d[1]||\"E\"!==d[5])&&(e===p||e<b.expressionsPrecedence.ClassExpression)?(b.write(\"(\"),this[a.superClass.type](c,b),b.write(\")\")):this[c.type](c,b),b.write(\" \")}this.ClassBody(a.body,b)},ImportDeclaration:function ImportDeclaration(a,b){b.write(\"import \");var c=a.specifiers,d=a.attributes,e=c.length,f=0;if(0<e){for(;f<e;){0<f&&b.write(\", \");var g=c[f],h=g.type[6];if(\"D\"===h)b.write(g.local.name,g),f++;else if(\"N\"===h)b.write(\"* as \"+g.local.name,g),f++;else break}if(f<e){for(b.write(\"{\");;){var j=c[f],k=j.imported.name;if(b.write(k,j),k!==j.local.name&&b.write(\" as \"+j.local.name),++f<e)b.write(\", \");else break}b.write(\"}\")}b.write(\" from \")}if(this.Literal(a.source,b),d&&0<d.length){b.write(\" with { \");for(var l=0;l<d.length;l++)this.ImportAttribute(d[l],b),l<d.length-1&&b.write(\", \");b.write(\" }\")}b.write(\";\")},ImportAttribute:function ImportAttribute(a,b){this.Identifier(a.key,b),b.write(\": \"),this.Literal(a.value,b)},ImportExpression:function ImportExpression(a,b){b.write(\"import(\"),this[a.source.type](a.source,b),b.write(\")\")},ExportDefaultDeclaration:function ExportDefaultDeclaration(a,b){b.write(\"export default \"),this[a.declaration.type](a.declaration,b),null!=b.expressionsPrecedence[a.declaration.type]&&\"F\"!==a.declaration.type[0]&&b.write(\";\")},ExportNamedDeclaration:function ExportNamedDeclaration(a,b){if(b.write(\"export \"),a.declaration)this[a.declaration.type](a.declaration,b);else{b.write(\"{\");var c=a.specifiers,d=c.length;if(0<d)for(var g=0;;){var e=c[g],f=e.local.name;if(b.write(f,e),f!==e.exported.name&&b.write(\" as \"+e.exported.name),++g<d)b.write(\", \");else break}if(b.write(\"}\"),a.source&&(b.write(\" from \"),this.Literal(a.source,b)),a.attributes&&0<a.attributes.length){b.write(\" with { \");for(var h=0;h<a.attributes.length;h++)this.ImportAttribute(a.attributes[h],b),h<a.attributes.length-1&&b.write(\", \");b.write(\" }\")}b.write(\";\")}},ExportAllDeclaration:function ExportAllDeclaration(a,b){if(null==a.exported?b.write(\"export * from \"):b.write(\"export * as \"+a.exported.name+\" from \"),this.Literal(a.source,b),a.attributes&&0<a.attributes.length){b.write(\" with { \");for(var c=0;c<a.attributes.length;c++)this.ImportAttribute(a.attributes[c],b),c<a.attributes.length-1&&b.write(\", \");b.write(\" }\")}b.write(\";\")},MethodDefinition:function MethodDefinition(a,b){a[\"static\"]&&b.write(\"static \");var c=a.kind[0];(\"g\"===c||\"s\"===c)&&b.write(a.kind+\" \"),a.value.async&&b.write(\"async \"),a.value.generator&&b.write(\"*\"),a.computed?(b.write(\"[\"),this[a.key.type](a.key,b),b.write(\"]\")):this[a.key.type](a.key,b),f(b,a.value.params),b.write(\" \"),this[a.value.body.type](a.value.body,b)},ClassExpression:function ClassExpression(a,b){this.ClassDeclaration(a,b)},ArrowFunctionExpression:function ArrowFunctionExpression(a,b){b.write(a.async?\"async \":\"\",a);var c=a.params;null!=c&&(1===c.length&&\"I\"===c[0].type[0]?b.write(c[0].name,c[0]):f(b,a.params)),b.write(\" => \"),\"O\"===a.body.type[0]?(b.write(\"(\"),this.ObjectExpression(a.body,b),b.write(\")\")):this[a.body.type](a.body,b)},ThisExpression:function ThisExpression(a,b){b.write(\"this\",a)},Super:function Super(a,b){b.write(\"super\",a)},RestElement:t=function(a,b){b.write(\"...\"),this[a.argument.type](a.argument,b)},SpreadElement:t,YieldExpression:function YieldExpression(a,b){b.write(a.delegate?\"yield*\":\"yield\"),a.argument&&(b.write(\" \"),this[a.argument.type](a.argument,b))},AwaitExpression:function AwaitExpression(a,b){b.write(\"await \",a),h(b,a.argument,a)},TemplateLiteral:function TemplateLiteral(a,b){var c=a.quasis,d=a.expressions;b.write(\"`\");for(var e=d.length,f=0;f<e;f++){var g=d[f],h=c[f];b.write(h.value.raw,h),b.write(\"${\"),this[g.type](g,b),b.write(\"}\")}var j=c[c.length-1];b.write(j.value.raw,j),b.write(\"`\")},TemplateElement:function TemplateElement(a,b){b.write(a.value.raw,a)},TaggedTemplateExpression:function TaggedTemplateExpression(a,b){h(b,a.tag,a),this[a.quasi.type](a.quasi,b)},ArrayExpression:v=function(a,b){if(b.write(\"[\"),0<a.elements.length)for(var c,d=a.elements,e=d.length,f=0;;)if(c=d[f],null!=c&&this[c.type](c,b),++f<e)b.write(\", \");else{null==c&&b.write(\", \");break}b.write(\"]\")},ArrayPattern:v,ObjectExpression:function ObjectExpression(a,b){var c=b.indent.repeat(b.indentLevel++),d=b.lineEnd,e=b.writeComments,f=c+b.indent;if(b.write(\"{\"),0<a.properties.length){b.write(d),e&&null!=a.comments&&k(b,a.comments,f,d);for(var g,h=a.properties,j=h.length,l=0;;)if(g=h[l],e&&null!=g.comments&&k(b,g.comments,f,d),b.write(f),this[g.type](g,b),++l<j)b.write(\",\"+d);else break;b.write(d),e&&null!=a.trailingComments&&k(b,a.trailingComments,f,d),b.write(c+\"}\")}else e?null==a.comments?null==a.trailingComments?b.write(\"}\"):(b.write(d),k(b,a.trailingComments,f,d),b.write(c+\"}\")):(b.write(d),k(b,a.comments,f,d),null!=a.trailingComments&&k(b,a.trailingComments,f,d),b.write(c+\"}\")):b.write(\"}\");b.indentLevel--},Property:function Property(a,b){a.method||\"i\"!==a.kind[0]?this.MethodDefinition(a,b):(!a.shorthand&&(a.computed?(b.write(\"[\"),this[a.key.type](a.key,b),b.write(\"]\")):this[a.key.type](a.key,b),b.write(\": \")),this[a.value.type](a.value,b))},PropertyDefinition:function PropertyDefinition(a,b){return a[\"static\"]&&b.write(\"static \"),a.computed&&b.write(\"[\"),this[a.key.type](a.key,b),a.computed&&b.write(\"]\"),null==a.value?void(\"F\"!==a.key.type[0]&&b.write(\";\")):void(b.write(\" = \"),this[a.value.type](a.value,b),b.write(\";\"))},ObjectPattern:function ObjectPattern(a,b){if(b.write(\"{\"),0<a.properties.length)for(var c=a.properties,d=c.length,e=0;;)if(this[c[e].type](c[e],b),++e<d)b.write(\", \");else break;b.write(\"}\")},SequenceExpression:function SequenceExpression(a,b){f(b,a.expressions)},UnaryExpression:function UnaryExpression(a,b){if(a.prefix){var c=a.operator,d=a.argument,e=a.argument.type;b.write(c);var f=g(b,d,a);!f&&(1<c.length||\"U\"===e[0]&&(\"n\"===e[1]||\"p\"===e[1])&&d.prefix&&d.operator[0]===c&&(\"+\"===c||\"-\"===c))&&b.write(\" \"),f?(b.write(1<c.length?\" (\":\"(\"),this[e](d,b),b.write(\")\")):this[e](d,b)}else this[a.argument.type](a.argument,b),b.write(a.operator)},UpdateExpression:function UpdateExpression(a,b){a.prefix?(b.write(a.operator),this[a.argument.type](a.argument,b)):(this[a.argument.type](a.argument,b),b.write(a.operator))},AssignmentExpression:function AssignmentExpression(a,b){this[a.left.type](a.left,b),b.write(\" \"+a.operator+\" \"),this[a.right.type](a.right,b)},AssignmentPattern:function AssignmentPattern(a,b){this[a.left.type](a.left,b),b.write(\" = \"),this[a.right.type](a.right,b)},BinaryExpression:u=function(a,b){var c=\"in\"===a.operator;c&&b.write(\"(\"),h(b,a.left,a,!1),b.write(\" \"+a.operator+\" \"),h(b,a.right,a,!0),c&&b.write(\")\")},LogicalExpression:u,ConditionalExpression:function ConditionalExpression(a,b){var c=a.test,d=b.expressionsPrecedence[c.type];d===p||d<=b.expressionsPrecedence.ConditionalExpression?(b.write(\"(\"),this[c.type](c,b),b.write(\")\")):this[c.type](c,b),b.write(\" ? \"),this[a.consequent.type](a.consequent,b),b.write(\" : \"),this[a.alternate.type](a.alternate,b)},NewExpression:function NewExpression(a,b){b.write(\"new \");var c=b.expressionsPrecedence[a.callee.type];c===p||c<b.expressionsPrecedence.CallExpression||l(a.callee)?(b.write(\"(\"),this[a.callee.type](a.callee,b),b.write(\")\")):this[a.callee.type](a.callee,b),f(b,a.arguments)},CallExpression:function CallExpression(a,b){var c=b.expressionsPrecedence[a.callee.type];c===p||c<b.expressionsPrecedence.CallExpression?(b.write(\"(\"),this[a.callee.type](a.callee,b),b.write(\")\")):this[a.callee.type](a.callee,b),a.optional&&b.write(\"?.\"),f(b,a.arguments)},ChainExpression:function ChainExpression(a,b){this[a.expression.type](a.expression,b)},MemberExpression:function MemberExpression(a,b){var c=b.expressionsPrecedence[a.object.type];c===p||c<b.expressionsPrecedence.MemberExpression?(b.write(\"(\"),this[a.object.type](a.object,b),b.write(\")\")):this[a.object.type](a.object,b),a.computed?(a.optional&&b.write(\"?.\"),b.write(\"[\"),this[a.property.type](a.property,b),b.write(\"]\")):(a.optional?b.write(\"?.\"):b.write(\".\"),this[a.property.type](a.property,b))},MetaProperty:function MetaProperty(a,b){b.write(a.meta.name+\".\"+a.property.name,a)},Identifier:function Identifier(a,b){b.write(a.name,a)},PrivateIdentifier:function PrivateIdentifier(a,b){b.write(\"#\".concat(a.name),a)},Literal:function Literal(a,b){null==a.raw?null==a.regex?null==a.bigint?b.write(n(a.value),a):b.write(a.bigint+\"n\",a):this.RegExpLiteral(a,b):b.write(a.raw,a)},RegExpLiteral:function RegExpLiteral(a,b){var c=a.regex;b.write(\"/\".concat(c.pattern,\"/\").concat(c.flags),a)}};a.GENERATOR=x;var y={};a.baseGenerator=x;var z=function(){function a(b){c(this,a);var d=null==b?y:b;this.output=\"\",null==d.output?this.output=\"\":(this.output=d.output,this.write=this.writeToStream),this.generator=null==d.generator?x:d.generator,this.expressionsPrecedence=null==d.expressionsPrecedence?q:d.expressionsPrecedence,this.indent=null==d.indent?\"  \":d.indent,this.lineEnd=null==d.lineEnd?\"\\n\":d.lineEnd,this.indentLevel=null==d.startingIndentLevel?0:d.startingIndentLevel,this.writeComments=!!d.comments&&d.comments,null!=d.sourceMap&&(this.write=null==d.output?this.writeAndMap:this.writeToStreamAndMap,this.sourceMap=d.sourceMap,this.line=1,this.column=0,this.lineEndSize=this.lineEnd.split(\"\\n\").length-1,this.mapping={original:null,generated:this,name:void 0,source:d.sourceMap.file||d.sourceMap._file})}return e(a,[{key:\"write\",value:function write(a){this.output+=a}},{key:\"writeToStream\",value:function writeToStream(a){this.output.write(a)}},{key:\"writeAndMap\",value:function writeAndMap(a,b){this.output+=a,this.map(a,b)}},{key:\"writeToStreamAndMap\",value:function writeToStreamAndMap(a,b){this.output.write(a),this.map(a,b)}},{key:\"map\",value:function map(a,b){if(null!=b){var c=b.type;if(\"L\"===c[0]&&\"n\"===c[2])return this.column=0,void this.line++;if(null!=b.loc){var d=this.mapping;d.original=b.loc.start,d.name=b.name,this.sourceMap.addMapping(d)}if(\"T\"===c[0]&&\"E\"===c[8]||\"L\"===c[0]&&\"i\"===c[1]&&\"string\"==typeof b.value){for(var e=a.length,f=this.column,g=this.line,h=0;h<e;h++)\"\\n\"===a[h]?(f=0,g++):f++;return this.column=f,void(this.line=g)}}var j=a.length,k=this.lineEnd;0<j&&(0<this.lineEndSize&&(1===k.length?a[j-1]===k:a.endsWith(k))?(this.line+=this.lineEndSize,this.column=0):this.column+=j)}},{key:\"toString\",value:function toString(){return this.output}}]),a}()});\n\n//# sourceMappingURL=astring.min.js.map"
  },
  {
    "path": "app/src/main/assets/solver/meriyah.js",
    "content": "(function (global, factory) {\n    typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :\n    typeof define === 'function' && define.amd ? define(['exports'], factory) :\n    (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.meriyah = {}));\n})(this, (function (exports) { 'use strict';\n\n    const unicodeLookup = ((compressed, lookup) => {\n        const result = new Uint32Array(69632);\n        let index = 0;\n        let subIndex = 0;\n        while (index < 2571) {\n            const inst = compressed[index++];\n            if (inst < 0) {\n                subIndex -= inst;\n            }\n            else {\n                let code = compressed[index++];\n                if (inst & 2)\n                    code = lookup[code];\n                if (inst & 1) {\n                    result.fill(code, subIndex, subIndex += compressed[index++]);\n                }\n                else {\n                    result[subIndex++] = code;\n                }\n            }\n        }\n        return result;\n    })([-1, 2, 26, 2, 27, 2, 5, -1, 0, 77595648, 3, 44, 2, 3, 0, 14, 2, 63, 2, 64, 3, 0, 3, 0, 3168796671, 0, 4294956992, 2, 1, 2, 0, 2, 41, 3, 0, 4, 0, 4294966523, 3, 0, 4, 2, 16, 2, 65, 2, 0, 0, 4294836735, 0, 3221225471, 0, 4294901942, 2, 66, 0, 134152192, 3, 0, 2, 0, 4294951935, 3, 0, 2, 0, 2683305983, 0, 2684354047, 2, 18, 2, 0, 0, 4294961151, 3, 0, 2, 2, 19, 2, 0, 0, 608174079, 2, 0, 2, 60, 2, 7, 2, 6, 0, 4286611199, 3, 0, 2, 2, 1, 3, 0, 3, 0, 4294901711, 2, 40, 0, 4089839103, 0, 2961209759, 0, 1342439375, 0, 4294543342, 0, 3547201023, 0, 1577204103, 0, 4194240, 0, 4294688750, 2, 2, 0, 80831, 0, 4261478351, 0, 4294549486, 2, 2, 0, 2967484831, 0, 196559, 0, 3594373100, 0, 3288319768, 0, 8469959, 0, 65472, 2, 3, 0, 4093640191, 0, 660618719, 0, 65487, 0, 4294828015, 0, 4092591615, 0, 1616920031, 0, 982991, 2, 3, 2, 0, 0, 2163244511, 0, 4227923919, 0, 4236247022, 2, 71, 0, 4284449919, 0, 851904, 2, 4, 2, 12, 0, 67076095, -1, 2, 72, 0, 1073741743, 0, 4093607775, -1, 0, 50331649, 0, 3265266687, 2, 33, 0, 4294844415, 0, 4278190047, 2, 20, 2, 137, -1, 3, 0, 2, 2, 23, 2, 0, 2, 10, 2, 0, 2, 15, 2, 22, 3, 0, 10, 2, 74, 2, 0, 2, 75, 2, 76, 2, 77, 2, 0, 2, 78, 2, 0, 2, 11, 0, 261632, 2, 25, 3, 0, 2, 2, 13, 2, 4, 3, 0, 18, 2, 79, 2, 5, 3, 0, 2, 2, 80, 0, 2151677951, 2, 29, 2, 9, 0, 909311, 3, 0, 2, 0, 814743551, 2, 49, 0, 67090432, 3, 0, 2, 2, 42, 2, 0, 2, 6, 2, 0, 2, 30, 2, 8, 0, 268374015, 2, 110, 2, 51, 2, 0, 2, 81, 0, 134153215, -1, 2, 7, 2, 0, 2, 8, 0, 2684354559, 0, 67044351, 0, 3221160064, 2, 17, -1, 3, 0, 2, 2, 53, 0, 1046528, 3, 0, 3, 2, 9, 2, 0, 2, 54, 0, 4294960127, 2, 10, 2, 6, 2, 11, 0, 4294377472, 2, 12, 3, 0, 16, 2, 13, 2, 0, 2, 82, 2, 10, 2, 0, 2, 83, 2, 84, 2, 85, 0, 12288, 2, 55, 0, 1048577, 2, 86, 2, 14, -1, 2, 14, 0, 131042, 2, 87, 2, 88, 2, 89, 2, 0, 2, 34, -83, 3, 0, 7, 0, 1046559, 2, 0, 2, 15, 2, 0, 0, 2147516671, 2, 21, 3, 90, 2, 2, 0, -16, 2, 91, 0, 524222462, 2, 4, 2, 0, 0, 4269801471, 2, 4, 3, 0, 2, 2, 28, 2, 16, 3, 0, 2, 2, 17, 2, 0, -1, 2, 18, -16, 3, 0, 206, -2, 3, 0, 692, 2, 73, -1, 2, 18, 2, 10, 3, 0, 8, 2, 93, 2, 133, 2, 0, 0, 3220242431, 3, 0, 3, 2, 19, 2, 94, 2, 95, 3, 0, 2, 2, 96, 2, 0, 2, 97, 2, 46, 2, 0, 0, 4351, 2, 0, 2, 9, 3, 0, 2, 0, 67043391, 0, 3909091327, 2, 0, 2, 24, 2, 9, 2, 20, 3, 0, 2, 0, 67076097, 2, 8, 2, 0, 2, 21, 0, 67059711, 0, 4236247039, 3, 0, 2, 0, 939524103, 0, 8191999, 2, 101, 2, 102, 2, 22, 2, 23, 3, 0, 3, 0, 67057663, 3, 0, 349, 2, 103, 2, 104, 2, 7, -264, 3, 0, 11, 2, 24, 3, 0, 2, 2, 32, -1, 0, 3774349439, 2, 105, 2, 106, 3, 0, 2, 2, 19, 2, 107, 3, 0, 10, 2, 10, 2, 18, 2, 0, 2, 47, 2, 0, 2, 31, 2, 108, 2, 25, 0, 1638399, 0, 57344, 2, 109, 3, 0, 3, 2, 20, 2, 26, 2, 27, 2, 5, 2, 28, 2, 0, 2, 8, 2, 111, -1, 2, 112, 2, 113, 2, 114, -1, 3, 0, 3, 2, 12, -2, 2, 0, 2, 29, -3, 0, 536870912, -4, 2, 20, 2, 0, 2, 36, 0, 1, 2, 0, 2, 67, 2, 6, 2, 12, 2, 10, 2, 0, 2, 115, -1, 3, 0, 4, 2, 10, 2, 23, 2, 116, 2, 7, 2, 0, 2, 117, 2, 0, 2, 118, 2, 119, 2, 120, 2, 0, 2, 9, 3, 0, 9, 2, 21, 2, 30, 2, 31, 2, 121, 2, 122, -2, 2, 123, 2, 124, 2, 30, 2, 21, 2, 8, -2, 2, 125, 2, 30, 2, 32, -2, 2, 0, 2, 39, -2, 0, 4277137519, 0, 2269118463, -1, 3, 20, 2, -1, 2, 33, 2, 38, 2, 0, 3, 30, 2, 2, 35, 2, 19, -3, 3, 0, 2, 2, 34, -1, 2, 0, 2, 35, 2, 0, 2, 35, 2, 0, 2, 48, 2, 0, 0, 4294950463, 2, 37, -7, 2, 0, 0, 203775, 2, 57, 0, 4026531840, 2, 20, 2, 43, 2, 36, 2, 18, 2, 37, 2, 18, 2, 126, 2, 21, 3, 0, 2, 2, 38, 0, 2151677888, 2, 0, 2, 12, 0, 4294901764, 2, 144, 2, 0, 2, 58, 2, 56, 0, 5242879, 3, 0, 2, 0, 402644511, -1, 2, 128, 2, 39, 0, 3, -1, 2, 129, 2, 130, 2, 0, 0, 67045375, 2, 40, 0, 4226678271, 0, 3766565279, 0, 2039759, 2, 132, 2, 41, 0, 1046437, 0, 6, 3, 0, 2, 0, 3288270847, 0, 3, 3, 0, 2, 0, 67043519, -5, 2, 0, 0, 4282384383, 0, 1056964609, -1, 3, 0, 2, 0, 67043345, -1, 2, 0, 2, 42, 2, 23, 2, 50, 2, 11, 2, 61, 2, 38, -5, 2, 0, 2, 12, -3, 3, 0, 2, 0, 2147484671, 2, 134, 0, 4190109695, 2, 52, -2, 2, 135, 0, 4244635647, 0, 27, 2, 0, 2, 8, 2, 43, 2, 0, 2, 68, 2, 18, 2, 0, 2, 42, -6, 2, 0, 2, 45, 2, 59, 2, 44, 2, 45, 2, 46, 2, 47, 0, 8388351, -2, 2, 136, 0, 3028287487, 2, 48, 2, 138, 0, 33259519, 2, 49, -9, 2, 21, 0, 4294836223, 0, 3355443199, 0, 134152199, -2, 2, 69, -2, 3, 0, 28, 2, 32, -3, 3, 0, 3, 2, 17, 3, 0, 6, 2, 50, -81, 2, 18, 3, 0, 2, 2, 36, 3, 0, 33, 2, 25, 2, 30, 3, 0, 124, 2, 12, 3, 0, 18, 2, 38, -213, 2, 0, 2, 32, -54, 3, 0, 17, 2, 42, 2, 8, 2, 23, 2, 0, 2, 8, 2, 23, 2, 51, 2, 0, 2, 21, 2, 52, 2, 139, 2, 25, -13, 2, 0, 2, 53, -6, 3, 0, 2, -4, 3, 0, 2, 0, 4294936575, 2, 0, 0, 4294934783, -2, 0, 196635, 3, 0, 191, 2, 54, 3, 0, 38, 2, 30, 2, 55, 2, 34, -278, 2, 140, 3, 0, 9, 2, 141, 2, 142, 2, 56, 3, 0, 11, 2, 7, -72, 3, 0, 3, 2, 143, 0, 1677656575, -130, 2, 26, -16, 2, 0, 2, 24, 2, 38, -16, 0, 4161266656, 0, 4071, 0, 15360, -4, 2, 57, -13, 3, 0, 2, 2, 58, 2, 0, 2, 145, 2, 146, 2, 62, 2, 0, 2, 147, 2, 148, 2, 149, 3, 0, 10, 2, 150, 2, 151, 2, 22, 3, 58, 2, 3, 152, 2, 3, 59, 2, 0, 4294954999, 2, 0, -16, 2, 0, 2, 92, 2, 0, 0, 2105343, 0, 4160749584, 0, 65534, -34, 2, 8, 2, 154, -6, 0, 4194303871, 0, 4294903771, 2, 0, 2, 60, 2, 100, -3, 2, 0, 0, 1073684479, 0, 17407, -9, 2, 18, 2, 17, 2, 0, 2, 32, -14, 2, 18, 2, 32, -6, 2, 18, 2, 12, -15, 2, 155, 3, 0, 6, 0, 8323103, -1, 3, 0, 2, 2, 61, -37, 2, 62, 2, 156, 2, 157, 2, 158, 2, 159, 2, 160, -105, 2, 26, -32, 3, 0, 1335, -1, 3, 0, 129, 2, 32, 3, 0, 6, 2, 10, 3, 0, 180, 2, 161, 3, 0, 233, 2, 162, 3, 0, 18, 2, 10, -77, 3, 0, 16, 2, 10, -47, 3, 0, 154, 2, 6, 3, 0, 130, 2, 25, -22250, 3, 0, 7, 2, 25, -6130, 3, 5, 2, -1, 0, 69207040, 3, 44, 2, 3, 0, 14, 2, 63, 2, 64, -3, 0, 3168731136, 0, 4294956864, 2, 1, 2, 0, 2, 41, 3, 0, 4, 0, 4294966275, 3, 0, 4, 2, 16, 2, 65, 2, 0, 2, 34, -1, 2, 18, 2, 66, -1, 2, 0, 0, 2047, 0, 4294885376, 3, 0, 2, 0, 3145727, 0, 2617294944, 0, 4294770688, 2, 25, 2, 67, 3, 0, 2, 0, 131135, 2, 98, 0, 70256639, 0, 71303167, 0, 272, 2, 42, 2, 6, 0, 32511, 2, 0, 2, 49, -1, 2, 99, 2, 68, 0, 4278255616, 0, 4294836227, 0, 4294549473, 0, 600178175, 0, 2952806400, 0, 268632067, 0, 4294543328, 0, 57540095, 0, 1577058304, 0, 1835008, 0, 4294688736, 2, 70, 2, 69, 0, 33554435, 2, 131, 2, 70, 0, 2952790016, 0, 131075, 0, 3594373096, 0, 67094296, 2, 69, -1, 0, 4294828000, 0, 603979263, 0, 654311424, 0, 3, 0, 4294828001, 0, 602930687, 0, 1610612736, 0, 393219, 0, 4294828016, 0, 671088639, 0, 2154840064, 0, 4227858435, 0, 4236247008, 2, 71, 2, 38, -1, 2, 4, 0, 917503, 2, 38, -1, 2, 72, 0, 537788335, 0, 4026531935, -1, 0, 1, -1, 2, 33, 2, 73, 0, 7936, -3, 2, 0, 0, 2147485695, 0, 1010761728, 0, 4292984930, 0, 16387, 2, 0, 2, 15, 2, 22, 3, 0, 10, 2, 74, 2, 0, 2, 75, 2, 76, 2, 77, 2, 0, 2, 78, 2, 0, 2, 12, -1, 2, 25, 3, 0, 2, 2, 13, 2, 4, 3, 0, 18, 2, 79, 2, 5, 3, 0, 2, 2, 80, 0, 2147745791, 3, 19, 2, 0, 122879, 2, 0, 2, 9, 0, 276824064, -2, 3, 0, 2, 2, 42, 2, 0, 0, 4294903295, 2, 0, 2, 30, 2, 8, -1, 2, 18, 2, 51, 2, 0, 2, 81, 2, 49, -1, 2, 21, 2, 0, 2, 29, -2, 0, 128, -2, 2, 28, 2, 9, 0, 8160, -1, 2, 127, 0, 4227907585, 2, 0, 2, 37, 2, 0, 2, 50, 0, 4227915776, 2, 10, 2, 6, 2, 11, -1, 0, 74440192, 3, 0, 6, -2, 3, 0, 8, 2, 13, 2, 0, 2, 82, 2, 10, 2, 0, 2, 83, 2, 84, 2, 85, -3, 2, 86, 2, 14, -3, 2, 87, 2, 88, 2, 89, 2, 0, 2, 34, -83, 3, 0, 7, 0, 817183, 2, 0, 2, 15, 2, 0, 0, 33023, 2, 21, 3, 90, 2, -17, 2, 91, 0, 524157950, 2, 4, 2, 0, 2, 92, 2, 4, 2, 0, 2, 22, 2, 28, 2, 16, 3, 0, 2, 2, 17, 2, 0, -1, 2, 18, -16, 3, 0, 206, -2, 3, 0, 692, 2, 73, -1, 2, 18, 2, 10, 3, 0, 8, 2, 93, 0, 3072, 2, 0, 0, 2147516415, 2, 10, 3, 0, 2, 2, 25, 2, 94, 2, 95, 3, 0, 2, 2, 96, 2, 0, 2, 97, 2, 46, 0, 4294965179, 0, 7, 2, 0, 2, 9, 2, 95, 2, 9, -1, 0, 1761345536, 2, 98, 0, 4294901823, 2, 38, 2, 20, 2, 99, 2, 35, 2, 100, 0, 2080440287, 2, 0, 2, 34, 2, 153, 0, 3296722943, 2, 0, 0, 1046675455, 0, 939524101, 0, 1837055, 2, 101, 2, 102, 2, 22, 2, 23, 3, 0, 3, 0, 7, 3, 0, 349, 2, 103, 2, 104, 2, 7, -264, 3, 0, 11, 2, 24, 3, 0, 2, 2, 32, -1, 0, 2700607615, 2, 105, 2, 106, 3, 0, 2, 2, 19, 2, 107, 3, 0, 10, 2, 10, 2, 18, 2, 0, 2, 47, 2, 0, 2, 31, 2, 108, -3, 2, 109, 3, 0, 3, 2, 20, -1, 3, 5, 2, 2, 110, 2, 0, 2, 8, 2, 111, -1, 2, 112, 2, 113, 2, 114, -1, 3, 0, 3, 2, 12, -2, 2, 0, 2, 29, -8, 2, 20, 2, 0, 2, 36, -1, 2, 0, 2, 67, 2, 6, 2, 30, 2, 10, 2, 0, 2, 115, -1, 3, 0, 4, 2, 10, 2, 18, 2, 116, 2, 7, 2, 0, 2, 117, 2, 0, 2, 118, 2, 119, 2, 120, 2, 0, 2, 9, 3, 0, 9, 2, 21, 2, 30, 2, 31, 2, 121, 2, 122, -2, 2, 123, 2, 124, 2, 30, 2, 21, 2, 8, -2, 2, 125, 2, 30, 2, 32, -2, 2, 0, 2, 39, -2, 0, 4277075969, 2, 30, -1, 3, 20, 2, -1, 2, 33, 2, 126, 2, 0, 3, 30, 2, 2, 35, 2, 19, -3, 3, 0, 2, 2, 34, -1, 2, 0, 2, 35, 2, 0, 2, 35, 2, 0, 2, 50, 2, 98, 0, 4294934591, 2, 37, -7, 2, 0, 0, 197631, 2, 57, -1, 2, 20, 2, 43, 2, 37, 2, 18, 0, 3, 2, 18, 2, 126, 2, 21, 2, 127, 2, 54, -1, 0, 2490368, 2, 127, 2, 25, 2, 18, 2, 34, 2, 127, 2, 38, 0, 4294901904, 0, 4718591, 2, 127, 2, 35, 0, 335544350, -1, 2, 128, 0, 2147487743, 0, 1, -1, 2, 129, 2, 130, 2, 8, -1, 2, 131, 2, 70, 0, 3758161920, 0, 3, 2, 132, 0, 12582911, 0, 655360, -1, 2, 0, 2, 29, 0, 2147485568, 0, 3, 2, 0, 2, 25, 0, 176, -5, 2, 0, 2, 17, 0, 251658240, -1, 2, 0, 2, 25, 0, 16, -1, 2, 0, 0, 16779263, -2, 2, 12, -1, 2, 38, -5, 2, 0, 2, 133, -3, 3, 0, 2, 2, 55, 2, 134, 0, 2147549183, 0, 2, -2, 2, 135, 2, 36, 0, 10, 0, 4294965249, 0, 67633151, 0, 4026597376, 2, 0, 0, 536871935, 2, 18, 2, 0, 2, 42, -6, 2, 0, 0, 1, 2, 59, 2, 17, 0, 1, 2, 46, 2, 25, -3, 2, 136, 2, 36, 2, 137, 2, 138, 0, 16778239, -10, 2, 35, 0, 4294836212, 2, 9, -3, 2, 69, -2, 3, 0, 28, 2, 32, -3, 3, 0, 3, 2, 17, 3, 0, 6, 2, 50, -81, 2, 18, 3, 0, 2, 2, 36, 3, 0, 33, 2, 25, 0, 126, 3, 0, 124, 2, 12, 3, 0, 18, 2, 38, -213, 2, 10, -55, 3, 0, 17, 2, 42, 2, 8, 2, 18, 2, 0, 2, 8, 2, 18, 2, 60, 2, 0, 2, 25, 2, 50, 2, 139, 2, 25, -13, 2, 0, 2, 73, -6, 3, 0, 2, -4, 3, 0, 2, 0, 67583, -1, 2, 107, -2, 0, 11, 3, 0, 191, 2, 54, 3, 0, 38, 2, 30, 2, 55, 2, 34, -278, 2, 140, 3, 0, 9, 2, 141, 2, 142, 2, 56, 3, 0, 11, 2, 7, -72, 3, 0, 3, 2, 143, 2, 144, -187, 3, 0, 2, 2, 58, 2, 0, 2, 145, 2, 146, 2, 62, 2, 0, 2, 147, 2, 148, 2, 149, 3, 0, 10, 2, 150, 2, 151, 2, 22, 3, 58, 2, 3, 152, 2, 3, 59, 2, 2, 153, -57, 2, 8, 2, 154, -7, 2, 18, 2, 0, 2, 60, -4, 2, 0, 0, 1065361407, 0, 16384, -9, 2, 18, 2, 60, 2, 0, 2, 133, -14, 2, 18, 2, 133, -6, 2, 18, 0, 81919, -15, 2, 155, 3, 0, 6, 2, 126, -1, 3, 0, 2, 0, 2063, -37, 2, 62, 2, 156, 2, 157, 2, 158, 2, 159, 2, 160, -138, 3, 0, 1335, -1, 3, 0, 129, 2, 32, 3, 0, 6, 2, 10, 3, 0, 180, 2, 161, 3, 0, 233, 2, 162, 3, 0, 18, 2, 10, -77, 3, 0, 16, 2, 10, -47, 3, 0, 154, 2, 6, 3, 0, 130, 2, 25, -28386], [4294967295, 4294967291, 4092460543, 4294828031, 4294967294, 134217726, 4294903807, 268435455, 2147483647, 1048575, 1073741823, 3892314111, 134217727, 1061158911, 536805376, 4294910143, 4294901759, 32767, 4294901760, 262143, 536870911, 8388607, 4160749567, 4294902783, 4294918143, 65535, 67043328, 2281701374, 4294967264, 2097151, 4194303, 255, 67108863, 4294967039, 511, 524287, 131071, 63, 127, 3238002687, 4294549487, 4290772991, 33554431, 4294901888, 4286578687, 67043329, 4294705152, 4294770687, 67043583, 1023, 15, 2047999, 67043343, 67051519, 16777215, 2147483648, 4294902000, 28, 4292870143, 4294966783, 16383, 67047423, 4294967279, 262083, 20511, 41943039, 493567, 4294959104, 603979775, 65536, 602799615, 805044223, 4294965206, 8191, 1031749119, 4294917631, 2134769663, 4286578493, 4282253311, 4294942719, 33540095, 4294905855, 2868854591, 1608515583, 265232348, 534519807, 2147614720, 1060109444, 4093640016, 17376, 2139062143, 224, 4169138175, 4294909951, 4286578688, 4294967292, 4294965759, 535511039, 4294966272, 4294967280, 32768, 8289918, 4294934399, 4294901775, 4294965375, 1602223615, 4294967259, 4294443008, 268369920, 4292804608, 4294967232, 486341884, 4294963199, 3087007615, 1073692671, 4128527, 4279238655, 4294902015, 4160684047, 4290246655, 469499899, 4294967231, 134086655, 4294966591, 2445279231, 3670015, 31, 4294967288, 4294705151, 3221208447, 4294902271, 4294549472, 4294921215, 4095, 4285526655, 4294966527, 4294966143, 64, 4294966719, 3774873592, 1877934080, 262151, 2555904, 536807423, 67043839, 3758096383, 3959414372, 3755993023, 2080374783, 4294835295, 4294967103, 4160749565, 4294934527, 4087, 2016, 2147446655, 184024726, 2862017156, 1593309078, 268434431, 268434414, 4294901763, 4294901761]);\n    const isIDContinue = (code) => (unicodeLookup[(code >>> 5) + 0] >>> code & 31 & 1) !== 0;\n    const isIDStart = (code) => (unicodeLookup[(code >>> 5) + 34816] >>> code & 31 & 1) !== 0;\n\n    function advanceChar(parser) {\n        parser.column++;\n        return (parser.currentChar = parser.source.charCodeAt(++parser.index));\n    }\n    function consumePossibleSurrogatePair(parser) {\n        const hi = parser.currentChar;\n        if ((hi & 0xfc00) !== 55296)\n            return 0;\n        const lo = parser.source.charCodeAt(parser.index + 1);\n        if ((lo & 0xfc00) !== 56320)\n            return 0;\n        return 65536 + ((hi & 0x3ff) << 10) + (lo & 0x3ff);\n    }\n    function consumeLineFeed(parser, state) {\n        parser.currentChar = parser.source.charCodeAt(++parser.index);\n        parser.flags |= 1;\n        if ((state & 4) === 0) {\n            parser.column = 0;\n            parser.line++;\n        }\n    }\n    function scanNewLine(parser) {\n        parser.flags |= 1;\n        parser.currentChar = parser.source.charCodeAt(++parser.index);\n        parser.column = 0;\n        parser.line++;\n    }\n    function isExoticECMAScriptWhitespace(ch) {\n        return (ch === 160 ||\n            ch === 65279 ||\n            ch === 133 ||\n            ch === 5760 ||\n            (ch >= 8192 && ch <= 8203) ||\n            ch === 8239 ||\n            ch === 8287 ||\n            ch === 12288 ||\n            ch === 8201 ||\n            ch === 65519);\n    }\n    function toHex(code) {\n        return code < 65 ? code - 48 : (code - 65 + 10) & 0xf;\n    }\n    function convertTokenType(t) {\n        switch (t) {\n            case 134283266:\n                return 'NumericLiteral';\n            case 134283267:\n                return 'StringLiteral';\n            case 86021:\n            case 86022:\n                return 'BooleanLiteral';\n            case 86023:\n                return 'NullLiteral';\n            case 65540:\n                return 'RegularExpression';\n            case 67174408:\n            case 67174409:\n            case 131:\n                return 'TemplateLiteral';\n            default:\n                if ((t & 143360) === 143360)\n                    return 'Identifier';\n                if ((t & 4096) === 4096)\n                    return 'Keyword';\n                return 'Punctuator';\n        }\n    }\n\n    const CharTypes = [\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        8 | 1024,\n        0,\n        0,\n        8 | 2048,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        8192,\n        0,\n        1 | 2,\n        0,\n        0,\n        8192,\n        0,\n        0,\n        0,\n        256,\n        0,\n        256 | 32768,\n        0,\n        0,\n        2 | 16 | 128 | 32 | 64,\n        2 | 16 | 128 | 32 | 64,\n        2 | 16 | 32 | 64,\n        2 | 16 | 32 | 64,\n        2 | 16 | 32 | 64,\n        2 | 16 | 32 | 64,\n        2 | 16 | 32 | 64,\n        2 | 16 | 32 | 64,\n        2 | 16 | 512 | 64,\n        2 | 16 | 512 | 64,\n        0,\n        0,\n        16384,\n        0,\n        0,\n        0,\n        0,\n        1 | 2 | 64,\n        1 | 2 | 64,\n        1 | 2 | 64,\n        1 | 2 | 64,\n        1 | 2 | 64,\n        1 | 2 | 64,\n        1 | 2,\n        1 | 2,\n        1 | 2,\n        1 | 2,\n        1 | 2,\n        1 | 2,\n        1 | 2,\n        1 | 2,\n        1 | 2,\n        1 | 2,\n        1 | 2,\n        1 | 2,\n        1 | 2,\n        1 | 2,\n        1 | 2,\n        1 | 2,\n        1 | 2,\n        1 | 2,\n        1 | 2,\n        1 | 2,\n        0,\n        1,\n        0,\n        0,\n        1 | 2 | 4096,\n        0,\n        1 | 2 | 4 | 64,\n        1 | 2 | 4 | 64,\n        1 | 2 | 4 | 64,\n        1 | 2 | 4 | 64,\n        1 | 2 | 4 | 64,\n        1 | 2 | 4 | 64,\n        1 | 2 | 4,\n        1 | 2 | 4,\n        1 | 2 | 4,\n        1 | 2 | 4,\n        1 | 2 | 4,\n        1 | 2 | 4,\n        1 | 2 | 4,\n        1 | 2 | 4,\n        1 | 2 | 4,\n        1 | 2 | 4,\n        1 | 2 | 4,\n        1 | 2 | 4,\n        1 | 2 | 4,\n        1 | 2 | 4,\n        1 | 2 | 4,\n        1 | 2 | 4,\n        1 | 2 | 4,\n        1 | 2 | 4,\n        1 | 2 | 4,\n        1 | 2 | 4,\n        16384,\n        0,\n        0,\n        0,\n        0\n    ];\n    const isIdStart = [\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        1,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        0,\n        0,\n        0,\n        0,\n        1,\n        0,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        0,\n        0,\n        0,\n        0,\n        0\n    ];\n    const isIdPart = [\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        1,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        0,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        0,\n        0,\n        0,\n        0,\n        1,\n        0,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        1,\n        0,\n        0,\n        0,\n        0,\n        0\n    ];\n    function isIdentifierStart(code) {\n        return code <= 0x7F\n            ? isIdStart[code] > 0\n            : isIDStart(code);\n    }\n    function isIdentifierPart(code) {\n        return code <= 0x7F\n            ? isIdPart[code] > 0\n            : isIDContinue(code) || (code === 8204 || code === 8205);\n    }\n\n    const CommentTypes = ['SingleLine', 'MultiLine', 'HTMLOpen', 'HTMLClose', 'HashbangComment'];\n    function skipHashBang(parser) {\n        const { source } = parser;\n        if (parser.currentChar === 35 && source.charCodeAt(parser.index + 1) === 33) {\n            advanceChar(parser);\n            advanceChar(parser);\n            skipSingleLineComment(parser, source, 0, 4, parser.tokenStart);\n        }\n    }\n    function skipSingleHTMLComment(parser, source, state, context, type, start) {\n        if (context & 2)\n            parser.report(0);\n        return skipSingleLineComment(parser, source, state, type, start);\n    }\n    function skipSingleLineComment(parser, source, state, type, start) {\n        const { index } = parser;\n        parser.tokenIndex = parser.index;\n        parser.tokenLine = parser.line;\n        parser.tokenColumn = parser.column;\n        while (parser.index < parser.end) {\n            if (CharTypes[parser.currentChar] & 8) {\n                const isCR = parser.currentChar === 13;\n                scanNewLine(parser);\n                if (isCR && parser.index < parser.end && parser.currentChar === 10)\n                    parser.currentChar = source.charCodeAt(++parser.index);\n                break;\n            }\n            else if ((parser.currentChar ^ 8232) <= 1) {\n                scanNewLine(parser);\n                break;\n            }\n            advanceChar(parser);\n            parser.tokenIndex = parser.index;\n            parser.tokenLine = parser.line;\n            parser.tokenColumn = parser.column;\n        }\n        if (parser.options.onComment) {\n            const loc = {\n                start: {\n                    line: start.line,\n                    column: start.column,\n                },\n                end: {\n                    line: parser.tokenLine,\n                    column: parser.tokenColumn,\n                },\n            };\n            parser.options.onComment(CommentTypes[type & 0xff], source.slice(index, parser.tokenIndex), start.index, parser.tokenIndex, loc);\n        }\n        return state | 1;\n    }\n    function skipMultiLineComment(parser, source, state) {\n        const { index } = parser;\n        while (parser.index < parser.end) {\n            if (parser.currentChar < 0x2b) {\n                let skippedOneAsterisk = false;\n                while (parser.currentChar === 42) {\n                    if (!skippedOneAsterisk) {\n                        state &= -5;\n                        skippedOneAsterisk = true;\n                    }\n                    if (advanceChar(parser) === 47) {\n                        advanceChar(parser);\n                        if (parser.options.onComment) {\n                            const loc = {\n                                start: {\n                                    line: parser.tokenLine,\n                                    column: parser.tokenColumn,\n                                },\n                                end: {\n                                    line: parser.line,\n                                    column: parser.column,\n                                },\n                            };\n                            parser.options.onComment(CommentTypes[1 & 0xff], source.slice(index, parser.index - 2), index - 2, parser.index, loc);\n                        }\n                        parser.tokenIndex = parser.index;\n                        parser.tokenLine = parser.line;\n                        parser.tokenColumn = parser.column;\n                        return state;\n                    }\n                }\n                if (skippedOneAsterisk) {\n                    continue;\n                }\n                if (CharTypes[parser.currentChar] & 8) {\n                    if (parser.currentChar === 13) {\n                        state |= 1 | 4;\n                        scanNewLine(parser);\n                    }\n                    else {\n                        consumeLineFeed(parser, state);\n                        state = (state & -5) | 1;\n                    }\n                }\n                else {\n                    advanceChar(parser);\n                }\n            }\n            else if ((parser.currentChar ^ 8232) <= 1) {\n                state = (state & -5) | 1;\n                scanNewLine(parser);\n            }\n            else {\n                state &= -5;\n                advanceChar(parser);\n            }\n        }\n        parser.report(18);\n    }\n\n    var RegexState;\n    (function (RegexState) {\n        RegexState[RegexState[\"Empty\"] = 0] = \"Empty\";\n        RegexState[RegexState[\"Escape\"] = 1] = \"Escape\";\n        RegexState[RegexState[\"Class\"] = 2] = \"Class\";\n    })(RegexState || (RegexState = {}));\n    var RegexFlags;\n    (function (RegexFlags) {\n        RegexFlags[RegexFlags[\"Empty\"] = 0] = \"Empty\";\n        RegexFlags[RegexFlags[\"IgnoreCase\"] = 1] = \"IgnoreCase\";\n        RegexFlags[RegexFlags[\"Global\"] = 2] = \"Global\";\n        RegexFlags[RegexFlags[\"Multiline\"] = 4] = \"Multiline\";\n        RegexFlags[RegexFlags[\"Unicode\"] = 16] = \"Unicode\";\n        RegexFlags[RegexFlags[\"Sticky\"] = 8] = \"Sticky\";\n        RegexFlags[RegexFlags[\"DotAll\"] = 32] = \"DotAll\";\n        RegexFlags[RegexFlags[\"Indices\"] = 64] = \"Indices\";\n        RegexFlags[RegexFlags[\"UnicodeSets\"] = 128] = \"UnicodeSets\";\n    })(RegexFlags || (RegexFlags = {}));\n    function scanRegularExpression(parser) {\n        const bodyStart = parser.index;\n        let preparseState = RegexState.Empty;\n        loop: while (true) {\n            const ch = parser.currentChar;\n            advanceChar(parser);\n            if (preparseState & RegexState.Escape) {\n                preparseState &= ~RegexState.Escape;\n            }\n            else {\n                switch (ch) {\n                    case 47:\n                        if (!preparseState)\n                            break loop;\n                        else\n                            break;\n                    case 92:\n                        preparseState |= RegexState.Escape;\n                        break;\n                    case 91:\n                        preparseState |= RegexState.Class;\n                        break;\n                    case 93:\n                        preparseState &= RegexState.Escape;\n                        break;\n                }\n            }\n            if (ch === 13 ||\n                ch === 10 ||\n                ch === 8232 ||\n                ch === 8233) {\n                parser.report(34);\n            }\n            if (parser.index >= parser.source.length) {\n                return parser.report(34);\n            }\n        }\n        const bodyEnd = parser.index - 1;\n        let mask = RegexFlags.Empty;\n        let char = parser.currentChar;\n        const { index: flagStart } = parser;\n        while (isIdentifierPart(char)) {\n            switch (char) {\n                case 103:\n                    if (mask & RegexFlags.Global)\n                        parser.report(36, 'g');\n                    mask |= RegexFlags.Global;\n                    break;\n                case 105:\n                    if (mask & RegexFlags.IgnoreCase)\n                        parser.report(36, 'i');\n                    mask |= RegexFlags.IgnoreCase;\n                    break;\n                case 109:\n                    if (mask & RegexFlags.Multiline)\n                        parser.report(36, 'm');\n                    mask |= RegexFlags.Multiline;\n                    break;\n                case 117:\n                    if (mask & RegexFlags.Unicode)\n                        parser.report(36, 'u');\n                    if (mask & RegexFlags.UnicodeSets)\n                        parser.report(36, 'vu');\n                    mask |= RegexFlags.Unicode;\n                    break;\n                case 118:\n                    if (mask & RegexFlags.Unicode)\n                        parser.report(36, 'uv');\n                    if (mask & RegexFlags.UnicodeSets)\n                        parser.report(36, 'v');\n                    mask |= RegexFlags.UnicodeSets;\n                    break;\n                case 121:\n                    if (mask & RegexFlags.Sticky)\n                        parser.report(36, 'y');\n                    mask |= RegexFlags.Sticky;\n                    break;\n                case 115:\n                    if (mask & RegexFlags.DotAll)\n                        parser.report(36, 's');\n                    mask |= RegexFlags.DotAll;\n                    break;\n                case 100:\n                    if (mask & RegexFlags.Indices)\n                        parser.report(36, 'd');\n                    mask |= RegexFlags.Indices;\n                    break;\n                default:\n                    parser.report(35);\n            }\n            char = advanceChar(parser);\n        }\n        const flags = parser.source.slice(flagStart, parser.index);\n        const pattern = parser.source.slice(bodyStart, bodyEnd);\n        parser.tokenRegExp = { pattern, flags };\n        if (parser.options.raw)\n            parser.tokenRaw = parser.source.slice(parser.tokenIndex, parser.index);\n        parser.tokenValue = validate(parser, pattern, flags);\n        return 65540;\n    }\n    function validate(parser, pattern, flags) {\n        try {\n            return new RegExp(pattern, flags);\n        }\n        catch {\n            try {\n                new RegExp(pattern, flags);\n                return null;\n            }\n            catch {\n                parser.report(34);\n            }\n        }\n    }\n\n    function scanString(parser, context, quote) {\n        const { index: start } = parser;\n        let ret = '';\n        let char = advanceChar(parser);\n        let marker = parser.index;\n        while ((CharTypes[char] & 8) === 0) {\n            if (char === quote) {\n                ret += parser.source.slice(marker, parser.index);\n                advanceChar(parser);\n                if (parser.options.raw)\n                    parser.tokenRaw = parser.source.slice(start, parser.index);\n                parser.tokenValue = ret;\n                return 134283267;\n            }\n            if ((char & 8) === 8 && char === 92) {\n                ret += parser.source.slice(marker, parser.index);\n                char = advanceChar(parser);\n                if (char < 0x7f || char === 8232 || char === 8233) {\n                    const code = parseEscape(parser, context, char);\n                    if (code >= 0)\n                        ret += String.fromCodePoint(code);\n                    else\n                        handleStringError(parser, code, 0);\n                }\n                else {\n                    ret += String.fromCodePoint(char);\n                }\n                marker = parser.index + 1;\n            }\n            else if (char === 8232 || char === 8233) {\n                parser.column = -1;\n                parser.line++;\n            }\n            if (parser.index >= parser.end)\n                parser.report(16);\n            char = advanceChar(parser);\n        }\n        parser.report(16);\n    }\n    function parseEscape(parser, context, first, isTemplate = 0) {\n        switch (first) {\n            case 98:\n                return 8;\n            case 102:\n                return 12;\n            case 114:\n                return 13;\n            case 110:\n                return 10;\n            case 116:\n                return 9;\n            case 118:\n                return 11;\n            case 13: {\n                if (parser.index < parser.end) {\n                    const nextChar = parser.source.charCodeAt(parser.index + 1);\n                    if (nextChar === 10) {\n                        parser.index = parser.index + 1;\n                        parser.currentChar = nextChar;\n                    }\n                }\n            }\n            case 10:\n            case 8232:\n            case 8233:\n                parser.column = -1;\n                parser.line++;\n                return -1;\n            case 48:\n            case 49:\n            case 50:\n            case 51: {\n                let code = first - 48;\n                let index = parser.index + 1;\n                let column = parser.column + 1;\n                if (index < parser.end) {\n                    const next = parser.source.charCodeAt(index);\n                    if ((CharTypes[next] & 32) === 0) {\n                        if (code !== 0 || CharTypes[next] & 512) {\n                            if (context & 1 || isTemplate)\n                                return -2;\n                            parser.flags |= 64;\n                        }\n                    }\n                    else if (context & 1 || isTemplate) {\n                        return -2;\n                    }\n                    else {\n                        parser.currentChar = next;\n                        code = (code << 3) | (next - 48);\n                        index++;\n                        column++;\n                        if (index < parser.end) {\n                            const next = parser.source.charCodeAt(index);\n                            if (CharTypes[next] & 32) {\n                                parser.currentChar = next;\n                                code = (code << 3) | (next - 48);\n                                index++;\n                                column++;\n                            }\n                        }\n                        parser.flags |= 64;\n                    }\n                    parser.index = index - 1;\n                    parser.column = column - 1;\n                }\n                return code;\n            }\n            case 52:\n            case 53:\n            case 54:\n            case 55: {\n                if (isTemplate || context & 1)\n                    return -2;\n                let code = first - 48;\n                const index = parser.index + 1;\n                const column = parser.column + 1;\n                if (index < parser.end) {\n                    const next = parser.source.charCodeAt(index);\n                    if (CharTypes[next] & 32) {\n                        code = (code << 3) | (next - 48);\n                        parser.currentChar = next;\n                        parser.index = index;\n                        parser.column = column;\n                    }\n                }\n                parser.flags |= 64;\n                return code;\n            }\n            case 120: {\n                const ch1 = advanceChar(parser);\n                if ((CharTypes[ch1] & 64) === 0)\n                    return -4;\n                const hi = toHex(ch1);\n                const ch2 = advanceChar(parser);\n                if ((CharTypes[ch2] & 64) === 0)\n                    return -4;\n                const lo = toHex(ch2);\n                return (hi << 4) | lo;\n            }\n            case 117: {\n                const ch = advanceChar(parser);\n                if (parser.currentChar === 123) {\n                    let code = 0;\n                    while ((CharTypes[advanceChar(parser)] & 64) !== 0) {\n                        code = (code << 4) | toHex(parser.currentChar);\n                        if (code > 1114111)\n                            return -5;\n                    }\n                    if (parser.currentChar < 1 || parser.currentChar !== 125) {\n                        return -4;\n                    }\n                    return code;\n                }\n                else {\n                    if ((CharTypes[ch] & 64) === 0)\n                        return -4;\n                    const ch2 = parser.source.charCodeAt(parser.index + 1);\n                    if ((CharTypes[ch2] & 64) === 0)\n                        return -4;\n                    const ch3 = parser.source.charCodeAt(parser.index + 2);\n                    if ((CharTypes[ch3] & 64) === 0)\n                        return -4;\n                    const ch4 = parser.source.charCodeAt(parser.index + 3);\n                    if ((CharTypes[ch4] & 64) === 0)\n                        return -4;\n                    parser.index += 3;\n                    parser.column += 3;\n                    parser.currentChar = parser.source.charCodeAt(parser.index);\n                    return (toHex(ch) << 12) | (toHex(ch2) << 8) | (toHex(ch3) << 4) | toHex(ch4);\n                }\n            }\n            case 56:\n            case 57:\n                if (isTemplate || !parser.options.webcompat || context & 1)\n                    return -3;\n                parser.flags |= 4096;\n            default:\n                return first;\n        }\n    }\n    function handleStringError(parser, code, isTemplate) {\n        switch (code) {\n            case -1:\n                return;\n            case -2:\n                parser.report(isTemplate ? 2 : 1);\n            case -3:\n                parser.report(isTemplate ? 3 : 14);\n            case -4:\n                parser.report(7);\n            case -5:\n                parser.report(104);\n        }\n    }\n\n    function scanTemplate(parser, context) {\n        const { index: start } = parser;\n        let token = 67174409;\n        let ret = '';\n        let char = advanceChar(parser);\n        while (char !== 96) {\n            if (char === 36 && parser.source.charCodeAt(parser.index + 1) === 123) {\n                advanceChar(parser);\n                token = 67174408;\n                break;\n            }\n            else if (char === 92) {\n                char = advanceChar(parser);\n                if (char > 0x7e) {\n                    ret += String.fromCodePoint(char);\n                }\n                else {\n                    const { index, line, column } = parser;\n                    const code = parseEscape(parser, context | 1, char, 1);\n                    if (code >= 0) {\n                        ret += String.fromCodePoint(code);\n                    }\n                    else if (code !== -1 && context & 64) {\n                        parser.index = index;\n                        parser.line = line;\n                        parser.column = column;\n                        ret = null;\n                        char = scanBadTemplate(parser, char);\n                        if (char < 0)\n                            token = 67174408;\n                        break;\n                    }\n                    else {\n                        handleStringError(parser, code, 1);\n                    }\n                }\n            }\n            else if (parser.index < parser.end) {\n                if (char === 13 && parser.source.charCodeAt(parser.index) === 10) {\n                    ret += String.fromCodePoint(char);\n                    parser.currentChar = parser.source.charCodeAt(++parser.index);\n                }\n                if (((char & 83) < 3 && char === 10) || (char ^ 8232) <= 1) {\n                    parser.column = -1;\n                    parser.line++;\n                }\n                ret += String.fromCodePoint(char);\n            }\n            if (parser.index >= parser.end)\n                parser.report(17);\n            char = advanceChar(parser);\n        }\n        advanceChar(parser);\n        parser.tokenValue = ret;\n        parser.tokenRaw = parser.source.slice(start + 1, parser.index - (token === 67174409 ? 1 : 2));\n        return token;\n    }\n    function scanBadTemplate(parser, ch) {\n        while (ch !== 96) {\n            switch (ch) {\n                case 36: {\n                    const index = parser.index + 1;\n                    if (index < parser.end && parser.source.charCodeAt(index) === 123) {\n                        parser.index = index;\n                        parser.column++;\n                        return -ch;\n                    }\n                    break;\n                }\n                case 10:\n                case 8232:\n                case 8233:\n                    parser.column = -1;\n                    parser.line++;\n            }\n            if (parser.index >= parser.end)\n                parser.report(17);\n            ch = advanceChar(parser);\n        }\n        return ch;\n    }\n    function scanTemplateTail(parser, context) {\n        if (parser.index >= parser.end)\n            parser.report(0);\n        parser.index--;\n        parser.column--;\n        return scanTemplate(parser, context);\n    }\n\n    const errorMessages = {\n        [0]: 'Unexpected token',\n        [30]: \"Unexpected token: '%0'\",\n        [1]: 'Octal escape sequences are not allowed in strict mode',\n        [2]: 'Octal escape sequences are not allowed in template strings',\n        [3]: '\\\\8 and \\\\9 are not allowed in template strings',\n        [4]: 'Private identifier #%0 is not defined',\n        [5]: 'Illegal Unicode escape sequence',\n        [6]: 'Invalid code point %0',\n        [7]: 'Invalid hexadecimal escape sequence',\n        [9]: 'Octal literals are not allowed in strict mode',\n        [8]: 'Decimal integer literals with a leading zero are forbidden in strict mode',\n        [10]: 'Expected number in radix %0',\n        [151]: 'Invalid left-hand side assignment to a destructible right-hand side',\n        [11]: 'Non-number found after exponent indicator',\n        [12]: 'Invalid BigIntLiteral',\n        [13]: 'No identifiers allowed directly after numeric literal',\n        [14]: 'Escapes \\\\8 or \\\\9 are not syntactically valid escapes',\n        [15]: 'Escapes \\\\8 or \\\\9 are not allowed in strict mode',\n        [16]: 'Unterminated string literal',\n        [17]: 'Unterminated template literal',\n        [18]: 'Multiline comment was not closed properly',\n        [19]: 'The identifier contained dynamic unicode escape that was not closed',\n        [20]: \"Illegal character '%0'\",\n        [21]: 'Missing hexadecimal digits',\n        [22]: 'Invalid implicit octal',\n        [23]: 'Invalid line break in string literal',\n        [24]: 'Only unicode escapes are legal in identifier names',\n        [25]: \"Expected '%0'\",\n        [26]: 'Invalid left-hand side in assignment',\n        [27]: 'Invalid left-hand side in async arrow',\n        [28]: 'Calls to super must be in the \"constructor\" method of a class expression or class declaration that has a superclass',\n        [29]: 'Member access on super must be in a method',\n        [31]: 'Await expression not allowed in formal parameter',\n        [32]: 'Yield expression not allowed in formal parameter',\n        [95]: \"Unexpected token: 'escaped keyword'\",\n        [33]: 'Unary expressions as the left operand of an exponentiation expression must be disambiguated with parentheses',\n        [123]: 'Async functions can only be declared at the top level or inside a block',\n        [34]: 'Unterminated regular expression',\n        [35]: 'Unexpected regular expression flag',\n        [36]: \"Duplicate regular expression flag '%0'\",\n        [37]: '%0 functions must have exactly %1 argument%2',\n        [38]: 'Setter function argument must not be a rest parameter',\n        [39]: '%0 declaration must have a name in this context',\n        [40]: 'Function name may not contain any reserved words or be eval or arguments in strict mode',\n        [41]: 'The rest operator is missing an argument',\n        [42]: 'A getter cannot be a generator',\n        [43]: 'A setter cannot be a generator',\n        [44]: 'A computed property name must be followed by a colon or paren',\n        [134]: 'Object literal keys that are strings or numbers must be a method or have a colon',\n        [46]: 'Found `* async x(){}` but this should be `async * x(){}`',\n        [45]: 'Getters and setters can not be generators',\n        [47]: \"'%0' can not be generator method\",\n        [48]: \"No line break is allowed after '=>'\",\n        [49]: 'The left-hand side of the arrow can only be destructed through assignment',\n        [50]: 'The binding declaration is not destructible',\n        [51]: 'Async arrow can not be followed by new expression',\n        [52]: \"Classes may not have a static property named 'prototype'\",\n        [53]: 'Class constructor may not be a %0',\n        [54]: 'Duplicate constructor method in class',\n        [55]: 'Invalid increment/decrement operand',\n        [56]: 'Invalid use of `new` keyword on an increment/decrement expression',\n        [57]: '`=>` is an invalid assignment target',\n        [58]: 'Rest element may not have a trailing comma',\n        [59]: 'Missing initializer in %0 declaration',\n        [60]: \"'for-%0' loop head declarations can not have an initializer\",\n        [61]: 'Invalid left-hand side in for-%0 loop: Must have a single binding',\n        [62]: 'Invalid shorthand property initializer',\n        [63]: 'Property name __proto__ appears more than once in object literal',\n        [64]: 'Let is disallowed as a lexically bound name',\n        [65]: \"Invalid use of '%0' inside new expression\",\n        [66]: \"Illegal 'use strict' directive in function with non-simple parameter list\",\n        [67]: 'Identifier \"let\" disallowed as left-hand side expression in strict mode',\n        [68]: 'Illegal continue statement',\n        [69]: 'Illegal break statement',\n        [70]: 'Cannot have `let[...]` as a var name in strict mode',\n        [71]: 'Invalid destructuring assignment target',\n        [72]: 'Rest parameter may not have a default initializer',\n        [73]: 'The rest argument must the be last parameter',\n        [74]: 'Invalid rest argument',\n        [76]: 'In strict mode code, functions can only be declared at top level or inside a block',\n        [77]: 'In non-strict mode code, functions can only be declared at top level, inside a block, or as the body of an if statement',\n        [78]: 'Without web compatibility enabled functions can not be declared at top level, inside a block, or as the body of an if statement',\n        [79]: \"Class declaration can't appear in single-statement context\",\n        [80]: 'Invalid left-hand side in for-%0',\n        [81]: 'Invalid assignment in for-%0',\n        [82]: 'for await (... of ...) is only valid in async functions and async generators',\n        [83]: 'The first token after the template expression should be a continuation of the template',\n        [85]: '`let` declaration not allowed here and `let` cannot be a regular var name in strict mode',\n        [84]: '`let \\n [` is a restricted production at the start of a statement',\n        [86]: 'Catch clause requires exactly one parameter, not more (and no trailing comma)',\n        [87]: 'Catch clause parameter does not support default values',\n        [88]: 'Missing catch or finally after try',\n        [89]: 'More than one default clause in switch statement',\n        [90]: 'Illegal newline after throw',\n        [91]: 'Strict mode code may not include a with statement',\n        [92]: 'Illegal return statement',\n        [93]: 'The left hand side of the for-header binding declaration is not destructible',\n        [94]: 'new.target only allowed within functions or static blocks',\n        [96]: \"'#' not followed by identifier\",\n        [102]: 'Invalid keyword',\n        [101]: \"Can not use 'let' as a class name\",\n        [100]: \"'A lexical declaration can't define a 'let' binding\",\n        [99]: 'Can not use `let` as variable name in strict mode',\n        [97]: \"'%0' may not be used as an identifier in this context\",\n        [98]: 'Await is only valid in async functions',\n        [103]: 'The %0 keyword can only be used with the module goal',\n        [104]: 'Unicode codepoint must not be greater than 0x10FFFF',\n        [105]: '%0 source must be string',\n        [106]: 'Only a identifier or string can be used to indicate alias',\n        [107]: \"Only '*' or '{...}' can be imported after default\",\n        [108]: 'Trailing decorator may be followed by method',\n        [109]: \"Decorators can't be used with a constructor\",\n        [110]: 'Can not use `await` as identifier in module or async func',\n        [111]: 'Can not use `await` as identifier in module',\n        [112]: 'HTML comments are only allowed with web compatibility (Annex B)',\n        [113]: \"The identifier 'let' must not be in expression position in strict mode\",\n        [114]: 'Cannot assign to `eval` and `arguments` in strict mode',\n        [115]: \"The left-hand side of a for-of loop may not start with 'let'\",\n        [116]: 'Block body arrows can not be immediately invoked without a group',\n        [117]: 'Block body arrows can not be immediately accessed without a group',\n        [118]: 'Unexpected strict mode reserved word',\n        [119]: 'Unexpected eval or arguments in strict mode',\n        [120]: 'Decorators must not be followed by a semicolon',\n        [121]: 'Calling delete on expression not allowed in strict mode',\n        [122]: 'Pattern can not have a tail',\n        [124]: 'Can not have a `yield` expression on the left side of a ternary',\n        [125]: 'An arrow function can not have a postfix update operator',\n        [126]: 'Invalid object literal key character after generator star',\n        [127]: 'Private fields can not be deleted',\n        [129]: 'Classes may not have a field called constructor',\n        [128]: 'Classes may not have a private element named constructor',\n        [130]: 'A class field initializer or static block may not contain arguments',\n        [131]: 'Generators can only be declared at the top level or inside a block',\n        [132]: 'Async methods are a restricted production and cannot have a newline following it',\n        [133]: 'Unexpected character after object literal property name',\n        [135]: 'Invalid key token',\n        [136]: \"Label '%0' has already been declared\",\n        [137]: 'continue statement must be nested within an iteration statement',\n        [138]: \"Undefined label '%0'\",\n        [139]: 'Trailing comma is disallowed inside import(...) arguments',\n        [140]: 'Invalid binding in JSON import',\n        [141]: 'import() requires exactly one argument',\n        [142]: 'Cannot use new with import(...)',\n        [143]: '... is not allowed in import()',\n        [144]: \"Expected '=>'\",\n        [145]: \"Duplicate binding '%0'\",\n        [146]: 'Duplicate private identifier #%0',\n        [147]: \"Cannot export a duplicate name '%0'\",\n        [150]: 'Duplicate %0 for-binding',\n        [148]: \"Exported binding '%0' needs to refer to a top-level declared variable\",\n        [149]: 'Unexpected private field',\n        [153]: 'Numeric separators are not allowed at the end of numeric literals',\n        [152]: 'Only one underscore is allowed as numeric separator',\n        [154]: 'JSX value should be either an expression or a quoted JSX text',\n        [155]: 'Expected corresponding JSX closing tag for %0',\n        [156]: 'Adjacent JSX elements must be wrapped in an enclosing tag',\n        [157]: \"JSX attributes must only be assigned a non-empty 'expression'\",\n        [158]: \"'%0' has already been declared\",\n        [159]: \"'%0' shadowed a catch clause binding\",\n        [160]: 'Dot property must be an identifier',\n        [161]: 'Encountered invalid input after spread/rest argument',\n        [162]: 'Catch without try',\n        [163]: 'Finally without try',\n        [164]: 'Expected corresponding closing tag for JSX fragment',\n        [165]: 'Coalescing and logical operators used together in the same expression must be disambiguated with parentheses',\n        [166]: 'Invalid tagged template on optional chain',\n        [167]: 'Invalid optional chain from super property',\n        [168]: 'Invalid optional chain from new expression',\n        [169]: 'Cannot use \"import.meta\" outside a module',\n        [170]: 'Leading decorators must be attached to a class declaration',\n        [171]: 'An export name cannot include a lone surrogate, found %0',\n        [172]: 'A string literal cannot be used as an exported binding without `from`',\n        [173]: \"Private fields can't be accessed on super\",\n        [174]: \"The only valid meta property for import is 'import.meta'\",\n        [175]: \"'import.meta' must not contain escaped characters\",\n        [176]: 'cannot use \"await\" as identifier inside an async function',\n        [177]: 'cannot use \"await\" in static blocks',\n    };\n    class ParseError extends SyntaxError {\n        start;\n        end;\n        range;\n        loc;\n        description;\n        constructor(start, end, type, ...params) {\n            const description = errorMessages[type].replace(/%(\\d+)/g, (_, i) => params[i]);\n            const message = '[' + start.line + ':' + start.column + '-' + end.line + ':' + end.column + ']: ' + description;\n            super(message);\n            this.start = start.index;\n            this.end = end.index;\n            this.range = [start.index, end.index];\n            this.loc = {\n                start: { line: start.line, column: start.column },\n                end: { line: end.line, column: end.column },\n            };\n            this.description = description;\n        }\n    }\n\n    function scanNumber(parser, context, kind) {\n        let char = parser.currentChar;\n        let value = 0;\n        let digit = 9;\n        let atStart = kind & 64 ? 0 : 1;\n        let digits = 0;\n        let allowSeparator = 0;\n        if (kind & 64) {\n            value = '.' + scanDecimalDigitsOrSeparator(parser, char);\n            char = parser.currentChar;\n            if (char === 110)\n                parser.report(12);\n        }\n        else {\n            if (char === 48) {\n                char = advanceChar(parser);\n                if ((char | 32) === 120) {\n                    kind = 8 | 128;\n                    char = advanceChar(parser);\n                    while (CharTypes[char] & (64 | 4096)) {\n                        if (char === 95) {\n                            if (!allowSeparator)\n                                parser.report(152);\n                            allowSeparator = 0;\n                            char = advanceChar(parser);\n                            continue;\n                        }\n                        allowSeparator = 1;\n                        value = value * 0x10 + toHex(char);\n                        digits++;\n                        char = advanceChar(parser);\n                    }\n                    if (digits === 0 || !allowSeparator) {\n                        parser.report(digits === 0 ? 21 : 153);\n                    }\n                }\n                else if ((char | 32) === 111) {\n                    kind = 4 | 128;\n                    char = advanceChar(parser);\n                    while (CharTypes[char] & (32 | 4096)) {\n                        if (char === 95) {\n                            if (!allowSeparator) {\n                                parser.report(152);\n                            }\n                            allowSeparator = 0;\n                            char = advanceChar(parser);\n                            continue;\n                        }\n                        allowSeparator = 1;\n                        value = value * 8 + (char - 48);\n                        digits++;\n                        char = advanceChar(parser);\n                    }\n                    if (digits === 0 || !allowSeparator) {\n                        parser.report(digits === 0 ? 0 : 153);\n                    }\n                }\n                else if ((char | 32) === 98) {\n                    kind = 2 | 128;\n                    char = advanceChar(parser);\n                    while (CharTypes[char] & (128 | 4096)) {\n                        if (char === 95) {\n                            if (!allowSeparator) {\n                                parser.report(152);\n                            }\n                            allowSeparator = 0;\n                            char = advanceChar(parser);\n                            continue;\n                        }\n                        allowSeparator = 1;\n                        value = value * 2 + (char - 48);\n                        digits++;\n                        char = advanceChar(parser);\n                    }\n                    if (digits === 0 || !allowSeparator) {\n                        parser.report(digits === 0 ? 0 : 153);\n                    }\n                }\n                else if (CharTypes[char] & 32) {\n                    if (context & 1)\n                        parser.report(1);\n                    kind = 1;\n                    while (CharTypes[char] & 16) {\n                        if (CharTypes[char] & 512) {\n                            kind = 32;\n                            atStart = 0;\n                            break;\n                        }\n                        value = value * 8 + (char - 48);\n                        char = advanceChar(parser);\n                    }\n                }\n                else if (CharTypes[char] & 512) {\n                    if (context & 1)\n                        parser.report(1);\n                    parser.flags |= 64;\n                    kind = 32;\n                }\n                else if (char === 95) {\n                    parser.report(0);\n                }\n            }\n            if (kind & 48) {\n                if (atStart) {\n                    while (digit >= 0 && CharTypes[char] & (16 | 4096)) {\n                        if (char === 95) {\n                            char = advanceChar(parser);\n                            if (char === 95 || kind & 32) {\n                                throw new ParseError(parser.currentLocation, { index: parser.index + 1, line: parser.line, column: parser.column }, 152);\n                            }\n                            allowSeparator = 1;\n                            continue;\n                        }\n                        allowSeparator = 0;\n                        value = 10 * value + (char - 48);\n                        char = advanceChar(parser);\n                        --digit;\n                    }\n                    if (allowSeparator) {\n                        throw new ParseError(parser.currentLocation, { index: parser.index + 1, line: parser.line, column: parser.column }, 153);\n                    }\n                    if (digit >= 0 && !isIdentifierStart(char) && char !== 46) {\n                        parser.tokenValue = value;\n                        if (parser.options.raw)\n                            parser.tokenRaw = parser.source.slice(parser.tokenIndex, parser.index);\n                        return 134283266;\n                    }\n                }\n                value += scanDecimalDigitsOrSeparator(parser, char);\n                char = parser.currentChar;\n                if (char === 46) {\n                    if (advanceChar(parser) === 95)\n                        parser.report(0);\n                    kind = 64;\n                    value += '.' + scanDecimalDigitsOrSeparator(parser, parser.currentChar);\n                    char = parser.currentChar;\n                }\n            }\n        }\n        const end = parser.index;\n        let isBigInt = 0;\n        if (char === 110 && kind & 128) {\n            isBigInt = 1;\n            char = advanceChar(parser);\n        }\n        else {\n            if ((char | 32) === 101) {\n                char = advanceChar(parser);\n                if (CharTypes[char] & 256)\n                    char = advanceChar(parser);\n                const { index } = parser;\n                if ((CharTypes[char] & 16) === 0)\n                    parser.report(11);\n                value += parser.source.substring(end, index) + scanDecimalDigitsOrSeparator(parser, char);\n                char = parser.currentChar;\n            }\n        }\n        if ((parser.index < parser.end && CharTypes[char] & 16) || isIdentifierStart(char)) {\n            parser.report(13);\n        }\n        if (isBigInt) {\n            parser.tokenRaw = parser.source.slice(parser.tokenIndex, parser.index);\n            parser.tokenValue = BigInt(parser.tokenRaw.slice(0, -1).replaceAll('_', ''));\n            return 134283388;\n        }\n        parser.tokenValue =\n            kind & (1 | 2 | 8 | 4)\n                ? value\n                : kind & 32\n                    ? parseFloat(parser.source.substring(parser.tokenIndex, parser.index))\n                    : +value;\n        if (parser.options.raw)\n            parser.tokenRaw = parser.source.slice(parser.tokenIndex, parser.index);\n        return 134283266;\n    }\n    function scanDecimalDigitsOrSeparator(parser, char) {\n        let allowSeparator = 0;\n        let start = parser.index;\n        let ret = '';\n        while (CharTypes[char] & (16 | 4096)) {\n            if (char === 95) {\n                const { index } = parser;\n                char = advanceChar(parser);\n                if (char === 95) {\n                    throw new ParseError(parser.currentLocation, { index: parser.index + 1, line: parser.line, column: parser.column }, 152);\n                }\n                allowSeparator = 1;\n                ret += parser.source.substring(start, index);\n                start = parser.index;\n                continue;\n            }\n            allowSeparator = 0;\n            char = advanceChar(parser);\n        }\n        if (allowSeparator) {\n            throw new ParseError(parser.currentLocation, { index: parser.index + 1, line: parser.line, column: parser.column }, 153);\n        }\n        return ret + parser.source.substring(start, parser.index);\n    }\n\n    const KeywordDescTable = [\n        'end of source',\n        'identifier', 'number', 'string', 'regular expression',\n        'false', 'true', 'null',\n        'template continuation', 'template tail',\n        '=>', '(', '{', '.', '...', '}', ')', ';', ',', '[', ']', ':', '?', '\\'', '\"',\n        '++', '--',\n        '=', '<<=', '>>=', '>>>=', '**=', '+=', '-=', '*=', '/=', '%=', '^=', '|=',\n        '&=', '||=', '&&=', '??=',\n        'typeof', 'delete', 'void', '!', '~', '+', '-', 'in', 'instanceof', '*', '%', '/', '**', '&&',\n        '||', '===', '!==', '==', '!=', '<=', '>=', '<', '>', '<<', '>>', '>>>', '&', '|', '^',\n        'var', 'let', 'const',\n        'break', 'case', 'catch', 'class', 'continue', 'debugger', 'default', 'do', 'else', 'export',\n        'extends', 'finally', 'for', 'function', 'if', 'import', 'new', 'return', 'super', 'switch',\n        'this', 'throw', 'try', 'while', 'with',\n        'implements', 'interface', 'package', 'private', 'protected', 'public', 'static', 'yield',\n        'as', 'async', 'await', 'constructor', 'get', 'set', 'accessor', 'from', 'of',\n        'enum', 'eval', 'arguments', 'escaped keyword', 'escaped future reserved keyword', 'reserved if strict', '#',\n        'BigIntLiteral', '??', '?.', 'WhiteSpace', 'Illegal', 'LineTerminator', 'PrivateField',\n        'Template', '@', 'target', 'meta', 'LineFeed', 'Escaped', 'JSXText'\n    ];\n    const descKeywordTable = {\n        this: 86111,\n        function: 86104,\n        if: 20569,\n        return: 20572,\n        var: 86088,\n        else: 20563,\n        for: 20567,\n        new: 86107,\n        in: 8673330,\n        typeof: 16863275,\n        while: 20578,\n        case: 20556,\n        break: 20555,\n        try: 20577,\n        catch: 20557,\n        delete: 16863276,\n        throw: 86112,\n        switch: 86110,\n        continue: 20559,\n        default: 20561,\n        instanceof: 8411187,\n        do: 20562,\n        void: 16863277,\n        finally: 20566,\n        async: 209005,\n        await: 209006,\n        class: 86094,\n        const: 86090,\n        constructor: 12399,\n        debugger: 20560,\n        export: 20564,\n        extends: 20565,\n        false: 86021,\n        from: 209011,\n        get: 209008,\n        implements: 36964,\n        import: 86106,\n        interface: 36965,\n        let: 241737,\n        null: 86023,\n        of: 471156,\n        package: 36966,\n        private: 36967,\n        protected: 36968,\n        public: 36969,\n        set: 209009,\n        static: 36970,\n        super: 86109,\n        true: 86022,\n        with: 20579,\n        yield: 241771,\n        enum: 86133,\n        eval: 537079926,\n        as: 77932,\n        arguments: 537079927,\n        target: 209029,\n        meta: 209030,\n        accessor: 12402,\n    };\n\n    function matchOrInsertSemicolon(parser, context) {\n        if ((parser.flags & 1) === 0 && (parser.getToken() & 1048576) !== 1048576) {\n            parser.report(30, KeywordDescTable[parser.getToken() & 255]);\n        }\n        if (!consumeOpt(parser, context, 1074790417)) {\n            parser.options.onInsertedSemicolon?.(parser.startIndex);\n        }\n    }\n    function isValidStrictMode(parser, index, tokenIndex, tokenValue) {\n        if (index - tokenIndex < 13 && tokenValue === 'use strict') {\n            if ((parser.getToken() & 1048576) === 1048576 || parser.flags & 1) {\n                return 1;\n            }\n        }\n        return 0;\n    }\n    function optionalBit(parser, context, t) {\n        if (parser.getToken() !== t)\n            return 0;\n        nextToken(parser, context);\n        return 1;\n    }\n    function consumeOpt(parser, context, t) {\n        if (parser.getToken() !== t)\n            return false;\n        nextToken(parser, context);\n        return true;\n    }\n    function consume(parser, context, t) {\n        if (parser.getToken() !== t)\n            parser.report(25, KeywordDescTable[t & 255]);\n        nextToken(parser, context);\n    }\n    function reinterpretToPattern(parser, node) {\n        switch (node.type) {\n            case 'ArrayExpression': {\n                node.type = 'ArrayPattern';\n                const { elements } = node;\n                for (let i = 0, n = elements.length; i < n; ++i) {\n                    const element = elements[i];\n                    if (element)\n                        reinterpretToPattern(parser, element);\n                }\n                return;\n            }\n            case 'ObjectExpression': {\n                node.type = 'ObjectPattern';\n                const { properties } = node;\n                for (let i = 0, n = properties.length; i < n; ++i) {\n                    reinterpretToPattern(parser, properties[i]);\n                }\n                return;\n            }\n            case 'AssignmentExpression':\n                node.type = 'AssignmentPattern';\n                if (node.operator !== '=')\n                    parser.report(71);\n                delete node.operator;\n                reinterpretToPattern(parser, node.left);\n                return;\n            case 'Property':\n                reinterpretToPattern(parser, node.value);\n                return;\n            case 'SpreadElement':\n                node.type = 'RestElement';\n                reinterpretToPattern(parser, node.argument);\n        }\n    }\n    function validateBindingIdentifier(parser, context, kind, t, skipEvalArgCheck) {\n        if (context & 1) {\n            if ((t & 36864) === 36864) {\n                parser.report(118);\n            }\n            if (!skipEvalArgCheck && (t & 537079808) === 537079808) {\n                parser.report(119);\n            }\n        }\n        if ((t & 20480) === 20480 || t === -2147483528) {\n            parser.report(102);\n        }\n        if (kind & (8 | 16) && (t & 255) === (241737 & 255)) {\n            parser.report(100);\n        }\n        if (context & (2048 | 2) && t === 209006) {\n            parser.report(110);\n        }\n        if (context & (1024 | 1) && t === 241771) {\n            parser.report(97, 'yield');\n        }\n    }\n    function validateFunctionName(parser, context, t) {\n        if (context & 1) {\n            if ((t & 36864) === 36864) {\n                parser.report(118);\n            }\n            if ((t & 537079808) === 537079808) {\n                parser.report(119);\n            }\n            if (t === -2147483527) {\n                parser.report(95);\n            }\n            if (t === -2147483528) {\n                parser.report(95);\n            }\n        }\n        if ((t & 20480) === 20480) {\n            parser.report(102);\n        }\n        if (context & (2048 | 2) && t === 209006) {\n            parser.report(110);\n        }\n        if (context & (1024 | 1) && t === 241771) {\n            parser.report(97, 'yield');\n        }\n    }\n    function isStrictReservedWord(parser, context, t) {\n        if (t === 209006) {\n            if (context & (2048 | 2))\n                parser.report(110);\n            parser.destructible |= 128;\n        }\n        if (t === 241771 && context & 1024)\n            parser.report(97, 'yield');\n        return ((t & 20480) === 20480 ||\n            (t & 36864) === 36864 ||\n            t == -2147483527);\n    }\n    function isPropertyWithPrivateFieldKey(expr) {\n        return !expr.property ? false : expr.property.type === 'PrivateIdentifier';\n    }\n    function isValidLabel(parser, labels, name, isIterationStatement) {\n        while (labels) {\n            if (labels['$' + name]) {\n                if (isIterationStatement)\n                    parser.report(137);\n                return 1;\n            }\n            if (isIterationStatement && labels.loop)\n                isIterationStatement = 0;\n            labels = labels['$'];\n        }\n        return 0;\n    }\n    function validateAndDeclareLabel(parser, labels, name) {\n        let set = labels;\n        while (set) {\n            if (set['$' + name])\n                parser.report(136, name);\n            set = set['$'];\n        }\n        labels['$' + name] = 1;\n    }\n    function isEqualTagName(elementName) {\n        switch (elementName.type) {\n            case 'JSXIdentifier':\n                return elementName.name;\n            case 'JSXNamespacedName':\n                return elementName.namespace + ':' + elementName.name;\n            case 'JSXMemberExpression':\n                return isEqualTagName(elementName.object) + '.' + isEqualTagName(elementName.property);\n        }\n    }\n    function isValidIdentifier(context, t) {\n        if (context & (1 | 1024)) {\n            if (context & 2 && t === 209006)\n                return false;\n            if (context & 1024 && t === 241771)\n                return false;\n            return (t & 12288) === 12288;\n        }\n        return (t & 12288) === 12288 || (t & 36864) === 36864;\n    }\n    function classifyIdentifier(parser, context, t) {\n        if ((t & 537079808) === 537079808) {\n            if (context & 1)\n                parser.report(119);\n            parser.flags |= 512;\n        }\n        if (!isValidIdentifier(context, t))\n            parser.report(0);\n    }\n    function getOwnProperty(object, key) {\n        return Object.hasOwn(object, key) ? object[key] : undefined;\n    }\n\n    function scanIdentifier(parser, context, isValidAsKeyword) {\n        while (isIdPart[advanceChar(parser)])\n            ;\n        parser.tokenValue = parser.source.slice(parser.tokenIndex, parser.index);\n        return parser.currentChar !== 92 && parser.currentChar <= 0x7e\n            ? (getOwnProperty(descKeywordTable, parser.tokenValue) ?? 208897)\n            : scanIdentifierSlowCase(parser, context, 0, isValidAsKeyword);\n    }\n    function scanUnicodeIdentifier(parser, context) {\n        const cookedChar = scanIdentifierUnicodeEscape(parser);\n        if (!isIdentifierStart(cookedChar))\n            parser.report(5);\n        parser.tokenValue = String.fromCodePoint(cookedChar);\n        return scanIdentifierSlowCase(parser, context, 1, CharTypes[cookedChar] & 4);\n    }\n    function scanIdentifierSlowCase(parser, context, hasEscape, isValidAsKeyword) {\n        let start = parser.index;\n        while (parser.index < parser.end) {\n            if (parser.currentChar === 92) {\n                parser.tokenValue += parser.source.slice(start, parser.index);\n                hasEscape = 1;\n                const code = scanIdentifierUnicodeEscape(parser);\n                if (!isIdentifierPart(code))\n                    parser.report(5);\n                isValidAsKeyword = isValidAsKeyword && CharTypes[code] & 4;\n                parser.tokenValue += String.fromCodePoint(code);\n                start = parser.index;\n            }\n            else {\n                const merged = consumePossibleSurrogatePair(parser);\n                if (merged > 0) {\n                    if (!isIdentifierPart(merged)) {\n                        parser.report(20, String.fromCodePoint(merged));\n                    }\n                    parser.currentChar = merged;\n                    parser.index++;\n                    parser.column++;\n                }\n                else if (!isIdentifierPart(parser.currentChar)) {\n                    break;\n                }\n                advanceChar(parser);\n            }\n        }\n        if (parser.index <= parser.end) {\n            parser.tokenValue += parser.source.slice(start, parser.index);\n        }\n        const { length } = parser.tokenValue;\n        if (isValidAsKeyword && length >= 2 && length <= 11) {\n            const token = getOwnProperty(descKeywordTable, parser.tokenValue);\n            if (token === void 0)\n                return 208897 | (hasEscape ? -2147483648 : 0);\n            if (!hasEscape)\n                return token;\n            if (token === 209006) {\n                if ((context & (2 | 2048)) === 0) {\n                    return token | -2147483648;\n                }\n                return -2147483528;\n            }\n            if (context & 1) {\n                if (token === 36970) {\n                    return -2147483527;\n                }\n                if ((token & 36864) === 36864) {\n                    return -2147483527;\n                }\n                if ((token & 20480) === 20480) {\n                    if (context & 262144 && (context & 8) === 0) {\n                        return token | -2147483648;\n                    }\n                    else {\n                        return -2147483528;\n                    }\n                }\n                return 209018 | -2147483648;\n            }\n            if (context & 262144 &&\n                (context & 8) === 0 &&\n                (token & 20480) === 20480) {\n                return token | -2147483648;\n            }\n            if (token === 241771) {\n                return context & 262144\n                    ? 209018 | -2147483648\n                    : context & 1024\n                        ? -2147483528\n                        : token | -2147483648;\n            }\n            if (token === 209005) {\n                return 209018 | -2147483648;\n            }\n            if ((token & 36864) === 36864) {\n                return token | 12288 | -2147483648;\n            }\n            return -2147483528;\n        }\n        return 208897 | (hasEscape ? -2147483648 : 0);\n    }\n    function scanPrivateIdentifier(parser) {\n        let char = advanceChar(parser);\n        if (char === 92)\n            return 130;\n        const merged = consumePossibleSurrogatePair(parser);\n        if (merged)\n            char = merged;\n        if (!isIdentifierStart(char))\n            parser.report(96);\n        return 130;\n    }\n    function scanIdentifierUnicodeEscape(parser) {\n        if (parser.source.charCodeAt(parser.index + 1) !== 117) {\n            parser.report(5);\n        }\n        parser.currentChar = parser.source.charCodeAt((parser.index += 2));\n        parser.column += 2;\n        return scanUnicodeEscape(parser);\n    }\n    function scanUnicodeEscape(parser) {\n        let codePoint = 0;\n        const char = parser.currentChar;\n        if (char === 123) {\n            const begin = parser.index - 2;\n            while (CharTypes[advanceChar(parser)] & 64) {\n                codePoint = (codePoint << 4) | toHex(parser.currentChar);\n                if (codePoint > 1114111)\n                    throw new ParseError({ index: begin, line: parser.line, column: parser.column }, parser.currentLocation, 104);\n            }\n            if (parser.currentChar !== 125) {\n                throw new ParseError({ index: begin, line: parser.line, column: parser.column }, parser.currentLocation, 7);\n            }\n            advanceChar(parser);\n            return codePoint;\n        }\n        if ((CharTypes[char] & 64) === 0)\n            parser.report(7);\n        const char2 = parser.source.charCodeAt(parser.index + 1);\n        if ((CharTypes[char2] & 64) === 0)\n            parser.report(7);\n        const char3 = parser.source.charCodeAt(parser.index + 2);\n        if ((CharTypes[char3] & 64) === 0)\n            parser.report(7);\n        const char4 = parser.source.charCodeAt(parser.index + 3);\n        if ((CharTypes[char4] & 64) === 0)\n            parser.report(7);\n        codePoint = (toHex(char) << 12) | (toHex(char2) << 8) | (toHex(char3) << 4) | toHex(char4);\n        parser.currentChar = parser.source.charCodeAt((parser.index += 4));\n        parser.column += 4;\n        return codePoint;\n    }\n\n    const TokenLookup = [\n        128,\n        128,\n        128,\n        128,\n        128,\n        128,\n        128,\n        128,\n        128,\n        127,\n        135,\n        127,\n        127,\n        129,\n        128,\n        128,\n        128,\n        128,\n        128,\n        128,\n        128,\n        128,\n        128,\n        128,\n        128,\n        128,\n        128,\n        128,\n        128,\n        128,\n        128,\n        128,\n        127,\n        16842798,\n        134283267,\n        130,\n        208897,\n        8391477,\n        8390213,\n        134283267,\n        67174411,\n        16,\n        8391476,\n        25233968,\n        18,\n        25233969,\n        67108877,\n        8457014,\n        134283266,\n        134283266,\n        134283266,\n        134283266,\n        134283266,\n        134283266,\n        134283266,\n        134283266,\n        134283266,\n        134283266,\n        21,\n        1074790417,\n        8456256,\n        1077936155,\n        8390721,\n        22,\n        132,\n        208897,\n        208897,\n        208897,\n        208897,\n        208897,\n        208897,\n        208897,\n        208897,\n        208897,\n        208897,\n        208897,\n        208897,\n        208897,\n        208897,\n        208897,\n        208897,\n        208897,\n        208897,\n        208897,\n        208897,\n        208897,\n        208897,\n        208897,\n        208897,\n        208897,\n        208897,\n        69271571,\n        136,\n        20,\n        8389959,\n        208897,\n        131,\n        4096,\n        4096,\n        4096,\n        4096,\n        4096,\n        4096,\n        4096,\n        208897,\n        4096,\n        208897,\n        208897,\n        4096,\n        208897,\n        4096,\n        208897,\n        4096,\n        208897,\n        4096,\n        4096,\n        4096,\n        208897,\n        4096,\n        4096,\n        208897,\n        4096,\n        4096,\n        2162700,\n        8389702,\n        1074790415,\n        16842799,\n        128,\n    ];\n    function nextToken(parser, context) {\n        parser.flags = (parser.flags | 1) ^ 1;\n        parser.startIndex = parser.index;\n        parser.startColumn = parser.column;\n        parser.startLine = parser.line;\n        parser.setToken(scanSingleToken(parser, context, 0));\n    }\n    function scanSingleToken(parser, context, state) {\n        const isStartOfLine = parser.index === 0;\n        const { source } = parser;\n        let start = parser.currentLocation;\n        while (parser.index < parser.end) {\n            parser.tokenIndex = parser.index;\n            parser.tokenColumn = parser.column;\n            parser.tokenLine = parser.line;\n            let char = parser.currentChar;\n            if (char <= 0x7e) {\n                const token = TokenLookup[char];\n                switch (token) {\n                    case 67174411:\n                    case 16:\n                    case 2162700:\n                    case 1074790415:\n                    case 69271571:\n                    case 20:\n                    case 21:\n                    case 1074790417:\n                    case 18:\n                    case 16842799:\n                    case 132:\n                    case 128:\n                        advanceChar(parser);\n                        return token;\n                    case 208897:\n                        return scanIdentifier(parser, context, 0);\n                    case 4096:\n                        return scanIdentifier(parser, context, 1);\n                    case 134283266:\n                        return scanNumber(parser, context, 16 | 128);\n                    case 134283267:\n                        return scanString(parser, context, char);\n                    case 131:\n                        return scanTemplate(parser, context);\n                    case 136:\n                        return scanUnicodeIdentifier(parser, context);\n                    case 130:\n                        return scanPrivateIdentifier(parser);\n                    case 127:\n                        advanceChar(parser);\n                        break;\n                    case 129:\n                        state |= 1 | 4;\n                        scanNewLine(parser);\n                        break;\n                    case 135:\n                        consumeLineFeed(parser, state);\n                        state = (state & -5) | 1;\n                        break;\n                    case 8456256: {\n                        const ch = advanceChar(parser);\n                        if (parser.index < parser.end) {\n                            if (ch === 60) {\n                                if (parser.index < parser.end && advanceChar(parser) === 61) {\n                                    advanceChar(parser);\n                                    return 4194332;\n                                }\n                                return 8390978;\n                            }\n                            else if (ch === 61) {\n                                advanceChar(parser);\n                                return 8390718;\n                            }\n                            if (ch === 33) {\n                                const index = parser.index + 1;\n                                if (index + 1 < parser.end &&\n                                    source.charCodeAt(index) === 45 &&\n                                    source.charCodeAt(index + 1) == 45) {\n                                    parser.column += 3;\n                                    parser.currentChar = source.charCodeAt((parser.index += 3));\n                                    state = skipSingleHTMLComment(parser, source, state, context, 2, parser.tokenStart);\n                                    start = parser.tokenStart;\n                                    continue;\n                                }\n                                return 8456256;\n                            }\n                        }\n                        return 8456256;\n                    }\n                    case 1077936155: {\n                        advanceChar(parser);\n                        const ch = parser.currentChar;\n                        if (ch === 61) {\n                            if (advanceChar(parser) === 61) {\n                                advanceChar(parser);\n                                return 8390458;\n                            }\n                            return 8390460;\n                        }\n                        if (ch === 62) {\n                            advanceChar(parser);\n                            return 10;\n                        }\n                        return 1077936155;\n                    }\n                    case 16842798:\n                        if (advanceChar(parser) !== 61) {\n                            return 16842798;\n                        }\n                        if (advanceChar(parser) !== 61) {\n                            return 8390461;\n                        }\n                        advanceChar(parser);\n                        return 8390459;\n                    case 8391477:\n                        if (advanceChar(parser) !== 61)\n                            return 8391477;\n                        advanceChar(parser);\n                        return 4194340;\n                    case 8391476: {\n                        advanceChar(parser);\n                        if (parser.index >= parser.end)\n                            return 8391476;\n                        const ch = parser.currentChar;\n                        if (ch === 61) {\n                            advanceChar(parser);\n                            return 4194338;\n                        }\n                        if (ch !== 42)\n                            return 8391476;\n                        if (advanceChar(parser) !== 61)\n                            return 8391735;\n                        advanceChar(parser);\n                        return 4194335;\n                    }\n                    case 8389959:\n                        if (advanceChar(parser) !== 61)\n                            return 8389959;\n                        advanceChar(parser);\n                        return 4194341;\n                    case 25233968: {\n                        advanceChar(parser);\n                        const ch = parser.currentChar;\n                        if (ch === 43) {\n                            advanceChar(parser);\n                            return 33619993;\n                        }\n                        if (ch === 61) {\n                            advanceChar(parser);\n                            return 4194336;\n                        }\n                        return 25233968;\n                    }\n                    case 25233969: {\n                        advanceChar(parser);\n                        const ch = parser.currentChar;\n                        if (ch === 45) {\n                            advanceChar(parser);\n                            if ((state & 1 || isStartOfLine) && parser.currentChar === 62) {\n                                if (!parser.options.webcompat)\n                                    parser.report(112);\n                                advanceChar(parser);\n                                state = skipSingleHTMLComment(parser, source, state, context, 3, start);\n                                start = parser.tokenStart;\n                                continue;\n                            }\n                            return 33619994;\n                        }\n                        if (ch === 61) {\n                            advanceChar(parser);\n                            return 4194337;\n                        }\n                        return 25233969;\n                    }\n                    case 8457014: {\n                        advanceChar(parser);\n                        if (parser.index < parser.end) {\n                            const ch = parser.currentChar;\n                            if (ch === 47) {\n                                advanceChar(parser);\n                                state = skipSingleLineComment(parser, source, state, 0, parser.tokenStart);\n                                start = parser.tokenStart;\n                                continue;\n                            }\n                            if (ch === 42) {\n                                advanceChar(parser);\n                                state = skipMultiLineComment(parser, source, state);\n                                start = parser.tokenStart;\n                                continue;\n                            }\n                            if (context & 32) {\n                                return scanRegularExpression(parser);\n                            }\n                            if (ch === 61) {\n                                advanceChar(parser);\n                                return 4259875;\n                            }\n                        }\n                        return 8457014;\n                    }\n                    case 67108877: {\n                        const next = advanceChar(parser);\n                        if (next >= 48 && next <= 57)\n                            return scanNumber(parser, context, 64 | 16);\n                        if (next === 46) {\n                            const index = parser.index + 1;\n                            if (index < parser.end && source.charCodeAt(index) === 46) {\n                                parser.column += 2;\n                                parser.currentChar = source.charCodeAt((parser.index += 2));\n                                return 14;\n                            }\n                        }\n                        return 67108877;\n                    }\n                    case 8389702: {\n                        advanceChar(parser);\n                        const ch = parser.currentChar;\n                        if (ch === 124) {\n                            advanceChar(parser);\n                            if (parser.currentChar === 61) {\n                                advanceChar(parser);\n                                return 4194344;\n                            }\n                            return 8913465;\n                        }\n                        if (ch === 61) {\n                            advanceChar(parser);\n                            return 4194342;\n                        }\n                        return 8389702;\n                    }\n                    case 8390721: {\n                        advanceChar(parser);\n                        const ch = parser.currentChar;\n                        if (ch === 61) {\n                            advanceChar(parser);\n                            return 8390719;\n                        }\n                        if (ch !== 62)\n                            return 8390721;\n                        advanceChar(parser);\n                        if (parser.index < parser.end) {\n                            const ch = parser.currentChar;\n                            if (ch === 62) {\n                                if (advanceChar(parser) === 61) {\n                                    advanceChar(parser);\n                                    return 4194334;\n                                }\n                                return 8390980;\n                            }\n                            if (ch === 61) {\n                                advanceChar(parser);\n                                return 4194333;\n                            }\n                        }\n                        return 8390979;\n                    }\n                    case 8390213: {\n                        advanceChar(parser);\n                        const ch = parser.currentChar;\n                        if (ch === 38) {\n                            advanceChar(parser);\n                            if (parser.currentChar === 61) {\n                                advanceChar(parser);\n                                return 4194345;\n                            }\n                            return 8913720;\n                        }\n                        if (ch === 61) {\n                            advanceChar(parser);\n                            return 4194343;\n                        }\n                        return 8390213;\n                    }\n                    case 22: {\n                        let ch = advanceChar(parser);\n                        if (ch === 63) {\n                            advanceChar(parser);\n                            if (parser.currentChar === 61) {\n                                advanceChar(parser);\n                                return 4194346;\n                            }\n                            return 276824445;\n                        }\n                        if (ch === 46) {\n                            const index = parser.index + 1;\n                            if (index < parser.end) {\n                                ch = source.charCodeAt(index);\n                                if (!(ch >= 48 && ch <= 57)) {\n                                    advanceChar(parser);\n                                    return 67108990;\n                                }\n                            }\n                        }\n                        return 22;\n                    }\n                }\n            }\n            else {\n                if ((char ^ 8232) <= 1) {\n                    state = (state & -5) | 1;\n                    scanNewLine(parser);\n                    continue;\n                }\n                const merged = consumePossibleSurrogatePair(parser);\n                if (merged > 0)\n                    char = merged;\n                if (isIDStart(char)) {\n                    parser.tokenValue = '';\n                    return scanIdentifierSlowCase(parser, context, 0, 0);\n                }\n                if (isExoticECMAScriptWhitespace(char)) {\n                    advanceChar(parser);\n                    continue;\n                }\n                parser.report(20, String.fromCodePoint(char));\n            }\n        }\n        return 1048576;\n    }\n\n    const entities = {\n        AElig: '\\u00C6',\n        AMP: '\\u0026',\n        Aacute: '\\u00C1',\n        Abreve: '\\u0102',\n        Acirc: '\\u00C2',\n        Acy: '\\u0410',\n        Afr: '\\uD835\\uDD04',\n        Agrave: '\\u00C0',\n        Alpha: '\\u0391',\n        Amacr: '\\u0100',\n        And: '\\u2A53',\n        Aogon: '\\u0104',\n        Aopf: '\\uD835\\uDD38',\n        ApplyFunction: '\\u2061',\n        Aring: '\\u00C5',\n        Ascr: '\\uD835\\uDC9C',\n        Assign: '\\u2254',\n        Atilde: '\\u00C3',\n        Auml: '\\u00C4',\n        Backslash: '\\u2216',\n        Barv: '\\u2AE7',\n        Barwed: '\\u2306',\n        Bcy: '\\u0411',\n        Because: '\\u2235',\n        Bernoullis: '\\u212C',\n        Beta: '\\u0392',\n        Bfr: '\\uD835\\uDD05',\n        Bopf: '\\uD835\\uDD39',\n        Breve: '\\u02D8',\n        Bscr: '\\u212C',\n        Bumpeq: '\\u224E',\n        CHcy: '\\u0427',\n        COPY: '\\u00A9',\n        Cacute: '\\u0106',\n        Cap: '\\u22D2',\n        CapitalDifferentialD: '\\u2145',\n        Cayleys: '\\u212D',\n        Ccaron: '\\u010C',\n        Ccedil: '\\u00C7',\n        Ccirc: '\\u0108',\n        Cconint: '\\u2230',\n        Cdot: '\\u010A',\n        Cedilla: '\\u00B8',\n        CenterDot: '\\u00B7',\n        Cfr: '\\u212D',\n        Chi: '\\u03A7',\n        CircleDot: '\\u2299',\n        CircleMinus: '\\u2296',\n        CirclePlus: '\\u2295',\n        CircleTimes: '\\u2297',\n        ClockwiseContourIntegral: '\\u2232',\n        CloseCurlyDoubleQuote: '\\u201D',\n        CloseCurlyQuote: '\\u2019',\n        Colon: '\\u2237',\n        Colone: '\\u2A74',\n        Congruent: '\\u2261',\n        Conint: '\\u222F',\n        ContourIntegral: '\\u222E',\n        Copf: '\\u2102',\n        Coproduct: '\\u2210',\n        CounterClockwiseContourIntegral: '\\u2233',\n        Cross: '\\u2A2F',\n        Cscr: '\\uD835\\uDC9E',\n        Cup: '\\u22D3',\n        CupCap: '\\u224D',\n        DD: '\\u2145',\n        DDotrahd: '\\u2911',\n        DJcy: '\\u0402',\n        DScy: '\\u0405',\n        DZcy: '\\u040F',\n        Dagger: '\\u2021',\n        Darr: '\\u21A1',\n        Dashv: '\\u2AE4',\n        Dcaron: '\\u010E',\n        Dcy: '\\u0414',\n        Del: '\\u2207',\n        Delta: '\\u0394',\n        Dfr: '\\uD835\\uDD07',\n        DiacriticalAcute: '\\u00B4',\n        DiacriticalDot: '\\u02D9',\n        DiacriticalDoubleAcute: '\\u02DD',\n        DiacriticalGrave: '\\u0060',\n        DiacriticalTilde: '\\u02DC',\n        Diamond: '\\u22C4',\n        DifferentialD: '\\u2146',\n        Dopf: '\\uD835\\uDD3B',\n        Dot: '\\u00A8',\n        DotDot: '\\u20DC',\n        DotEqual: '\\u2250',\n        DoubleContourIntegral: '\\u222F',\n        DoubleDot: '\\u00A8',\n        DoubleDownArrow: '\\u21D3',\n        DoubleLeftArrow: '\\u21D0',\n        DoubleLeftRightArrow: '\\u21D4',\n        DoubleLeftTee: '\\u2AE4',\n        DoubleLongLeftArrow: '\\u27F8',\n        DoubleLongLeftRightArrow: '\\u27FA',\n        DoubleLongRightArrow: '\\u27F9',\n        DoubleRightArrow: '\\u21D2',\n        DoubleRightTee: '\\u22A8',\n        DoubleUpArrow: '\\u21D1',\n        DoubleUpDownArrow: '\\u21D5',\n        DoubleVerticalBar: '\\u2225',\n        DownArrow: '\\u2193',\n        DownArrowBar: '\\u2913',\n        DownArrowUpArrow: '\\u21F5',\n        DownBreve: '\\u0311',\n        DownLeftRightVector: '\\u2950',\n        DownLeftTeeVector: '\\u295E',\n        DownLeftVector: '\\u21BD',\n        DownLeftVectorBar: '\\u2956',\n        DownRightTeeVector: '\\u295F',\n        DownRightVector: '\\u21C1',\n        DownRightVectorBar: '\\u2957',\n        DownTee: '\\u22A4',\n        DownTeeArrow: '\\u21A7',\n        Downarrow: '\\u21D3',\n        Dscr: '\\uD835\\uDC9F',\n        Dstrok: '\\u0110',\n        ENG: '\\u014A',\n        ETH: '\\u00D0',\n        Eacute: '\\u00C9',\n        Ecaron: '\\u011A',\n        Ecirc: '\\u00CA',\n        Ecy: '\\u042D',\n        Edot: '\\u0116',\n        Efr: '\\uD835\\uDD08',\n        Egrave: '\\u00C8',\n        Element: '\\u2208',\n        Emacr: '\\u0112',\n        EmptySmallSquare: '\\u25FB',\n        EmptyVerySmallSquare: '\\u25AB',\n        Eogon: '\\u0118',\n        Eopf: '\\uD835\\uDD3C',\n        Epsilon: '\\u0395',\n        Equal: '\\u2A75',\n        EqualTilde: '\\u2242',\n        Equilibrium: '\\u21CC',\n        Escr: '\\u2130',\n        Esim: '\\u2A73',\n        Eta: '\\u0397',\n        Euml: '\\u00CB',\n        Exists: '\\u2203',\n        ExponentialE: '\\u2147',\n        Fcy: '\\u0424',\n        Ffr: '\\uD835\\uDD09',\n        FilledSmallSquare: '\\u25FC',\n        FilledVerySmallSquare: '\\u25AA',\n        Fopf: '\\uD835\\uDD3D',\n        ForAll: '\\u2200',\n        Fouriertrf: '\\u2131',\n        Fscr: '\\u2131',\n        GJcy: '\\u0403',\n        GT: '\\u003E',\n        Gamma: '\\u0393',\n        Gammad: '\\u03DC',\n        Gbreve: '\\u011E',\n        Gcedil: '\\u0122',\n        Gcirc: '\\u011C',\n        Gcy: '\\u0413',\n        Gdot: '\\u0120',\n        Gfr: '\\uD835\\uDD0A',\n        Gg: '\\u22D9',\n        Gopf: '\\uD835\\uDD3E',\n        GreaterEqual: '\\u2265',\n        GreaterEqualLess: '\\u22DB',\n        GreaterFullEqual: '\\u2267',\n        GreaterGreater: '\\u2AA2',\n        GreaterLess: '\\u2277',\n        GreaterSlantEqual: '\\u2A7E',\n        GreaterTilde: '\\u2273',\n        Gscr: '\\uD835\\uDCA2',\n        Gt: '\\u226B',\n        HARDcy: '\\u042A',\n        Hacek: '\\u02C7',\n        Hat: '\\u005E',\n        Hcirc: '\\u0124',\n        Hfr: '\\u210C',\n        HilbertSpace: '\\u210B',\n        Hopf: '\\u210D',\n        HorizontalLine: '\\u2500',\n        Hscr: '\\u210B',\n        Hstrok: '\\u0126',\n        HumpDownHump: '\\u224E',\n        HumpEqual: '\\u224F',\n        IEcy: '\\u0415',\n        IJlig: '\\u0132',\n        IOcy: '\\u0401',\n        Iacute: '\\u00CD',\n        Icirc: '\\u00CE',\n        Icy: '\\u0418',\n        Idot: '\\u0130',\n        Ifr: '\\u2111',\n        Igrave: '\\u00CC',\n        Im: '\\u2111',\n        Imacr: '\\u012A',\n        ImaginaryI: '\\u2148',\n        Implies: '\\u21D2',\n        Int: '\\u222C',\n        Integral: '\\u222B',\n        Intersection: '\\u22C2',\n        InvisibleComma: '\\u2063',\n        InvisibleTimes: '\\u2062',\n        Iogon: '\\u012E',\n        Iopf: '\\uD835\\uDD40',\n        Iota: '\\u0399',\n        Iscr: '\\u2110',\n        Itilde: '\\u0128',\n        Iukcy: '\\u0406',\n        Iuml: '\\u00CF',\n        Jcirc: '\\u0134',\n        Jcy: '\\u0419',\n        Jfr: '\\uD835\\uDD0D',\n        Jopf: '\\uD835\\uDD41',\n        Jscr: '\\uD835\\uDCA5',\n        Jsercy: '\\u0408',\n        Jukcy: '\\u0404',\n        KHcy: '\\u0425',\n        KJcy: '\\u040C',\n        Kappa: '\\u039A',\n        Kcedil: '\\u0136',\n        Kcy: '\\u041A',\n        Kfr: '\\uD835\\uDD0E',\n        Kopf: '\\uD835\\uDD42',\n        Kscr: '\\uD835\\uDCA6',\n        LJcy: '\\u0409',\n        LT: '\\u003C',\n        Lacute: '\\u0139',\n        Lambda: '\\u039B',\n        Lang: '\\u27EA',\n        Laplacetrf: '\\u2112',\n        Larr: '\\u219E',\n        Lcaron: '\\u013D',\n        Lcedil: '\\u013B',\n        Lcy: '\\u041B',\n        LeftAngleBracket: '\\u27E8',\n        LeftArrow: '\\u2190',\n        LeftArrowBar: '\\u21E4',\n        LeftArrowRightArrow: '\\u21C6',\n        LeftCeiling: '\\u2308',\n        LeftDoubleBracket: '\\u27E6',\n        LeftDownTeeVector: '\\u2961',\n        LeftDownVector: '\\u21C3',\n        LeftDownVectorBar: '\\u2959',\n        LeftFloor: '\\u230A',\n        LeftRightArrow: '\\u2194',\n        LeftRightVector: '\\u294E',\n        LeftTee: '\\u22A3',\n        LeftTeeArrow: '\\u21A4',\n        LeftTeeVector: '\\u295A',\n        LeftTriangle: '\\u22B2',\n        LeftTriangleBar: '\\u29CF',\n        LeftTriangleEqual: '\\u22B4',\n        LeftUpDownVector: '\\u2951',\n        LeftUpTeeVector: '\\u2960',\n        LeftUpVector: '\\u21BF',\n        LeftUpVectorBar: '\\u2958',\n        LeftVector: '\\u21BC',\n        LeftVectorBar: '\\u2952',\n        Leftarrow: '\\u21D0',\n        Leftrightarrow: '\\u21D4',\n        LessEqualGreater: '\\u22DA',\n        LessFullEqual: '\\u2266',\n        LessGreater: '\\u2276',\n        LessLess: '\\u2AA1',\n        LessSlantEqual: '\\u2A7D',\n        LessTilde: '\\u2272',\n        Lfr: '\\uD835\\uDD0F',\n        Ll: '\\u22D8',\n        Lleftarrow: '\\u21DA',\n        Lmidot: '\\u013F',\n        LongLeftArrow: '\\u27F5',\n        LongLeftRightArrow: '\\u27F7',\n        LongRightArrow: '\\u27F6',\n        Longleftarrow: '\\u27F8',\n        Longleftrightarrow: '\\u27FA',\n        Longrightarrow: '\\u27F9',\n        Lopf: '\\uD835\\uDD43',\n        LowerLeftArrow: '\\u2199',\n        LowerRightArrow: '\\u2198',\n        Lscr: '\\u2112',\n        Lsh: '\\u21B0',\n        Lstrok: '\\u0141',\n        Lt: '\\u226A',\n        Map: '\\u2905',\n        Mcy: '\\u041C',\n        MediumSpace: '\\u205F',\n        Mellintrf: '\\u2133',\n        Mfr: '\\uD835\\uDD10',\n        MinusPlus: '\\u2213',\n        Mopf: '\\uD835\\uDD44',\n        Mscr: '\\u2133',\n        Mu: '\\u039C',\n        NJcy: '\\u040A',\n        Nacute: '\\u0143',\n        Ncaron: '\\u0147',\n        Ncedil: '\\u0145',\n        Ncy: '\\u041D',\n        NegativeMediumSpace: '\\u200B',\n        NegativeThickSpace: '\\u200B',\n        NegativeThinSpace: '\\u200B',\n        NegativeVeryThinSpace: '\\u200B',\n        NestedGreaterGreater: '\\u226B',\n        NestedLessLess: '\\u226A',\n        NewLine: '\\u000A',\n        Nfr: '\\uD835\\uDD11',\n        NoBreak: '\\u2060',\n        NonBreakingSpace: '\\u00A0',\n        Nopf: '\\u2115',\n        Not: '\\u2AEC',\n        NotCongruent: '\\u2262',\n        NotCupCap: '\\u226D',\n        NotDoubleVerticalBar: '\\u2226',\n        NotElement: '\\u2209',\n        NotEqual: '\\u2260',\n        NotEqualTilde: '\\u2242\\u0338',\n        NotExists: '\\u2204',\n        NotGreater: '\\u226F',\n        NotGreaterEqual: '\\u2271',\n        NotGreaterFullEqual: '\\u2267\\u0338',\n        NotGreaterGreater: '\\u226B\\u0338',\n        NotGreaterLess: '\\u2279',\n        NotGreaterSlantEqual: '\\u2A7E\\u0338',\n        NotGreaterTilde: '\\u2275',\n        NotHumpDownHump: '\\u224E\\u0338',\n        NotHumpEqual: '\\u224F\\u0338',\n        NotLeftTriangle: '\\u22EA',\n        NotLeftTriangleBar: '\\u29CF\\u0338',\n        NotLeftTriangleEqual: '\\u22EC',\n        NotLess: '\\u226E',\n        NotLessEqual: '\\u2270',\n        NotLessGreater: '\\u2278',\n        NotLessLess: '\\u226A\\u0338',\n        NotLessSlantEqual: '\\u2A7D\\u0338',\n        NotLessTilde: '\\u2274',\n        NotNestedGreaterGreater: '\\u2AA2\\u0338',\n        NotNestedLessLess: '\\u2AA1\\u0338',\n        NotPrecedes: '\\u2280',\n        NotPrecedesEqual: '\\u2AAF\\u0338',\n        NotPrecedesSlantEqual: '\\u22E0',\n        NotReverseElement: '\\u220C',\n        NotRightTriangle: '\\u22EB',\n        NotRightTriangleBar: '\\u29D0\\u0338',\n        NotRightTriangleEqual: '\\u22ED',\n        NotSquareSubset: '\\u228F\\u0338',\n        NotSquareSubsetEqual: '\\u22E2',\n        NotSquareSuperset: '\\u2290\\u0338',\n        NotSquareSupersetEqual: '\\u22E3',\n        NotSubset: '\\u2282\\u20D2',\n        NotSubsetEqual: '\\u2288',\n        NotSucceeds: '\\u2281',\n        NotSucceedsEqual: '\\u2AB0\\u0338',\n        NotSucceedsSlantEqual: '\\u22E1',\n        NotSucceedsTilde: '\\u227F\\u0338',\n        NotSuperset: '\\u2283\\u20D2',\n        NotSupersetEqual: '\\u2289',\n        NotTilde: '\\u2241',\n        NotTildeEqual: '\\u2244',\n        NotTildeFullEqual: '\\u2247',\n        NotTildeTilde: '\\u2249',\n        NotVerticalBar: '\\u2224',\n        Nscr: '\\uD835\\uDCA9',\n        Ntilde: '\\u00D1',\n        Nu: '\\u039D',\n        OElig: '\\u0152',\n        Oacute: '\\u00D3',\n        Ocirc: '\\u00D4',\n        Ocy: '\\u041E',\n        Odblac: '\\u0150',\n        Ofr: '\\uD835\\uDD12',\n        Ograve: '\\u00D2',\n        Omacr: '\\u014C',\n        Omega: '\\u03A9',\n        Omicron: '\\u039F',\n        Oopf: '\\uD835\\uDD46',\n        OpenCurlyDoubleQuote: '\\u201C',\n        OpenCurlyQuote: '\\u2018',\n        Or: '\\u2A54',\n        Oscr: '\\uD835\\uDCAA',\n        Oslash: '\\u00D8',\n        Otilde: '\\u00D5',\n        Otimes: '\\u2A37',\n        Ouml: '\\u00D6',\n        OverBar: '\\u203E',\n        OverBrace: '\\u23DE',\n        OverBracket: '\\u23B4',\n        OverParenthesis: '\\u23DC',\n        PartialD: '\\u2202',\n        Pcy: '\\u041F',\n        Pfr: '\\uD835\\uDD13',\n        Phi: '\\u03A6',\n        Pi: '\\u03A0',\n        PlusMinus: '\\u00B1',\n        Poincareplane: '\\u210C',\n        Popf: '\\u2119',\n        Pr: '\\u2ABB',\n        Precedes: '\\u227A',\n        PrecedesEqual: '\\u2AAF',\n        PrecedesSlantEqual: '\\u227C',\n        PrecedesTilde: '\\u227E',\n        Prime: '\\u2033',\n        Product: '\\u220F',\n        Proportion: '\\u2237',\n        Proportional: '\\u221D',\n        Pscr: '\\uD835\\uDCAB',\n        Psi: '\\u03A8',\n        QUOT: '\\u0022',\n        Qfr: '\\uD835\\uDD14',\n        Qopf: '\\u211A',\n        Qscr: '\\uD835\\uDCAC',\n        RBarr: '\\u2910',\n        REG: '\\u00AE',\n        Racute: '\\u0154',\n        Rang: '\\u27EB',\n        Rarr: '\\u21A0',\n        Rarrtl: '\\u2916',\n        Rcaron: '\\u0158',\n        Rcedil: '\\u0156',\n        Rcy: '\\u0420',\n        Re: '\\u211C',\n        ReverseElement: '\\u220B',\n        ReverseEquilibrium: '\\u21CB',\n        ReverseUpEquilibrium: '\\u296F',\n        Rfr: '\\u211C',\n        Rho: '\\u03A1',\n        RightAngleBracket: '\\u27E9',\n        RightArrow: '\\u2192',\n        RightArrowBar: '\\u21E5',\n        RightArrowLeftArrow: '\\u21C4',\n        RightCeiling: '\\u2309',\n        RightDoubleBracket: '\\u27E7',\n        RightDownTeeVector: '\\u295D',\n        RightDownVector: '\\u21C2',\n        RightDownVectorBar: '\\u2955',\n        RightFloor: '\\u230B',\n        RightTee: '\\u22A2',\n        RightTeeArrow: '\\u21A6',\n        RightTeeVector: '\\u295B',\n        RightTriangle: '\\u22B3',\n        RightTriangleBar: '\\u29D0',\n        RightTriangleEqual: '\\u22B5',\n        RightUpDownVector: '\\u294F',\n        RightUpTeeVector: '\\u295C',\n        RightUpVector: '\\u21BE',\n        RightUpVectorBar: '\\u2954',\n        RightVector: '\\u21C0',\n        RightVectorBar: '\\u2953',\n        Rightarrow: '\\u21D2',\n        Ropf: '\\u211D',\n        RoundImplies: '\\u2970',\n        Rrightarrow: '\\u21DB',\n        Rscr: '\\u211B',\n        Rsh: '\\u21B1',\n        RuleDelayed: '\\u29F4',\n        SHCHcy: '\\u0429',\n        SHcy: '\\u0428',\n        SOFTcy: '\\u042C',\n        Sacute: '\\u015A',\n        Sc: '\\u2ABC',\n        Scaron: '\\u0160',\n        Scedil: '\\u015E',\n        Scirc: '\\u015C',\n        Scy: '\\u0421',\n        Sfr: '\\uD835\\uDD16',\n        ShortDownArrow: '\\u2193',\n        ShortLeftArrow: '\\u2190',\n        ShortRightArrow: '\\u2192',\n        ShortUpArrow: '\\u2191',\n        Sigma: '\\u03A3',\n        SmallCircle: '\\u2218',\n        Sopf: '\\uD835\\uDD4A',\n        Sqrt: '\\u221A',\n        Square: '\\u25A1',\n        SquareIntersection: '\\u2293',\n        SquareSubset: '\\u228F',\n        SquareSubsetEqual: '\\u2291',\n        SquareSuperset: '\\u2290',\n        SquareSupersetEqual: '\\u2292',\n        SquareUnion: '\\u2294',\n        Sscr: '\\uD835\\uDCAE',\n        Star: '\\u22C6',\n        Sub: '\\u22D0',\n        Subset: '\\u22D0',\n        SubsetEqual: '\\u2286',\n        Succeeds: '\\u227B',\n        SucceedsEqual: '\\u2AB0',\n        SucceedsSlantEqual: '\\u227D',\n        SucceedsTilde: '\\u227F',\n        SuchThat: '\\u220B',\n        Sum: '\\u2211',\n        Sup: '\\u22D1',\n        Superset: '\\u2283',\n        SupersetEqual: '\\u2287',\n        Supset: '\\u22D1',\n        THORN: '\\u00DE',\n        TRADE: '\\u2122',\n        TSHcy: '\\u040B',\n        TScy: '\\u0426',\n        Tab: '\\u0009',\n        Tau: '\\u03A4',\n        Tcaron: '\\u0164',\n        Tcedil: '\\u0162',\n        Tcy: '\\u0422',\n        Tfr: '\\uD835\\uDD17',\n        Therefore: '\\u2234',\n        Theta: '\\u0398',\n        ThickSpace: '\\u205F\\u200A',\n        ThinSpace: '\\u2009',\n        Tilde: '\\u223C',\n        TildeEqual: '\\u2243',\n        TildeFullEqual: '\\u2245',\n        TildeTilde: '\\u2248',\n        Topf: '\\uD835\\uDD4B',\n        TripleDot: '\\u20DB',\n        Tscr: '\\uD835\\uDCAF',\n        Tstrok: '\\u0166',\n        Uacute: '\\u00DA',\n        Uarr: '\\u219F',\n        Uarrocir: '\\u2949',\n        Ubrcy: '\\u040E',\n        Ubreve: '\\u016C',\n        Ucirc: '\\u00DB',\n        Ucy: '\\u0423',\n        Udblac: '\\u0170',\n        Ufr: '\\uD835\\uDD18',\n        Ugrave: '\\u00D9',\n        Umacr: '\\u016A',\n        UnderBar: '\\u005F',\n        UnderBrace: '\\u23DF',\n        UnderBracket: '\\u23B5',\n        UnderParenthesis: '\\u23DD',\n        Union: '\\u22C3',\n        UnionPlus: '\\u228E',\n        Uogon: '\\u0172',\n        Uopf: '\\uD835\\uDD4C',\n        UpArrow: '\\u2191',\n        UpArrowBar: '\\u2912',\n        UpArrowDownArrow: '\\u21C5',\n        UpDownArrow: '\\u2195',\n        UpEquilibrium: '\\u296E',\n        UpTee: '\\u22A5',\n        UpTeeArrow: '\\u21A5',\n        Uparrow: '\\u21D1',\n        Updownarrow: '\\u21D5',\n        UpperLeftArrow: '\\u2196',\n        UpperRightArrow: '\\u2197',\n        Upsi: '\\u03D2',\n        Upsilon: '\\u03A5',\n        Uring: '\\u016E',\n        Uscr: '\\uD835\\uDCB0',\n        Utilde: '\\u0168',\n        Uuml: '\\u00DC',\n        VDash: '\\u22AB',\n        Vbar: '\\u2AEB',\n        Vcy: '\\u0412',\n        Vdash: '\\u22A9',\n        Vdashl: '\\u2AE6',\n        Vee: '\\u22C1',\n        Verbar: '\\u2016',\n        Vert: '\\u2016',\n        VerticalBar: '\\u2223',\n        VerticalLine: '\\u007C',\n        VerticalSeparator: '\\u2758',\n        VerticalTilde: '\\u2240',\n        VeryThinSpace: '\\u200A',\n        Vfr: '\\uD835\\uDD19',\n        Vopf: '\\uD835\\uDD4D',\n        Vscr: '\\uD835\\uDCB1',\n        Vvdash: '\\u22AA',\n        Wcirc: '\\u0174',\n        Wedge: '\\u22C0',\n        Wfr: '\\uD835\\uDD1A',\n        Wopf: '\\uD835\\uDD4E',\n        Wscr: '\\uD835\\uDCB2',\n        Xfr: '\\uD835\\uDD1B',\n        Xi: '\\u039E',\n        Xopf: '\\uD835\\uDD4F',\n        Xscr: '\\uD835\\uDCB3',\n        YAcy: '\\u042F',\n        YIcy: '\\u0407',\n        YUcy: '\\u042E',\n        Yacute: '\\u00DD',\n        Ycirc: '\\u0176',\n        Ycy: '\\u042B',\n        Yfr: '\\uD835\\uDD1C',\n        Yopf: '\\uD835\\uDD50',\n        Yscr: '\\uD835\\uDCB4',\n        Yuml: '\\u0178',\n        ZHcy: '\\u0416',\n        Zacute: '\\u0179',\n        Zcaron: '\\u017D',\n        Zcy: '\\u0417',\n        Zdot: '\\u017B',\n        ZeroWidthSpace: '\\u200B',\n        Zeta: '\\u0396',\n        Zfr: '\\u2128',\n        Zopf: '\\u2124',\n        Zscr: '\\uD835\\uDCB5',\n        aacute: '\\u00E1',\n        abreve: '\\u0103',\n        ac: '\\u223E',\n        acE: '\\u223E\\u0333',\n        acd: '\\u223F',\n        acirc: '\\u00E2',\n        acute: '\\u00B4',\n        acy: '\\u0430',\n        aelig: '\\u00E6',\n        af: '\\u2061',\n        afr: '\\uD835\\uDD1E',\n        agrave: '\\u00E0',\n        alefsym: '\\u2135',\n        aleph: '\\u2135',\n        alpha: '\\u03B1',\n        amacr: '\\u0101',\n        amalg: '\\u2A3F',\n        amp: '\\u0026',\n        and: '\\u2227',\n        andand: '\\u2A55',\n        andd: '\\u2A5C',\n        andslope: '\\u2A58',\n        andv: '\\u2A5A',\n        ang: '\\u2220',\n        ange: '\\u29A4',\n        angle: '\\u2220',\n        angmsd: '\\u2221',\n        angmsdaa: '\\u29A8',\n        angmsdab: '\\u29A9',\n        angmsdac: '\\u29AA',\n        angmsdad: '\\u29AB',\n        angmsdae: '\\u29AC',\n        angmsdaf: '\\u29AD',\n        angmsdag: '\\u29AE',\n        angmsdah: '\\u29AF',\n        angrt: '\\u221F',\n        angrtvb: '\\u22BE',\n        angrtvbd: '\\u299D',\n        angsph: '\\u2222',\n        angst: '\\u00C5',\n        angzarr: '\\u237C',\n        aogon: '\\u0105',\n        aopf: '\\uD835\\uDD52',\n        ap: '\\u2248',\n        apE: '\\u2A70',\n        apacir: '\\u2A6F',\n        ape: '\\u224A',\n        apid: '\\u224B',\n        apos: '\\u0027',\n        approx: '\\u2248',\n        approxeq: '\\u224A',\n        aring: '\\u00E5',\n        ascr: '\\uD835\\uDCB6',\n        ast: '\\u002A',\n        asymp: '\\u2248',\n        asympeq: '\\u224D',\n        atilde: '\\u00E3',\n        auml: '\\u00E4',\n        awconint: '\\u2233',\n        awint: '\\u2A11',\n        bNot: '\\u2AED',\n        backcong: '\\u224C',\n        backepsilon: '\\u03F6',\n        backprime: '\\u2035',\n        backsim: '\\u223D',\n        backsimeq: '\\u22CD',\n        barvee: '\\u22BD',\n        barwed: '\\u2305',\n        barwedge: '\\u2305',\n        bbrk: '\\u23B5',\n        bbrktbrk: '\\u23B6',\n        bcong: '\\u224C',\n        bcy: '\\u0431',\n        bdquo: '\\u201E',\n        becaus: '\\u2235',\n        because: '\\u2235',\n        bemptyv: '\\u29B0',\n        bepsi: '\\u03F6',\n        bernou: '\\u212C',\n        beta: '\\u03B2',\n        beth: '\\u2136',\n        between: '\\u226C',\n        bfr: '\\uD835\\uDD1F',\n        bigcap: '\\u22C2',\n        bigcirc: '\\u25EF',\n        bigcup: '\\u22C3',\n        bigodot: '\\u2A00',\n        bigoplus: '\\u2A01',\n        bigotimes: '\\u2A02',\n        bigsqcup: '\\u2A06',\n        bigstar: '\\u2605',\n        bigtriangledown: '\\u25BD',\n        bigtriangleup: '\\u25B3',\n        biguplus: '\\u2A04',\n        bigvee: '\\u22C1',\n        bigwedge: '\\u22C0',\n        bkarow: '\\u290D',\n        blacklozenge: '\\u29EB',\n        blacksquare: '\\u25AA',\n        blacktriangle: '\\u25B4',\n        blacktriangledown: '\\u25BE',\n        blacktriangleleft: '\\u25C2',\n        blacktriangleright: '\\u25B8',\n        blank: '\\u2423',\n        blk12: '\\u2592',\n        blk14: '\\u2591',\n        blk34: '\\u2593',\n        block: '\\u2588',\n        bne: '\\u003D\\u20E5',\n        bnequiv: '\\u2261\\u20E5',\n        bnot: '\\u2310',\n        bopf: '\\uD835\\uDD53',\n        bot: '\\u22A5',\n        bottom: '\\u22A5',\n        bowtie: '\\u22C8',\n        boxDL: '\\u2557',\n        boxDR: '\\u2554',\n        boxDl: '\\u2556',\n        boxDr: '\\u2553',\n        boxH: '\\u2550',\n        boxHD: '\\u2566',\n        boxHU: '\\u2569',\n        boxHd: '\\u2564',\n        boxHu: '\\u2567',\n        boxUL: '\\u255D',\n        boxUR: '\\u255A',\n        boxUl: '\\u255C',\n        boxUr: '\\u2559',\n        boxV: '\\u2551',\n        boxVH: '\\u256C',\n        boxVL: '\\u2563',\n        boxVR: '\\u2560',\n        boxVh: '\\u256B',\n        boxVl: '\\u2562',\n        boxVr: '\\u255F',\n        boxbox: '\\u29C9',\n        boxdL: '\\u2555',\n        boxdR: '\\u2552',\n        boxdl: '\\u2510',\n        boxdr: '\\u250C',\n        boxh: '\\u2500',\n        boxhD: '\\u2565',\n        boxhU: '\\u2568',\n        boxhd: '\\u252C',\n        boxhu: '\\u2534',\n        boxminus: '\\u229F',\n        boxplus: '\\u229E',\n        boxtimes: '\\u22A0',\n        boxuL: '\\u255B',\n        boxuR: '\\u2558',\n        boxul: '\\u2518',\n        boxur: '\\u2514',\n        boxv: '\\u2502',\n        boxvH: '\\u256A',\n        boxvL: '\\u2561',\n        boxvR: '\\u255E',\n        boxvh: '\\u253C',\n        boxvl: '\\u2524',\n        boxvr: '\\u251C',\n        bprime: '\\u2035',\n        breve: '\\u02D8',\n        brvbar: '\\u00A6',\n        bscr: '\\uD835\\uDCB7',\n        bsemi: '\\u204F',\n        bsim: '\\u223D',\n        bsime: '\\u22CD',\n        bsol: '\\u005C',\n        bsolb: '\\u29C5',\n        bsolhsub: '\\u27C8',\n        bull: '\\u2022',\n        bullet: '\\u2022',\n        bump: '\\u224E',\n        bumpE: '\\u2AAE',\n        bumpe: '\\u224F',\n        bumpeq: '\\u224F',\n        cacute: '\\u0107',\n        cap: '\\u2229',\n        capand: '\\u2A44',\n        capbrcup: '\\u2A49',\n        capcap: '\\u2A4B',\n        capcup: '\\u2A47',\n        capdot: '\\u2A40',\n        caps: '\\u2229\\uFE00',\n        caret: '\\u2041',\n        caron: '\\u02C7',\n        ccaps: '\\u2A4D',\n        ccaron: '\\u010D',\n        ccedil: '\\u00E7',\n        ccirc: '\\u0109',\n        ccups: '\\u2A4C',\n        ccupssm: '\\u2A50',\n        cdot: '\\u010B',\n        cedil: '\\u00B8',\n        cemptyv: '\\u29B2',\n        cent: '\\u00A2',\n        centerdot: '\\u00B7',\n        cfr: '\\uD835\\uDD20',\n        chcy: '\\u0447',\n        check: '\\u2713',\n        checkmark: '\\u2713',\n        chi: '\\u03C7',\n        cir: '\\u25CB',\n        cirE: '\\u29C3',\n        circ: '\\u02C6',\n        circeq: '\\u2257',\n        circlearrowleft: '\\u21BA',\n        circlearrowright: '\\u21BB',\n        circledR: '\\u00AE',\n        circledS: '\\u24C8',\n        circledast: '\\u229B',\n        circledcirc: '\\u229A',\n        circleddash: '\\u229D',\n        cire: '\\u2257',\n        cirfnint: '\\u2A10',\n        cirmid: '\\u2AEF',\n        cirscir: '\\u29C2',\n        clubs: '\\u2663',\n        clubsuit: '\\u2663',\n        colon: '\\u003A',\n        colone: '\\u2254',\n        coloneq: '\\u2254',\n        comma: '\\u002C',\n        commat: '\\u0040',\n        comp: '\\u2201',\n        compfn: '\\u2218',\n        complement: '\\u2201',\n        complexes: '\\u2102',\n        cong: '\\u2245',\n        congdot: '\\u2A6D',\n        conint: '\\u222E',\n        copf: '\\uD835\\uDD54',\n        coprod: '\\u2210',\n        copy: '\\u00A9',\n        copysr: '\\u2117',\n        crarr: '\\u21B5',\n        cross: '\\u2717',\n        cscr: '\\uD835\\uDCB8',\n        csub: '\\u2ACF',\n        csube: '\\u2AD1',\n        csup: '\\u2AD0',\n        csupe: '\\u2AD2',\n        ctdot: '\\u22EF',\n        cudarrl: '\\u2938',\n        cudarrr: '\\u2935',\n        cuepr: '\\u22DE',\n        cuesc: '\\u22DF',\n        cularr: '\\u21B6',\n        cularrp: '\\u293D',\n        cup: '\\u222A',\n        cupbrcap: '\\u2A48',\n        cupcap: '\\u2A46',\n        cupcup: '\\u2A4A',\n        cupdot: '\\u228D',\n        cupor: '\\u2A45',\n        cups: '\\u222A\\uFE00',\n        curarr: '\\u21B7',\n        curarrm: '\\u293C',\n        curlyeqprec: '\\u22DE',\n        curlyeqsucc: '\\u22DF',\n        curlyvee: '\\u22CE',\n        curlywedge: '\\u22CF',\n        curren: '\\u00A4',\n        curvearrowleft: '\\u21B6',\n        curvearrowright: '\\u21B7',\n        cuvee: '\\u22CE',\n        cuwed: '\\u22CF',\n        cwconint: '\\u2232',\n        cwint: '\\u2231',\n        cylcty: '\\u232D',\n        dArr: '\\u21D3',\n        dHar: '\\u2965',\n        dagger: '\\u2020',\n        daleth: '\\u2138',\n        darr: '\\u2193',\n        dash: '\\u2010',\n        dashv: '\\u22A3',\n        dbkarow: '\\u290F',\n        dblac: '\\u02DD',\n        dcaron: '\\u010F',\n        dcy: '\\u0434',\n        dd: '\\u2146',\n        ddagger: '\\u2021',\n        ddarr: '\\u21CA',\n        ddotseq: '\\u2A77',\n        deg: '\\u00B0',\n        delta: '\\u03B4',\n        demptyv: '\\u29B1',\n        dfisht: '\\u297F',\n        dfr: '\\uD835\\uDD21',\n        dharl: '\\u21C3',\n        dharr: '\\u21C2',\n        diam: '\\u22C4',\n        diamond: '\\u22C4',\n        diamondsuit: '\\u2666',\n        diams: '\\u2666',\n        die: '\\u00A8',\n        digamma: '\\u03DD',\n        disin: '\\u22F2',\n        div: '\\u00F7',\n        divide: '\\u00F7',\n        divideontimes: '\\u22C7',\n        divonx: '\\u22C7',\n        djcy: '\\u0452',\n        dlcorn: '\\u231E',\n        dlcrop: '\\u230D',\n        dollar: '\\u0024',\n        dopf: '\\uD835\\uDD55',\n        dot: '\\u02D9',\n        doteq: '\\u2250',\n        doteqdot: '\\u2251',\n        dotminus: '\\u2238',\n        dotplus: '\\u2214',\n        dotsquare: '\\u22A1',\n        doublebarwedge: '\\u2306',\n        downarrow: '\\u2193',\n        downdownarrows: '\\u21CA',\n        downharpoonleft: '\\u21C3',\n        downharpoonright: '\\u21C2',\n        drbkarow: '\\u2910',\n        drcorn: '\\u231F',\n        drcrop: '\\u230C',\n        dscr: '\\uD835\\uDCB9',\n        dscy: '\\u0455',\n        dsol: '\\u29F6',\n        dstrok: '\\u0111',\n        dtdot: '\\u22F1',\n        dtri: '\\u25BF',\n        dtrif: '\\u25BE',\n        duarr: '\\u21F5',\n        duhar: '\\u296F',\n        dwangle: '\\u29A6',\n        dzcy: '\\u045F',\n        dzigrarr: '\\u27FF',\n        eDDot: '\\u2A77',\n        eDot: '\\u2251',\n        eacute: '\\u00E9',\n        easter: '\\u2A6E',\n        ecaron: '\\u011B',\n        ecir: '\\u2256',\n        ecirc: '\\u00EA',\n        ecolon: '\\u2255',\n        ecy: '\\u044D',\n        edot: '\\u0117',\n        ee: '\\u2147',\n        efDot: '\\u2252',\n        efr: '\\uD835\\uDD22',\n        eg: '\\u2A9A',\n        egrave: '\\u00E8',\n        egs: '\\u2A96',\n        egsdot: '\\u2A98',\n        el: '\\u2A99',\n        elinters: '\\u23E7',\n        ell: '\\u2113',\n        els: '\\u2A95',\n        elsdot: '\\u2A97',\n        emacr: '\\u0113',\n        empty: '\\u2205',\n        emptyset: '\\u2205',\n        emptyv: '\\u2205',\n        emsp13: '\\u2004',\n        emsp14: '\\u2005',\n        emsp: '\\u2003',\n        eng: '\\u014B',\n        ensp: '\\u2002',\n        eogon: '\\u0119',\n        eopf: '\\uD835\\uDD56',\n        epar: '\\u22D5',\n        eparsl: '\\u29E3',\n        eplus: '\\u2A71',\n        epsi: '\\u03B5',\n        epsilon: '\\u03B5',\n        epsiv: '\\u03F5',\n        eqcirc: '\\u2256',\n        eqcolon: '\\u2255',\n        eqsim: '\\u2242',\n        eqslantgtr: '\\u2A96',\n        eqslantless: '\\u2A95',\n        equals: '\\u003D',\n        equest: '\\u225F',\n        equiv: '\\u2261',\n        equivDD: '\\u2A78',\n        eqvparsl: '\\u29E5',\n        erDot: '\\u2253',\n        erarr: '\\u2971',\n        escr: '\\u212F',\n        esdot: '\\u2250',\n        esim: '\\u2242',\n        eta: '\\u03B7',\n        eth: '\\u00F0',\n        euml: '\\u00EB',\n        euro: '\\u20AC',\n        excl: '\\u0021',\n        exist: '\\u2203',\n        expectation: '\\u2130',\n        exponentiale: '\\u2147',\n        fallingdotseq: '\\u2252',\n        fcy: '\\u0444',\n        female: '\\u2640',\n        ffilig: '\\uFB03',\n        fflig: '\\uFB00',\n        ffllig: '\\uFB04',\n        ffr: '\\uD835\\uDD23',\n        filig: '\\uFB01',\n        fjlig: '\\u0066\\u006A',\n        flat: '\\u266D',\n        fllig: '\\uFB02',\n        fltns: '\\u25B1',\n        fnof: '\\u0192',\n        fopf: '\\uD835\\uDD57',\n        forall: '\\u2200',\n        fork: '\\u22D4',\n        forkv: '\\u2AD9',\n        fpartint: '\\u2A0D',\n        frac12: '\\u00BD',\n        frac13: '\\u2153',\n        frac14: '\\u00BC',\n        frac15: '\\u2155',\n        frac16: '\\u2159',\n        frac18: '\\u215B',\n        frac23: '\\u2154',\n        frac25: '\\u2156',\n        frac34: '\\u00BE',\n        frac35: '\\u2157',\n        frac38: '\\u215C',\n        frac45: '\\u2158',\n        frac56: '\\u215A',\n        frac58: '\\u215D',\n        frac78: '\\u215E',\n        frasl: '\\u2044',\n        frown: '\\u2322',\n        fscr: '\\uD835\\uDCBB',\n        gE: '\\u2267',\n        gEl: '\\u2A8C',\n        gacute: '\\u01F5',\n        gamma: '\\u03B3',\n        gammad: '\\u03DD',\n        gap: '\\u2A86',\n        gbreve: '\\u011F',\n        gcirc: '\\u011D',\n        gcy: '\\u0433',\n        gdot: '\\u0121',\n        ge: '\\u2265',\n        gel: '\\u22DB',\n        geq: '\\u2265',\n        geqq: '\\u2267',\n        geqslant: '\\u2A7E',\n        ges: '\\u2A7E',\n        gescc: '\\u2AA9',\n        gesdot: '\\u2A80',\n        gesdoto: '\\u2A82',\n        gesdotol: '\\u2A84',\n        gesl: '\\u22DB\\uFE00',\n        gesles: '\\u2A94',\n        gfr: '\\uD835\\uDD24',\n        gg: '\\u226B',\n        ggg: '\\u22D9',\n        gimel: '\\u2137',\n        gjcy: '\\u0453',\n        gl: '\\u2277',\n        glE: '\\u2A92',\n        gla: '\\u2AA5',\n        glj: '\\u2AA4',\n        gnE: '\\u2269',\n        gnap: '\\u2A8A',\n        gnapprox: '\\u2A8A',\n        gne: '\\u2A88',\n        gneq: '\\u2A88',\n        gneqq: '\\u2269',\n        gnsim: '\\u22E7',\n        gopf: '\\uD835\\uDD58',\n        grave: '\\u0060',\n        gscr: '\\u210A',\n        gsim: '\\u2273',\n        gsime: '\\u2A8E',\n        gsiml: '\\u2A90',\n        gt: '\\u003E',\n        gtcc: '\\u2AA7',\n        gtcir: '\\u2A7A',\n        gtdot: '\\u22D7',\n        gtlPar: '\\u2995',\n        gtquest: '\\u2A7C',\n        gtrapprox: '\\u2A86',\n        gtrarr: '\\u2978',\n        gtrdot: '\\u22D7',\n        gtreqless: '\\u22DB',\n        gtreqqless: '\\u2A8C',\n        gtrless: '\\u2277',\n        gtrsim: '\\u2273',\n        gvertneqq: '\\u2269\\uFE00',\n        gvnE: '\\u2269\\uFE00',\n        hArr: '\\u21D4',\n        hairsp: '\\u200A',\n        half: '\\u00BD',\n        hamilt: '\\u210B',\n        hardcy: '\\u044A',\n        harr: '\\u2194',\n        harrcir: '\\u2948',\n        harrw: '\\u21AD',\n        hbar: '\\u210F',\n        hcirc: '\\u0125',\n        hearts: '\\u2665',\n        heartsuit: '\\u2665',\n        hellip: '\\u2026',\n        hercon: '\\u22B9',\n        hfr: '\\uD835\\uDD25',\n        hksearow: '\\u2925',\n        hkswarow: '\\u2926',\n        hoarr: '\\u21FF',\n        homtht: '\\u223B',\n        hookleftarrow: '\\u21A9',\n        hookrightarrow: '\\u21AA',\n        hopf: '\\uD835\\uDD59',\n        horbar: '\\u2015',\n        hscr: '\\uD835\\uDCBD',\n        hslash: '\\u210F',\n        hstrok: '\\u0127',\n        hybull: '\\u2043',\n        hyphen: '\\u2010',\n        iacute: '\\u00ED',\n        ic: '\\u2063',\n        icirc: '\\u00EE',\n        icy: '\\u0438',\n        iecy: '\\u0435',\n        iexcl: '\\u00A1',\n        iff: '\\u21D4',\n        ifr: '\\uD835\\uDD26',\n        igrave: '\\u00EC',\n        ii: '\\u2148',\n        iiiint: '\\u2A0C',\n        iiint: '\\u222D',\n        iinfin: '\\u29DC',\n        iiota: '\\u2129',\n        ijlig: '\\u0133',\n        imacr: '\\u012B',\n        image: '\\u2111',\n        imagline: '\\u2110',\n        imagpart: '\\u2111',\n        imath: '\\u0131',\n        imof: '\\u22B7',\n        imped: '\\u01B5',\n        in: '\\u2208',\n        incare: '\\u2105',\n        infin: '\\u221E',\n        infintie: '\\u29DD',\n        inodot: '\\u0131',\n        int: '\\u222B',\n        intcal: '\\u22BA',\n        integers: '\\u2124',\n        intercal: '\\u22BA',\n        intlarhk: '\\u2A17',\n        intprod: '\\u2A3C',\n        iocy: '\\u0451',\n        iogon: '\\u012F',\n        iopf: '\\uD835\\uDD5A',\n        iota: '\\u03B9',\n        iprod: '\\u2A3C',\n        iquest: '\\u00BF',\n        iscr: '\\uD835\\uDCBE',\n        isin: '\\u2208',\n        isinE: '\\u22F9',\n        isindot: '\\u22F5',\n        isins: '\\u22F4',\n        isinsv: '\\u22F3',\n        isinv: '\\u2208',\n        it: '\\u2062',\n        itilde: '\\u0129',\n        iukcy: '\\u0456',\n        iuml: '\\u00EF',\n        jcirc: '\\u0135',\n        jcy: '\\u0439',\n        jfr: '\\uD835\\uDD27',\n        jmath: '\\u0237',\n        jopf: '\\uD835\\uDD5B',\n        jscr: '\\uD835\\uDCBF',\n        jsercy: '\\u0458',\n        jukcy: '\\u0454',\n        kappa: '\\u03BA',\n        kappav: '\\u03F0',\n        kcedil: '\\u0137',\n        kcy: '\\u043A',\n        kfr: '\\uD835\\uDD28',\n        kgreen: '\\u0138',\n        khcy: '\\u0445',\n        kjcy: '\\u045C',\n        kopf: '\\uD835\\uDD5C',\n        kscr: '\\uD835\\uDCC0',\n        lAarr: '\\u21DA',\n        lArr: '\\u21D0',\n        lAtail: '\\u291B',\n        lBarr: '\\u290E',\n        lE: '\\u2266',\n        lEg: '\\u2A8B',\n        lHar: '\\u2962',\n        lacute: '\\u013A',\n        laemptyv: '\\u29B4',\n        lagran: '\\u2112',\n        lambda: '\\u03BB',\n        lang: '\\u27E8',\n        langd: '\\u2991',\n        langle: '\\u27E8',\n        lap: '\\u2A85',\n        laquo: '\\u00AB',\n        larr: '\\u2190',\n        larrb: '\\u21E4',\n        larrbfs: '\\u291F',\n        larrfs: '\\u291D',\n        larrhk: '\\u21A9',\n        larrlp: '\\u21AB',\n        larrpl: '\\u2939',\n        larrsim: '\\u2973',\n        larrtl: '\\u21A2',\n        lat: '\\u2AAB',\n        latail: '\\u2919',\n        late: '\\u2AAD',\n        lates: '\\u2AAD\\uFE00',\n        lbarr: '\\u290C',\n        lbbrk: '\\u2772',\n        lbrace: '\\u007B',\n        lbrack: '\\u005B',\n        lbrke: '\\u298B',\n        lbrksld: '\\u298F',\n        lbrkslu: '\\u298D',\n        lcaron: '\\u013E',\n        lcedil: '\\u013C',\n        lceil: '\\u2308',\n        lcub: '\\u007B',\n        lcy: '\\u043B',\n        ldca: '\\u2936',\n        ldquo: '\\u201C',\n        ldquor: '\\u201E',\n        ldrdhar: '\\u2967',\n        ldrushar: '\\u294B',\n        ldsh: '\\u21B2',\n        le: '\\u2264',\n        leftarrow: '\\u2190',\n        leftarrowtail: '\\u21A2',\n        leftharpoondown: '\\u21BD',\n        leftharpoonup: '\\u21BC',\n        leftleftarrows: '\\u21C7',\n        leftrightarrow: '\\u2194',\n        leftrightarrows: '\\u21C6',\n        leftrightharpoons: '\\u21CB',\n        leftrightsquigarrow: '\\u21AD',\n        leftthreetimes: '\\u22CB',\n        leg: '\\u22DA',\n        leq: '\\u2264',\n        leqq: '\\u2266',\n        leqslant: '\\u2A7D',\n        les: '\\u2A7D',\n        lescc: '\\u2AA8',\n        lesdot: '\\u2A7F',\n        lesdoto: '\\u2A81',\n        lesdotor: '\\u2A83',\n        lesg: '\\u22DA\\uFE00',\n        lesges: '\\u2A93',\n        lessapprox: '\\u2A85',\n        lessdot: '\\u22D6',\n        lesseqgtr: '\\u22DA',\n        lesseqqgtr: '\\u2A8B',\n        lessgtr: '\\u2276',\n        lesssim: '\\u2272',\n        lfisht: '\\u297C',\n        lfloor: '\\u230A',\n        lfr: '\\uD835\\uDD29',\n        lg: '\\u2276',\n        lgE: '\\u2A91',\n        lhard: '\\u21BD',\n        lharu: '\\u21BC',\n        lharul: '\\u296A',\n        lhblk: '\\u2584',\n        ljcy: '\\u0459',\n        ll: '\\u226A',\n        llarr: '\\u21C7',\n        llcorner: '\\u231E',\n        llhard: '\\u296B',\n        lltri: '\\u25FA',\n        lmidot: '\\u0140',\n        lmoust: '\\u23B0',\n        lmoustache: '\\u23B0',\n        lnE: '\\u2268',\n        lnap: '\\u2A89',\n        lnapprox: '\\u2A89',\n        lne: '\\u2A87',\n        lneq: '\\u2A87',\n        lneqq: '\\u2268',\n        lnsim: '\\u22E6',\n        loang: '\\u27EC',\n        loarr: '\\u21FD',\n        lobrk: '\\u27E6',\n        longleftarrow: '\\u27F5',\n        longleftrightarrow: '\\u27F7',\n        longmapsto: '\\u27FC',\n        longrightarrow: '\\u27F6',\n        looparrowleft: '\\u21AB',\n        looparrowright: '\\u21AC',\n        lopar: '\\u2985',\n        lopf: '\\uD835\\uDD5D',\n        loplus: '\\u2A2D',\n        lotimes: '\\u2A34',\n        lowast: '\\u2217',\n        lowbar: '\\u005F',\n        loz: '\\u25CA',\n        lozenge: '\\u25CA',\n        lozf: '\\u29EB',\n        lpar: '\\u0028',\n        lparlt: '\\u2993',\n        lrarr: '\\u21C6',\n        lrcorner: '\\u231F',\n        lrhar: '\\u21CB',\n        lrhard: '\\u296D',\n        lrm: '\\u200E',\n        lrtri: '\\u22BF',\n        lsaquo: '\\u2039',\n        lscr: '\\uD835\\uDCC1',\n        lsh: '\\u21B0',\n        lsim: '\\u2272',\n        lsime: '\\u2A8D',\n        lsimg: '\\u2A8F',\n        lsqb: '\\u005B',\n        lsquo: '\\u2018',\n        lsquor: '\\u201A',\n        lstrok: '\\u0142',\n        lt: '\\u003C',\n        ltcc: '\\u2AA6',\n        ltcir: '\\u2A79',\n        ltdot: '\\u22D6',\n        lthree: '\\u22CB',\n        ltimes: '\\u22C9',\n        ltlarr: '\\u2976',\n        ltquest: '\\u2A7B',\n        ltrPar: '\\u2996',\n        ltri: '\\u25C3',\n        ltrie: '\\u22B4',\n        ltrif: '\\u25C2',\n        lurdshar: '\\u294A',\n        luruhar: '\\u2966',\n        lvertneqq: '\\u2268\\uFE00',\n        lvnE: '\\u2268\\uFE00',\n        mDDot: '\\u223A',\n        macr: '\\u00AF',\n        male: '\\u2642',\n        malt: '\\u2720',\n        maltese: '\\u2720',\n        map: '\\u21A6',\n        mapsto: '\\u21A6',\n        mapstodown: '\\u21A7',\n        mapstoleft: '\\u21A4',\n        mapstoup: '\\u21A5',\n        marker: '\\u25AE',\n        mcomma: '\\u2A29',\n        mcy: '\\u043C',\n        mdash: '\\u2014',\n        measuredangle: '\\u2221',\n        mfr: '\\uD835\\uDD2A',\n        mho: '\\u2127',\n        micro: '\\u00B5',\n        mid: '\\u2223',\n        midast: '\\u002A',\n        midcir: '\\u2AF0',\n        middot: '\\u00B7',\n        minus: '\\u2212',\n        minusb: '\\u229F',\n        minusd: '\\u2238',\n        minusdu: '\\u2A2A',\n        mlcp: '\\u2ADB',\n        mldr: '\\u2026',\n        mnplus: '\\u2213',\n        models: '\\u22A7',\n        mopf: '\\uD835\\uDD5E',\n        mp: '\\u2213',\n        mscr: '\\uD835\\uDCC2',\n        mstpos: '\\u223E',\n        mu: '\\u03BC',\n        multimap: '\\u22B8',\n        mumap: '\\u22B8',\n        nGg: '\\u22D9\\u0338',\n        nGt: '\\u226B\\u20D2',\n        nGtv: '\\u226B\\u0338',\n        nLeftarrow: '\\u21CD',\n        nLeftrightarrow: '\\u21CE',\n        nLl: '\\u22D8\\u0338',\n        nLt: '\\u226A\\u20D2',\n        nLtv: '\\u226A\\u0338',\n        nRightarrow: '\\u21CF',\n        nVDash: '\\u22AF',\n        nVdash: '\\u22AE',\n        nabla: '\\u2207',\n        nacute: '\\u0144',\n        nang: '\\u2220\\u20D2',\n        nap: '\\u2249',\n        napE: '\\u2A70\\u0338',\n        napid: '\\u224B\\u0338',\n        napos: '\\u0149',\n        napprox: '\\u2249',\n        natur: '\\u266E',\n        natural: '\\u266E',\n        naturals: '\\u2115',\n        nbsp: '\\u00A0',\n        nbump: '\\u224E\\u0338',\n        nbumpe: '\\u224F\\u0338',\n        ncap: '\\u2A43',\n        ncaron: '\\u0148',\n        ncedil: '\\u0146',\n        ncong: '\\u2247',\n        ncongdot: '\\u2A6D\\u0338',\n        ncup: '\\u2A42',\n        ncy: '\\u043D',\n        ndash: '\\u2013',\n        ne: '\\u2260',\n        neArr: '\\u21D7',\n        nearhk: '\\u2924',\n        nearr: '\\u2197',\n        nearrow: '\\u2197',\n        nedot: '\\u2250\\u0338',\n        nequiv: '\\u2262',\n        nesear: '\\u2928',\n        nesim: '\\u2242\\u0338',\n        nexist: '\\u2204',\n        nexists: '\\u2204',\n        nfr: '\\uD835\\uDD2B',\n        ngE: '\\u2267\\u0338',\n        nge: '\\u2271',\n        ngeq: '\\u2271',\n        ngeqq: '\\u2267\\u0338',\n        ngeqslant: '\\u2A7E\\u0338',\n        nges: '\\u2A7E\\u0338',\n        ngsim: '\\u2275',\n        ngt: '\\u226F',\n        ngtr: '\\u226F',\n        nhArr: '\\u21CE',\n        nharr: '\\u21AE',\n        nhpar: '\\u2AF2',\n        ni: '\\u220B',\n        nis: '\\u22FC',\n        nisd: '\\u22FA',\n        niv: '\\u220B',\n        njcy: '\\u045A',\n        nlArr: '\\u21CD',\n        nlE: '\\u2266\\u0338',\n        nlarr: '\\u219A',\n        nldr: '\\u2025',\n        nle: '\\u2270',\n        nleftarrow: '\\u219A',\n        nleftrightarrow: '\\u21AE',\n        nleq: '\\u2270',\n        nleqq: '\\u2266\\u0338',\n        nleqslant: '\\u2A7D\\u0338',\n        nles: '\\u2A7D\\u0338',\n        nless: '\\u226E',\n        nlsim: '\\u2274',\n        nlt: '\\u226E',\n        nltri: '\\u22EA',\n        nltrie: '\\u22EC',\n        nmid: '\\u2224',\n        nopf: '\\uD835\\uDD5F',\n        not: '\\u00AC',\n        notin: '\\u2209',\n        notinE: '\\u22F9\\u0338',\n        notindot: '\\u22F5\\u0338',\n        notinva: '\\u2209',\n        notinvb: '\\u22F7',\n        notinvc: '\\u22F6',\n        notni: '\\u220C',\n        notniva: '\\u220C',\n        notnivb: '\\u22FE',\n        notnivc: '\\u22FD',\n        npar: '\\u2226',\n        nparallel: '\\u2226',\n        nparsl: '\\u2AFD\\u20E5',\n        npart: '\\u2202\\u0338',\n        npolint: '\\u2A14',\n        npr: '\\u2280',\n        nprcue: '\\u22E0',\n        npre: '\\u2AAF\\u0338',\n        nprec: '\\u2280',\n        npreceq: '\\u2AAF\\u0338',\n        nrArr: '\\u21CF',\n        nrarr: '\\u219B',\n        nrarrc: '\\u2933\\u0338',\n        nrarrw: '\\u219D\\u0338',\n        nrightarrow: '\\u219B',\n        nrtri: '\\u22EB',\n        nrtrie: '\\u22ED',\n        nsc: '\\u2281',\n        nsccue: '\\u22E1',\n        nsce: '\\u2AB0\\u0338',\n        nscr: '\\uD835\\uDCC3',\n        nshortmid: '\\u2224',\n        nshortparallel: '\\u2226',\n        nsim: '\\u2241',\n        nsime: '\\u2244',\n        nsimeq: '\\u2244',\n        nsmid: '\\u2224',\n        nspar: '\\u2226',\n        nsqsube: '\\u22E2',\n        nsqsupe: '\\u22E3',\n        nsub: '\\u2284',\n        nsubE: '\\u2AC5\\u0338',\n        nsube: '\\u2288',\n        nsubset: '\\u2282\\u20D2',\n        nsubseteq: '\\u2288',\n        nsubseteqq: '\\u2AC5\\u0338',\n        nsucc: '\\u2281',\n        nsucceq: '\\u2AB0\\u0338',\n        nsup: '\\u2285',\n        nsupE: '\\u2AC6\\u0338',\n        nsupe: '\\u2289',\n        nsupset: '\\u2283\\u20D2',\n        nsupseteq: '\\u2289',\n        nsupseteqq: '\\u2AC6\\u0338',\n        ntgl: '\\u2279',\n        ntilde: '\\u00F1',\n        ntlg: '\\u2278',\n        ntriangleleft: '\\u22EA',\n        ntrianglelefteq: '\\u22EC',\n        ntriangleright: '\\u22EB',\n        ntrianglerighteq: '\\u22ED',\n        nu: '\\u03BD',\n        num: '\\u0023',\n        numero: '\\u2116',\n        numsp: '\\u2007',\n        nvDash: '\\u22AD',\n        nvHarr: '\\u2904',\n        nvap: '\\u224D\\u20D2',\n        nvdash: '\\u22AC',\n        nvge: '\\u2265\\u20D2',\n        nvgt: '\\u003E\\u20D2',\n        nvinfin: '\\u29DE',\n        nvlArr: '\\u2902',\n        nvle: '\\u2264\\u20D2',\n        nvlt: '\\u003C\\u20D2',\n        nvltrie: '\\u22B4\\u20D2',\n        nvrArr: '\\u2903',\n        nvrtrie: '\\u22B5\\u20D2',\n        nvsim: '\\u223C\\u20D2',\n        nwArr: '\\u21D6',\n        nwarhk: '\\u2923',\n        nwarr: '\\u2196',\n        nwarrow: '\\u2196',\n        nwnear: '\\u2927',\n        oS: '\\u24C8',\n        oacute: '\\u00F3',\n        oast: '\\u229B',\n        ocir: '\\u229A',\n        ocirc: '\\u00F4',\n        ocy: '\\u043E',\n        odash: '\\u229D',\n        odblac: '\\u0151',\n        odiv: '\\u2A38',\n        odot: '\\u2299',\n        odsold: '\\u29BC',\n        oelig: '\\u0153',\n        ofcir: '\\u29BF',\n        ofr: '\\uD835\\uDD2C',\n        ogon: '\\u02DB',\n        ograve: '\\u00F2',\n        ogt: '\\u29C1',\n        ohbar: '\\u29B5',\n        ohm: '\\u03A9',\n        oint: '\\u222E',\n        olarr: '\\u21BA',\n        olcir: '\\u29BE',\n        olcross: '\\u29BB',\n        oline: '\\u203E',\n        olt: '\\u29C0',\n        omacr: '\\u014D',\n        omega: '\\u03C9',\n        omicron: '\\u03BF',\n        omid: '\\u29B6',\n        ominus: '\\u2296',\n        oopf: '\\uD835\\uDD60',\n        opar: '\\u29B7',\n        operp: '\\u29B9',\n        oplus: '\\u2295',\n        or: '\\u2228',\n        orarr: '\\u21BB',\n        ord: '\\u2A5D',\n        order: '\\u2134',\n        orderof: '\\u2134',\n        ordf: '\\u00AA',\n        ordm: '\\u00BA',\n        origof: '\\u22B6',\n        oror: '\\u2A56',\n        orslope: '\\u2A57',\n        orv: '\\u2A5B',\n        oscr: '\\u2134',\n        oslash: '\\u00F8',\n        osol: '\\u2298',\n        otilde: '\\u00F5',\n        otimes: '\\u2297',\n        otimesas: '\\u2A36',\n        ouml: '\\u00F6',\n        ovbar: '\\u233D',\n        par: '\\u2225',\n        para: '\\u00B6',\n        parallel: '\\u2225',\n        parsim: '\\u2AF3',\n        parsl: '\\u2AFD',\n        part: '\\u2202',\n        pcy: '\\u043F',\n        percnt: '\\u0025',\n        period: '\\u002E',\n        permil: '\\u2030',\n        perp: '\\u22A5',\n        pertenk: '\\u2031',\n        pfr: '\\uD835\\uDD2D',\n        phi: '\\u03C6',\n        phiv: '\\u03D5',\n        phmmat: '\\u2133',\n        phone: '\\u260E',\n        pi: '\\u03C0',\n        pitchfork: '\\u22D4',\n        piv: '\\u03D6',\n        planck: '\\u210F',\n        planckh: '\\u210E',\n        plankv: '\\u210F',\n        plus: '\\u002B',\n        plusacir: '\\u2A23',\n        plusb: '\\u229E',\n        pluscir: '\\u2A22',\n        plusdo: '\\u2214',\n        plusdu: '\\u2A25',\n        pluse: '\\u2A72',\n        plusmn: '\\u00B1',\n        plussim: '\\u2A26',\n        plustwo: '\\u2A27',\n        pm: '\\u00B1',\n        pointint: '\\u2A15',\n        popf: '\\uD835\\uDD61',\n        pound: '\\u00A3',\n        pr: '\\u227A',\n        prE: '\\u2AB3',\n        prap: '\\u2AB7',\n        prcue: '\\u227C',\n        pre: '\\u2AAF',\n        prec: '\\u227A',\n        precapprox: '\\u2AB7',\n        preccurlyeq: '\\u227C',\n        preceq: '\\u2AAF',\n        precnapprox: '\\u2AB9',\n        precneqq: '\\u2AB5',\n        precnsim: '\\u22E8',\n        precsim: '\\u227E',\n        prime: '\\u2032',\n        primes: '\\u2119',\n        prnE: '\\u2AB5',\n        prnap: '\\u2AB9',\n        prnsim: '\\u22E8',\n        prod: '\\u220F',\n        profalar: '\\u232E',\n        profline: '\\u2312',\n        profsurf: '\\u2313',\n        prop: '\\u221D',\n        propto: '\\u221D',\n        prsim: '\\u227E',\n        prurel: '\\u22B0',\n        pscr: '\\uD835\\uDCC5',\n        psi: '\\u03C8',\n        puncsp: '\\u2008',\n        qfr: '\\uD835\\uDD2E',\n        qint: '\\u2A0C',\n        qopf: '\\uD835\\uDD62',\n        qprime: '\\u2057',\n        qscr: '\\uD835\\uDCC6',\n        quaternions: '\\u210D',\n        quatint: '\\u2A16',\n        quest: '\\u003F',\n        questeq: '\\u225F',\n        quot: '\\u0022',\n        rAarr: '\\u21DB',\n        rArr: '\\u21D2',\n        rAtail: '\\u291C',\n        rBarr: '\\u290F',\n        rHar: '\\u2964',\n        race: '\\u223D\\u0331',\n        racute: '\\u0155',\n        radic: '\\u221A',\n        raemptyv: '\\u29B3',\n        rang: '\\u27E9',\n        rangd: '\\u2992',\n        range: '\\u29A5',\n        rangle: '\\u27E9',\n        raquo: '\\u00BB',\n        rarr: '\\u2192',\n        rarrap: '\\u2975',\n        rarrb: '\\u21E5',\n        rarrbfs: '\\u2920',\n        rarrc: '\\u2933',\n        rarrfs: '\\u291E',\n        rarrhk: '\\u21AA',\n        rarrlp: '\\u21AC',\n        rarrpl: '\\u2945',\n        rarrsim: '\\u2974',\n        rarrtl: '\\u21A3',\n        rarrw: '\\u219D',\n        ratail: '\\u291A',\n        ratio: '\\u2236',\n        rationals: '\\u211A',\n        rbarr: '\\u290D',\n        rbbrk: '\\u2773',\n        rbrace: '\\u007D',\n        rbrack: '\\u005D',\n        rbrke: '\\u298C',\n        rbrksld: '\\u298E',\n        rbrkslu: '\\u2990',\n        rcaron: '\\u0159',\n        rcedil: '\\u0157',\n        rceil: '\\u2309',\n        rcub: '\\u007D',\n        rcy: '\\u0440',\n        rdca: '\\u2937',\n        rdldhar: '\\u2969',\n        rdquo: '\\u201D',\n        rdquor: '\\u201D',\n        rdsh: '\\u21B3',\n        real: '\\u211C',\n        realine: '\\u211B',\n        realpart: '\\u211C',\n        reals: '\\u211D',\n        rect: '\\u25AD',\n        reg: '\\u00AE',\n        rfisht: '\\u297D',\n        rfloor: '\\u230B',\n        rfr: '\\uD835\\uDD2F',\n        rhard: '\\u21C1',\n        rharu: '\\u21C0',\n        rharul: '\\u296C',\n        rho: '\\u03C1',\n        rhov: '\\u03F1',\n        rightarrow: '\\u2192',\n        rightarrowtail: '\\u21A3',\n        rightharpoondown: '\\u21C1',\n        rightharpoonup: '\\u21C0',\n        rightleftarrows: '\\u21C4',\n        rightleftharpoons: '\\u21CC',\n        rightrightarrows: '\\u21C9',\n        rightsquigarrow: '\\u219D',\n        rightthreetimes: '\\u22CC',\n        ring: '\\u02DA',\n        risingdotseq: '\\u2253',\n        rlarr: '\\u21C4',\n        rlhar: '\\u21CC',\n        rlm: '\\u200F',\n        rmoust: '\\u23B1',\n        rmoustache: '\\u23B1',\n        rnmid: '\\u2AEE',\n        roang: '\\u27ED',\n        roarr: '\\u21FE',\n        robrk: '\\u27E7',\n        ropar: '\\u2986',\n        ropf: '\\uD835\\uDD63',\n        roplus: '\\u2A2E',\n        rotimes: '\\u2A35',\n        rpar: '\\u0029',\n        rpargt: '\\u2994',\n        rppolint: '\\u2A12',\n        rrarr: '\\u21C9',\n        rsaquo: '\\u203A',\n        rscr: '\\uD835\\uDCC7',\n        rsh: '\\u21B1',\n        rsqb: '\\u005D',\n        rsquo: '\\u2019',\n        rsquor: '\\u2019',\n        rthree: '\\u22CC',\n        rtimes: '\\u22CA',\n        rtri: '\\u25B9',\n        rtrie: '\\u22B5',\n        rtrif: '\\u25B8',\n        rtriltri: '\\u29CE',\n        ruluhar: '\\u2968',\n        rx: '\\u211E',\n        sacute: '\\u015B',\n        sbquo: '\\u201A',\n        sc: '\\u227B',\n        scE: '\\u2AB4',\n        scap: '\\u2AB8',\n        scaron: '\\u0161',\n        sccue: '\\u227D',\n        sce: '\\u2AB0',\n        scedil: '\\u015F',\n        scirc: '\\u015D',\n        scnE: '\\u2AB6',\n        scnap: '\\u2ABA',\n        scnsim: '\\u22E9',\n        scpolint: '\\u2A13',\n        scsim: '\\u227F',\n        scy: '\\u0441',\n        sdot: '\\u22C5',\n        sdotb: '\\u22A1',\n        sdote: '\\u2A66',\n        seArr: '\\u21D8',\n        searhk: '\\u2925',\n        searr: '\\u2198',\n        searrow: '\\u2198',\n        sect: '\\u00A7',\n        semi: '\\u003B',\n        seswar: '\\u2929',\n        setminus: '\\u2216',\n        setmn: '\\u2216',\n        sext: '\\u2736',\n        sfr: '\\uD835\\uDD30',\n        sfrown: '\\u2322',\n        sharp: '\\u266F',\n        shchcy: '\\u0449',\n        shcy: '\\u0448',\n        shortmid: '\\u2223',\n        shortparallel: '\\u2225',\n        shy: '\\u00AD',\n        sigma: '\\u03C3',\n        sigmaf: '\\u03C2',\n        sigmav: '\\u03C2',\n        sim: '\\u223C',\n        simdot: '\\u2A6A',\n        sime: '\\u2243',\n        simeq: '\\u2243',\n        simg: '\\u2A9E',\n        simgE: '\\u2AA0',\n        siml: '\\u2A9D',\n        simlE: '\\u2A9F',\n        simne: '\\u2246',\n        simplus: '\\u2A24',\n        simrarr: '\\u2972',\n        slarr: '\\u2190',\n        smallsetminus: '\\u2216',\n        smashp: '\\u2A33',\n        smeparsl: '\\u29E4',\n        smid: '\\u2223',\n        smile: '\\u2323',\n        smt: '\\u2AAA',\n        smte: '\\u2AAC',\n        smtes: '\\u2AAC\\uFE00',\n        softcy: '\\u044C',\n        sol: '\\u002F',\n        solb: '\\u29C4',\n        solbar: '\\u233F',\n        sopf: '\\uD835\\uDD64',\n        spades: '\\u2660',\n        spadesuit: '\\u2660',\n        spar: '\\u2225',\n        sqcap: '\\u2293',\n        sqcaps: '\\u2293\\uFE00',\n        sqcup: '\\u2294',\n        sqcups: '\\u2294\\uFE00',\n        sqsub: '\\u228F',\n        sqsube: '\\u2291',\n        sqsubset: '\\u228F',\n        sqsubseteq: '\\u2291',\n        sqsup: '\\u2290',\n        sqsupe: '\\u2292',\n        sqsupset: '\\u2290',\n        sqsupseteq: '\\u2292',\n        squ: '\\u25A1',\n        square: '\\u25A1',\n        squarf: '\\u25AA',\n        squf: '\\u25AA',\n        srarr: '\\u2192',\n        sscr: '\\uD835\\uDCC8',\n        ssetmn: '\\u2216',\n        ssmile: '\\u2323',\n        sstarf: '\\u22C6',\n        star: '\\u2606',\n        starf: '\\u2605',\n        straightepsilon: '\\u03F5',\n        straightphi: '\\u03D5',\n        strns: '\\u00AF',\n        sub: '\\u2282',\n        subE: '\\u2AC5',\n        subdot: '\\u2ABD',\n        sube: '\\u2286',\n        subedot: '\\u2AC3',\n        submult: '\\u2AC1',\n        subnE: '\\u2ACB',\n        subne: '\\u228A',\n        subplus: '\\u2ABF',\n        subrarr: '\\u2979',\n        subset: '\\u2282',\n        subseteq: '\\u2286',\n        subseteqq: '\\u2AC5',\n        subsetneq: '\\u228A',\n        subsetneqq: '\\u2ACB',\n        subsim: '\\u2AC7',\n        subsub: '\\u2AD5',\n        subsup: '\\u2AD3',\n        succ: '\\u227B',\n        succapprox: '\\u2AB8',\n        succcurlyeq: '\\u227D',\n        succeq: '\\u2AB0',\n        succnapprox: '\\u2ABA',\n        succneqq: '\\u2AB6',\n        succnsim: '\\u22E9',\n        succsim: '\\u227F',\n        sum: '\\u2211',\n        sung: '\\u266A',\n        sup1: '\\u00B9',\n        sup2: '\\u00B2',\n        sup3: '\\u00B3',\n        sup: '\\u2283',\n        supE: '\\u2AC6',\n        supdot: '\\u2ABE',\n        supdsub: '\\u2AD8',\n        supe: '\\u2287',\n        supedot: '\\u2AC4',\n        suphsol: '\\u27C9',\n        suphsub: '\\u2AD7',\n        suplarr: '\\u297B',\n        supmult: '\\u2AC2',\n        supnE: '\\u2ACC',\n        supne: '\\u228B',\n        supplus: '\\u2AC0',\n        supset: '\\u2283',\n        supseteq: '\\u2287',\n        supseteqq: '\\u2AC6',\n        supsetneq: '\\u228B',\n        supsetneqq: '\\u2ACC',\n        supsim: '\\u2AC8',\n        supsub: '\\u2AD4',\n        supsup: '\\u2AD6',\n        swArr: '\\u21D9',\n        swarhk: '\\u2926',\n        swarr: '\\u2199',\n        swarrow: '\\u2199',\n        swnwar: '\\u292A',\n        szlig: '\\u00DF',\n        target: '\\u2316',\n        tau: '\\u03C4',\n        tbrk: '\\u23B4',\n        tcaron: '\\u0165',\n        tcedil: '\\u0163',\n        tcy: '\\u0442',\n        tdot: '\\u20DB',\n        telrec: '\\u2315',\n        tfr: '\\uD835\\uDD31',\n        there4: '\\u2234',\n        therefore: '\\u2234',\n        theta: '\\u03B8',\n        thetasym: '\\u03D1',\n        thetav: '\\u03D1',\n        thickapprox: '\\u2248',\n        thicksim: '\\u223C',\n        thinsp: '\\u2009',\n        thkap: '\\u2248',\n        thksim: '\\u223C',\n        thorn: '\\u00FE',\n        tilde: '\\u02DC',\n        times: '\\u00D7',\n        timesb: '\\u22A0',\n        timesbar: '\\u2A31',\n        timesd: '\\u2A30',\n        tint: '\\u222D',\n        toea: '\\u2928',\n        top: '\\u22A4',\n        topbot: '\\u2336',\n        topcir: '\\u2AF1',\n        topf: '\\uD835\\uDD65',\n        topfork: '\\u2ADA',\n        tosa: '\\u2929',\n        tprime: '\\u2034',\n        trade: '\\u2122',\n        triangle: '\\u25B5',\n        triangledown: '\\u25BF',\n        triangleleft: '\\u25C3',\n        trianglelefteq: '\\u22B4',\n        triangleq: '\\u225C',\n        triangleright: '\\u25B9',\n        trianglerighteq: '\\u22B5',\n        tridot: '\\u25EC',\n        trie: '\\u225C',\n        triminus: '\\u2A3A',\n        triplus: '\\u2A39',\n        trisb: '\\u29CD',\n        tritime: '\\u2A3B',\n        trpezium: '\\u23E2',\n        tscr: '\\uD835\\uDCC9',\n        tscy: '\\u0446',\n        tshcy: '\\u045B',\n        tstrok: '\\u0167',\n        twixt: '\\u226C',\n        twoheadleftarrow: '\\u219E',\n        twoheadrightarrow: '\\u21A0',\n        uArr: '\\u21D1',\n        uHar: '\\u2963',\n        uacute: '\\u00FA',\n        uarr: '\\u2191',\n        ubrcy: '\\u045E',\n        ubreve: '\\u016D',\n        ucirc: '\\u00FB',\n        ucy: '\\u0443',\n        udarr: '\\u21C5',\n        udblac: '\\u0171',\n        udhar: '\\u296E',\n        ufisht: '\\u297E',\n        ufr: '\\uD835\\uDD32',\n        ugrave: '\\u00F9',\n        uharl: '\\u21BF',\n        uharr: '\\u21BE',\n        uhblk: '\\u2580',\n        ulcorn: '\\u231C',\n        ulcorner: '\\u231C',\n        ulcrop: '\\u230F',\n        ultri: '\\u25F8',\n        umacr: '\\u016B',\n        uml: '\\u00A8',\n        uogon: '\\u0173',\n        uopf: '\\uD835\\uDD66',\n        uparrow: '\\u2191',\n        updownarrow: '\\u2195',\n        upharpoonleft: '\\u21BF',\n        upharpoonright: '\\u21BE',\n        uplus: '\\u228E',\n        upsi: '\\u03C5',\n        upsih: '\\u03D2',\n        upsilon: '\\u03C5',\n        upuparrows: '\\u21C8',\n        urcorn: '\\u231D',\n        urcorner: '\\u231D',\n        urcrop: '\\u230E',\n        uring: '\\u016F',\n        urtri: '\\u25F9',\n        uscr: '\\uD835\\uDCCA',\n        utdot: '\\u22F0',\n        utilde: '\\u0169',\n        utri: '\\u25B5',\n        utrif: '\\u25B4',\n        uuarr: '\\u21C8',\n        uuml: '\\u00FC',\n        uwangle: '\\u29A7',\n        vArr: '\\u21D5',\n        vBar: '\\u2AE8',\n        vBarv: '\\u2AE9',\n        vDash: '\\u22A8',\n        vangrt: '\\u299C',\n        varepsilon: '\\u03F5',\n        varkappa: '\\u03F0',\n        varnothing: '\\u2205',\n        varphi: '\\u03D5',\n        varpi: '\\u03D6',\n        varpropto: '\\u221D',\n        varr: '\\u2195',\n        varrho: '\\u03F1',\n        varsigma: '\\u03C2',\n        varsubsetneq: '\\u228A\\uFE00',\n        varsubsetneqq: '\\u2ACB\\uFE00',\n        varsupsetneq: '\\u228B\\uFE00',\n        varsupsetneqq: '\\u2ACC\\uFE00',\n        vartheta: '\\u03D1',\n        vartriangleleft: '\\u22B2',\n        vartriangleright: '\\u22B3',\n        vcy: '\\u0432',\n        vdash: '\\u22A2',\n        vee: '\\u2228',\n        veebar: '\\u22BB',\n        veeeq: '\\u225A',\n        vellip: '\\u22EE',\n        verbar: '\\u007C',\n        vert: '\\u007C',\n        vfr: '\\uD835\\uDD33',\n        vltri: '\\u22B2',\n        vnsub: '\\u2282\\u20D2',\n        vnsup: '\\u2283\\u20D2',\n        vopf: '\\uD835\\uDD67',\n        vprop: '\\u221D',\n        vrtri: '\\u22B3',\n        vscr: '\\uD835\\uDCCB',\n        vsubnE: '\\u2ACB\\uFE00',\n        vsubne: '\\u228A\\uFE00',\n        vsupnE: '\\u2ACC\\uFE00',\n        vsupne: '\\u228B\\uFE00',\n        vzigzag: '\\u299A',\n        wcirc: '\\u0175',\n        wedbar: '\\u2A5F',\n        wedge: '\\u2227',\n        wedgeq: '\\u2259',\n        weierp: '\\u2118',\n        wfr: '\\uD835\\uDD34',\n        wopf: '\\uD835\\uDD68',\n        wp: '\\u2118',\n        wr: '\\u2240',\n        wreath: '\\u2240',\n        wscr: '\\uD835\\uDCCC',\n        xcap: '\\u22C2',\n        xcirc: '\\u25EF',\n        xcup: '\\u22C3',\n        xdtri: '\\u25BD',\n        xfr: '\\uD835\\uDD35',\n        xhArr: '\\u27FA',\n        xharr: '\\u27F7',\n        xi: '\\u03BE',\n        xlArr: '\\u27F8',\n        xlarr: '\\u27F5',\n        xmap: '\\u27FC',\n        xnis: '\\u22FB',\n        xodot: '\\u2A00',\n        xopf: '\\uD835\\uDD69',\n        xoplus: '\\u2A01',\n        xotime: '\\u2A02',\n        xrArr: '\\u27F9',\n        xrarr: '\\u27F6',\n        xscr: '\\uD835\\uDCCD',\n        xsqcup: '\\u2A06',\n        xuplus: '\\u2A04',\n        xutri: '\\u25B3',\n        xvee: '\\u22C1',\n        xwedge: '\\u22C0',\n        yacute: '\\u00FD',\n        yacy: '\\u044F',\n        ycirc: '\\u0177',\n        ycy: '\\u044B',\n        yen: '\\u00A5',\n        yfr: '\\uD835\\uDD36',\n        yicy: '\\u0457',\n        yopf: '\\uD835\\uDD6A',\n        yscr: '\\uD835\\uDCCE',\n        yucy: '\\u044E',\n        yuml: '\\u00FF',\n        zacute: '\\u017A',\n        zcaron: '\\u017E',\n        zcy: '\\u0437',\n        zdot: '\\u017C',\n        zeetrf: '\\u2128',\n        zeta: '\\u03B6',\n        zfr: '\\uD835\\uDD37',\n        zhcy: '\\u0436',\n        zigrarr: '\\u21DD',\n        zopf: '\\uD835\\uDD6B',\n        zscr: '\\uD835\\uDCCF',\n        zwj: '\\u200D',\n        zwnj: '\\u200C',\n    };\n    const decodeMap = {\n        '0': 65533,\n        '128': 8364,\n        '130': 8218,\n        '131': 402,\n        '132': 8222,\n        '133': 8230,\n        '134': 8224,\n        '135': 8225,\n        '136': 710,\n        '137': 8240,\n        '138': 352,\n        '139': 8249,\n        '140': 338,\n        '142': 381,\n        '145': 8216,\n        '146': 8217,\n        '147': 8220,\n        '148': 8221,\n        '149': 8226,\n        '150': 8211,\n        '151': 8212,\n        '152': 732,\n        '153': 8482,\n        '154': 353,\n        '155': 8250,\n        '156': 339,\n        '158': 382,\n        '159': 376,\n    };\n    function decodeHTMLStrict(text) {\n        return text.replace(/&(?:[a-zA-Z]+|#[xX][\\da-fA-F]+|#\\d+);/g, (key) => {\n            if (key.charAt(1) === '#') {\n                const secondChar = key.charAt(2);\n                const codePoint = secondChar === 'X' || secondChar === 'x' ? parseInt(key.slice(3), 16) : parseInt(key.slice(2), 10);\n                return decodeCodePoint(codePoint);\n            }\n            return getOwnProperty(entities, key.slice(1, -1)) ?? key;\n        });\n    }\n    function decodeCodePoint(codePoint) {\n        if ((codePoint >= 0xd800 && codePoint <= 0xdfff) || codePoint > 0x10ffff) {\n            return '\\uFFFD';\n        }\n        return String.fromCodePoint(getOwnProperty(decodeMap, codePoint) ?? codePoint);\n    }\n\n    function scanJSXAttributeValue(parser, context) {\n        parser.startIndex = parser.tokenIndex = parser.index;\n        parser.startColumn = parser.tokenColumn = parser.column;\n        parser.startLine = parser.tokenLine = parser.line;\n        parser.setToken(CharTypes[parser.currentChar] & 8192\n            ? scanJSXString(parser)\n            : scanSingleToken(parser, context, 0));\n        return parser.getToken();\n    }\n    function scanJSXString(parser) {\n        const quote = parser.currentChar;\n        let char = advanceChar(parser);\n        const start = parser.index;\n        while (char !== quote) {\n            if (parser.index >= parser.end)\n                parser.report(16);\n            char = advanceChar(parser);\n        }\n        if (char !== quote)\n            parser.report(16);\n        parser.tokenValue = parser.source.slice(start, parser.index);\n        advanceChar(parser);\n        if (parser.options.raw)\n            parser.tokenRaw = parser.source.slice(parser.tokenIndex, parser.index);\n        return 134283267;\n    }\n    function nextJSXToken(parser) {\n        parser.startIndex = parser.tokenIndex = parser.index;\n        parser.startColumn = parser.tokenColumn = parser.column;\n        parser.startLine = parser.tokenLine = parser.line;\n        if (parser.index >= parser.end) {\n            parser.setToken(1048576);\n            return;\n        }\n        if (parser.currentChar === 60) {\n            advanceChar(parser);\n            parser.setToken(8456256);\n            return;\n        }\n        if (parser.currentChar === 123) {\n            advanceChar(parser);\n            parser.setToken(2162700);\n            return;\n        }\n        let state = 0;\n        while (parser.index < parser.end) {\n            const type = CharTypes[parser.source.charCodeAt(parser.index)];\n            if (type & 1024) {\n                state |= 1 | 4;\n                scanNewLine(parser);\n            }\n            else if (type & 2048) {\n                consumeLineFeed(parser, state);\n                state = (state & -5) | 1;\n            }\n            else {\n                advanceChar(parser);\n            }\n            if (CharTypes[parser.currentChar] & 16384)\n                break;\n        }\n        if (parser.tokenIndex === parser.index)\n            parser.report(0);\n        const raw = parser.source.slice(parser.tokenIndex, parser.index);\n        if (parser.options.raw)\n            parser.tokenRaw = raw;\n        parser.tokenValue = decodeHTMLStrict(raw);\n        parser.setToken(137);\n    }\n    function rescanJSXIdentifier(parser) {\n        if ((parser.getToken() & 143360) === 143360) {\n            const { index } = parser;\n            let char = parser.currentChar;\n            while (CharTypes[char] & (32768 | 2)) {\n                char = advanceChar(parser);\n            }\n            parser.tokenValue += parser.source.slice(index, parser.index);\n            parser.setToken(208897, true);\n        }\n        return parser.getToken();\n    }\n\n    class Scope {\n        parser;\n        type;\n        parent;\n        scopeError;\n        variableBindings = new Map();\n        constructor(parser, type = 2, parent) {\n            this.parser = parser;\n            this.type = type;\n            this.parent = parent;\n        }\n        createChildScope(type) {\n            return new Scope(this.parser, type, this);\n        }\n        addVarOrBlock(context, name, kind, origin) {\n            if (kind & 4) {\n                this.addVarName(context, name, kind);\n            }\n            else {\n                this.addBlockName(context, name, kind, origin);\n            }\n            if (origin & 64) {\n                this.parser.declareUnboundVariable(name);\n            }\n        }\n        addVarName(context, name, kind) {\n            const { parser } = this;\n            let currentScope = this;\n            while (currentScope && (currentScope.type & 128) === 0) {\n                const { variableBindings } = currentScope;\n                const value = variableBindings.get(name);\n                if (value && value & 248) {\n                    if (parser.options.webcompat &&\n                        (context & 1) === 0 &&\n                        ((kind & 128 && value & 68) ||\n                            (value & 128 && kind & 68))) ;\n                    else {\n                        parser.report(145, name);\n                    }\n                }\n                if (currentScope === this) {\n                    if (value && value & 1 && kind & 1) {\n                        currentScope.recordScopeError(145, name);\n                    }\n                }\n                if (value &&\n                    (value & 256 || (value & 512 && !parser.options.webcompat))) {\n                    parser.report(145, name);\n                }\n                currentScope.variableBindings.set(name, kind);\n                currentScope = currentScope.parent;\n            }\n        }\n        hasVariable(name) {\n            return this.variableBindings.has(name);\n        }\n        addBlockName(context, name, kind, origin) {\n            const { parser } = this;\n            const value = this.variableBindings.get(name);\n            if (value && (value & 2) === 0) {\n                if (kind & 1) {\n                    this.recordScopeError(145, name);\n                }\n                else if (parser.options.webcompat &&\n                    (context & 1) === 0 &&\n                    origin & 2 &&\n                    value === 64 &&\n                    kind === 64) ;\n                else {\n                    parser.report(145, name);\n                }\n            }\n            if (this.type & 64 &&\n                this.parent?.hasVariable(name) &&\n                (this.parent.variableBindings.get(name) & 2) === 0) {\n                parser.report(145, name);\n            }\n            if (this.type & 512 && value && (value & 2) === 0) {\n                if (kind & 1) {\n                    this.recordScopeError(145, name);\n                }\n            }\n            if (this.type & 32) {\n                if (this.parent.variableBindings.get(name) & 768)\n                    parser.report(159, name);\n            }\n            this.variableBindings.set(name, kind);\n        }\n        recordScopeError(type, ...params) {\n            this.scopeError = {\n                type,\n                params,\n                start: this.parser.tokenStart,\n                end: this.parser.currentLocation,\n            };\n        }\n        reportScopeError() {\n            const { scopeError } = this;\n            if (!scopeError) {\n                return;\n            }\n            throw new ParseError(scopeError.start, scopeError.end, scopeError.type, ...scopeError.params);\n        }\n    }\n    function createArrowHeadParsingScope(parser, context, value) {\n        const scope = parser.createScope().createChildScope(512);\n        scope.addBlockName(context, value, 1, 0);\n        return scope;\n    }\n\n    class PrivateScope {\n        parser;\n        parent;\n        refs = Object.create(null);\n        privateIdentifiers = new Map();\n        constructor(parser, parent) {\n            this.parser = parser;\n            this.parent = parent;\n        }\n        addPrivateIdentifier(name, kind) {\n            const { privateIdentifiers } = this;\n            let focusKind = kind & (32 | 768);\n            if (!(focusKind & 768))\n                focusKind |= 768;\n            const value = privateIdentifiers.get(name);\n            if (this.hasPrivateIdentifier(name) &&\n                ((value & 32) !== (focusKind & 32) || value & focusKind & 768)) {\n                this.parser.report(146, name);\n            }\n            privateIdentifiers.set(name, this.hasPrivateIdentifier(name) ? value | focusKind : focusKind);\n        }\n        addPrivateIdentifierRef(name) {\n            this.refs[name] ??= [];\n            this.refs[name].push(this.parser.tokenStart);\n        }\n        isPrivateIdentifierDefined(name) {\n            return this.hasPrivateIdentifier(name) || Boolean(this.parent?.isPrivateIdentifierDefined(name));\n        }\n        validatePrivateIdentifierRefs() {\n            for (const name in this.refs) {\n                if (!this.isPrivateIdentifierDefined(name)) {\n                    const { index, line, column } = this.refs[name][0];\n                    throw new ParseError({ index, line, column }, { index: index + name.length, line, column: column + name.length }, 4, name);\n                }\n            }\n        }\n        hasPrivateIdentifier(name) {\n            return this.privateIdentifiers.has(name);\n        }\n    }\n\n    class Parser {\n        source;\n        options;\n        lastOnToken = null;\n        token = 1048576;\n        flags = 0;\n        index = 0;\n        line = 1;\n        column = 0;\n        startIndex = 0;\n        end = 0;\n        tokenIndex = 0;\n        startColumn = 0;\n        tokenColumn = 0;\n        tokenLine = 1;\n        startLine = 1;\n        tokenValue = '';\n        tokenRaw = '';\n        tokenRegExp = void 0;\n        currentChar = 0;\n        exportedNames = new Set();\n        exportedBindings = new Set();\n        assignable = 1;\n        destructible = 0;\n        leadingDecorators = { decorators: [] };\n        constructor(source, options = {}) {\n            this.source = source;\n            this.options = options;\n            this.end = source.length;\n            this.currentChar = source.charCodeAt(0);\n        }\n        getToken() {\n            return this.token;\n        }\n        setToken(value, replaceLast = false) {\n            this.token = value;\n            const { onToken } = this.options;\n            if (onToken) {\n                if (value !== 1048576) {\n                    const loc = {\n                        start: {\n                            line: this.tokenLine,\n                            column: this.tokenColumn,\n                        },\n                        end: {\n                            line: this.line,\n                            column: this.column,\n                        },\n                    };\n                    if (!replaceLast && this.lastOnToken) {\n                        onToken(...this.lastOnToken);\n                    }\n                    this.lastOnToken = [convertTokenType(value), this.tokenIndex, this.index, loc];\n                }\n                else {\n                    if (this.lastOnToken) {\n                        onToken(...this.lastOnToken);\n                        this.lastOnToken = null;\n                    }\n                }\n            }\n            return value;\n        }\n        get tokenStart() {\n            return {\n                index: this.tokenIndex,\n                line: this.tokenLine,\n                column: this.tokenColumn,\n            };\n        }\n        get currentLocation() {\n            return { index: this.index, line: this.line, column: this.column };\n        }\n        finishNode(node, start, end) {\n            if (this.options.ranges) {\n                node.start = start.index;\n                const endIndex = end ? end.index : this.startIndex;\n                node.end = endIndex;\n                node.range = [start.index, endIndex];\n            }\n            if (this.options.loc) {\n                node.loc = {\n                    start: {\n                        line: start.line,\n                        column: start.column,\n                    },\n                    end: end ? { line: end.line, column: end.column } : { line: this.startLine, column: this.startColumn },\n                };\n                if (this.options.source) {\n                    node.loc.source = this.options.source;\n                }\n            }\n            return node;\n        }\n        addBindingToExports(name) {\n            this.exportedBindings.add(name);\n        }\n        declareUnboundVariable(name) {\n            const { exportedNames } = this;\n            if (exportedNames.has(name)) {\n                this.report(147, name);\n            }\n            exportedNames.add(name);\n        }\n        report(type, ...params) {\n            throw new ParseError(this.tokenStart, this.currentLocation, type, ...params);\n        }\n        createScopeIfLexical(type, parent) {\n            if (this.options.lexical) {\n                return this.createScope(type, parent);\n            }\n            return undefined;\n        }\n        createScope(type, parent) {\n            return new Scope(this, type, parent);\n        }\n        createPrivateScopeIfLexical(parent) {\n            if (this.options.lexical) {\n                return new PrivateScope(this, parent);\n            }\n            return undefined;\n        }\n    }\n    function pushComment(comments, options) {\n        return function (type, value, start, end, loc) {\n            const comment = {\n                type,\n                value,\n            };\n            if (options.ranges) {\n                comment.start = start;\n                comment.end = end;\n                comment.range = [start, end];\n            }\n            if (options.loc) {\n                comment.loc = loc;\n            }\n            comments.push(comment);\n        };\n    }\n    function pushToken(tokens, options) {\n        return function (type, start, end, loc) {\n            const token = {\n                token: type,\n            };\n            if (options.ranges) {\n                token.start = start;\n                token.end = end;\n                token.range = [start, end];\n            }\n            if (options.loc) {\n                token.loc = loc;\n            }\n            tokens.push(token);\n        };\n    }\n\n    function normalizeOptions(rawOptions) {\n        const options = { ...rawOptions };\n        if (options.onComment) {\n            options.onComment = Array.isArray(options.onComment) ? pushComment(options.onComment, options) : options.onComment;\n        }\n        if (options.onToken) {\n            options.onToken = Array.isArray(options.onToken) ? pushToken(options.onToken, options) : options.onToken;\n        }\n        return options;\n    }\n\n    function parseSource(source, rawOptions = {}, context = 0) {\n        const options = normalizeOptions(rawOptions);\n        if (options.module)\n            context |= 2 | 1;\n        if (options.globalReturn)\n            context |= 4096;\n        if (options.impliedStrict)\n            context |= 1;\n        const parser = new Parser(source, options);\n        skipHashBang(parser);\n        const scope = parser.createScopeIfLexical();\n        let body = [];\n        let sourceType = 'script';\n        if (context & 2) {\n            sourceType = 'module';\n            body = parseModuleItemList(parser, context | 8, scope);\n            if (scope) {\n                for (const name of parser.exportedBindings) {\n                    if (!scope.hasVariable(name))\n                        parser.report(148, name);\n                }\n            }\n        }\n        else {\n            body = parseStatementList(parser, context | 8, scope);\n        }\n        return parser.finishNode({\n            type: 'Program',\n            sourceType,\n            body,\n        }, { index: 0, line: 1, column: 0 }, parser.currentLocation);\n    }\n    function parseStatementList(parser, context, scope) {\n        nextToken(parser, context | 32 | 262144);\n        const statements = [];\n        while (parser.getToken() === 134283267) {\n            const { index, tokenValue, tokenStart, tokenIndex } = parser;\n            const token = parser.getToken();\n            const expr = parseLiteral(parser, context);\n            if (isValidStrictMode(parser, index, tokenIndex, tokenValue)) {\n                context |= 1;\n                if (parser.flags & 64) {\n                    throw new ParseError(parser.tokenStart, parser.currentLocation, 9);\n                }\n                if (parser.flags & 4096) {\n                    throw new ParseError(parser.tokenStart, parser.currentLocation, 15);\n                }\n            }\n            statements.push(parseDirective(parser, context, expr, token, tokenStart));\n        }\n        while (parser.getToken() !== 1048576) {\n            statements.push(parseStatementListItem(parser, context, scope, undefined, 4, {}));\n        }\n        return statements;\n    }\n    function parseModuleItemList(parser, context, scope) {\n        nextToken(parser, context | 32);\n        const statements = [];\n        while (parser.getToken() === 134283267) {\n            const { tokenStart } = parser;\n            const token = parser.getToken();\n            statements.push(parseDirective(parser, context, parseLiteral(parser, context), token, tokenStart));\n        }\n        while (parser.getToken() !== 1048576) {\n            statements.push(parseModuleItem(parser, context, scope));\n        }\n        return statements;\n    }\n    function parseModuleItem(parser, context, scope) {\n        if (parser.getToken() === 132) {\n            Object.assign(parser.leadingDecorators, {\n                start: parser.tokenStart,\n                decorators: parseDecorators(parser, context, undefined),\n            });\n        }\n        let moduleItem;\n        switch (parser.getToken()) {\n            case 20564:\n                moduleItem = parseExportDeclaration(parser, context, scope);\n                break;\n            case 86106:\n                moduleItem = parseImportDeclaration(parser, context, scope);\n                break;\n            default:\n                moduleItem = parseStatementListItem(parser, context, scope, undefined, 4, {});\n        }\n        if (parser.leadingDecorators?.decorators.length) {\n            parser.report(170);\n        }\n        return moduleItem;\n    }\n    function parseStatementListItem(parser, context, scope, privateScope, origin, labels) {\n        const start = parser.tokenStart;\n        switch (parser.getToken()) {\n            case 86104:\n                return parseFunctionDeclaration(parser, context, scope, privateScope, origin, 1, 0, 0, start);\n            case 132:\n            case 86094:\n                return parseClassDeclaration(parser, context, scope, privateScope, 0);\n            case 86090:\n                return parseLexicalDeclaration(parser, context, scope, privateScope, 16, 0);\n            case 241737:\n                return parseLetIdentOrVarDeclarationStatement(parser, context, scope, privateScope, origin);\n            case 20564:\n                parser.report(103, 'export');\n            case 86106:\n                nextToken(parser, context);\n                switch (parser.getToken()) {\n                    case 67174411:\n                        return parseImportCallDeclaration(parser, context, privateScope, start);\n                    case 67108877:\n                        return parseImportMetaDeclaration(parser, context, start);\n                    default:\n                        parser.report(103, 'import');\n                }\n            case 209005:\n                return parseAsyncArrowOrAsyncFunctionDeclaration(parser, context, scope, privateScope, origin, labels, 1);\n            default:\n                return parseStatement(parser, context, scope, privateScope, origin, labels, 1);\n        }\n    }\n    function parseStatement(parser, context, scope, privateScope, origin, labels, allowFuncDecl) {\n        switch (parser.getToken()) {\n            case 86088:\n                return parseVariableStatement(parser, context, scope, privateScope, 0);\n            case 20572:\n                return parseReturnStatement(parser, context, privateScope);\n            case 20569:\n                return parseIfStatement(parser, context, scope, privateScope, labels);\n            case 20567:\n                return parseForStatement(parser, context, scope, privateScope, labels);\n            case 20562:\n                return parseDoWhileStatement(parser, context, scope, privateScope, labels);\n            case 20578:\n                return parseWhileStatement(parser, context, scope, privateScope, labels);\n            case 86110:\n                return parseSwitchStatement(parser, context, scope, privateScope, labels);\n            case 1074790417:\n                return parseEmptyStatement(parser, context);\n            case 2162700:\n                return parseBlock(parser, context, scope?.createChildScope(), privateScope, labels, parser.tokenStart);\n            case 86112:\n                return parseThrowStatement(parser, context, privateScope);\n            case 20555:\n                return parseBreakStatement(parser, context, labels);\n            case 20559:\n                return parseContinueStatement(parser, context, labels);\n            case 20577:\n                return parseTryStatement(parser, context, scope, privateScope, labels);\n            case 20579:\n                return parseWithStatement(parser, context, scope, privateScope, labels);\n            case 20560:\n                return parseDebuggerStatement(parser, context);\n            case 209005:\n                return parseAsyncArrowOrAsyncFunctionDeclaration(parser, context, scope, privateScope, origin, labels, 0);\n            case 20557:\n                parser.report(162);\n            case 20566:\n                parser.report(163);\n            case 86104:\n                parser.report(context & 1\n                    ? 76\n                    : !parser.options.webcompat\n                        ? 78\n                        : 77);\n            case 86094:\n                parser.report(79);\n            default:\n                return parseExpressionOrLabelledStatement(parser, context, scope, privateScope, origin, labels, allowFuncDecl);\n        }\n    }\n    function parseExpressionOrLabelledStatement(parser, context, scope, privateScope, origin, labels, allowFuncDecl) {\n        const { tokenValue, tokenStart } = parser;\n        const token = parser.getToken();\n        let expr;\n        switch (token) {\n            case 241737:\n                expr = parseIdentifier(parser, context);\n                if (context & 1)\n                    parser.report(85);\n                if (parser.getToken() === 69271571)\n                    parser.report(84);\n                break;\n            default:\n                expr = parsePrimaryExpression(parser, context, privateScope, 2, 0, 1, 0, 1, parser.tokenStart);\n        }\n        if (token & 143360 && parser.getToken() === 21) {\n            return parseLabelledStatement(parser, context, scope, privateScope, origin, labels, tokenValue, expr, token, allowFuncDecl, tokenStart);\n        }\n        expr = parseMemberOrUpdateExpression(parser, context, privateScope, expr, 0, 0, tokenStart);\n        expr = parseAssignmentExpression(parser, context, privateScope, 0, 0, tokenStart, expr);\n        if (parser.getToken() === 18) {\n            expr = parseSequenceExpression(parser, context, privateScope, 0, tokenStart, expr);\n        }\n        return parseExpressionStatement(parser, context, expr, tokenStart);\n    }\n    function parseBlock(parser, context, scope, privateScope, labels, start = parser.tokenStart, type = 'BlockStatement') {\n        const body = [];\n        consume(parser, context | 32, 2162700);\n        while (parser.getToken() !== 1074790415) {\n            body.push(parseStatementListItem(parser, context, scope, privateScope, 2, { $: labels }));\n        }\n        consume(parser, context | 32, 1074790415);\n        return parser.finishNode({\n            type,\n            body,\n        }, start);\n    }\n    function parseReturnStatement(parser, context, privateScope) {\n        if ((context & 4096) === 0)\n            parser.report(92);\n        const start = parser.tokenStart;\n        nextToken(parser, context | 32);\n        const argument = parser.flags & 1 || parser.getToken() & 1048576\n            ? null\n            : parseExpressions(parser, context, privateScope, 0, 1, parser.tokenStart);\n        matchOrInsertSemicolon(parser, context | 32);\n        return parser.finishNode({\n            type: 'ReturnStatement',\n            argument,\n        }, start);\n    }\n    function parseExpressionStatement(parser, context, expression, start) {\n        matchOrInsertSemicolon(parser, context | 32);\n        return parser.finishNode({\n            type: 'ExpressionStatement',\n            expression,\n        }, start);\n    }\n    function parseLabelledStatement(parser, context, scope, privateScope, origin, labels, value, expr, token, allowFuncDecl, start) {\n        validateBindingIdentifier(parser, context, 0, token, 1);\n        validateAndDeclareLabel(parser, labels, value);\n        nextToken(parser, context | 32);\n        const body = allowFuncDecl &&\n            (context & 1) === 0 &&\n            parser.options.webcompat &&\n            parser.getToken() === 86104\n            ? parseFunctionDeclaration(parser, context, scope?.createChildScope(), privateScope, origin, 0, 0, 0, parser.tokenStart)\n            : parseStatement(parser, context, scope, privateScope, origin, labels, allowFuncDecl);\n        return parser.finishNode({\n            type: 'LabeledStatement',\n            label: expr,\n            body,\n        }, start);\n    }\n    function parseAsyncArrowOrAsyncFunctionDeclaration(parser, context, scope, privateScope, origin, labels, allowFuncDecl) {\n        const { tokenValue, tokenStart: start } = parser;\n        const token = parser.getToken();\n        let expr = parseIdentifier(parser, context);\n        if (parser.getToken() === 21) {\n            return parseLabelledStatement(parser, context, scope, privateScope, origin, labels, tokenValue, expr, token, 1, start);\n        }\n        const asyncNewLine = parser.flags & 1;\n        if (!asyncNewLine) {\n            if (parser.getToken() === 86104) {\n                if (!allowFuncDecl)\n                    parser.report(123);\n                return parseFunctionDeclaration(parser, context, scope, privateScope, origin, 1, 0, 1, start);\n            }\n            if (isValidIdentifier(context, parser.getToken())) {\n                expr = parseAsyncArrowAfterIdent(parser, context, privateScope, 1, start);\n                if (parser.getToken() === 18)\n                    expr = parseSequenceExpression(parser, context, privateScope, 0, start, expr);\n                return parseExpressionStatement(parser, context, expr, start);\n            }\n        }\n        if (parser.getToken() === 67174411) {\n            expr = parseAsyncArrowOrCallExpression(parser, context, privateScope, expr, 1, 1, 0, asyncNewLine, start);\n        }\n        else {\n            if (parser.getToken() === 10) {\n                classifyIdentifier(parser, context, token);\n                if ((token & 36864) === 36864) {\n                    parser.flags |= 256;\n                }\n                expr = parseArrowFromIdentifier(parser, context | 2048, privateScope, parser.tokenValue, expr, 0, 1, 0, start);\n            }\n            parser.assignable = 1;\n        }\n        expr = parseMemberOrUpdateExpression(parser, context, privateScope, expr, 0, 0, start);\n        expr = parseAssignmentExpression(parser, context, privateScope, 0, 0, start, expr);\n        parser.assignable = 1;\n        if (parser.getToken() === 18) {\n            expr = parseSequenceExpression(parser, context, privateScope, 0, start, expr);\n        }\n        return parseExpressionStatement(parser, context, expr, start);\n    }\n    function parseDirective(parser, context, expression, token, start) {\n        const endIndex = parser.startIndex;\n        if (token !== 1074790417) {\n            parser.assignable = 2;\n            expression = parseMemberOrUpdateExpression(parser, context, undefined, expression, 0, 0, start);\n            if (parser.getToken() !== 1074790417) {\n                expression = parseAssignmentExpression(parser, context, undefined, 0, 0, start, expression);\n                if (parser.getToken() === 18) {\n                    expression = parseSequenceExpression(parser, context, undefined, 0, start, expression);\n                }\n            }\n            matchOrInsertSemicolon(parser, context | 32);\n        }\n        const node = {\n            type: 'ExpressionStatement',\n            expression,\n        };\n        if (expression.type === 'Literal' && typeof expression.value === 'string') {\n            node.directive = parser.source.slice(start.index + 1, endIndex - 1);\n        }\n        return parser.finishNode(node, start);\n    }\n    function parseEmptyStatement(parser, context) {\n        const start = parser.tokenStart;\n        nextToken(parser, context | 32);\n        return parser.finishNode({\n            type: 'EmptyStatement',\n        }, start);\n    }\n    function parseThrowStatement(parser, context, privateScope) {\n        const start = parser.tokenStart;\n        nextToken(parser, context | 32);\n        if (parser.flags & 1)\n            parser.report(90);\n        const argument = parseExpressions(parser, context, privateScope, 0, 1, parser.tokenStart);\n        matchOrInsertSemicolon(parser, context | 32);\n        return parser.finishNode({\n            type: 'ThrowStatement',\n            argument,\n        }, start);\n    }\n    function parseIfStatement(parser, context, scope, privateScope, labels) {\n        const start = parser.tokenStart;\n        nextToken(parser, context);\n        consume(parser, context | 32, 67174411);\n        parser.assignable = 1;\n        const test = parseExpressions(parser, context, privateScope, 0, 1, parser.tokenStart);\n        consume(parser, context | 32, 16);\n        const consequent = parseConsequentOrAlternative(parser, context, scope, privateScope, labels);\n        let alternate = null;\n        if (parser.getToken() === 20563) {\n            nextToken(parser, context | 32);\n            alternate = parseConsequentOrAlternative(parser, context, scope, privateScope, labels);\n        }\n        return parser.finishNode({\n            type: 'IfStatement',\n            test,\n            consequent,\n            alternate,\n        }, start);\n    }\n    function parseConsequentOrAlternative(parser, context, scope, privateScope, labels) {\n        const { tokenStart } = parser;\n        return context & 1 ||\n            !parser.options.webcompat ||\n            parser.getToken() !== 86104\n            ? parseStatement(parser, context, scope, privateScope, 0, { $: labels }, 0)\n            : parseFunctionDeclaration(parser, context, scope?.createChildScope(), privateScope, 0, 0, 0, 0, tokenStart);\n    }\n    function parseSwitchStatement(parser, context, scope, privateScope, labels) {\n        const start = parser.tokenStart;\n        nextToken(parser, context);\n        consume(parser, context | 32, 67174411);\n        const discriminant = parseExpressions(parser, context, privateScope, 0, 1, parser.tokenStart);\n        consume(parser, context, 16);\n        consume(parser, context, 2162700);\n        const cases = [];\n        let seenDefault = 0;\n        scope = scope?.createChildScope(8);\n        while (parser.getToken() !== 1074790415) {\n            const { tokenStart } = parser;\n            let test = null;\n            const consequent = [];\n            if (consumeOpt(parser, context | 32, 20556)) {\n                test = parseExpressions(parser, context, privateScope, 0, 1, parser.tokenStart);\n            }\n            else {\n                consume(parser, context | 32, 20561);\n                if (seenDefault)\n                    parser.report(89);\n                seenDefault = 1;\n            }\n            consume(parser, context | 32, 21);\n            while (parser.getToken() !== 20556 &&\n                parser.getToken() !== 1074790415 &&\n                parser.getToken() !== 20561) {\n                consequent.push(parseStatementListItem(parser, context | 4, scope, privateScope, 2, {\n                    $: labels,\n                }));\n            }\n            cases.push(parser.finishNode({\n                type: 'SwitchCase',\n                test,\n                consequent,\n            }, tokenStart));\n        }\n        consume(parser, context | 32, 1074790415);\n        return parser.finishNode({\n            type: 'SwitchStatement',\n            discriminant,\n            cases,\n        }, start);\n    }\n    function parseWhileStatement(parser, context, scope, privateScope, labels) {\n        const start = parser.tokenStart;\n        nextToken(parser, context);\n        consume(parser, context | 32, 67174411);\n        const test = parseExpressions(parser, context, privateScope, 0, 1, parser.tokenStart);\n        consume(parser, context | 32, 16);\n        const body = parseIterationStatementBody(parser, context, scope, privateScope, labels);\n        return parser.finishNode({\n            type: 'WhileStatement',\n            test,\n            body,\n        }, start);\n    }\n    function parseIterationStatementBody(parser, context, scope, privateScope, labels) {\n        return parseStatement(parser, ((context | 131072) ^ 131072) | 128, scope, privateScope, 0, { loop: 1, $: labels }, 0);\n    }\n    function parseContinueStatement(parser, context, labels) {\n        if ((context & 128) === 0)\n            parser.report(68);\n        const start = parser.tokenStart;\n        nextToken(parser, context);\n        let label = null;\n        if ((parser.flags & 1) === 0 && parser.getToken() & 143360) {\n            const { tokenValue } = parser;\n            label = parseIdentifier(parser, context | 32);\n            if (!isValidLabel(parser, labels, tokenValue, 1))\n                parser.report(138, tokenValue);\n        }\n        matchOrInsertSemicolon(parser, context | 32);\n        return parser.finishNode({\n            type: 'ContinueStatement',\n            label,\n        }, start);\n    }\n    function parseBreakStatement(parser, context, labels) {\n        const start = parser.tokenStart;\n        nextToken(parser, context | 32);\n        let label = null;\n        if ((parser.flags & 1) === 0 && parser.getToken() & 143360) {\n            const { tokenValue } = parser;\n            label = parseIdentifier(parser, context | 32);\n            if (!isValidLabel(parser, labels, tokenValue, 0))\n                parser.report(138, tokenValue);\n        }\n        else if ((context & (4 | 128)) === 0) {\n            parser.report(69);\n        }\n        matchOrInsertSemicolon(parser, context | 32);\n        return parser.finishNode({\n            type: 'BreakStatement',\n            label,\n        }, start);\n    }\n    function parseWithStatement(parser, context, scope, privateScope, labels) {\n        const start = parser.tokenStart;\n        nextToken(parser, context);\n        if (context & 1)\n            parser.report(91);\n        consume(parser, context | 32, 67174411);\n        const object = parseExpressions(parser, context, privateScope, 0, 1, parser.tokenStart);\n        consume(parser, context | 32, 16);\n        const body = parseStatement(parser, context, scope, privateScope, 2, labels, 0);\n        return parser.finishNode({\n            type: 'WithStatement',\n            object,\n            body,\n        }, start);\n    }\n    function parseDebuggerStatement(parser, context) {\n        const start = parser.tokenStart;\n        nextToken(parser, context | 32);\n        matchOrInsertSemicolon(parser, context | 32);\n        return parser.finishNode({\n            type: 'DebuggerStatement',\n        }, start);\n    }\n    function parseTryStatement(parser, context, scope, privateScope, labels) {\n        const start = parser.tokenStart;\n        nextToken(parser, context | 32);\n        const firstScope = scope?.createChildScope(16);\n        const block = parseBlock(parser, context, firstScope, privateScope, { $: labels });\n        const { tokenStart } = parser;\n        const handler = consumeOpt(parser, context | 32, 20557)\n            ? parseCatchBlock(parser, context, scope, privateScope, labels, tokenStart)\n            : null;\n        let finalizer = null;\n        if (parser.getToken() === 20566) {\n            nextToken(parser, context | 32);\n            const finalizerScope = scope?.createChildScope(4);\n            const block = parseBlock(parser, context, finalizerScope, privateScope, { $: labels });\n            finalizer = block;\n        }\n        if (!handler && !finalizer) {\n            parser.report(88);\n        }\n        return parser.finishNode({\n            type: 'TryStatement',\n            block,\n            handler,\n            finalizer,\n        }, start);\n    }\n    function parseCatchBlock(parser, context, scope, privateScope, labels, start) {\n        let param = null;\n        let additionalScope = scope;\n        if (consumeOpt(parser, context, 67174411)) {\n            scope = scope?.createChildScope(4);\n            param = parseBindingPattern(parser, context, scope, privateScope, (parser.getToken() & 2097152) === 2097152\n                ? 256\n                : 512, 0);\n            if (parser.getToken() === 18) {\n                parser.report(86);\n            }\n            else if (parser.getToken() === 1077936155) {\n                parser.report(87);\n            }\n            consume(parser, context | 32, 16);\n        }\n        additionalScope = scope?.createChildScope(32);\n        const body = parseBlock(parser, context, additionalScope, privateScope, { $: labels });\n        return parser.finishNode({\n            type: 'CatchClause',\n            param,\n            body,\n        }, start);\n    }\n    function parseStaticBlock(parser, context, scope, privateScope, start) {\n        scope = scope?.createChildScope();\n        const ctorContext = 512 | 4096 | 1024 | 4 | 128;\n        context =\n            ((context | ctorContext) ^ ctorContext) |\n                256 |\n                2048 |\n                524288 |\n                65536;\n        return parseBlock(parser, context, scope, privateScope, {}, start, 'StaticBlock');\n    }\n    function parseDoWhileStatement(parser, context, scope, privateScope, labels) {\n        const start = parser.tokenStart;\n        nextToken(parser, context | 32);\n        const body = parseIterationStatementBody(parser, context, scope, privateScope, labels);\n        consume(parser, context, 20578);\n        consume(parser, context | 32, 67174411);\n        const test = parseExpressions(parser, context, privateScope, 0, 1, parser.tokenStart);\n        consume(parser, context | 32, 16);\n        consumeOpt(parser, context | 32, 1074790417);\n        return parser.finishNode({\n            type: 'DoWhileStatement',\n            body,\n            test,\n        }, start);\n    }\n    function parseLetIdentOrVarDeclarationStatement(parser, context, scope, privateScope, origin) {\n        const { tokenValue, tokenStart } = parser;\n        const token = parser.getToken();\n        let expr = parseIdentifier(parser, context);\n        if (parser.getToken() & (143360 | 2097152)) {\n            const declarations = parseVariableDeclarationList(parser, context, scope, privateScope, 8, 0);\n            matchOrInsertSemicolon(parser, context | 32);\n            return parser.finishNode({\n                type: 'VariableDeclaration',\n                kind: 'let',\n                declarations,\n            }, tokenStart);\n        }\n        parser.assignable = 1;\n        if (context & 1)\n            parser.report(85);\n        if (parser.getToken() === 21) {\n            return parseLabelledStatement(parser, context, scope, privateScope, origin, {}, tokenValue, expr, token, 0, tokenStart);\n        }\n        if (parser.getToken() === 10) {\n            let scope = void 0;\n            if (parser.options.lexical)\n                scope = createArrowHeadParsingScope(parser, context, tokenValue);\n            parser.flags = (parser.flags | 128) ^ 128;\n            expr = parseArrowFunctionExpression(parser, context, scope, privateScope, [expr], 0, tokenStart);\n        }\n        else {\n            expr = parseMemberOrUpdateExpression(parser, context, privateScope, expr, 0, 0, tokenStart);\n            expr = parseAssignmentExpression(parser, context, privateScope, 0, 0, tokenStart, expr);\n        }\n        if (parser.getToken() === 18) {\n            expr = parseSequenceExpression(parser, context, privateScope, 0, tokenStart, expr);\n        }\n        return parseExpressionStatement(parser, context, expr, tokenStart);\n    }\n    function parseLexicalDeclaration(parser, context, scope, privateScope, kind, origin) {\n        const start = parser.tokenStart;\n        nextToken(parser, context);\n        const declarations = parseVariableDeclarationList(parser, context, scope, privateScope, kind, origin);\n        matchOrInsertSemicolon(parser, context | 32);\n        return parser.finishNode({\n            type: 'VariableDeclaration',\n            kind: kind & 8 ? 'let' : 'const',\n            declarations,\n        }, start);\n    }\n    function parseVariableStatement(parser, context, scope, privateScope, origin) {\n        const start = parser.tokenStart;\n        nextToken(parser, context);\n        const declarations = parseVariableDeclarationList(parser, context, scope, privateScope, 4, origin);\n        matchOrInsertSemicolon(parser, context | 32);\n        return parser.finishNode({\n            type: 'VariableDeclaration',\n            kind: 'var',\n            declarations,\n        }, start);\n    }\n    function parseVariableDeclarationList(parser, context, scope, privateScope, kind, origin) {\n        let bindingCount = 1;\n        const list = [\n            parseVariableDeclaration(parser, context, scope, privateScope, kind, origin),\n        ];\n        while (consumeOpt(parser, context, 18)) {\n            bindingCount++;\n            list.push(parseVariableDeclaration(parser, context, scope, privateScope, kind, origin));\n        }\n        if (bindingCount > 1 && origin & 32 && parser.getToken() & 262144) {\n            parser.report(61, KeywordDescTable[parser.getToken() & 255]);\n        }\n        return list;\n    }\n    function parseVariableDeclaration(parser, context, scope, privateScope, kind, origin) {\n        const { tokenStart } = parser;\n        const token = parser.getToken();\n        let init = null;\n        const id = parseBindingPattern(parser, context, scope, privateScope, kind, origin);\n        if (parser.getToken() === 1077936155) {\n            nextToken(parser, context | 32);\n            init = parseExpression(parser, context, privateScope, 1, 0, parser.tokenStart);\n            if (origin & 32 || (token & 2097152) === 0) {\n                if (parser.getToken() === 471156 ||\n                    (parser.getToken() === 8673330 &&\n                        (token & 2097152 || (kind & 4) === 0 || context & 1))) {\n                    throw new ParseError(tokenStart, parser.currentLocation, 60, parser.getToken() === 471156 ? 'of' : 'in');\n                }\n            }\n        }\n        else if ((kind & 16 || (token & 2097152) > 0) &&\n            (parser.getToken() & 262144) !== 262144) {\n            parser.report(59, kind & 16 ? 'const' : 'destructuring');\n        }\n        return parser.finishNode({\n            type: 'VariableDeclarator',\n            id,\n            init,\n        }, tokenStart);\n    }\n    function parseForStatement(parser, context, scope, privateScope, labels) {\n        const start = parser.tokenStart;\n        nextToken(parser, context);\n        const forAwait = ((context & 2048) > 0 || ((context & 2) > 0 && (context & 8) > 0)) &&\n            consumeOpt(parser, context, 209006);\n        consume(parser, context | 32, 67174411);\n        scope = scope?.createChildScope(1);\n        let test = null;\n        let update = null;\n        let destructible = 0;\n        let init = null;\n        let isVarDecl = parser.getToken() === 86088 ||\n            parser.getToken() === 241737 ||\n            parser.getToken() === 86090;\n        let right;\n        const { tokenStart } = parser;\n        const token = parser.getToken();\n        if (isVarDecl) {\n            if (token === 241737) {\n                init = parseIdentifier(parser, context);\n                if (parser.getToken() & (143360 | 2097152)) {\n                    if (parser.getToken() === 8673330) {\n                        if (context & 1)\n                            parser.report(67);\n                    }\n                    else {\n                        init = parser.finishNode({\n                            type: 'VariableDeclaration',\n                            kind: 'let',\n                            declarations: parseVariableDeclarationList(parser, context | 131072, scope, privateScope, 8, 32),\n                        }, tokenStart);\n                    }\n                    parser.assignable = 1;\n                }\n                else if (context & 1) {\n                    parser.report(67);\n                }\n                else {\n                    isVarDecl = false;\n                    parser.assignable = 1;\n                    init = parseMemberOrUpdateExpression(parser, context, privateScope, init, 0, 0, tokenStart);\n                    if (parser.getToken() === 471156)\n                        parser.report(115);\n                }\n            }\n            else {\n                nextToken(parser, context);\n                init = parser.finishNode(token === 86088\n                    ? {\n                        type: 'VariableDeclaration',\n                        kind: 'var',\n                        declarations: parseVariableDeclarationList(parser, context | 131072, scope, privateScope, 4, 32),\n                    }\n                    : {\n                        type: 'VariableDeclaration',\n                        kind: 'const',\n                        declarations: parseVariableDeclarationList(parser, context | 131072, scope, privateScope, 16, 32),\n                    }, tokenStart);\n                parser.assignable = 1;\n            }\n        }\n        else if (token === 1074790417) {\n            if (forAwait)\n                parser.report(82);\n        }\n        else if ((token & 2097152) === 2097152) {\n            const patternStart = parser.tokenStart;\n            init =\n                token === 2162700\n                    ? parseObjectLiteralOrPattern(parser, context, void 0, privateScope, 1, 0, 0, 2, 32)\n                    : parseArrayExpressionOrPattern(parser, context, void 0, privateScope, 1, 0, 0, 2, 32);\n            destructible = parser.destructible;\n            if (destructible & 64) {\n                parser.report(63);\n            }\n            parser.assignable =\n                destructible & 16 ? 2 : 1;\n            init = parseMemberOrUpdateExpression(parser, context | 131072, privateScope, init, 0, 0, patternStart);\n        }\n        else {\n            init = parseLeftHandSideExpression(parser, context | 131072, privateScope, 1, 0, 1);\n        }\n        if ((parser.getToken() & 262144) === 262144) {\n            if (parser.getToken() === 471156) {\n                if (parser.assignable & 2)\n                    parser.report(80, forAwait ? 'await' : 'of');\n                reinterpretToPattern(parser, init);\n                nextToken(parser, context | 32);\n                right = parseExpression(parser, context, privateScope, 1, 0, parser.tokenStart);\n                consume(parser, context | 32, 16);\n                const body = parseIterationStatementBody(parser, context, scope, privateScope, labels);\n                return parser.finishNode({\n                    type: 'ForOfStatement',\n                    left: init,\n                    right,\n                    body,\n                    await: forAwait,\n                }, start);\n            }\n            if (parser.assignable & 2)\n                parser.report(80, 'in');\n            reinterpretToPattern(parser, init);\n            nextToken(parser, context | 32);\n            if (forAwait)\n                parser.report(82);\n            right = parseExpressions(parser, context, privateScope, 0, 1, parser.tokenStart);\n            consume(parser, context | 32, 16);\n            const body = parseIterationStatementBody(parser, context, scope, privateScope, labels);\n            return parser.finishNode({\n                type: 'ForInStatement',\n                body,\n                left: init,\n                right,\n            }, start);\n        }\n        if (forAwait)\n            parser.report(82);\n        if (!isVarDecl) {\n            if (destructible & 8 && parser.getToken() !== 1077936155) {\n                parser.report(80, 'loop');\n            }\n            init = parseAssignmentExpression(parser, context | 131072, privateScope, 0, 0, tokenStart, init);\n        }\n        if (parser.getToken() === 18)\n            init = parseSequenceExpression(parser, context, privateScope, 0, tokenStart, init);\n        consume(parser, context | 32, 1074790417);\n        if (parser.getToken() !== 1074790417)\n            test = parseExpressions(parser, context, privateScope, 0, 1, parser.tokenStart);\n        consume(parser, context | 32, 1074790417);\n        if (parser.getToken() !== 16)\n            update = parseExpressions(parser, context, privateScope, 0, 1, parser.tokenStart);\n        consume(parser, context | 32, 16);\n        const body = parseIterationStatementBody(parser, context, scope, privateScope, labels);\n        return parser.finishNode({\n            type: 'ForStatement',\n            init,\n            test,\n            update,\n            body,\n        }, start);\n    }\n    function parseRestrictedIdentifier(parser, context, scope) {\n        if (!isValidIdentifier(context, parser.getToken()))\n            parser.report(118);\n        if ((parser.getToken() & 537079808) === 537079808)\n            parser.report(119);\n        scope?.addBlockName(context, parser.tokenValue, 8, 0);\n        return parseIdentifier(parser, context);\n    }\n    function parseImportDeclaration(parser, context, scope) {\n        const start = parser.tokenStart;\n        nextToken(parser, context);\n        let source = null;\n        const { tokenStart } = parser;\n        let specifiers = [];\n        if (parser.getToken() === 134283267) {\n            source = parseLiteral(parser, context);\n        }\n        else {\n            if (parser.getToken() & 143360) {\n                const local = parseRestrictedIdentifier(parser, context, scope);\n                specifiers = [\n                    parser.finishNode({\n                        type: 'ImportDefaultSpecifier',\n                        local,\n                    }, tokenStart),\n                ];\n                if (consumeOpt(parser, context, 18)) {\n                    switch (parser.getToken()) {\n                        case 8391476:\n                            specifiers.push(parseImportNamespaceSpecifier(parser, context, scope));\n                            break;\n                        case 2162700:\n                            parseImportSpecifierOrNamedImports(parser, context, scope, specifiers);\n                            break;\n                        default:\n                            parser.report(107);\n                    }\n                }\n            }\n            else {\n                switch (parser.getToken()) {\n                    case 8391476:\n                        specifiers = [parseImportNamespaceSpecifier(parser, context, scope)];\n                        break;\n                    case 2162700:\n                        parseImportSpecifierOrNamedImports(parser, context, scope, specifiers);\n                        break;\n                    case 67174411:\n                        return parseImportCallDeclaration(parser, context, undefined, start);\n                    case 67108877:\n                        return parseImportMetaDeclaration(parser, context, start);\n                    default:\n                        parser.report(30, KeywordDescTable[parser.getToken() & 255]);\n                }\n            }\n            source = parseModuleSpecifier(parser, context);\n        }\n        const attributes = parseImportAttributes(parser, context);\n        const node = {\n            type: 'ImportDeclaration',\n            specifiers,\n            source,\n            attributes,\n        };\n        matchOrInsertSemicolon(parser, context | 32);\n        return parser.finishNode(node, start);\n    }\n    function parseImportNamespaceSpecifier(parser, context, scope) {\n        const { tokenStart } = parser;\n        nextToken(parser, context);\n        consume(parser, context, 77932);\n        if ((parser.getToken() & 134217728) === 134217728) {\n            throw new ParseError(tokenStart, parser.currentLocation, 30, KeywordDescTable[parser.getToken() & 255]);\n        }\n        return parser.finishNode({\n            type: 'ImportNamespaceSpecifier',\n            local: parseRestrictedIdentifier(parser, context, scope),\n        }, tokenStart);\n    }\n    function parseModuleSpecifier(parser, context) {\n        consume(parser, context, 209011);\n        if (parser.getToken() !== 134283267)\n            parser.report(105, 'Import');\n        return parseLiteral(parser, context);\n    }\n    function parseImportSpecifierOrNamedImports(parser, context, scope, specifiers) {\n        nextToken(parser, context);\n        while (parser.getToken() & 143360 || parser.getToken() === 134283267) {\n            let { tokenValue, tokenStart } = parser;\n            const token = parser.getToken();\n            const imported = parseModuleExportName(parser, context);\n            let local;\n            if (consumeOpt(parser, context, 77932)) {\n                if ((parser.getToken() & 134217728) === 134217728 ||\n                    parser.getToken() === 18) {\n                    parser.report(106);\n                }\n                else {\n                    validateBindingIdentifier(parser, context, 16, parser.getToken(), 0);\n                }\n                tokenValue = parser.tokenValue;\n                local = parseIdentifier(parser, context);\n            }\n            else if (imported.type === 'Identifier') {\n                validateBindingIdentifier(parser, context, 16, token, 0);\n                local = imported;\n            }\n            else {\n                parser.report(25, KeywordDescTable[77932 & 255]);\n            }\n            scope?.addBlockName(context, tokenValue, 8, 0);\n            specifiers.push(parser.finishNode({\n                type: 'ImportSpecifier',\n                local,\n                imported,\n            }, tokenStart));\n            if (parser.getToken() !== 1074790415)\n                consume(parser, context, 18);\n        }\n        consume(parser, context, 1074790415);\n        return specifiers;\n    }\n    function parseImportMetaDeclaration(parser, context, start) {\n        let expr = parseImportMetaExpression(parser, context, parser.finishNode({\n            type: 'Identifier',\n            name: 'import',\n        }, start), start);\n        expr = parseMemberOrUpdateExpression(parser, context, undefined, expr, 0, 0, start);\n        expr = parseAssignmentExpression(parser, context, undefined, 0, 0, start, expr);\n        if (parser.getToken() === 18) {\n            expr = parseSequenceExpression(parser, context, undefined, 0, start, expr);\n        }\n        return parseExpressionStatement(parser, context, expr, start);\n    }\n    function parseImportCallDeclaration(parser, context, privateScope, start) {\n        let expr = parseImportExpression(parser, context, privateScope, 0, start);\n        expr = parseMemberOrUpdateExpression(parser, context, privateScope, expr, 0, 0, start);\n        if (parser.getToken() === 18) {\n            expr = parseSequenceExpression(parser, context, privateScope, 0, start, expr);\n        }\n        return parseExpressionStatement(parser, context, expr, start);\n    }\n    function parseExportDeclaration(parser, context, scope) {\n        const start = parser.leadingDecorators.decorators.length ? parser.leadingDecorators.start : parser.tokenStart;\n        nextToken(parser, context | 32);\n        const specifiers = [];\n        let declaration = null;\n        let source = null;\n        let attributes = [];\n        if (consumeOpt(parser, context | 32, 20561)) {\n            switch (parser.getToken()) {\n                case 86104: {\n                    declaration = parseFunctionDeclaration(parser, context, scope, undefined, 4, 1, 1, 0, parser.tokenStart);\n                    break;\n                }\n                case 132:\n                case 86094:\n                    declaration = parseClassDeclaration(parser, context, scope, undefined, 1);\n                    break;\n                case 209005: {\n                    const { tokenStart } = parser;\n                    declaration = parseIdentifier(parser, context);\n                    const { flags } = parser;\n                    if ((flags & 1) === 0) {\n                        if (parser.getToken() === 86104) {\n                            declaration = parseFunctionDeclaration(parser, context, scope, undefined, 4, 1, 1, 1, tokenStart);\n                        }\n                        else {\n                            if (parser.getToken() === 67174411) {\n                                declaration = parseAsyncArrowOrCallExpression(parser, context, undefined, declaration, 1, 1, 0, flags, tokenStart);\n                                declaration = parseMemberOrUpdateExpression(parser, context, undefined, declaration, 0, 0, tokenStart);\n                                declaration = parseAssignmentExpression(parser, context, undefined, 0, 0, tokenStart, declaration);\n                            }\n                            else if (parser.getToken() & 143360) {\n                                if (scope)\n                                    scope = createArrowHeadParsingScope(parser, context, parser.tokenValue);\n                                declaration = parseIdentifier(parser, context);\n                                declaration = parseArrowFunctionExpression(parser, context, scope, undefined, [declaration], 1, tokenStart);\n                            }\n                        }\n                    }\n                    break;\n                }\n                default:\n                    declaration = parseExpression(parser, context, undefined, 1, 0, parser.tokenStart);\n                    matchOrInsertSemicolon(parser, context | 32);\n            }\n            if (scope)\n                parser.declareUnboundVariable('default');\n            return parser.finishNode({\n                type: 'ExportDefaultDeclaration',\n                declaration,\n            }, start);\n        }\n        switch (parser.getToken()) {\n            case 8391476: {\n                nextToken(parser, context);\n                let exported = null;\n                const isNamedDeclaration = consumeOpt(parser, context, 77932);\n                if (isNamedDeclaration) {\n                    if (scope)\n                        parser.declareUnboundVariable(parser.tokenValue);\n                    exported = parseModuleExportName(parser, context);\n                }\n                consume(parser, context, 209011);\n                if (parser.getToken() !== 134283267)\n                    parser.report(105, 'Export');\n                source = parseLiteral(parser, context);\n                const attributes = parseImportAttributes(parser, context);\n                const node = {\n                    type: 'ExportAllDeclaration',\n                    source,\n                    exported,\n                    attributes,\n                };\n                matchOrInsertSemicolon(parser, context | 32);\n                return parser.finishNode(node, start);\n            }\n            case 2162700: {\n                nextToken(parser, context);\n                const tmpExportedNames = [];\n                const tmpExportedBindings = [];\n                let hasLiteralLocal = 0;\n                while (parser.getToken() & 143360 || parser.getToken() === 134283267) {\n                    const { tokenStart, tokenValue } = parser;\n                    const local = parseModuleExportName(parser, context);\n                    if (local.type === 'Literal') {\n                        hasLiteralLocal = 1;\n                    }\n                    let exported;\n                    if (parser.getToken() === 77932) {\n                        nextToken(parser, context);\n                        if ((parser.getToken() & 143360) === 0 && parser.getToken() !== 134283267) {\n                            parser.report(106);\n                        }\n                        if (scope) {\n                            tmpExportedNames.push(parser.tokenValue);\n                            tmpExportedBindings.push(tokenValue);\n                        }\n                        exported = parseModuleExportName(parser, context);\n                    }\n                    else {\n                        if (scope) {\n                            tmpExportedNames.push(parser.tokenValue);\n                            tmpExportedBindings.push(parser.tokenValue);\n                        }\n                        exported = local;\n                    }\n                    specifiers.push(parser.finishNode({\n                        type: 'ExportSpecifier',\n                        local,\n                        exported,\n                    }, tokenStart));\n                    if (parser.getToken() !== 1074790415)\n                        consume(parser, context, 18);\n                }\n                consume(parser, context, 1074790415);\n                if (consumeOpt(parser, context, 209011)) {\n                    if (parser.getToken() !== 134283267)\n                        parser.report(105, 'Export');\n                    source = parseLiteral(parser, context);\n                    attributes = parseImportAttributes(parser, context);\n                    if (scope) {\n                        tmpExportedNames.forEach((n) => parser.declareUnboundVariable(n));\n                    }\n                }\n                else {\n                    if (hasLiteralLocal) {\n                        parser.report(172);\n                    }\n                    if (scope) {\n                        tmpExportedNames.forEach((n) => parser.declareUnboundVariable(n));\n                        tmpExportedBindings.forEach((b) => parser.addBindingToExports(b));\n                    }\n                }\n                matchOrInsertSemicolon(parser, context | 32);\n                break;\n            }\n            case 132:\n            case 86094:\n                declaration = parseClassDeclaration(parser, context, scope, undefined, 2);\n                break;\n            case 86104:\n                declaration = parseFunctionDeclaration(parser, context, scope, undefined, 4, 1, 2, 0, parser.tokenStart);\n                break;\n            case 241737:\n                declaration = parseLexicalDeclaration(parser, context, scope, undefined, 8, 64);\n                break;\n            case 86090:\n                declaration = parseLexicalDeclaration(parser, context, scope, undefined, 16, 64);\n                break;\n            case 86088:\n                declaration = parseVariableStatement(parser, context, scope, undefined, 64);\n                break;\n            case 209005: {\n                const { tokenStart } = parser;\n                nextToken(parser, context);\n                if ((parser.flags & 1) === 0 && parser.getToken() === 86104) {\n                    declaration = parseFunctionDeclaration(parser, context, scope, undefined, 4, 1, 2, 1, tokenStart);\n                    break;\n                }\n            }\n            default:\n                parser.report(30, KeywordDescTable[parser.getToken() & 255]);\n        }\n        const node = {\n            type: 'ExportNamedDeclaration',\n            declaration,\n            specifiers,\n            source,\n            attributes: attributes,\n        };\n        return parser.finishNode(node, start);\n    }\n    function parseExpression(parser, context, privateScope, canAssign, inGroup, start) {\n        let expr = parsePrimaryExpression(parser, context, privateScope, 2, 0, canAssign, inGroup, 1, start);\n        expr = parseMemberOrUpdateExpression(parser, context, privateScope, expr, inGroup, 0, start);\n        return parseAssignmentExpression(parser, context, privateScope, inGroup, 0, start, expr);\n    }\n    function parseSequenceExpression(parser, context, privateScope, inGroup, start, expr) {\n        const expressions = [expr];\n        while (consumeOpt(parser, context | 32, 18)) {\n            expressions.push(parseExpression(parser, context, privateScope, 1, inGroup, parser.tokenStart));\n        }\n        return parser.finishNode({\n            type: 'SequenceExpression',\n            expressions,\n        }, start);\n    }\n    function parseExpressions(parser, context, privateScope, inGroup, canAssign, start) {\n        const expr = parseExpression(parser, context, privateScope, canAssign, inGroup, start);\n        return parser.getToken() === 18\n            ? parseSequenceExpression(parser, context, privateScope, inGroup, start, expr)\n            : expr;\n    }\n    function parseAssignmentExpression(parser, context, privateScope, inGroup, isPattern, start, left) {\n        const token = parser.getToken();\n        if ((token & 4194304) === 4194304) {\n            if (parser.assignable & 2)\n                parser.report(26);\n            if ((!isPattern && token === 1077936155 && left.type === 'ArrayExpression') ||\n                left.type === 'ObjectExpression') {\n                reinterpretToPattern(parser, left);\n            }\n            nextToken(parser, context | 32);\n            const right = parseExpression(parser, context, privateScope, 1, inGroup, parser.tokenStart);\n            parser.assignable = 2;\n            return parser.finishNode(isPattern\n                ? {\n                    type: 'AssignmentPattern',\n                    left,\n                    right,\n                }\n                : {\n                    type: 'AssignmentExpression',\n                    left,\n                    operator: KeywordDescTable[token & 255],\n                    right,\n                }, start);\n        }\n        if ((token & 8388608) === 8388608) {\n            left = parseBinaryExpression(parser, context, privateScope, inGroup, start, 4, token, left);\n        }\n        if (consumeOpt(parser, context | 32, 22)) {\n            left = parseConditionalExpression(parser, context, privateScope, left, start);\n        }\n        return left;\n    }\n    function parseAssignmentExpressionOrPattern(parser, context, privateScope, inGroup, isPattern, start, left) {\n        const token = parser.getToken();\n        nextToken(parser, context | 32);\n        const right = parseExpression(parser, context, privateScope, 1, inGroup, parser.tokenStart);\n        left = parser.finishNode(isPattern\n            ? {\n                type: 'AssignmentPattern',\n                left,\n                right,\n            }\n            : {\n                type: 'AssignmentExpression',\n                left,\n                operator: KeywordDescTable[token & 255],\n                right,\n            }, start);\n        parser.assignable = 2;\n        return left;\n    }\n    function parseConditionalExpression(parser, context, privateScope, test, start) {\n        const consequent = parseExpression(parser, (context | 131072) ^ 131072, privateScope, 1, 0, parser.tokenStart);\n        consume(parser, context | 32, 21);\n        parser.assignable = 1;\n        const alternate = parseExpression(parser, context, privateScope, 1, 0, parser.tokenStart);\n        parser.assignable = 2;\n        return parser.finishNode({\n            type: 'ConditionalExpression',\n            test,\n            consequent,\n            alternate,\n        }, start);\n    }\n    function parseBinaryExpression(parser, context, privateScope, inGroup, start, minPrecedence, operator, left) {\n        const bit = -((context & 131072) > 0) & 8673330;\n        let t;\n        let precedence;\n        parser.assignable = 2;\n        while (parser.getToken() & 8388608) {\n            t = parser.getToken();\n            precedence = t & 3840;\n            if ((t & 524288 && operator & 268435456) || (operator & 524288 && t & 268435456)) {\n                parser.report(165);\n            }\n            if (precedence + ((t === 8391735) << 8) - ((bit === t) << 12) <= minPrecedence)\n                break;\n            nextToken(parser, context | 32);\n            left = parser.finishNode({\n                type: t & 524288 || t & 268435456 ? 'LogicalExpression' : 'BinaryExpression',\n                left,\n                right: parseBinaryExpression(parser, context, privateScope, inGroup, parser.tokenStart, precedence, t, parseLeftHandSideExpression(parser, context, privateScope, 0, inGroup, 1)),\n                operator: KeywordDescTable[t & 255],\n            }, start);\n        }\n        if (parser.getToken() === 1077936155)\n            parser.report(26);\n        return left;\n    }\n    function parseUnaryExpression(parser, context, privateScope, isLHS, inGroup) {\n        if (!isLHS)\n            parser.report(0);\n        const { tokenStart } = parser;\n        const unaryOperator = parser.getToken();\n        nextToken(parser, context | 32);\n        const arg = parseLeftHandSideExpression(parser, context, privateScope, 0, inGroup, 1);\n        if (parser.getToken() === 8391735)\n            parser.report(33);\n        if (context & 1 && unaryOperator === 16863276) {\n            if (arg.type === 'Identifier') {\n                parser.report(121);\n            }\n            else if (isPropertyWithPrivateFieldKey(arg)) {\n                parser.report(127);\n            }\n        }\n        parser.assignable = 2;\n        return parser.finishNode({\n            type: 'UnaryExpression',\n            operator: KeywordDescTable[unaryOperator & 255],\n            argument: arg,\n            prefix: true,\n        }, tokenStart);\n    }\n    function parseAsyncExpression(parser, context, privateScope, inGroup, isLHS, canAssign, inNew, start) {\n        const token = parser.getToken();\n        const expr = parseIdentifier(parser, context);\n        const { flags } = parser;\n        if ((flags & 1) === 0) {\n            if (parser.getToken() === 86104) {\n                return parseFunctionExpression(parser, context, privateScope, 1, inGroup, start);\n            }\n            if (isValidIdentifier(context, parser.getToken())) {\n                if (!isLHS)\n                    parser.report(0);\n                if ((parser.getToken() & 36864) === 36864) {\n                    parser.flags |= 256;\n                }\n                return parseAsyncArrowAfterIdent(parser, context, privateScope, canAssign, start);\n            }\n        }\n        if (!inNew && parser.getToken() === 67174411) {\n            return parseAsyncArrowOrCallExpression(parser, context, privateScope, expr, canAssign, 1, 0, flags, start);\n        }\n        if (parser.getToken() === 10) {\n            classifyIdentifier(parser, context, token);\n            if (inNew)\n                parser.report(51);\n            if ((token & 36864) === 36864) {\n                parser.flags |= 256;\n            }\n            return parseArrowFromIdentifier(parser, context, privateScope, parser.tokenValue, expr, inNew, canAssign, 0, start);\n        }\n        parser.assignable = 1;\n        return expr;\n    }\n    function parseYieldExpressionOrIdentifier(parser, context, privateScope, inGroup, canAssign, start) {\n        if (inGroup)\n            parser.destructible |= 256;\n        if (context & 1024) {\n            nextToken(parser, context | 32);\n            if (context & 8192)\n                parser.report(32);\n            if (!canAssign)\n                parser.report(26);\n            if (parser.getToken() === 22)\n                parser.report(124);\n            let argument = null;\n            let delegate = false;\n            if ((parser.flags & 1) === 0) {\n                delegate = consumeOpt(parser, context | 32, 8391476);\n                if (parser.getToken() & (12288 | 65536) || delegate) {\n                    argument = parseExpression(parser, context, privateScope, 1, 0, parser.tokenStart);\n                }\n            }\n            else if (parser.getToken() === 8391476) {\n                parser.report(30, KeywordDescTable[parser.getToken() & 255]);\n            }\n            parser.assignable = 2;\n            return parser.finishNode({\n                type: 'YieldExpression',\n                argument,\n                delegate,\n            }, start);\n        }\n        if (context & 1)\n            parser.report(97, 'yield');\n        return parseIdentifierOrArrow(parser, context, privateScope);\n    }\n    function parseAwaitExpressionOrIdentifier(parser, context, privateScope, inNew, inGroup, start) {\n        if (inGroup)\n            parser.destructible |= 128;\n        if (context & 524288)\n            parser.report(177);\n        const possibleIdentifierOrArrowFunc = parseIdentifierOrArrow(parser, context, privateScope);\n        const isIdentifier = possibleIdentifierOrArrowFunc.type === 'ArrowFunctionExpression' ||\n            (parser.getToken() & 65536) === 0;\n        if (isIdentifier) {\n            if (context & 2048)\n                throw new ParseError(start, { index: parser.startIndex, line: parser.startLine, column: parser.startColumn }, 176);\n            if (context & 2)\n                throw new ParseError(start, { index: parser.startIndex, line: parser.startLine, column: parser.startColumn }, 110);\n            if (context & 8192 && context & 2048)\n                throw new ParseError(start, { index: parser.startIndex, line: parser.startLine, column: parser.startColumn }, 110);\n            return possibleIdentifierOrArrowFunc;\n        }\n        if (context & 8192) {\n            throw new ParseError(start, { index: parser.startIndex, line: parser.startLine, column: parser.startColumn }, 31);\n        }\n        if (context & 2048 || (context & 2 && context & 8)) {\n            if (inNew)\n                throw new ParseError(start, { index: parser.startIndex, line: parser.startLine, column: parser.startColumn }, 0);\n            const argument = parseLeftHandSideExpression(parser, context, privateScope, 0, 0, 1);\n            if (parser.getToken() === 8391735)\n                parser.report(33);\n            parser.assignable = 2;\n            return parser.finishNode({\n                type: 'AwaitExpression',\n                argument,\n            }, start);\n        }\n        if (context & 2)\n            throw new ParseError(start, { index: parser.startIndex, line: parser.startLine, column: parser.startColumn }, 98);\n        return possibleIdentifierOrArrowFunc;\n    }\n    function parseFunctionBody(parser, context, scope, privateScope, origin, funcNameToken, functionScope) {\n        const { tokenStart } = parser;\n        consume(parser, context | 32, 2162700);\n        const body = [];\n        if (parser.getToken() !== 1074790415) {\n            while (parser.getToken() === 134283267) {\n                const { index, tokenStart, tokenIndex, tokenValue } = parser;\n                const token = parser.getToken();\n                const expr = parseLiteral(parser, context);\n                if (isValidStrictMode(parser, index, tokenIndex, tokenValue)) {\n                    context |= 1;\n                    if (parser.flags & 128) {\n                        throw new ParseError(tokenStart, parser.currentLocation, 66);\n                    }\n                    if (parser.flags & 64) {\n                        throw new ParseError(tokenStart, parser.currentLocation, 9);\n                    }\n                    if (parser.flags & 4096) {\n                        throw new ParseError(tokenStart, parser.currentLocation, 15);\n                    }\n                    functionScope?.reportScopeError();\n                }\n                body.push(parseDirective(parser, context, expr, token, tokenStart));\n            }\n            if (context & 1) {\n                if (funcNameToken) {\n                    if ((funcNameToken & 537079808) === 537079808) {\n                        parser.report(119);\n                    }\n                    if ((funcNameToken & 36864) === 36864) {\n                        parser.report(40);\n                    }\n                }\n                if (parser.flags & 512)\n                    parser.report(119);\n                if (parser.flags & 256)\n                    parser.report(118);\n            }\n        }\n        parser.flags =\n            (parser.flags | 512 | 256 | 64 | 4096) ^\n                (512 | 256 | 64 | 4096);\n        parser.destructible = (parser.destructible | 256) ^ 256;\n        while (parser.getToken() !== 1074790415) {\n            body.push(parseStatementListItem(parser, context, scope, privateScope, 4, {}));\n        }\n        consume(parser, origin & (16 | 8) ? context | 32 : context, 1074790415);\n        parser.flags &= -4289;\n        if (parser.getToken() === 1077936155)\n            parser.report(26);\n        return parser.finishNode({\n            type: 'BlockStatement',\n            body,\n        }, tokenStart);\n    }\n    function parseSuperExpression(parser, context) {\n        const { tokenStart } = parser;\n        nextToken(parser, context);\n        switch (parser.getToken()) {\n            case 67108990:\n                parser.report(167);\n            case 67174411: {\n                if ((context & 512) === 0)\n                    parser.report(28);\n                parser.assignable = 2;\n                break;\n            }\n            case 69271571:\n            case 67108877: {\n                if ((context & 256) === 0)\n                    parser.report(29);\n                parser.assignable = 1;\n                break;\n            }\n            default:\n                parser.report(30, 'super');\n        }\n        return parser.finishNode({ type: 'Super' }, tokenStart);\n    }\n    function parseLeftHandSideExpression(parser, context, privateScope, canAssign, inGroup, isLHS) {\n        const start = parser.tokenStart;\n        const expression = parsePrimaryExpression(parser, context, privateScope, 2, 0, canAssign, inGroup, isLHS, start);\n        return parseMemberOrUpdateExpression(parser, context, privateScope, expression, inGroup, 0, start);\n    }\n    function parseUpdateExpression(parser, context, expr, start) {\n        if (parser.assignable & 2)\n            parser.report(55);\n        const token = parser.getToken();\n        nextToken(parser, context);\n        parser.assignable = 2;\n        return parser.finishNode({\n            type: 'UpdateExpression',\n            argument: expr,\n            operator: KeywordDescTable[token & 255],\n            prefix: false,\n        }, start);\n    }\n    function parseMemberOrUpdateExpression(parser, context, privateScope, expr, inGroup, inChain, start) {\n        if ((parser.getToken() & 33619968) === 33619968 && (parser.flags & 1) === 0) {\n            expr = parseUpdateExpression(parser, context, expr, start);\n        }\n        else if ((parser.getToken() & 67108864) === 67108864) {\n            context = (context | 131072) ^ 131072;\n            switch (parser.getToken()) {\n                case 67108877: {\n                    nextToken(parser, (context | 262144 | 8) ^ 8);\n                    if (context & 16 && parser.getToken() === 130 && parser.tokenValue === 'super') {\n                        parser.report(173);\n                    }\n                    parser.assignable = 1;\n                    const property = parsePropertyOrPrivatePropertyName(parser, context | 64, privateScope);\n                    expr = parser.finishNode({\n                        type: 'MemberExpression',\n                        object: expr,\n                        computed: false,\n                        property,\n                        optional: false,\n                    }, start);\n                    break;\n                }\n                case 69271571: {\n                    let restoreHasOptionalChaining = false;\n                    if ((parser.flags & 2048) === 2048) {\n                        restoreHasOptionalChaining = true;\n                        parser.flags = (parser.flags | 2048) ^ 2048;\n                    }\n                    nextToken(parser, context | 32);\n                    const { tokenStart } = parser;\n                    const property = parseExpressions(parser, context, privateScope, inGroup, 1, tokenStart);\n                    consume(parser, context, 20);\n                    parser.assignable = 1;\n                    expr = parser.finishNode({\n                        type: 'MemberExpression',\n                        object: expr,\n                        computed: true,\n                        property,\n                        optional: false,\n                    }, start);\n                    if (restoreHasOptionalChaining) {\n                        parser.flags |= 2048;\n                    }\n                    break;\n                }\n                case 67174411: {\n                    if ((parser.flags & 1024) === 1024) {\n                        parser.flags = (parser.flags | 1024) ^ 1024;\n                        return expr;\n                    }\n                    let restoreHasOptionalChaining = false;\n                    if ((parser.flags & 2048) === 2048) {\n                        restoreHasOptionalChaining = true;\n                        parser.flags = (parser.flags | 2048) ^ 2048;\n                    }\n                    const args = parseArguments(parser, context, privateScope, inGroup);\n                    parser.assignable = 2;\n                    expr = parser.finishNode({\n                        type: 'CallExpression',\n                        callee: expr,\n                        arguments: args,\n                        optional: false,\n                    }, start);\n                    if (restoreHasOptionalChaining) {\n                        parser.flags |= 2048;\n                    }\n                    break;\n                }\n                case 67108990: {\n                    nextToken(parser, (context | 262144 | 8) ^ 8);\n                    parser.flags |= 2048;\n                    parser.assignable = 2;\n                    expr = parseOptionalChain(parser, context, privateScope, expr, start);\n                    break;\n                }\n                default:\n                    if ((parser.flags & 2048) === 2048) {\n                        parser.report(166);\n                    }\n                    parser.assignable = 2;\n                    expr = parser.finishNode({\n                        type: 'TaggedTemplateExpression',\n                        tag: expr,\n                        quasi: parser.getToken() === 67174408\n                            ? parseTemplate(parser, context | 64, privateScope)\n                            : parseTemplateLiteral(parser, context),\n                    }, start);\n            }\n            expr = parseMemberOrUpdateExpression(parser, context, privateScope, expr, 0, 1, start);\n        }\n        if (inChain === 0 && (parser.flags & 2048) === 2048) {\n            parser.flags = (parser.flags | 2048) ^ 2048;\n            expr = parser.finishNode({\n                type: 'ChainExpression',\n                expression: expr,\n            }, start);\n        }\n        return expr;\n    }\n    function parseOptionalChain(parser, context, privateScope, expr, start) {\n        let restoreHasOptionalChaining = false;\n        let node;\n        if (parser.getToken() === 69271571 || parser.getToken() === 67174411) {\n            if ((parser.flags & 2048) === 2048) {\n                restoreHasOptionalChaining = true;\n                parser.flags = (parser.flags | 2048) ^ 2048;\n            }\n        }\n        if (parser.getToken() === 69271571) {\n            nextToken(parser, context | 32);\n            const { tokenStart } = parser;\n            const property = parseExpressions(parser, context, privateScope, 0, 1, tokenStart);\n            consume(parser, context, 20);\n            parser.assignable = 2;\n            node = parser.finishNode({\n                type: 'MemberExpression',\n                object: expr,\n                computed: true,\n                optional: true,\n                property,\n            }, start);\n        }\n        else if (parser.getToken() === 67174411) {\n            const args = parseArguments(parser, context, privateScope, 0);\n            parser.assignable = 2;\n            node = parser.finishNode({\n                type: 'CallExpression',\n                callee: expr,\n                arguments: args,\n                optional: true,\n            }, start);\n        }\n        else {\n            const property = parsePropertyOrPrivatePropertyName(parser, context, privateScope);\n            parser.assignable = 2;\n            node = parser.finishNode({\n                type: 'MemberExpression',\n                object: expr,\n                computed: false,\n                optional: true,\n                property,\n            }, start);\n        }\n        if (restoreHasOptionalChaining) {\n            parser.flags |= 2048;\n        }\n        return node;\n    }\n    function parsePropertyOrPrivatePropertyName(parser, context, privateScope) {\n        if ((parser.getToken() & 143360) === 0 &&\n            parser.getToken() !== -2147483528 &&\n            parser.getToken() !== -2147483527 &&\n            parser.getToken() !== 130) {\n            parser.report(160);\n        }\n        return parser.getToken() === 130\n            ? parsePrivateIdentifier(parser, context, privateScope, 0)\n            : parseIdentifier(parser, context);\n    }\n    function parseUpdateExpressionPrefixed(parser, context, privateScope, inNew, isLHS, start) {\n        if (inNew)\n            parser.report(56);\n        if (!isLHS)\n            parser.report(0);\n        const token = parser.getToken();\n        nextToken(parser, context | 32);\n        const arg = parseLeftHandSideExpression(parser, context, privateScope, 0, 0, 1);\n        if (parser.assignable & 2) {\n            parser.report(55);\n        }\n        parser.assignable = 2;\n        return parser.finishNode({\n            type: 'UpdateExpression',\n            argument: arg,\n            operator: KeywordDescTable[token & 255],\n            prefix: true,\n        }, start);\n    }\n    function parsePrimaryExpression(parser, context, privateScope, kind, inNew, canAssign, inGroup, isLHS, start) {\n        if ((parser.getToken() & 143360) === 143360) {\n            switch (parser.getToken()) {\n                case 209006:\n                    return parseAwaitExpressionOrIdentifier(parser, context, privateScope, inNew, inGroup, start);\n                case 241771:\n                    return parseYieldExpressionOrIdentifier(parser, context, privateScope, inGroup, canAssign, start);\n                case 209005:\n                    return parseAsyncExpression(parser, context, privateScope, inGroup, isLHS, canAssign, inNew, start);\n            }\n            const { tokenValue } = parser;\n            const token = parser.getToken();\n            const expr = parseIdentifier(parser, context | 64);\n            if (parser.getToken() === 10) {\n                if (!isLHS)\n                    parser.report(0);\n                classifyIdentifier(parser, context, token);\n                if ((token & 36864) === 36864) {\n                    parser.flags |= 256;\n                }\n                return parseArrowFromIdentifier(parser, context, privateScope, tokenValue, expr, inNew, canAssign, 0, start);\n            }\n            if (context & 16 &&\n                !(context & 32768) &&\n                !(context & 8192) &&\n                parser.tokenValue === 'arguments')\n                parser.report(130);\n            if ((token & 255) === (241737 & 255)) {\n                if (context & 1)\n                    parser.report(113);\n                if (kind & (8 | 16))\n                    parser.report(100);\n            }\n            parser.assignable =\n                context & 1 && (token & 537079808) === 537079808\n                    ? 2\n                    : 1;\n            return expr;\n        }\n        if ((parser.getToken() & 134217728) === 134217728) {\n            return parseLiteral(parser, context);\n        }\n        switch (parser.getToken()) {\n            case 33619993:\n            case 33619994:\n                return parseUpdateExpressionPrefixed(parser, context, privateScope, inNew, isLHS, start);\n            case 16863276:\n            case 16842798:\n            case 16842799:\n            case 25233968:\n            case 25233969:\n            case 16863275:\n            case 16863277:\n                return parseUnaryExpression(parser, context, privateScope, isLHS, inGroup);\n            case 86104:\n                return parseFunctionExpression(parser, context, privateScope, 0, inGroup, start);\n            case 2162700:\n                return parseObjectLiteral(parser, context, privateScope, canAssign ? 0 : 1, inGroup);\n            case 69271571:\n                return parseArrayLiteral(parser, context, privateScope, canAssign ? 0 : 1, inGroup);\n            case 67174411:\n                return parseParenthesizedExpression(parser, context | 64, privateScope, canAssign, 1, 0, start);\n            case 86021:\n            case 86022:\n            case 86023:\n                return parseNullOrTrueOrFalseLiteral(parser, context);\n            case 86111:\n                return parseThisExpression(parser, context);\n            case 65540:\n                return parseRegExpLiteral(parser, context);\n            case 132:\n            case 86094:\n                return parseClassExpression(parser, context, privateScope, inGroup, start);\n            case 86109:\n                return parseSuperExpression(parser, context);\n            case 67174409:\n                return parseTemplateLiteral(parser, context);\n            case 67174408:\n                return parseTemplate(parser, context, privateScope);\n            case 86107:\n                return parseNewExpression(parser, context, privateScope, inGroup);\n            case 134283388:\n                return parseBigIntLiteral(parser, context);\n            case 130:\n                return parsePrivateIdentifier(parser, context, privateScope, 0);\n            case 86106:\n                return parseImportCallOrMetaExpression(parser, context, privateScope, inNew, inGroup, start);\n            case 8456256:\n                if (parser.options.jsx)\n                    return parseJSXRootElementOrFragment(parser, context, privateScope, 0, parser.tokenStart);\n            default:\n                if (isValidIdentifier(context, parser.getToken()))\n                    return parseIdentifierOrArrow(parser, context, privateScope);\n                parser.report(30, KeywordDescTable[parser.getToken() & 255]);\n        }\n    }\n    function parseImportCallOrMetaExpression(parser, context, privateScope, inNew, inGroup, start) {\n        let expr = parseIdentifier(parser, context);\n        if (parser.getToken() === 67108877) {\n            return parseImportMetaExpression(parser, context, expr, start);\n        }\n        if (inNew)\n            parser.report(142);\n        expr = parseImportExpression(parser, context, privateScope, inGroup, start);\n        parser.assignable = 2;\n        return parseMemberOrUpdateExpression(parser, context, privateScope, expr, inGroup, 0, start);\n    }\n    function parseImportMetaExpression(parser, context, meta, start) {\n        if ((context & 2) === 0)\n            parser.report(169);\n        nextToken(parser, context);\n        const token = parser.getToken();\n        if (token !== 209030 && parser.tokenValue !== 'meta') {\n            parser.report(174);\n        }\n        else if (token & -2147483648) {\n            parser.report(175);\n        }\n        parser.assignable = 2;\n        return parser.finishNode({\n            type: 'MetaProperty',\n            meta,\n            property: parseIdentifier(parser, context),\n        }, start);\n    }\n    function parseImportExpression(parser, context, privateScope, inGroup, start) {\n        consume(parser, context | 32, 67174411);\n        if (parser.getToken() === 14)\n            parser.report(143);\n        const source = parseExpression(parser, context, privateScope, 1, inGroup, parser.tokenStart);\n        let options = null;\n        if (parser.getToken() === 18) {\n            consume(parser, context, 18);\n            if (parser.getToken() !== 16) {\n                const expContext = (context | 131072) ^ 131072;\n                options = parseExpression(parser, expContext, privateScope, 1, inGroup, parser.tokenStart);\n            }\n            consumeOpt(parser, context, 18);\n        }\n        const node = {\n            type: 'ImportExpression',\n            source,\n            options,\n        };\n        consume(parser, context, 16);\n        return parser.finishNode(node, start);\n    }\n    function parseImportAttributes(parser, context) {\n        if (!consumeOpt(parser, context, 20579))\n            return [];\n        consume(parser, context, 2162700);\n        const attributes = [];\n        const keysContent = new Set();\n        while (parser.getToken() !== 1074790415) {\n            const start = parser.tokenStart;\n            const key = parseIdentifierOrStringLiteral(parser, context);\n            consume(parser, context, 21);\n            const value = parseStringLiteral(parser, context);\n            const keyContent = key.type === 'Literal' ? key.value : key.name;\n            if (keysContent.has(keyContent)) {\n                parser.report(145, `${keyContent}`);\n            }\n            keysContent.add(keyContent);\n            attributes.push(parser.finishNode({\n                type: 'ImportAttribute',\n                key,\n                value,\n            }, start));\n            if (parser.getToken() !== 1074790415) {\n                consume(parser, context, 18);\n            }\n        }\n        consume(parser, context, 1074790415);\n        return attributes;\n    }\n    function parseStringLiteral(parser, context) {\n        if (parser.getToken() === 134283267) {\n            return parseLiteral(parser, context);\n        }\n        else {\n            parser.report(30, KeywordDescTable[parser.getToken() & 255]);\n        }\n    }\n    function parseIdentifierOrStringLiteral(parser, context) {\n        if (parser.getToken() === 134283267) {\n            return parseLiteral(parser, context);\n        }\n        else if (parser.getToken() & 143360) {\n            return parseIdentifier(parser, context);\n        }\n        else {\n            parser.report(30, KeywordDescTable[parser.getToken() & 255]);\n        }\n    }\n    function validateStringWellFormed(parser, str) {\n        const len = str.length;\n        for (let i = 0; i < len; i++) {\n            const code = str.charCodeAt(i);\n            if ((code & 0xfc00) !== 55296)\n                continue;\n            if (code > 56319 || ++i >= len || (str.charCodeAt(i) & 0xfc00) !== 56320) {\n                parser.report(171, JSON.stringify(str.charAt(i--)));\n            }\n        }\n    }\n    function parseModuleExportName(parser, context) {\n        if (parser.getToken() === 134283267) {\n            validateStringWellFormed(parser, parser.tokenValue);\n            return parseLiteral(parser, context);\n        }\n        else if (parser.getToken() & 143360) {\n            return parseIdentifier(parser, context);\n        }\n        else {\n            parser.report(30, KeywordDescTable[parser.getToken() & 255]);\n        }\n    }\n    function parseBigIntLiteral(parser, context) {\n        const { tokenRaw, tokenValue, tokenStart } = parser;\n        nextToken(parser, context);\n        parser.assignable = 2;\n        const node = {\n            type: 'Literal',\n            value: tokenValue,\n            bigint: String(tokenValue),\n        };\n        if (parser.options.raw) {\n            node.raw = tokenRaw;\n        }\n        return parser.finishNode(node, tokenStart);\n    }\n    function parseTemplateLiteral(parser, context) {\n        parser.assignable = 2;\n        const { tokenValue, tokenRaw, tokenStart } = parser;\n        consume(parser, context, 67174409);\n        const quasis = [parseTemplateElement(parser, tokenValue, tokenRaw, tokenStart, true)];\n        return parser.finishNode({\n            type: 'TemplateLiteral',\n            expressions: [],\n            quasis,\n        }, tokenStart);\n    }\n    function parseTemplate(parser, context, privateScope) {\n        context = (context | 131072) ^ 131072;\n        const { tokenValue, tokenRaw, tokenStart } = parser;\n        consume(parser, (context & -65) | 32, 67174408);\n        const quasis = [parseTemplateElement(parser, tokenValue, tokenRaw, tokenStart, false)];\n        const expressions = [\n            parseExpressions(parser, context & -65, privateScope, 0, 1, parser.tokenStart),\n        ];\n        if (parser.getToken() !== 1074790415)\n            parser.report(83);\n        while (parser.setToken(scanTemplateTail(parser, context), true) !== 67174409) {\n            const { tokenValue, tokenRaw, tokenStart } = parser;\n            consume(parser, (context & -65) | 32, 67174408);\n            quasis.push(parseTemplateElement(parser, tokenValue, tokenRaw, tokenStart, false));\n            expressions.push(parseExpressions(parser, context, privateScope, 0, 1, parser.tokenStart));\n            if (parser.getToken() !== 1074790415)\n                parser.report(83);\n        }\n        {\n            const { tokenValue, tokenRaw, tokenStart } = parser;\n            consume(parser, context, 67174409);\n            quasis.push(parseTemplateElement(parser, tokenValue, tokenRaw, tokenStart, true));\n        }\n        return parser.finishNode({\n            type: 'TemplateLiteral',\n            expressions,\n            quasis,\n        }, tokenStart);\n    }\n    function parseTemplateElement(parser, cooked, raw, start, tail) {\n        const node = parser.finishNode({\n            type: 'TemplateElement',\n            value: {\n                cooked,\n                raw,\n            },\n            tail,\n        }, start);\n        const tailSize = tail ? 1 : 2;\n        if (parser.options.ranges) {\n            node.start += 1;\n            node.range[0] += 1;\n            node.end -= tailSize;\n            node.range[1] -= tailSize;\n        }\n        if (parser.options.loc) {\n            node.loc.start.column += 1;\n            node.loc.end.column -= tailSize;\n        }\n        return node;\n    }\n    function parseSpreadElement(parser, context, privateScope) {\n        const start = parser.tokenStart;\n        context = (context | 131072) ^ 131072;\n        consume(parser, context | 32, 14);\n        const argument = parseExpression(parser, context, privateScope, 1, 0, parser.tokenStart);\n        parser.assignable = 1;\n        return parser.finishNode({\n            type: 'SpreadElement',\n            argument,\n        }, start);\n    }\n    function parseArguments(parser, context, privateScope, inGroup) {\n        nextToken(parser, context | 32);\n        const args = [];\n        if (parser.getToken() === 16) {\n            nextToken(parser, context | 64);\n            return args;\n        }\n        while (parser.getToken() !== 16) {\n            if (parser.getToken() === 14) {\n                args.push(parseSpreadElement(parser, context, privateScope));\n            }\n            else {\n                args.push(parseExpression(parser, context, privateScope, 1, inGroup, parser.tokenStart));\n            }\n            if (parser.getToken() !== 18)\n                break;\n            nextToken(parser, context | 32);\n            if (parser.getToken() === 16)\n                break;\n        }\n        consume(parser, context | 64, 16);\n        return args;\n    }\n    function parseIdentifier(parser, context) {\n        const { tokenValue, tokenStart } = parser;\n        const allowRegex = tokenValue === 'await' && (parser.getToken() & -2147483648) === 0;\n        nextToken(parser, context | (allowRegex ? 32 : 0));\n        return parser.finishNode({\n            type: 'Identifier',\n            name: tokenValue,\n        }, tokenStart);\n    }\n    function parseLiteral(parser, context) {\n        const { tokenValue, tokenRaw, tokenStart } = parser;\n        if (parser.getToken() === 134283388) {\n            return parseBigIntLiteral(parser, context);\n        }\n        nextToken(parser, context);\n        parser.assignable = 2;\n        return parser.finishNode(parser.options.raw\n            ? {\n                type: 'Literal',\n                value: tokenValue,\n                raw: tokenRaw,\n            }\n            : {\n                type: 'Literal',\n                value: tokenValue,\n            }, tokenStart);\n    }\n    function parseNullOrTrueOrFalseLiteral(parser, context) {\n        const start = parser.tokenStart;\n        const raw = KeywordDescTable[parser.getToken() & 255];\n        const value = parser.getToken() === 86023 ? null : raw === 'true';\n        nextToken(parser, context);\n        parser.assignable = 2;\n        return parser.finishNode(parser.options.raw\n            ? {\n                type: 'Literal',\n                value,\n                raw,\n            }\n            : {\n                type: 'Literal',\n                value,\n            }, start);\n    }\n    function parseThisExpression(parser, context) {\n        const { tokenStart } = parser;\n        nextToken(parser, context);\n        parser.assignable = 2;\n        return parser.finishNode({\n            type: 'ThisExpression',\n        }, tokenStart);\n    }\n    function parseFunctionDeclaration(parser, context, scope, privateScope, origin, allowGen, flags, isAsync, start) {\n        nextToken(parser, context | 32);\n        const isGenerator = allowGen ? optionalBit(parser, context, 8391476) : 0;\n        let id = null;\n        let funcNameToken;\n        let functionScope = scope ? parser.createScope() : void 0;\n        if (parser.getToken() === 67174411) {\n            if ((flags & 1) === 0)\n                parser.report(39, 'Function');\n        }\n        else {\n            const kind = origin & 4 && ((context & 8) === 0 || (context & 2) === 0)\n                ? 4\n                : 64 | (isAsync ? 1024 : 0) | (isGenerator ? 1024 : 0);\n            validateFunctionName(parser, context, parser.getToken());\n            if (scope) {\n                if (kind & 4) {\n                    scope.addVarName(context, parser.tokenValue, kind);\n                }\n                else {\n                    scope.addBlockName(context, parser.tokenValue, kind, origin);\n                }\n                functionScope = functionScope?.createChildScope(128);\n                if (flags) {\n                    if (flags & 2) {\n                        parser.declareUnboundVariable(parser.tokenValue);\n                    }\n                }\n            }\n            funcNameToken = parser.getToken();\n            if (parser.getToken() & 143360) {\n                id = parseIdentifier(parser, context);\n            }\n            else {\n                parser.report(30, KeywordDescTable[parser.getToken() & 255]);\n            }\n        }\n        {\n            const modifierFlags = 256 |\n                512 |\n                1024 |\n                2048 |\n                8192 |\n                16384;\n            context =\n                ((context | modifierFlags) ^ modifierFlags) |\n                    65536 |\n                    (isAsync ? 2048 : 0) |\n                    (isGenerator ? 1024 : 0) |\n                    (isGenerator ? 0 : 262144);\n        }\n        functionScope = functionScope?.createChildScope(256);\n        const params = parseFormalParametersOrFormalList(parser, (context | 8192) & -524289, functionScope, privateScope, 0, 1);\n        const modifierFlags = 8 | 4 | 128 | 524288;\n        const body = parseFunctionBody(parser, ((context | modifierFlags) ^ modifierFlags) | 32768 | 4096, functionScope?.createChildScope(64), privateScope, 8, funcNameToken, functionScope);\n        return parser.finishNode({\n            type: 'FunctionDeclaration',\n            id,\n            params,\n            body,\n            async: isAsync === 1,\n            generator: isGenerator === 1,\n        }, start);\n    }\n    function parseFunctionExpression(parser, context, privateScope, isAsync, inGroup, start) {\n        nextToken(parser, context | 32);\n        const isGenerator = optionalBit(parser, context, 8391476);\n        const generatorAndAsyncFlags = (isAsync ? 2048 : 0) | (isGenerator ? 1024 : 0);\n        let id = null;\n        let funcNameToken;\n        let scope = parser.createScopeIfLexical();\n        const modifierFlags = 256 |\n            512 |\n            1024 |\n            2048 |\n            8192 |\n            16384 |\n            524288;\n        if (parser.getToken() & 143360) {\n            validateFunctionName(parser, ((context | modifierFlags) ^ modifierFlags) | generatorAndAsyncFlags, parser.getToken());\n            scope = scope?.createChildScope(128);\n            funcNameToken = parser.getToken();\n            id = parseIdentifier(parser, context);\n        }\n        context =\n            ((context | modifierFlags) ^ modifierFlags) |\n                65536 |\n                generatorAndAsyncFlags |\n                (isGenerator ? 0 : 262144);\n        scope = scope?.createChildScope(256);\n        const params = parseFormalParametersOrFormalList(parser, (context | 8192) & -524289, scope, privateScope, inGroup, 1);\n        const body = parseFunctionBody(parser, (context & -131229) |\n            32768 |\n            4096, scope?.createChildScope(64), privateScope, 0, funcNameToken, scope);\n        parser.assignable = 2;\n        return parser.finishNode({\n            type: 'FunctionExpression',\n            id,\n            params,\n            body,\n            async: isAsync === 1,\n            generator: isGenerator === 1,\n        }, start);\n    }\n    function parseArrayLiteral(parser, context, privateScope, skipInitializer, inGroup) {\n        const expr = parseArrayExpressionOrPattern(parser, context, void 0, privateScope, skipInitializer, inGroup, 0, 2, 0);\n        if (parser.destructible & 64) {\n            parser.report(63);\n        }\n        if (parser.destructible & 8) {\n            parser.report(62);\n        }\n        return expr;\n    }\n    function parseArrayExpressionOrPattern(parser, context, scope, privateScope, skipInitializer, inGroup, isPattern, kind, origin) {\n        const { tokenStart: start } = parser;\n        nextToken(parser, context | 32);\n        const elements = [];\n        let destructible = 0;\n        context = (context | 131072) ^ 131072;\n        while (parser.getToken() !== 20) {\n            if (consumeOpt(parser, context | 32, 18)) {\n                elements.push(null);\n            }\n            else {\n                let left;\n                const { tokenStart, tokenValue } = parser;\n                const token = parser.getToken();\n                if (token & 143360) {\n                    left = parsePrimaryExpression(parser, context, privateScope, kind, 0, 1, inGroup, 1, tokenStart);\n                    if (parser.getToken() === 1077936155) {\n                        if (parser.assignable & 2)\n                            parser.report(26);\n                        nextToken(parser, context | 32);\n                        scope?.addVarOrBlock(context, tokenValue, kind, origin);\n                        const right = parseExpression(parser, context, privateScope, 1, inGroup, parser.tokenStart);\n                        left = parser.finishNode(isPattern\n                            ? {\n                                type: 'AssignmentPattern',\n                                left,\n                                right,\n                            }\n                            : {\n                                type: 'AssignmentExpression',\n                                operator: '=',\n                                left,\n                                right,\n                            }, tokenStart);\n                        destructible |=\n                            parser.destructible & 256\n                                ? 256\n                                : 0 | (parser.destructible & 128)\n                                    ? 128\n                                    : 0;\n                    }\n                    else if (parser.getToken() === 18 || parser.getToken() === 20) {\n                        if (parser.assignable & 2) {\n                            destructible |= 16;\n                        }\n                        else {\n                            scope?.addVarOrBlock(context, tokenValue, kind, origin);\n                        }\n                        destructible |=\n                            parser.destructible & 256\n                                ? 256\n                                : 0 | (parser.destructible & 128)\n                                    ? 128\n                                    : 0;\n                    }\n                    else {\n                        destructible |=\n                            kind & 1\n                                ? 32\n                                : (kind & 2) === 0\n                                    ? 16\n                                    : 0;\n                        left = parseMemberOrUpdateExpression(parser, context, privateScope, left, inGroup, 0, tokenStart);\n                        if (parser.getToken() !== 18 && parser.getToken() !== 20) {\n                            if (parser.getToken() !== 1077936155)\n                                destructible |= 16;\n                            left = parseAssignmentExpression(parser, context, privateScope, inGroup, isPattern, tokenStart, left);\n                        }\n                        else if (parser.getToken() !== 1077936155) {\n                            destructible |=\n                                parser.assignable & 2\n                                    ? 16\n                                    : 32;\n                        }\n                    }\n                }\n                else if (token & 2097152) {\n                    left =\n                        parser.getToken() === 2162700\n                            ? parseObjectLiteralOrPattern(parser, context, scope, privateScope, 0, inGroup, isPattern, kind, origin)\n                            : parseArrayExpressionOrPattern(parser, context, scope, privateScope, 0, inGroup, isPattern, kind, origin);\n                    destructible |= parser.destructible;\n                    parser.assignable =\n                        parser.destructible & 16\n                            ? 2\n                            : 1;\n                    if (parser.getToken() === 18 || parser.getToken() === 20) {\n                        if (parser.assignable & 2) {\n                            destructible |= 16;\n                        }\n                    }\n                    else if (parser.destructible & 8) {\n                        parser.report(71);\n                    }\n                    else {\n                        left = parseMemberOrUpdateExpression(parser, context, privateScope, left, inGroup, 0, tokenStart);\n                        destructible = parser.assignable & 2 ? 16 : 0;\n                        if (parser.getToken() !== 18 && parser.getToken() !== 20) {\n                            left = parseAssignmentExpression(parser, context, privateScope, inGroup, isPattern, tokenStart, left);\n                        }\n                        else if (parser.getToken() !== 1077936155) {\n                            destructible |=\n                                parser.assignable & 2\n                                    ? 16\n                                    : 32;\n                        }\n                    }\n                }\n                else if (token === 14) {\n                    left = parseSpreadOrRestElement(parser, context, scope, privateScope, 20, kind, origin, 0, inGroup, isPattern);\n                    destructible |= parser.destructible;\n                    if (parser.getToken() !== 18 && parser.getToken() !== 20)\n                        parser.report(30, KeywordDescTable[parser.getToken() & 255]);\n                }\n                else {\n                    left = parseLeftHandSideExpression(parser, context, privateScope, 1, 0, 1);\n                    if (parser.getToken() !== 18 && parser.getToken() !== 20) {\n                        left = parseAssignmentExpression(parser, context, privateScope, inGroup, isPattern, tokenStart, left);\n                        if ((kind & (2 | 1)) === 0 && token === 67174411)\n                            destructible |= 16;\n                    }\n                    else if (parser.assignable & 2) {\n                        destructible |= 16;\n                    }\n                    else if (token === 67174411) {\n                        destructible |=\n                            parser.assignable & 1 && kind & (2 | 1)\n                                ? 32\n                                : 16;\n                    }\n                }\n                elements.push(left);\n                if (consumeOpt(parser, context | 32, 18)) {\n                    if (parser.getToken() === 20)\n                        break;\n                }\n                else\n                    break;\n            }\n        }\n        consume(parser, context, 20);\n        const node = parser.finishNode({\n            type: isPattern ? 'ArrayPattern' : 'ArrayExpression',\n            elements,\n        }, start);\n        if (!skipInitializer && parser.getToken() & 4194304) {\n            return parseArrayOrObjectAssignmentPattern(parser, context, privateScope, destructible, inGroup, isPattern, start, node);\n        }\n        parser.destructible = destructible;\n        return node;\n    }\n    function parseArrayOrObjectAssignmentPattern(parser, context, privateScope, destructible, inGroup, isPattern, start, node) {\n        if (parser.getToken() !== 1077936155)\n            parser.report(26);\n        nextToken(parser, context | 32);\n        if (destructible & 16)\n            parser.report(26);\n        if (!isPattern)\n            reinterpretToPattern(parser, node);\n        const { tokenStart } = parser;\n        const right = parseExpression(parser, context, privateScope, 1, inGroup, tokenStart);\n        parser.destructible =\n            ((destructible | 64 | 8) ^\n                (8 | 64)) |\n                (parser.destructible & 128 ? 128 : 0) |\n                (parser.destructible & 256 ? 256 : 0);\n        return parser.finishNode(isPattern\n            ? {\n                type: 'AssignmentPattern',\n                left: node,\n                right,\n            }\n            : {\n                type: 'AssignmentExpression',\n                left: node,\n                operator: '=',\n                right,\n            }, start);\n    }\n    function parseSpreadOrRestElement(parser, context, scope, privateScope, closingToken, kind, origin, isAsync, inGroup, isPattern) {\n        const { tokenStart: start } = parser;\n        nextToken(parser, context | 32);\n        let argument = null;\n        let destructible = 0;\n        const { tokenValue, tokenStart } = parser;\n        let token = parser.getToken();\n        if (token & 143360) {\n            parser.assignable = 1;\n            argument = parsePrimaryExpression(parser, context, privateScope, kind, 0, 1, inGroup, 1, tokenStart);\n            token = parser.getToken();\n            argument = parseMemberOrUpdateExpression(parser, context, privateScope, argument, inGroup, 0, tokenStart);\n            if (parser.getToken() !== 18 && parser.getToken() !== closingToken) {\n                if (parser.assignable & 2 && parser.getToken() === 1077936155)\n                    parser.report(71);\n                destructible |= 16;\n                argument = parseAssignmentExpression(parser, context, privateScope, inGroup, isPattern, tokenStart, argument);\n            }\n            if (parser.assignable & 2) {\n                destructible |= 16;\n            }\n            else if (token === closingToken || token === 18) {\n                scope?.addVarOrBlock(context, tokenValue, kind, origin);\n            }\n            else {\n                destructible |= 32;\n            }\n            destructible |= parser.destructible & 128 ? 128 : 0;\n        }\n        else if (token === closingToken) {\n            parser.report(41);\n        }\n        else if (token & 2097152) {\n            argument =\n                parser.getToken() === 2162700\n                    ? parseObjectLiteralOrPattern(parser, context, scope, privateScope, 1, inGroup, isPattern, kind, origin)\n                    : parseArrayExpressionOrPattern(parser, context, scope, privateScope, 1, inGroup, isPattern, kind, origin);\n            token = parser.getToken();\n            if (token !== 1077936155 && token !== closingToken && token !== 18) {\n                if (parser.destructible & 8)\n                    parser.report(71);\n                argument = parseMemberOrUpdateExpression(parser, context, privateScope, argument, inGroup, 0, tokenStart);\n                destructible |= parser.assignable & 2 ? 16 : 0;\n                if ((parser.getToken() & 4194304) === 4194304) {\n                    if (parser.getToken() !== 1077936155)\n                        destructible |= 16;\n                    argument = parseAssignmentExpression(parser, context, privateScope, inGroup, isPattern, tokenStart, argument);\n                }\n                else {\n                    if ((parser.getToken() & 8388608) === 8388608) {\n                        argument = parseBinaryExpression(parser, context, privateScope, 1, tokenStart, 4, token, argument);\n                    }\n                    if (consumeOpt(parser, context | 32, 22)) {\n                        argument = parseConditionalExpression(parser, context, privateScope, argument, tokenStart);\n                    }\n                    destructible |=\n                        parser.assignable & 2\n                            ? 16\n                            : 32;\n                }\n            }\n            else {\n                destructible |=\n                    closingToken === 1074790415 && token !== 1077936155\n                        ? 16\n                        : parser.destructible;\n            }\n        }\n        else {\n            destructible |= 32;\n            argument = parseLeftHandSideExpression(parser, context, privateScope, 1, inGroup, 1);\n            const { tokenStart } = parser;\n            const token = parser.getToken();\n            if (token === 1077936155) {\n                if (parser.assignable & 2)\n                    parser.report(26);\n                argument = parseAssignmentExpression(parser, context, privateScope, inGroup, isPattern, tokenStart, argument);\n                destructible |= 16;\n            }\n            else {\n                if (token === 18) {\n                    destructible |= 16;\n                }\n                else if (token !== closingToken) {\n                    argument = parseAssignmentExpression(parser, context, privateScope, inGroup, isPattern, tokenStart, argument);\n                }\n                destructible |=\n                    parser.assignable & 1 ? 32 : 16;\n            }\n            parser.destructible = destructible;\n            if (parser.getToken() !== closingToken && parser.getToken() !== 18)\n                parser.report(161);\n            return parser.finishNode({\n                type: isPattern ? 'RestElement' : 'SpreadElement',\n                argument: argument,\n            }, start);\n        }\n        if (parser.getToken() !== closingToken) {\n            if (kind & 1)\n                destructible |= isAsync ? 16 : 32;\n            if (consumeOpt(parser, context | 32, 1077936155)) {\n                if (destructible & 16)\n                    parser.report(26);\n                reinterpretToPattern(parser, argument);\n                const right = parseExpression(parser, context, privateScope, 1, inGroup, parser.tokenStart);\n                argument = parser.finishNode(isPattern\n                    ? {\n                        type: 'AssignmentPattern',\n                        left: argument,\n                        right,\n                    }\n                    : {\n                        type: 'AssignmentExpression',\n                        left: argument,\n                        operator: '=',\n                        right,\n                    }, tokenStart);\n                destructible = 16;\n            }\n            else {\n                destructible |= 16;\n            }\n        }\n        parser.destructible = destructible;\n        return parser.finishNode({\n            type: isPattern ? 'RestElement' : 'SpreadElement',\n            argument: argument,\n        }, start);\n    }\n    function parseMethodDefinition(parser, context, privateScope, kind, inGroup, start) {\n        const modifierFlags = 1024 |\n            2048 |\n            8192 |\n            ((kind & 64) === 0 ? 512 | 16384 : 0);\n        context =\n            ((context | modifierFlags) ^ modifierFlags) |\n                (kind & 8 ? 1024 : 0) |\n                (kind & 16 ? 2048 : 0) |\n                (kind & 64 ? 16384 : 0) |\n                256 |\n                32768 |\n                65536;\n        let scope = parser.createScopeIfLexical(256);\n        const params = parseMethodFormals(parser, (context | 8192) & -524289, scope, privateScope, kind, 1, inGroup);\n        scope = scope?.createChildScope(64);\n        const body = parseFunctionBody(parser, (context & -655373) |\n            32768 |\n            4096, scope, privateScope, 0, void 0, scope?.parent);\n        return parser.finishNode({\n            type: 'FunctionExpression',\n            params,\n            body,\n            async: (kind & 16) > 0,\n            generator: (kind & 8) > 0,\n            id: null,\n        }, start);\n    }\n    function parseObjectLiteral(parser, context, privateScope, skipInitializer, inGroup) {\n        const expr = parseObjectLiteralOrPattern(parser, context, void 0, privateScope, skipInitializer, inGroup, 0, 2, 0);\n        if (parser.destructible & 64) {\n            parser.report(63);\n        }\n        if (parser.destructible & 8) {\n            parser.report(62);\n        }\n        return expr;\n    }\n    function parseObjectLiteralOrPattern(parser, context, scope, privateScope, skipInitializer, inGroup, isPattern, kind, origin) {\n        const { tokenStart: start } = parser;\n        nextToken(parser, context);\n        const properties = [];\n        let destructible = 0;\n        let prototypeCount = 0;\n        context = (context | 131072) ^ 131072;\n        while (parser.getToken() !== 1074790415) {\n            const { tokenValue, tokenStart } = parser;\n            const token = parser.getToken();\n            if (token === 14) {\n                properties.push(parseSpreadOrRestElement(parser, context, scope, privateScope, 1074790415, kind, origin, 0, inGroup, isPattern));\n            }\n            else {\n                let state = 0;\n                let key = null;\n                let value;\n                if (parser.getToken() & 143360 ||\n                    parser.getToken() === -2147483528 ||\n                    parser.getToken() === -2147483527) {\n                    if (parser.getToken() === -2147483527)\n                        destructible |= 16;\n                    key = parseIdentifier(parser, context);\n                    if (parser.getToken() === 18 ||\n                        parser.getToken() === 1074790415 ||\n                        parser.getToken() === 1077936155) {\n                        state |= 4;\n                        if (context & 1 && (token & 537079808) === 537079808) {\n                            destructible |= 16;\n                        }\n                        else {\n                            validateBindingIdentifier(parser, context, kind, token, 0);\n                        }\n                        scope?.addVarOrBlock(context, tokenValue, kind, origin);\n                        if (consumeOpt(parser, context | 32, 1077936155)) {\n                            destructible |= 8;\n                            const right = parseExpression(parser, context, privateScope, 1, inGroup, parser.tokenStart);\n                            destructible |=\n                                parser.destructible & 256\n                                    ? 256\n                                    : 0 | (parser.destructible & 128)\n                                        ? 128\n                                        : 0;\n                            value = parser.finishNode({\n                                type: 'AssignmentPattern',\n                                left: parser.options.uniqueKeyInPattern ? Object.assign({}, key) : key,\n                                right,\n                            }, tokenStart);\n                        }\n                        else {\n                            destructible |=\n                                (token === 209006 ? 128 : 0) |\n                                    (token === -2147483528 ? 16 : 0);\n                            value = parser.options.uniqueKeyInPattern ? Object.assign({}, key) : key;\n                        }\n                    }\n                    else if (consumeOpt(parser, context | 32, 21)) {\n                        const { tokenStart } = parser;\n                        if (tokenValue === '__proto__')\n                            prototypeCount++;\n                        if (parser.getToken() & 143360) {\n                            const tokenAfterColon = parser.getToken();\n                            const valueAfterColon = parser.tokenValue;\n                            value = parsePrimaryExpression(parser, context, privateScope, kind, 0, 1, inGroup, 1, tokenStart);\n                            const token = parser.getToken();\n                            value = parseMemberOrUpdateExpression(parser, context, privateScope, value, inGroup, 0, tokenStart);\n                            if (parser.getToken() === 18 || parser.getToken() === 1074790415) {\n                                if (token === 1077936155 || token === 1074790415 || token === 18) {\n                                    destructible |= parser.destructible & 128 ? 128 : 0;\n                                    if (parser.assignable & 2) {\n                                        destructible |= 16;\n                                    }\n                                    else if ((tokenAfterColon & 143360) === 143360) {\n                                        scope?.addVarOrBlock(context, valueAfterColon, kind, origin);\n                                    }\n                                }\n                                else {\n                                    destructible |=\n                                        parser.assignable & 1\n                                            ? 32\n                                            : 16;\n                                }\n                            }\n                            else if ((parser.getToken() & 4194304) === 4194304) {\n                                if (parser.assignable & 2) {\n                                    destructible |= 16;\n                                }\n                                else if (token !== 1077936155) {\n                                    destructible |= 32;\n                                }\n                                else {\n                                    scope?.addVarOrBlock(context, valueAfterColon, kind, origin);\n                                }\n                                value = parseAssignmentExpression(parser, context, privateScope, inGroup, isPattern, tokenStart, value);\n                            }\n                            else {\n                                destructible |= 16;\n                                if ((parser.getToken() & 8388608) === 8388608) {\n                                    value = parseBinaryExpression(parser, context, privateScope, 1, tokenStart, 4, token, value);\n                                }\n                                if (consumeOpt(parser, context | 32, 22)) {\n                                    value = parseConditionalExpression(parser, context, privateScope, value, tokenStart);\n                                }\n                            }\n                        }\n                        else if ((parser.getToken() & 2097152) === 2097152) {\n                            value =\n                                parser.getToken() === 69271571\n                                    ? parseArrayExpressionOrPattern(parser, context, scope, privateScope, 0, inGroup, isPattern, kind, origin)\n                                    : parseObjectLiteralOrPattern(parser, context, scope, privateScope, 0, inGroup, isPattern, kind, origin);\n                            destructible = parser.destructible;\n                            parser.assignable =\n                                destructible & 16 ? 2 : 1;\n                            if (parser.getToken() === 18 || parser.getToken() === 1074790415) {\n                                if (parser.assignable & 2)\n                                    destructible |= 16;\n                            }\n                            else if (parser.destructible & 8) {\n                                parser.report(71);\n                            }\n                            else {\n                                value = parseMemberOrUpdateExpression(parser, context, privateScope, value, inGroup, 0, tokenStart);\n                                destructible = parser.assignable & 2 ? 16 : 0;\n                                if ((parser.getToken() & 4194304) === 4194304) {\n                                    value = parseAssignmentExpressionOrPattern(parser, context, privateScope, inGroup, isPattern, tokenStart, value);\n                                }\n                                else {\n                                    if ((parser.getToken() & 8388608) === 8388608) {\n                                        value = parseBinaryExpression(parser, context, privateScope, 1, tokenStart, 4, token, value);\n                                    }\n                                    if (consumeOpt(parser, context | 32, 22)) {\n                                        value = parseConditionalExpression(parser, context, privateScope, value, tokenStart);\n                                    }\n                                    destructible |=\n                                        parser.assignable & 2\n                                            ? 16\n                                            : 32;\n                                }\n                            }\n                        }\n                        else {\n                            value = parseLeftHandSideExpression(parser, context, privateScope, 1, inGroup, 1);\n                            destructible |=\n                                parser.assignable & 1\n                                    ? 32\n                                    : 16;\n                            if (parser.getToken() === 18 || parser.getToken() === 1074790415) {\n                                if (parser.assignable & 2)\n                                    destructible |= 16;\n                            }\n                            else {\n                                value = parseMemberOrUpdateExpression(parser, context, privateScope, value, inGroup, 0, tokenStart);\n                                destructible = parser.assignable & 2 ? 16 : 0;\n                                if (parser.getToken() !== 18 && token !== 1074790415) {\n                                    if (parser.getToken() !== 1077936155)\n                                        destructible |= 16;\n                                    value = parseAssignmentExpression(parser, context, privateScope, inGroup, isPattern, tokenStart, value);\n                                }\n                            }\n                        }\n                    }\n                    else if (parser.getToken() === 69271571) {\n                        destructible |= 16;\n                        if (token === 209005)\n                            state |= 16;\n                        state |=\n                            (token === 209008\n                                ? 256\n                                : token === 209009\n                                    ? 512\n                                    : 1) | 2;\n                        key = parseComputedPropertyName(parser, context, privateScope, inGroup);\n                        destructible |= parser.assignable;\n                        value = parseMethodDefinition(parser, context, privateScope, state, inGroup, parser.tokenStart);\n                    }\n                    else if (parser.getToken() & 143360) {\n                        destructible |= 16;\n                        if (token === -2147483528)\n                            parser.report(95);\n                        if (token === 209005) {\n                            if (parser.flags & 1)\n                                parser.report(132);\n                            state |= 16 | 1;\n                        }\n                        else if (token === 209008) {\n                            state |= 256;\n                        }\n                        else if (token === 209009) {\n                            state |= 512;\n                        }\n                        else {\n                            parser.report(0);\n                        }\n                        key = parseIdentifier(parser, context);\n                        value = parseMethodDefinition(parser, context, privateScope, state, inGroup, parser.tokenStart);\n                    }\n                    else if (parser.getToken() === 67174411) {\n                        destructible |= 16;\n                        state |= 1;\n                        value = parseMethodDefinition(parser, context, privateScope, state, inGroup, parser.tokenStart);\n                    }\n                    else if (parser.getToken() === 8391476) {\n                        destructible |= 16;\n                        if (token === 209008) {\n                            parser.report(42);\n                        }\n                        else if (token === 209009) {\n                            parser.report(43);\n                        }\n                        else if (token !== 209005) {\n                            parser.report(30, KeywordDescTable[8391476 & 255]);\n                        }\n                        nextToken(parser, context);\n                        state |=\n                            8 | 1 | (token === 209005 ? 16 : 0);\n                        if (parser.getToken() & 143360) {\n                            key = parseIdentifier(parser, context);\n                        }\n                        else if ((parser.getToken() & 134217728) === 134217728) {\n                            key = parseLiteral(parser, context);\n                        }\n                        else if (parser.getToken() === 69271571) {\n                            state |= 2;\n                            key = parseComputedPropertyName(parser, context, privateScope, inGroup);\n                            destructible |= parser.assignable;\n                        }\n                        else {\n                            parser.report(30, KeywordDescTable[parser.getToken() & 255]);\n                        }\n                        value = parseMethodDefinition(parser, context, privateScope, state, inGroup, parser.tokenStart);\n                    }\n                    else if ((parser.getToken() & 134217728) === 134217728) {\n                        if (token === 209005)\n                            state |= 16;\n                        state |=\n                            token === 209008\n                                ? 256\n                                : token === 209009\n                                    ? 512\n                                    : 1;\n                        destructible |= 16;\n                        key = parseLiteral(parser, context);\n                        value = parseMethodDefinition(parser, context, privateScope, state, inGroup, parser.tokenStart);\n                    }\n                    else {\n                        parser.report(133);\n                    }\n                }\n                else if ((parser.getToken() & 134217728) === 134217728) {\n                    key = parseLiteral(parser, context);\n                    if (parser.getToken() === 21) {\n                        consume(parser, context | 32, 21);\n                        const { tokenStart } = parser;\n                        if (tokenValue === '__proto__')\n                            prototypeCount++;\n                        if (parser.getToken() & 143360) {\n                            value = parsePrimaryExpression(parser, context, privateScope, kind, 0, 1, inGroup, 1, tokenStart);\n                            const { tokenValue: valueAfterColon } = parser;\n                            const token = parser.getToken();\n                            value = parseMemberOrUpdateExpression(parser, context, privateScope, value, inGroup, 0, tokenStart);\n                            if (parser.getToken() === 18 || parser.getToken() === 1074790415) {\n                                if (token === 1077936155 || token === 1074790415 || token === 18) {\n                                    if (parser.assignable & 2) {\n                                        destructible |= 16;\n                                    }\n                                    else {\n                                        scope?.addVarOrBlock(context, valueAfterColon, kind, origin);\n                                    }\n                                }\n                                else {\n                                    destructible |=\n                                        parser.assignable & 1\n                                            ? 32\n                                            : 16;\n                                }\n                            }\n                            else if (parser.getToken() === 1077936155) {\n                                if (parser.assignable & 2)\n                                    destructible |= 16;\n                                value = parseAssignmentExpression(parser, context, privateScope, inGroup, isPattern, tokenStart, value);\n                            }\n                            else {\n                                destructible |= 16;\n                                value = parseAssignmentExpression(parser, context, privateScope, inGroup, isPattern, tokenStart, value);\n                            }\n                        }\n                        else if ((parser.getToken() & 2097152) === 2097152) {\n                            value =\n                                parser.getToken() === 69271571\n                                    ? parseArrayExpressionOrPattern(parser, context, scope, privateScope, 0, inGroup, isPattern, kind, origin)\n                                    : parseObjectLiteralOrPattern(parser, context, scope, privateScope, 0, inGroup, isPattern, kind, origin);\n                            destructible = parser.destructible;\n                            parser.assignable =\n                                destructible & 16 ? 2 : 1;\n                            if (parser.getToken() === 18 || parser.getToken() === 1074790415) {\n                                if (parser.assignable & 2) {\n                                    destructible |= 16;\n                                }\n                            }\n                            else if ((parser.destructible & 8) !== 8) {\n                                value = parseMemberOrUpdateExpression(parser, context, privateScope, value, inGroup, 0, tokenStart);\n                                destructible = parser.assignable & 2 ? 16 : 0;\n                                if ((parser.getToken() & 4194304) === 4194304) {\n                                    value = parseAssignmentExpressionOrPattern(parser, context, privateScope, inGroup, isPattern, tokenStart, value);\n                                }\n                                else {\n                                    if ((parser.getToken() & 8388608) === 8388608) {\n                                        value = parseBinaryExpression(parser, context, privateScope, 1, tokenStart, 4, token, value);\n                                    }\n                                    if (consumeOpt(parser, context | 32, 22)) {\n                                        value = parseConditionalExpression(parser, context, privateScope, value, tokenStart);\n                                    }\n                                    destructible |=\n                                        parser.assignable & 2\n                                            ? 16\n                                            : 32;\n                                }\n                            }\n                        }\n                        else {\n                            value = parseLeftHandSideExpression(parser, context, privateScope, 1, 0, 1);\n                            destructible |=\n                                parser.assignable & 1\n                                    ? 32\n                                    : 16;\n                            if (parser.getToken() === 18 || parser.getToken() === 1074790415) {\n                                if (parser.assignable & 2) {\n                                    destructible |= 16;\n                                }\n                            }\n                            else {\n                                value = parseMemberOrUpdateExpression(parser, context, privateScope, value, inGroup, 0, tokenStart);\n                                destructible = parser.assignable & 1 ? 0 : 16;\n                                if (parser.getToken() !== 18 && parser.getToken() !== 1074790415) {\n                                    if (parser.getToken() !== 1077936155)\n                                        destructible |= 16;\n                                    value = parseAssignmentExpression(parser, context, privateScope, inGroup, isPattern, tokenStart, value);\n                                }\n                            }\n                        }\n                    }\n                    else if (parser.getToken() === 67174411) {\n                        state |= 1;\n                        value = parseMethodDefinition(parser, context, privateScope, state, inGroup, parser.tokenStart);\n                        destructible = parser.assignable | 16;\n                    }\n                    else {\n                        parser.report(134);\n                    }\n                }\n                else if (parser.getToken() === 69271571) {\n                    key = parseComputedPropertyName(parser, context, privateScope, inGroup);\n                    destructible |= parser.destructible & 256 ? 256 : 0;\n                    state |= 2;\n                    if (parser.getToken() === 21) {\n                        nextToken(parser, context | 32);\n                        const { tokenStart, tokenValue } = parser;\n                        const tokenAfterColon = parser.getToken();\n                        if (parser.getToken() & 143360) {\n                            value = parsePrimaryExpression(parser, context, privateScope, kind, 0, 1, inGroup, 1, tokenStart);\n                            const token = parser.getToken();\n                            value = parseMemberOrUpdateExpression(parser, context, privateScope, value, inGroup, 0, tokenStart);\n                            if ((parser.getToken() & 4194304) === 4194304) {\n                                destructible |=\n                                    parser.assignable & 2\n                                        ? 16\n                                        : token === 1077936155\n                                            ? 0\n                                            : 32;\n                                value = parseAssignmentExpressionOrPattern(parser, context, privateScope, inGroup, isPattern, tokenStart, value);\n                            }\n                            else if (parser.getToken() === 18 || parser.getToken() === 1074790415) {\n                                if (token === 1077936155 || token === 1074790415 || token === 18) {\n                                    if (parser.assignable & 2) {\n                                        destructible |= 16;\n                                    }\n                                    else if ((tokenAfterColon & 143360) === 143360) {\n                                        scope?.addVarOrBlock(context, tokenValue, kind, origin);\n                                    }\n                                }\n                                else {\n                                    destructible |=\n                                        parser.assignable & 1\n                                            ? 32\n                                            : 16;\n                                }\n                            }\n                            else {\n                                destructible |= 16;\n                                value = parseAssignmentExpression(parser, context, privateScope, inGroup, isPattern, tokenStart, value);\n                            }\n                        }\n                        else if ((parser.getToken() & 2097152) === 2097152) {\n                            value =\n                                parser.getToken() === 69271571\n                                    ? parseArrayExpressionOrPattern(parser, context, scope, privateScope, 0, inGroup, isPattern, kind, origin)\n                                    : parseObjectLiteralOrPattern(parser, context, scope, privateScope, 0, inGroup, isPattern, kind, origin);\n                            destructible = parser.destructible;\n                            parser.assignable =\n                                destructible & 16 ? 2 : 1;\n                            if (parser.getToken() === 18 || parser.getToken() === 1074790415) {\n                                if (parser.assignable & 2)\n                                    destructible |= 16;\n                            }\n                            else if (destructible & 8) {\n                                parser.report(62);\n                            }\n                            else {\n                                value = parseMemberOrUpdateExpression(parser, context, privateScope, value, inGroup, 0, tokenStart);\n                                destructible =\n                                    parser.assignable & 2 ? destructible | 16 : 0;\n                                if ((parser.getToken() & 4194304) === 4194304) {\n                                    if (parser.getToken() !== 1077936155)\n                                        destructible |= 16;\n                                    value = parseAssignmentExpressionOrPattern(parser, context, privateScope, inGroup, isPattern, tokenStart, value);\n                                }\n                                else {\n                                    if ((parser.getToken() & 8388608) === 8388608) {\n                                        value = parseBinaryExpression(parser, context, privateScope, 1, tokenStart, 4, token, value);\n                                    }\n                                    if (consumeOpt(parser, context | 32, 22)) {\n                                        value = parseConditionalExpression(parser, context, privateScope, value, tokenStart);\n                                    }\n                                    destructible |=\n                                        parser.assignable & 2\n                                            ? 16\n                                            : 32;\n                                }\n                            }\n                        }\n                        else {\n                            value = parseLeftHandSideExpression(parser, context, privateScope, 1, 0, 1);\n                            destructible |=\n                                parser.assignable & 1\n                                    ? 32\n                                    : 16;\n                            if (parser.getToken() === 18 || parser.getToken() === 1074790415) {\n                                if (parser.assignable & 2)\n                                    destructible |= 16;\n                            }\n                            else {\n                                value = parseMemberOrUpdateExpression(parser, context, privateScope, value, inGroup, 0, tokenStart);\n                                destructible = parser.assignable & 1 ? 0 : 16;\n                                if (parser.getToken() !== 18 && parser.getToken() !== 1074790415) {\n                                    if (parser.getToken() !== 1077936155)\n                                        destructible |= 16;\n                                    value = parseAssignmentExpression(parser, context, privateScope, inGroup, isPattern, tokenStart, value);\n                                }\n                            }\n                        }\n                    }\n                    else if (parser.getToken() === 67174411) {\n                        state |= 1;\n                        value = parseMethodDefinition(parser, context, privateScope, state, inGroup, parser.tokenStart);\n                        destructible = 16;\n                    }\n                    else {\n                        parser.report(44);\n                    }\n                }\n                else if (token === 8391476) {\n                    consume(parser, context | 32, 8391476);\n                    state |= 8;\n                    if (parser.getToken() & 143360) {\n                        const token = parser.getToken();\n                        key = parseIdentifier(parser, context);\n                        state |= 1;\n                        if (parser.getToken() === 67174411) {\n                            destructible |= 16;\n                            value = parseMethodDefinition(parser, context, privateScope, state, inGroup, parser.tokenStart);\n                        }\n                        else {\n                            throw new ParseError(parser.tokenStart, parser.currentLocation, token === 209005\n                                ? 46\n                                : token === 209008 || parser.getToken() === 209009\n                                    ? 45\n                                    : 47, KeywordDescTable[token & 255]);\n                        }\n                    }\n                    else if ((parser.getToken() & 134217728) === 134217728) {\n                        destructible |= 16;\n                        key = parseLiteral(parser, context);\n                        state |= 1;\n                        value = parseMethodDefinition(parser, context, privateScope, state, inGroup, parser.tokenStart);\n                    }\n                    else if (parser.getToken() === 69271571) {\n                        destructible |= 16;\n                        state |= 2 | 1;\n                        key = parseComputedPropertyName(parser, context, privateScope, inGroup);\n                        value = parseMethodDefinition(parser, context, privateScope, state, inGroup, parser.tokenStart);\n                    }\n                    else {\n                        parser.report(126);\n                    }\n                }\n                else {\n                    parser.report(30, KeywordDescTable[token & 255]);\n                }\n                destructible |= parser.destructible & 128 ? 128 : 0;\n                parser.destructible = destructible;\n                properties.push(parser.finishNode({\n                    type: 'Property',\n                    key: key,\n                    value,\n                    kind: !(state & 768) ? 'init' : state & 512 ? 'set' : 'get',\n                    computed: (state & 2) > 0,\n                    method: (state & 1) > 0,\n                    shorthand: (state & 4) > 0,\n                }, tokenStart));\n            }\n            destructible |= parser.destructible;\n            if (parser.getToken() !== 18)\n                break;\n            nextToken(parser, context);\n        }\n        consume(parser, context, 1074790415);\n        if (prototypeCount > 1)\n            destructible |= 64;\n        const node = parser.finishNode({\n            type: isPattern ? 'ObjectPattern' : 'ObjectExpression',\n            properties,\n        }, start);\n        if (!skipInitializer && parser.getToken() & 4194304) {\n            return parseArrayOrObjectAssignmentPattern(parser, context, privateScope, destructible, inGroup, isPattern, start, node);\n        }\n        parser.destructible = destructible;\n        return node;\n    }\n    function parseMethodFormals(parser, context, scope, privateScope, kind, type, inGroup) {\n        consume(parser, context, 67174411);\n        const params = [];\n        parser.flags = (parser.flags | 128) ^ 128;\n        if (parser.getToken() === 16) {\n            if (kind & 512) {\n                parser.report(37, 'Setter', 'one', '');\n            }\n            nextToken(parser, context);\n            return params;\n        }\n        if (kind & 256) {\n            parser.report(37, 'Getter', 'no', 's');\n        }\n        if (kind & 512 && parser.getToken() === 14) {\n            parser.report(38);\n        }\n        context = (context | 131072) ^ 131072;\n        let setterArgs = 0;\n        let isNonSimpleParameterList = 0;\n        while (parser.getToken() !== 18) {\n            let left = null;\n            const { tokenStart } = parser;\n            if (parser.getToken() & 143360) {\n                if ((context & 1) === 0) {\n                    if ((parser.getToken() & 36864) === 36864) {\n                        parser.flags |= 256;\n                    }\n                    if ((parser.getToken() & 537079808) === 537079808) {\n                        parser.flags |= 512;\n                    }\n                }\n                left = parseAndClassifyIdentifier(parser, context, scope, kind | 1, 0);\n            }\n            else {\n                if (parser.getToken() === 2162700) {\n                    left = parseObjectLiteralOrPattern(parser, context, scope, privateScope, 1, inGroup, 1, type, 0);\n                }\n                else if (parser.getToken() === 69271571) {\n                    left = parseArrayExpressionOrPattern(parser, context, scope, privateScope, 1, inGroup, 1, type, 0);\n                }\n                else if (parser.getToken() === 14) {\n                    left = parseSpreadOrRestElement(parser, context, scope, privateScope, 16, type, 0, 0, inGroup, 1);\n                }\n                isNonSimpleParameterList = 1;\n                if (parser.destructible & (32 | 16))\n                    parser.report(50);\n            }\n            if (parser.getToken() === 1077936155) {\n                nextToken(parser, context | 32);\n                isNonSimpleParameterList = 1;\n                const right = parseExpression(parser, context, privateScope, 1, 0, parser.tokenStart);\n                left = parser.finishNode({\n                    type: 'AssignmentPattern',\n                    left: left,\n                    right,\n                }, tokenStart);\n            }\n            setterArgs++;\n            params.push(left);\n            if (!consumeOpt(parser, context, 18))\n                break;\n            if (parser.getToken() === 16) {\n                break;\n            }\n        }\n        if (kind & 512 && setterArgs !== 1) {\n            parser.report(37, 'Setter', 'one', '');\n        }\n        scope?.reportScopeError();\n        if (isNonSimpleParameterList)\n            parser.flags |= 128;\n        consume(parser, context, 16);\n        return params;\n    }\n    function parseComputedPropertyName(parser, context, privateScope, inGroup) {\n        nextToken(parser, context | 32);\n        const key = parseExpression(parser, (context | 131072) ^ 131072, privateScope, 1, inGroup, parser.tokenStart);\n        consume(parser, context, 20);\n        return key;\n    }\n    function parseParenthesizedExpression(parser, context, privateScope, canAssign, kind, origin, start) {\n        parser.flags = (parser.flags | 128) ^ 128;\n        const parenthesesStart = parser.tokenStart;\n        nextToken(parser, context | 32 | 262144);\n        const scope = parser.createScopeIfLexical()?.createChildScope(512);\n        context = (context | 131072) ^ 131072;\n        if (consumeOpt(parser, context, 16)) {\n            return parseParenthesizedArrow(parser, context, scope, privateScope, [], canAssign, 0, start);\n        }\n        let destructible = 0;\n        parser.destructible &= -385;\n        let expr;\n        let expressions = [];\n        let isSequence = 0;\n        let isNonSimpleParameterList = 0;\n        let hasStrictReserved = 0;\n        const tokenAfterParenthesesStart = parser.tokenStart;\n        parser.assignable = 1;\n        while (parser.getToken() !== 16) {\n            const { tokenStart } = parser;\n            const token = parser.getToken();\n            if (token & 143360) {\n                scope?.addBlockName(context, parser.tokenValue, 1, 0);\n                if ((token & 537079808) === 537079808) {\n                    isNonSimpleParameterList = 1;\n                }\n                else if ((token & 36864) === 36864) {\n                    hasStrictReserved = 1;\n                }\n                expr = parsePrimaryExpression(parser, context, privateScope, kind, 0, 1, 1, 1, tokenStart);\n                if (parser.getToken() === 16 || parser.getToken() === 18) {\n                    if (parser.assignable & 2) {\n                        destructible |= 16;\n                        isNonSimpleParameterList = 1;\n                    }\n                }\n                else {\n                    if (parser.getToken() === 1077936155) {\n                        isNonSimpleParameterList = 1;\n                    }\n                    else {\n                        destructible |= 16;\n                    }\n                    expr = parseMemberOrUpdateExpression(parser, context, privateScope, expr, 1, 0, tokenStart);\n                    if (parser.getToken() !== 16 && parser.getToken() !== 18) {\n                        expr = parseAssignmentExpression(parser, context, privateScope, 1, 0, tokenStart, expr);\n                    }\n                }\n            }\n            else if ((token & 2097152) === 2097152) {\n                expr =\n                    token === 2162700\n                        ? parseObjectLiteralOrPattern(parser, context | 262144, scope, privateScope, 0, 1, 0, kind, origin)\n                        : parseArrayExpressionOrPattern(parser, context | 262144, scope, privateScope, 0, 1, 0, kind, origin);\n                destructible |= parser.destructible;\n                isNonSimpleParameterList = 1;\n                parser.assignable = 2;\n                if (parser.getToken() !== 16 && parser.getToken() !== 18) {\n                    if (destructible & 8)\n                        parser.report(122);\n                    expr = parseMemberOrUpdateExpression(parser, context, privateScope, expr, 0, 0, tokenStart);\n                    destructible |= 16;\n                    if (parser.getToken() !== 16 && parser.getToken() !== 18) {\n                        expr = parseAssignmentExpression(parser, context, privateScope, 0, 0, tokenStart, expr);\n                    }\n                }\n            }\n            else if (token === 14) {\n                expr = parseSpreadOrRestElement(parser, context, scope, privateScope, 16, kind, origin, 0, 1, 0);\n                if (parser.destructible & 16)\n                    parser.report(74);\n                isNonSimpleParameterList = 1;\n                if (isSequence && (parser.getToken() === 16 || parser.getToken() === 18)) {\n                    expressions.push(expr);\n                }\n                destructible |= 8;\n                break;\n            }\n            else {\n                destructible |= 16;\n                expr = parseExpression(parser, context, privateScope, 1, 1, tokenStart);\n                if (isSequence && (parser.getToken() === 16 || parser.getToken() === 18)) {\n                    expressions.push(expr);\n                }\n                if (parser.getToken() === 18) {\n                    if (!isSequence) {\n                        isSequence = 1;\n                        expressions = [expr];\n                    }\n                }\n                if (isSequence) {\n                    while (consumeOpt(parser, context | 32, 18)) {\n                        expressions.push(parseExpression(parser, context, privateScope, 1, 1, parser.tokenStart));\n                    }\n                    parser.assignable = 2;\n                    expr = parser.finishNode({\n                        type: 'SequenceExpression',\n                        expressions,\n                    }, tokenAfterParenthesesStart);\n                }\n                consume(parser, context, 16);\n                parser.destructible = destructible;\n                return parser.options.preserveParens\n                    ? parser.finishNode({\n                        type: 'ParenthesizedExpression',\n                        expression: expr,\n                    }, parenthesesStart)\n                    : expr;\n            }\n            if (isSequence && (parser.getToken() === 16 || parser.getToken() === 18)) {\n                expressions.push(expr);\n            }\n            if (!consumeOpt(parser, context | 32, 18))\n                break;\n            if (!isSequence) {\n                isSequence = 1;\n                expressions = [expr];\n            }\n            if (parser.getToken() === 16) {\n                destructible |= 8;\n                break;\n            }\n        }\n        if (isSequence) {\n            parser.assignable = 2;\n            expr = parser.finishNode({\n                type: 'SequenceExpression',\n                expressions,\n            }, tokenAfterParenthesesStart);\n        }\n        consume(parser, context, 16);\n        if (destructible & 16 && destructible & 8)\n            parser.report(151);\n        destructible |=\n            parser.destructible & 256\n                ? 256\n                : 0 | (parser.destructible & 128)\n                    ? 128\n                    : 0;\n        if (parser.getToken() === 10) {\n            if (destructible & (32 | 16))\n                parser.report(49);\n            if (context & (2048 | 2) && destructible & 128)\n                parser.report(31);\n            if (context & (1 | 1024) && destructible & 256) {\n                parser.report(32);\n            }\n            if (isNonSimpleParameterList)\n                parser.flags |= 128;\n            if (hasStrictReserved)\n                parser.flags |= 256;\n            return parseParenthesizedArrow(parser, context, scope, privateScope, isSequence ? expressions : [expr], canAssign, 0, start);\n        }\n        if (destructible & 64) {\n            parser.report(63);\n        }\n        if (destructible & 8) {\n            parser.report(144);\n        }\n        parser.destructible = ((parser.destructible | 256) ^ 256) | destructible;\n        return parser.options.preserveParens\n            ? parser.finishNode({\n                type: 'ParenthesizedExpression',\n                expression: expr,\n            }, parenthesesStart)\n            : expr;\n    }\n    function parseIdentifierOrArrow(parser, context, privateScope) {\n        const { tokenStart: start } = parser;\n        const { tokenValue } = parser;\n        let isNonSimpleParameterList = 0;\n        let hasStrictReserved = 0;\n        if ((parser.getToken() & 537079808) === 537079808) {\n            isNonSimpleParameterList = 1;\n        }\n        else if ((parser.getToken() & 36864) === 36864) {\n            hasStrictReserved = 1;\n        }\n        const expr = parseIdentifier(parser, context);\n        parser.assignable = 1;\n        if (parser.getToken() === 10) {\n            const scope = parser.options.lexical ? createArrowHeadParsingScope(parser, context, tokenValue) : undefined;\n            if (isNonSimpleParameterList)\n                parser.flags |= 128;\n            if (hasStrictReserved)\n                parser.flags |= 256;\n            return parseArrowFunctionExpression(parser, context, scope, privateScope, [expr], 0, start);\n        }\n        return expr;\n    }\n    function parseArrowFromIdentifier(parser, context, privateScope, value, expr, inNew, canAssign, isAsync, start) {\n        if (!canAssign)\n            parser.report(57);\n        if (inNew)\n            parser.report(51);\n        parser.flags &= -129;\n        const scope = parser.options.lexical ? createArrowHeadParsingScope(parser, context, value) : void 0;\n        return parseArrowFunctionExpression(parser, context, scope, privateScope, [expr], isAsync, start);\n    }\n    function parseParenthesizedArrow(parser, context, scope, privateScope, params, canAssign, isAsync, start) {\n        if (!canAssign)\n            parser.report(57);\n        for (let i = 0; i < params.length; ++i)\n            reinterpretToPattern(parser, params[i]);\n        return parseArrowFunctionExpression(parser, context, scope, privateScope, params, isAsync, start);\n    }\n    function parseArrowFunctionExpression(parser, context, scope, privateScope, params, isAsync, start) {\n        if (parser.flags & 1)\n            parser.report(48);\n        consume(parser, context | 32, 10);\n        const modifierFlags = 1024 | 2048 | 8192 | 524288;\n        context = ((context | modifierFlags) ^ modifierFlags) | (isAsync ? 2048 : 0);\n        const expression = parser.getToken() !== 2162700;\n        let body;\n        scope?.reportScopeError();\n        if (expression) {\n            parser.flags =\n                (parser.flags | 512 | 256 | 64 | 4096) ^\n                    (512 | 256 | 64 | 4096);\n            body = parseExpression(parser, context, privateScope, 1, 0, parser.tokenStart);\n        }\n        else {\n            scope = scope?.createChildScope(64);\n            const modifierFlags = 4 | 131072 | 8;\n            body = parseFunctionBody(parser, ((context | modifierFlags) ^ modifierFlags) | 4096, scope, privateScope, 16, void 0, void 0);\n            switch (parser.getToken()) {\n                case 69271571:\n                    if ((parser.flags & 1) === 0) {\n                        parser.report(116);\n                    }\n                    break;\n                case 67108877:\n                case 67174409:\n                case 22:\n                    parser.report(117);\n                case 67174411:\n                    if ((parser.flags & 1) === 0) {\n                        parser.report(116);\n                    }\n                    parser.flags |= 1024;\n                    break;\n            }\n            if ((parser.getToken() & 8388608) === 8388608 && (parser.flags & 1) === 0)\n                parser.report(30, KeywordDescTable[parser.getToken() & 255]);\n            if ((parser.getToken() & 33619968) === 33619968)\n                parser.report(125);\n        }\n        parser.assignable = 2;\n        return parser.finishNode({\n            type: 'ArrowFunctionExpression',\n            params,\n            body,\n            async: isAsync === 1,\n            expression,\n            generator: false,\n        }, start);\n    }\n    function parseFormalParametersOrFormalList(parser, context, scope, privateScope, inGroup, kind) {\n        consume(parser, context, 67174411);\n        parser.flags = (parser.flags | 128) ^ 128;\n        const params = [];\n        if (consumeOpt(parser, context, 16))\n            return params;\n        context = (context | 131072) ^ 131072;\n        let isNonSimpleParameterList = 0;\n        while (parser.getToken() !== 18) {\n            let left;\n            const { tokenStart } = parser;\n            const token = parser.getToken();\n            if (token & 143360) {\n                if ((context & 1) === 0) {\n                    if ((token & 36864) === 36864) {\n                        parser.flags |= 256;\n                    }\n                    if ((token & 537079808) === 537079808) {\n                        parser.flags |= 512;\n                    }\n                }\n                left = parseAndClassifyIdentifier(parser, context, scope, kind | 1, 0);\n            }\n            else {\n                if (token === 2162700) {\n                    left = parseObjectLiteralOrPattern(parser, context, scope, privateScope, 1, inGroup, 1, kind, 0);\n                }\n                else if (token === 69271571) {\n                    left = parseArrayExpressionOrPattern(parser, context, scope, privateScope, 1, inGroup, 1, kind, 0);\n                }\n                else if (token === 14) {\n                    left = parseSpreadOrRestElement(parser, context, scope, privateScope, 16, kind, 0, 0, inGroup, 1);\n                }\n                else {\n                    parser.report(30, KeywordDescTable[token & 255]);\n                }\n                isNonSimpleParameterList = 1;\n                if (parser.destructible & (32 | 16)) {\n                    parser.report(50);\n                }\n            }\n            if (parser.getToken() === 1077936155) {\n                nextToken(parser, context | 32);\n                isNonSimpleParameterList = 1;\n                const right = parseExpression(parser, context, privateScope, 1, inGroup, parser.tokenStart);\n                left = parser.finishNode({\n                    type: 'AssignmentPattern',\n                    left,\n                    right,\n                }, tokenStart);\n            }\n            params.push(left);\n            if (!consumeOpt(parser, context, 18))\n                break;\n            if (parser.getToken() === 16) {\n                break;\n            }\n        }\n        if (isNonSimpleParameterList)\n            parser.flags |= 128;\n        if (isNonSimpleParameterList || context & 1) {\n            scope?.reportScopeError();\n        }\n        consume(parser, context, 16);\n        return params;\n    }\n    function parseMemberExpressionNoCall(parser, context, privateScope, expr, inGroup, start) {\n        const token = parser.getToken();\n        if (token & 67108864) {\n            if (token === 67108877) {\n                nextToken(parser, context | 262144);\n                parser.assignable = 1;\n                const property = parsePropertyOrPrivatePropertyName(parser, context, privateScope);\n                return parseMemberExpressionNoCall(parser, context, privateScope, parser.finishNode({\n                    type: 'MemberExpression',\n                    object: expr,\n                    computed: false,\n                    property,\n                    optional: false,\n                }, start), 0, start);\n            }\n            else if (token === 69271571) {\n                nextToken(parser, context | 32);\n                const { tokenStart } = parser;\n                const property = parseExpressions(parser, context, privateScope, inGroup, 1, tokenStart);\n                consume(parser, context, 20);\n                parser.assignable = 1;\n                return parseMemberExpressionNoCall(parser, context, privateScope, parser.finishNode({\n                    type: 'MemberExpression',\n                    object: expr,\n                    computed: true,\n                    property,\n                    optional: false,\n                }, start), 0, start);\n            }\n            else if (token === 67174408 || token === 67174409) {\n                parser.assignable = 2;\n                return parseMemberExpressionNoCall(parser, context, privateScope, parser.finishNode({\n                    type: 'TaggedTemplateExpression',\n                    tag: expr,\n                    quasi: parser.getToken() === 67174408\n                        ? parseTemplate(parser, context | 64, privateScope)\n                        : parseTemplateLiteral(parser, context | 64),\n                }, start), 0, start);\n            }\n        }\n        return expr;\n    }\n    function parseNewExpression(parser, context, privateScope, inGroup) {\n        const { tokenStart: start } = parser;\n        const id = parseIdentifier(parser, context | 32);\n        const { tokenStart } = parser;\n        if (consumeOpt(parser, context, 67108877)) {\n            if (context & 65536 && parser.getToken() === 209029) {\n                parser.assignable = 2;\n                return parseMetaProperty(parser, context, id, start);\n            }\n            parser.report(94);\n        }\n        parser.assignable = 2;\n        if ((parser.getToken() & 16842752) === 16842752) {\n            parser.report(65, KeywordDescTable[parser.getToken() & 255]);\n        }\n        const expr = parsePrimaryExpression(parser, context, privateScope, 2, 1, 0, inGroup, 1, tokenStart);\n        context = (context | 131072) ^ 131072;\n        if (parser.getToken() === 67108990)\n            parser.report(168);\n        const callee = parseMemberExpressionNoCall(parser, context, privateScope, expr, inGroup, tokenStart);\n        parser.assignable = 2;\n        return parser.finishNode({\n            type: 'NewExpression',\n            callee,\n            arguments: parser.getToken() === 67174411 ? parseArguments(parser, context, privateScope, inGroup) : [],\n        }, start);\n    }\n    function parseMetaProperty(parser, context, meta, start) {\n        const property = parseIdentifier(parser, context);\n        return parser.finishNode({\n            type: 'MetaProperty',\n            meta,\n            property,\n        }, start);\n    }\n    function parseAsyncArrowAfterIdent(parser, context, privateScope, canAssign, start) {\n        if (parser.getToken() === 209006)\n            parser.report(31);\n        if (context & (1 | 1024) && parser.getToken() === 241771) {\n            parser.report(32);\n        }\n        classifyIdentifier(parser, context, parser.getToken());\n        if ((parser.getToken() & 36864) === 36864) {\n            parser.flags |= 256;\n        }\n        return parseArrowFromIdentifier(parser, (context & -524289) | 2048, privateScope, parser.tokenValue, parseIdentifier(parser, context), 0, canAssign, 1, start);\n    }\n    function parseAsyncArrowOrCallExpression(parser, context, privateScope, callee, canAssign, kind, origin, flags, start) {\n        nextToken(parser, context | 32);\n        const scope = parser.createScopeIfLexical()?.createChildScope(512);\n        context = (context | 131072) ^ 131072;\n        if (consumeOpt(parser, context, 16)) {\n            if (parser.getToken() === 10) {\n                if (flags & 1)\n                    parser.report(48);\n                return parseParenthesizedArrow(parser, context, scope, privateScope, [], canAssign, 1, start);\n            }\n            return parser.finishNode({\n                type: 'CallExpression',\n                callee,\n                arguments: [],\n                optional: false,\n            }, start);\n        }\n        let destructible = 0;\n        let expr = null;\n        let isNonSimpleParameterList = 0;\n        parser.destructible =\n            (parser.destructible | 256 | 128) ^\n                (256 | 128);\n        const params = [];\n        while (parser.getToken() !== 16) {\n            const { tokenStart } = parser;\n            const token = parser.getToken();\n            if (token & 143360) {\n                scope?.addBlockName(context, parser.tokenValue, kind, 0);\n                if ((token & 537079808) === 537079808) {\n                    parser.flags |= 512;\n                }\n                else if ((token & 36864) === 36864) {\n                    parser.flags |= 256;\n                }\n                expr = parsePrimaryExpression(parser, context, privateScope, kind, 0, 1, 1, 1, tokenStart);\n                if (parser.getToken() === 16 || parser.getToken() === 18) {\n                    if (parser.assignable & 2) {\n                        destructible |= 16;\n                        isNonSimpleParameterList = 1;\n                    }\n                }\n                else {\n                    if (parser.getToken() === 1077936155) {\n                        isNonSimpleParameterList = 1;\n                    }\n                    else {\n                        destructible |= 16;\n                    }\n                    expr = parseMemberOrUpdateExpression(parser, context, privateScope, expr, 1, 0, tokenStart);\n                    if (parser.getToken() !== 16 && parser.getToken() !== 18) {\n                        expr = parseAssignmentExpression(parser, context, privateScope, 1, 0, tokenStart, expr);\n                    }\n                }\n            }\n            else if (token & 2097152) {\n                expr =\n                    token === 2162700\n                        ? parseObjectLiteralOrPattern(parser, context, scope, privateScope, 0, 1, 0, kind, origin)\n                        : parseArrayExpressionOrPattern(parser, context, scope, privateScope, 0, 1, 0, kind, origin);\n                destructible |= parser.destructible;\n                isNonSimpleParameterList = 1;\n                if (parser.getToken() !== 16 && parser.getToken() !== 18) {\n                    if (destructible & 8)\n                        parser.report(122);\n                    expr = parseMemberOrUpdateExpression(parser, context, privateScope, expr, 0, 0, tokenStart);\n                    destructible |= 16;\n                    if ((parser.getToken() & 8388608) === 8388608) {\n                        expr = parseBinaryExpression(parser, context, privateScope, 1, start, 4, token, expr);\n                    }\n                    if (consumeOpt(parser, context | 32, 22)) {\n                        expr = parseConditionalExpression(parser, context, privateScope, expr, start);\n                    }\n                }\n            }\n            else if (token === 14) {\n                expr = parseSpreadOrRestElement(parser, context, scope, privateScope, 16, kind, origin, 1, 1, 0);\n                destructible |=\n                    (parser.getToken() === 16 ? 0 : 16) | parser.destructible;\n                isNonSimpleParameterList = 1;\n            }\n            else {\n                expr = parseExpression(parser, context, privateScope, 1, 0, tokenStart);\n                destructible = parser.assignable;\n                params.push(expr);\n                while (consumeOpt(parser, context | 32, 18)) {\n                    params.push(parseExpression(parser, context, privateScope, 1, 0, tokenStart));\n                }\n                destructible |= parser.assignable;\n                consume(parser, context, 16);\n                parser.destructible = destructible | 16;\n                parser.assignable = 2;\n                return parser.finishNode({\n                    type: 'CallExpression',\n                    callee,\n                    arguments: params,\n                    optional: false,\n                }, start);\n            }\n            params.push(expr);\n            if (!consumeOpt(parser, context | 32, 18))\n                break;\n        }\n        consume(parser, context, 16);\n        destructible |=\n            parser.destructible & 256\n                ? 256\n                : 0 | (parser.destructible & 128)\n                    ? 128\n                    : 0;\n        if (parser.getToken() === 10) {\n            if (destructible & (32 | 16))\n                parser.report(27);\n            if (parser.flags & 1 || flags & 1)\n                parser.report(48);\n            if (destructible & 128)\n                parser.report(31);\n            if (context & (1 | 1024) && destructible & 256)\n                parser.report(32);\n            if (isNonSimpleParameterList)\n                parser.flags |= 128;\n            return parseParenthesizedArrow(parser, context | 2048, scope, privateScope, params, canAssign, 1, start);\n        }\n        if (destructible & 64) {\n            parser.report(63);\n        }\n        if (destructible & 8) {\n            parser.report(62);\n        }\n        parser.assignable = 2;\n        return parser.finishNode({\n            type: 'CallExpression',\n            callee,\n            arguments: params,\n            optional: false,\n        }, start);\n    }\n    function parseRegExpLiteral(parser, context) {\n        const { tokenRaw, tokenRegExp, tokenValue, tokenStart } = parser;\n        nextToken(parser, context);\n        parser.assignable = 2;\n        const node = {\n            type: 'Literal',\n            value: tokenValue,\n            regex: tokenRegExp,\n        };\n        if (parser.options.raw) {\n            node.raw = tokenRaw;\n        }\n        return parser.finishNode(node, tokenStart);\n    }\n    function parseClassDeclaration(parser, context, scope, privateScope, flags) {\n        let start;\n        let decorators;\n        if (parser.leadingDecorators.decorators.length) {\n            if (parser.getToken() === 132) {\n                parser.report(30, '@');\n            }\n            start = parser.leadingDecorators.start;\n            decorators = [...parser.leadingDecorators.decorators];\n            parser.leadingDecorators.decorators.length = 0;\n        }\n        else {\n            start = parser.tokenStart;\n            decorators = parseDecorators(parser, context, privateScope);\n        }\n        context = (context | 16384 | 1) ^ 16384;\n        nextToken(parser, context);\n        let id = null;\n        let superClass = null;\n        const { tokenValue } = parser;\n        if (parser.getToken() & 4096 && parser.getToken() !== 20565) {\n            if (isStrictReservedWord(parser, context, parser.getToken())) {\n                parser.report(118);\n            }\n            if ((parser.getToken() & 537079808) === 537079808) {\n                parser.report(119);\n            }\n            if (scope) {\n                scope.addBlockName(context, tokenValue, 32, 0);\n                if (flags) {\n                    if (flags & 2) {\n                        parser.declareUnboundVariable(tokenValue);\n                    }\n                }\n            }\n            id = parseIdentifier(parser, context);\n        }\n        else {\n            if ((flags & 1) === 0)\n                parser.report(39, 'Class');\n        }\n        let inheritedContext = context;\n        if (consumeOpt(parser, context | 32, 20565)) {\n            superClass = parseLeftHandSideExpression(parser, context, privateScope, 0, 0, 0);\n            inheritedContext |= 512;\n        }\n        else {\n            inheritedContext = (inheritedContext | 512) ^ 512;\n        }\n        const body = parseClassBody(parser, inheritedContext, context, scope, privateScope, 2, 8, 0);\n        return parser.finishNode({\n            type: 'ClassDeclaration',\n            id,\n            superClass,\n            body,\n            ...(parser.options.next ? { decorators } : null),\n        }, start);\n    }\n    function parseClassExpression(parser, context, privateScope, inGroup, start) {\n        let id = null;\n        let superClass = null;\n        const decorators = parseDecorators(parser, context, privateScope);\n        context = (context | 1 | 16384) ^ 16384;\n        nextToken(parser, context);\n        if (parser.getToken() & 4096 && parser.getToken() !== 20565) {\n            if (isStrictReservedWord(parser, context, parser.getToken()))\n                parser.report(118);\n            if ((parser.getToken() & 537079808) === 537079808) {\n                parser.report(119);\n            }\n            id = parseIdentifier(parser, context);\n        }\n        let inheritedContext = context;\n        if (consumeOpt(parser, context | 32, 20565)) {\n            superClass = parseLeftHandSideExpression(parser, context, privateScope, 0, inGroup, 0);\n            inheritedContext |= 512;\n        }\n        else {\n            inheritedContext = (inheritedContext | 512) ^ 512;\n        }\n        const body = parseClassBody(parser, inheritedContext, context, void 0, privateScope, 2, 0, inGroup);\n        parser.assignable = 2;\n        return parser.finishNode({\n            type: 'ClassExpression',\n            id,\n            superClass,\n            body,\n            ...(parser.options.next ? { decorators } : null),\n        }, start);\n    }\n    function parseDecorators(parser, context, privateScope) {\n        const list = [];\n        if (parser.options.next) {\n            while (parser.getToken() === 132) {\n                list.push(parseDecoratorList(parser, context, privateScope));\n            }\n        }\n        return list;\n    }\n    function parseDecoratorList(parser, context, privateScope) {\n        const start = parser.tokenStart;\n        nextToken(parser, context | 32);\n        let expression = parsePrimaryExpression(parser, context, privateScope, 2, 0, 1, 0, 1, start);\n        expression = parseMemberOrUpdateExpression(parser, context, privateScope, expression, 0, 0, parser.tokenStart);\n        return parser.finishNode({\n            type: 'Decorator',\n            expression,\n        }, start);\n    }\n    function parseClassBody(parser, context, inheritedContext, scope, parentScope, kind, origin, inGroup) {\n        const { tokenStart } = parser;\n        const privateScope = parser.createPrivateScopeIfLexical(parentScope);\n        consume(parser, context | 32, 2162700);\n        const modifierFlags = 131072 | 524288;\n        context = (context | modifierFlags) ^ modifierFlags;\n        const hasConstr = parser.flags & 32;\n        parser.flags = (parser.flags | 32) ^ 32;\n        const body = [];\n        while (parser.getToken() !== 1074790415) {\n            const decoratorStart = parser.tokenStart;\n            const decorators = parseDecorators(parser, context, privateScope);\n            if (decorators.length > 0 && parser.tokenValue === 'constructor') {\n                parser.report(109);\n            }\n            if (parser.getToken() === 1074790415)\n                parser.report(108);\n            if (consumeOpt(parser, context, 1074790417)) {\n                if (decorators.length > 0)\n                    parser.report(120);\n                continue;\n            }\n            body.push(parseClassElementList(parser, context, scope, privateScope, inheritedContext, kind, decorators, 0, inGroup, decorators.length > 0 ? decoratorStart : parser.tokenStart));\n        }\n        consume(parser, origin & 8 ? context | 32 : context, 1074790415);\n        privateScope?.validatePrivateIdentifierRefs();\n        parser.flags = (parser.flags & -33) | hasConstr;\n        return parser.finishNode({\n            type: 'ClassBody',\n            body,\n        }, tokenStart);\n    }\n    function parseClassElementList(parser, context, scope, privateScope, inheritedContext, type, decorators, isStatic, inGroup, start) {\n        let kind = isStatic ? 32 : 0;\n        let key = null;\n        const token = parser.getToken();\n        if (token & (143360 | 36864) || token === -2147483528) {\n            key = parseIdentifier(parser, context);\n            switch (token) {\n                case 36970:\n                    if (!isStatic &&\n                        parser.getToken() !== 67174411 &&\n                        (parser.getToken() & 1048576) !== 1048576 &&\n                        parser.getToken() !== 1077936155) {\n                        return parseClassElementList(parser, context, scope, privateScope, inheritedContext, type, decorators, 1, inGroup, start);\n                    }\n                    break;\n                case 209005:\n                    if (parser.getToken() !== 67174411 && (parser.flags & 1) === 0) {\n                        if ((parser.getToken() & 1073741824) === 1073741824) {\n                            return parsePropertyDefinition(parser, context, privateScope, key, kind, decorators, start);\n                        }\n                        kind |= 16 | (optionalBit(parser, context, 8391476) ? 8 : 0);\n                    }\n                    break;\n                case 209008:\n                    if (parser.getToken() !== 67174411) {\n                        if ((parser.getToken() & 1073741824) === 1073741824) {\n                            return parsePropertyDefinition(parser, context, privateScope, key, kind, decorators, start);\n                        }\n                        kind |= 256;\n                    }\n                    break;\n                case 209009:\n                    if (parser.getToken() !== 67174411) {\n                        if ((parser.getToken() & 1073741824) === 1073741824) {\n                            return parsePropertyDefinition(parser, context, privateScope, key, kind, decorators, start);\n                        }\n                        kind |= 512;\n                    }\n                    break;\n                case 12402:\n                    if (parser.getToken() !== 67174411 && (parser.flags & 1) === 0) {\n                        if ((parser.getToken() & 1073741824) === 1073741824) {\n                            return parsePropertyDefinition(parser, context, privateScope, key, kind, decorators, start);\n                        }\n                        if (parser.options.next)\n                            kind |= 1024;\n                    }\n                    break;\n            }\n        }\n        else if (token === 69271571) {\n            kind |= 2;\n            key = parseComputedPropertyName(parser, inheritedContext, privateScope, inGroup);\n        }\n        else if ((token & 134217728) === 134217728) {\n            key = parseLiteral(parser, context);\n        }\n        else if (token === 8391476) {\n            kind |= 8;\n            nextToken(parser, context);\n        }\n        else if (parser.getToken() === 130) {\n            kind |= 8192;\n            key = parsePrivateIdentifier(parser, context | 16, privateScope, 768);\n        }\n        else if ((parser.getToken() & 1073741824) === 1073741824) {\n            kind |= 128;\n        }\n        else if (isStatic && token === 2162700) {\n            return parseStaticBlock(parser, context | 16, scope, privateScope, start);\n        }\n        else if (token === -2147483527) {\n            key = parseIdentifier(parser, context);\n            if (parser.getToken() !== 67174411)\n                parser.report(30, KeywordDescTable[parser.getToken() & 255]);\n        }\n        else {\n            parser.report(30, KeywordDescTable[parser.getToken() & 255]);\n        }\n        if (kind & (8 | 16 | 768 | 1024)) {\n            if (parser.getToken() & 143360 ||\n                parser.getToken() === -2147483528 ||\n                parser.getToken() === -2147483527) {\n                key = parseIdentifier(parser, context);\n            }\n            else if ((parser.getToken() & 134217728) === 134217728) {\n                key = parseLiteral(parser, context);\n            }\n            else if (parser.getToken() === 69271571) {\n                kind |= 2;\n                key = parseComputedPropertyName(parser, context, privateScope, 0);\n            }\n            else if (parser.getToken() === 130) {\n                kind |= 8192;\n                key = parsePrivateIdentifier(parser, context, privateScope, kind);\n            }\n            else\n                parser.report(135);\n        }\n        if ((kind & 2) === 0) {\n            if (parser.tokenValue === 'constructor') {\n                if ((parser.getToken() & 1073741824) === 1073741824) {\n                    parser.report(129);\n                }\n                else if ((kind & 32) === 0 && parser.getToken() === 67174411) {\n                    if (kind & (768 | 16 | 128 | 8)) {\n                        parser.report(53, 'accessor');\n                    }\n                    else if ((context & 512) === 0) {\n                        if (parser.flags & 32)\n                            parser.report(54);\n                        else\n                            parser.flags |= 32;\n                    }\n                }\n                kind |= 64;\n            }\n            else if ((kind & 8192) === 0 &&\n                kind & 32 &&\n                parser.tokenValue === 'prototype') {\n                parser.report(52);\n            }\n        }\n        if (kind & 1024 || (parser.getToken() !== 67174411 && (kind & 768) === 0)) {\n            return parsePropertyDefinition(parser, context, privateScope, key, kind, decorators, start);\n        }\n        const value = parseMethodDefinition(parser, context | 16, privateScope, kind, inGroup, parser.tokenStart);\n        return parser.finishNode({\n            type: 'MethodDefinition',\n            kind: (kind & 32) === 0 && kind & 64\n                ? 'constructor'\n                : kind & 256\n                    ? 'get'\n                    : kind & 512\n                        ? 'set'\n                        : 'method',\n            static: (kind & 32) > 0,\n            computed: (kind & 2) > 0,\n            key,\n            value,\n            ...(parser.options.next ? { decorators } : null),\n        }, start);\n    }\n    function parsePrivateIdentifier(parser, context, privateScope, kind) {\n        const { tokenStart } = parser;\n        nextToken(parser, context);\n        const { tokenValue } = parser;\n        if (tokenValue === 'constructor')\n            parser.report(128);\n        if (parser.options.lexical) {\n            if (!privateScope)\n                parser.report(4, tokenValue);\n            if (kind) {\n                privateScope.addPrivateIdentifier(tokenValue, kind);\n            }\n            else {\n                privateScope.addPrivateIdentifierRef(tokenValue);\n            }\n        }\n        nextToken(parser, context);\n        return parser.finishNode({\n            type: 'PrivateIdentifier',\n            name: tokenValue,\n        }, tokenStart);\n    }\n    function parsePropertyDefinition(parser, context, privateScope, key, state, decorators, start) {\n        let value = null;\n        if (state & 8)\n            parser.report(0);\n        if (parser.getToken() === 1077936155) {\n            nextToken(parser, context | 32);\n            const { tokenStart } = parser;\n            if (parser.getToken() === 537079927)\n                parser.report(119);\n            const modifierFlags = 1024 |\n                2048 |\n                8192 |\n                ((state & 64) === 0 ? 512 | 16384 : 0);\n            context =\n                ((context | modifierFlags) ^ modifierFlags) |\n                    (state & 8 ? 1024 : 0) |\n                    (state & 16 ? 2048 : 0) |\n                    (state & 64 ? 16384 : 0) |\n                    256 |\n                    65536;\n            value = parsePrimaryExpression(parser, context | 16, privateScope, 2, 0, 1, 0, 1, tokenStart);\n            if ((parser.getToken() & 1073741824) !== 1073741824 ||\n                (parser.getToken() & 4194304) === 4194304) {\n                value = parseMemberOrUpdateExpression(parser, context | 16, privateScope, value, 0, 0, tokenStart);\n                value = parseAssignmentExpression(parser, context | 16, privateScope, 0, 0, tokenStart, value);\n            }\n        }\n        matchOrInsertSemicolon(parser, context);\n        return parser.finishNode({\n            type: state & 1024 ? 'AccessorProperty' : 'PropertyDefinition',\n            key,\n            value,\n            static: (state & 32) > 0,\n            computed: (state & 2) > 0,\n            ...(parser.options.next ? { decorators } : null),\n        }, start);\n    }\n    function parseBindingPattern(parser, context, scope, privateScope, type, origin) {\n        if (parser.getToken() & 143360 ||\n            ((context & 1) === 0 && parser.getToken() === -2147483527))\n            return parseAndClassifyIdentifier(parser, context, scope, type, origin);\n        if ((parser.getToken() & 2097152) !== 2097152)\n            parser.report(30, KeywordDescTable[parser.getToken() & 255]);\n        const left = parser.getToken() === 69271571\n            ? parseArrayExpressionOrPattern(parser, context, scope, privateScope, 1, 0, 1, type, origin)\n            : parseObjectLiteralOrPattern(parser, context, scope, privateScope, 1, 0, 1, type, origin);\n        if (parser.destructible & 16)\n            parser.report(50);\n        if (parser.destructible & 32)\n            parser.report(50);\n        return left;\n    }\n    function parseAndClassifyIdentifier(parser, context, scope, kind, origin) {\n        const token = parser.getToken();\n        if (context & 1) {\n            if ((token & 537079808) === 537079808) {\n                parser.report(119);\n            }\n            else if ((token & 36864) === 36864 || token === -2147483527) {\n                parser.report(118);\n            }\n        }\n        if ((token & 20480) === 20480) {\n            parser.report(102);\n        }\n        if (token === 241771) {\n            if (context & 1024)\n                parser.report(32);\n            if (context & 2)\n                parser.report(111);\n        }\n        if ((token & 255) === (241737 & 255)) {\n            if (kind & (8 | 16))\n                parser.report(100);\n        }\n        if (token === 209006) {\n            if (context & 2048)\n                parser.report(176);\n            if (context & 2)\n                parser.report(110);\n        }\n        const { tokenValue, tokenStart: start } = parser;\n        nextToken(parser, context);\n        scope?.addVarOrBlock(context, tokenValue, kind, origin);\n        return parser.finishNode({\n            type: 'Identifier',\n            name: tokenValue,\n        }, start);\n    }\n    function parseJSXRootElementOrFragment(parser, context, privateScope, inJSXChild, start) {\n        if (!inJSXChild)\n            consume(parser, context, 8456256);\n        if (parser.getToken() === 8390721) {\n            const openingFragment = parseJSXOpeningFragment(parser, start);\n            const [children, closingFragment] = parseJSXChildrenAndClosingFragment(parser, context, privateScope, inJSXChild);\n            return parser.finishNode({\n                type: 'JSXFragment',\n                openingFragment,\n                children,\n                closingFragment,\n            }, start);\n        }\n        if (parser.getToken() === 8457014)\n            parser.report(30, KeywordDescTable[parser.getToken() & 255]);\n        let closingElement = null;\n        let children = [];\n        const openingElement = parseJSXOpeningElementOrSelfCloseElement(parser, context, privateScope, inJSXChild, start);\n        if (!openingElement.selfClosing) {\n            [children, closingElement] = parseJSXChildrenAndClosingElement(parser, context, privateScope, inJSXChild);\n            const close = isEqualTagName(closingElement.name);\n            if (isEqualTagName(openingElement.name) !== close)\n                parser.report(155, close);\n        }\n        return parser.finishNode({\n            type: 'JSXElement',\n            children,\n            openingElement,\n            closingElement,\n        }, start);\n    }\n    function parseJSXOpeningFragment(parser, start) {\n        nextJSXToken(parser);\n        return parser.finishNode({\n            type: 'JSXOpeningFragment',\n        }, start);\n    }\n    function parseJSXClosingElement(parser, context, inJSXChild, start) {\n        consume(parser, context, 8457014);\n        const name = parseJSXElementName(parser, context);\n        if (parser.getToken() !== 8390721) {\n            parser.report(25, KeywordDescTable[8390721 & 255]);\n        }\n        if (inJSXChild) {\n            nextJSXToken(parser);\n        }\n        else {\n            nextToken(parser, context);\n        }\n        return parser.finishNode({\n            type: 'JSXClosingElement',\n            name,\n        }, start);\n    }\n    function parseJSXClosingFragment(parser, context, inJSXChild, start) {\n        consume(parser, context, 8457014);\n        if (parser.getToken() !== 8390721) {\n            parser.report(25, KeywordDescTable[8390721 & 255]);\n        }\n        if (inJSXChild) {\n            nextJSXToken(parser);\n        }\n        else {\n            nextToken(parser, context);\n        }\n        return parser.finishNode({\n            type: 'JSXClosingFragment',\n        }, start);\n    }\n    function parseJSXChildrenAndClosingElement(parser, context, privateScope, inJSXChild) {\n        const children = [];\n        while (true) {\n            const child = parseJSXChildOrClosingElement(parser, context, privateScope, inJSXChild);\n            if (child.type === 'JSXClosingElement') {\n                return [children, child];\n            }\n            children.push(child);\n        }\n    }\n    function parseJSXChildrenAndClosingFragment(parser, context, privateScope, inJSXChild) {\n        const children = [];\n        while (true) {\n            const child = parseJSXChildOrClosingFragment(parser, context, privateScope, inJSXChild);\n            if (child.type === 'JSXClosingFragment') {\n                return [children, child];\n            }\n            children.push(child);\n        }\n    }\n    function parseJSXChildOrClosingElement(parser, context, privateScope, inJSXChild) {\n        if (parser.getToken() === 137)\n            return parseJSXText(parser, context);\n        if (parser.getToken() === 2162700)\n            return parseJSXExpressionContainer(parser, context, privateScope, 1, 0);\n        if (parser.getToken() === 8456256) {\n            const { tokenStart } = parser;\n            nextToken(parser, context);\n            if (parser.getToken() === 8457014)\n                return parseJSXClosingElement(parser, context, inJSXChild, tokenStart);\n            return parseJSXRootElementOrFragment(parser, context, privateScope, 1, tokenStart);\n        }\n        parser.report(0);\n    }\n    function parseJSXChildOrClosingFragment(parser, context, privateScope, inJSXChild) {\n        if (parser.getToken() === 137)\n            return parseJSXText(parser, context);\n        if (parser.getToken() === 2162700)\n            return parseJSXExpressionContainer(parser, context, privateScope, 1, 0);\n        if (parser.getToken() === 8456256) {\n            const { tokenStart } = parser;\n            nextToken(parser, context);\n            if (parser.getToken() === 8457014)\n                return parseJSXClosingFragment(parser, context, inJSXChild, tokenStart);\n            return parseJSXRootElementOrFragment(parser, context, privateScope, 1, tokenStart);\n        }\n        parser.report(0);\n    }\n    function parseJSXText(parser, context) {\n        const start = parser.tokenStart;\n        nextToken(parser, context);\n        const node = {\n            type: 'JSXText',\n            value: parser.tokenValue,\n        };\n        if (parser.options.raw) {\n            node.raw = parser.tokenRaw;\n        }\n        return parser.finishNode(node, start);\n    }\n    function parseJSXOpeningElementOrSelfCloseElement(parser, context, privateScope, inJSXChild, start) {\n        if ((parser.getToken() & 143360) !== 143360 &&\n            (parser.getToken() & 4096) !== 4096)\n            parser.report(0);\n        const tagName = parseJSXElementName(parser, context);\n        const attributes = parseJSXAttributes(parser, context, privateScope);\n        const selfClosing = parser.getToken() === 8457014;\n        if (selfClosing)\n            consume(parser, context, 8457014);\n        if (parser.getToken() !== 8390721) {\n            parser.report(25, KeywordDescTable[8390721 & 255]);\n        }\n        if (inJSXChild || !selfClosing) {\n            nextJSXToken(parser);\n        }\n        else {\n            nextToken(parser, context);\n        }\n        return parser.finishNode({\n            type: 'JSXOpeningElement',\n            name: tagName,\n            attributes,\n            selfClosing,\n        }, start);\n    }\n    function parseJSXElementName(parser, context) {\n        const { tokenStart } = parser;\n        rescanJSXIdentifier(parser);\n        let key = parseJSXIdentifier(parser, context);\n        if (parser.getToken() === 21)\n            return parseJSXNamespacedName(parser, context, key, tokenStart);\n        while (consumeOpt(parser, context, 67108877)) {\n            rescanJSXIdentifier(parser);\n            key = parseJSXMemberExpression(parser, context, key, tokenStart);\n        }\n        return key;\n    }\n    function parseJSXMemberExpression(parser, context, object, start) {\n        const property = parseJSXIdentifier(parser, context);\n        return parser.finishNode({\n            type: 'JSXMemberExpression',\n            object,\n            property,\n        }, start);\n    }\n    function parseJSXAttributes(parser, context, privateScope) {\n        const attributes = [];\n        while (parser.getToken() !== 8457014 &&\n            parser.getToken() !== 8390721 &&\n            parser.getToken() !== 1048576) {\n            attributes.push(parseJsxAttribute(parser, context, privateScope));\n        }\n        return attributes;\n    }\n    function parseJSXSpreadAttribute(parser, context, privateScope) {\n        const start = parser.tokenStart;\n        nextToken(parser, context);\n        consume(parser, context, 14);\n        const expression = parseExpression(parser, context, privateScope, 1, 0, parser.tokenStart);\n        consume(parser, context, 1074790415);\n        return parser.finishNode({\n            type: 'JSXSpreadAttribute',\n            argument: expression,\n        }, start);\n    }\n    function parseJsxAttribute(parser, context, privateScope) {\n        const { tokenStart } = parser;\n        if (parser.getToken() === 2162700)\n            return parseJSXSpreadAttribute(parser, context, privateScope);\n        rescanJSXIdentifier(parser);\n        let value = null;\n        let name = parseJSXIdentifier(parser, context);\n        if (parser.getToken() === 21) {\n            name = parseJSXNamespacedName(parser, context, name, tokenStart);\n        }\n        if (parser.getToken() === 1077936155) {\n            const token = scanJSXAttributeValue(parser, context);\n            switch (token) {\n                case 134283267:\n                    value = parseLiteral(parser, context);\n                    break;\n                case 8456256:\n                    value = parseJSXRootElementOrFragment(parser, context, privateScope, 0, parser.tokenStart);\n                    break;\n                case 2162700:\n                    value = parseJSXExpressionContainer(parser, context, privateScope, 0, 1);\n                    break;\n                default:\n                    parser.report(154);\n            }\n        }\n        return parser.finishNode({\n            type: 'JSXAttribute',\n            value,\n            name,\n        }, tokenStart);\n    }\n    function parseJSXNamespacedName(parser, context, namespace, start) {\n        consume(parser, context, 21);\n        const name = parseJSXIdentifier(parser, context);\n        return parser.finishNode({\n            type: 'JSXNamespacedName',\n            namespace,\n            name,\n        }, start);\n    }\n    function parseJSXExpressionContainer(parser, context, privateScope, inJSXChild, isAttr) {\n        const { tokenStart: start } = parser;\n        nextToken(parser, context | 32);\n        const { tokenStart } = parser;\n        if (parser.getToken() === 14)\n            return parseJSXSpreadChild(parser, context, privateScope, start);\n        let expression = null;\n        if (parser.getToken() === 1074790415) {\n            if (isAttr)\n                parser.report(157);\n            expression = parseJSXEmptyExpression(parser, {\n                index: parser.startIndex,\n                line: parser.startLine,\n                column: parser.startColumn,\n            });\n        }\n        else {\n            expression = parseExpression(parser, context, privateScope, 1, 0, tokenStart);\n        }\n        if (parser.getToken() !== 1074790415) {\n            parser.report(25, KeywordDescTable[1074790415 & 255]);\n        }\n        if (inJSXChild) {\n            nextJSXToken(parser);\n        }\n        else {\n            nextToken(parser, context);\n        }\n        return parser.finishNode({\n            type: 'JSXExpressionContainer',\n            expression,\n        }, start);\n    }\n    function parseJSXSpreadChild(parser, context, privateScope, start) {\n        consume(parser, context, 14);\n        const expression = parseExpression(parser, context, privateScope, 1, 0, parser.tokenStart);\n        consume(parser, context, 1074790415);\n        return parser.finishNode({\n            type: 'JSXSpreadChild',\n            expression,\n        }, start);\n    }\n    function parseJSXEmptyExpression(parser, start) {\n        return parser.finishNode({\n            type: 'JSXEmptyExpression',\n        }, start, parser.tokenStart);\n    }\n    function parseJSXIdentifier(parser, context) {\n        const start = parser.tokenStart;\n        if (!(parser.getToken() & 143360)) {\n            parser.report(30, KeywordDescTable[parser.getToken() & 255]);\n        }\n        const { tokenValue } = parser;\n        nextToken(parser, context);\n        return parser.finishNode({\n            type: 'JSXIdentifier',\n            name: tokenValue,\n        }, start);\n    }\n\n    var version$1 = \"6.1.4\";\n\n    const version = version$1;\n    function parseScript(source, options) {\n        return parseSource(source, options);\n    }\n    function parseModule(source, options) {\n        return parseSource(source, options, 1 | 2);\n    }\n    function parse(source, options) {\n        return parseSource(source, options);\n    }\n\n    exports.parse = parse;\n    exports.parseModule = parseModule;\n    exports.parseScript = parseScript;\n    exports.version = version;\n\n}));\n"
  },
  {
    "path": "app/src/main/assets/solver/yt.solver.core.js",
    "content": "/*!\n * SPDX-License-Identifier: Unlicense\n * This file was automatically generated by https://github.com/yt-dlp/ejs\n */\nvar jsc = (function (meriyah, astring) {\n  'use strict';\n  function matchesStructure(obj, structure) {\n    if (Array.isArray(structure)) {\n      if (!Array.isArray(obj)) {\n        return false;\n      }\n      return (\n        structure.length === obj.length &&\n        structure.every((value, index) => matchesStructure(obj[index], value))\n      );\n    }\n    if (typeof structure === 'object') {\n      if (!obj) {\n        return !structure;\n      }\n      if ('or' in structure) {\n        return structure.or.some((node) => matchesStructure(obj, node));\n      }\n      if ('anykey' in structure && Array.isArray(structure.anykey)) {\n        const haystack = Array.isArray(obj) ? obj : Object.values(obj);\n        return structure.anykey.every((value) =>\n          haystack.some((el) => matchesStructure(el, value)),\n        );\n      }\n      for (const [key, value] of Object.entries(structure)) {\n        if (!matchesStructure(obj[key], value)) {\n          return false;\n        }\n      }\n      return true;\n    }\n    return structure === obj;\n  }\n  function isOneOf(value, ...of) {\n    return of.includes(value);\n  }\n  function _optionalChain$2(ops) {\n    let lastAccessLHS = undefined;\n    let value = ops[0];\n    let i = 1;\n    while (i < ops.length) {\n      const op = ops[i];\n      const fn = ops[i + 1];\n      i += 2;\n      if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) {\n        return undefined;\n      }\n      if (op === 'access' || op === 'optionalAccess') {\n        lastAccessLHS = value;\n        value = fn(value);\n      } else if (op === 'call' || op === 'optionalCall') {\n        value = fn((...args) => value.call(lastAccessLHS, ...args));\n        lastAccessLHS = undefined;\n      }\n    }\n    return value;\n  }\n  const nsigExpression = {\n    type: 'VariableDeclaration',\n    kind: 'var',\n    declarations: [\n      {\n        type: 'VariableDeclarator',\n        init: {\n          type: 'CallExpression',\n          callee: { type: 'Identifier' },\n          arguments: [\n            { type: 'Literal' },\n            {\n              type: 'CallExpression',\n              callee: { type: 'Identifier', name: 'decodeURIComponent' },\n            },\n          ],\n        },\n      },\n    ],\n  };\n  const logicalExpression = {\n    type: 'ExpressionStatement',\n    expression: {\n      type: 'LogicalExpression',\n      left: { type: 'Identifier' },\n      right: {\n        type: 'SequenceExpression',\n        expressions: [\n          {\n            type: 'AssignmentExpression',\n            left: { type: 'Identifier' },\n            operator: '=',\n            right: {\n              type: 'CallExpression',\n              callee: { type: 'Identifier' },\n              arguments: {\n                or: [\n                  [\n                    { type: 'Literal' },\n                    {\n                      type: 'CallExpression',\n                      callee: {\n                        type: 'Identifier',\n                        name: 'decodeURIComponent',\n                      },\n                      arguments: [{ type: 'Identifier' }],\n                      optional: false,\n                    },\n                  ],\n                  [\n                    {\n                      type: 'CallExpression',\n                      callee: {\n                        type: 'Identifier',\n                        name: 'decodeURIComponent',\n                      },\n                      arguments: [{ type: 'Identifier' }],\n                      optional: false,\n                    },\n                  ],\n                ],\n              },\n              optional: false,\n            },\n          },\n          { type: 'CallExpression' },\n        ],\n      },\n      operator: '&&',\n    },\n  };\n  const identifier$1 = {\n    or: [\n      {\n        type: 'ExpressionStatement',\n        expression: {\n          type: 'AssignmentExpression',\n          operator: '=',\n          left: { type: 'Identifier' },\n          right: { type: 'FunctionExpression', params: [{}, {}, {}] },\n        },\n      },\n      { type: 'FunctionDeclaration', params: [{}, {}, {}] },\n      {\n        type: 'VariableDeclaration',\n        declarations: {\n          anykey: [\n            {\n              type: 'VariableDeclarator',\n              init: { type: 'FunctionExpression', params: [{}, {}, {}] },\n            },\n          ],\n        },\n      },\n    ],\n  };\n  function extract$1(node) {\n    if (!matchesStructure(node, identifier$1)) {\n      return null;\n    }\n    let block;\n    if (\n      node.type === 'ExpressionStatement' &&\n      node.expression.type === 'AssignmentExpression' &&\n      node.expression.right.type === 'FunctionExpression'\n    ) {\n      block = node.expression.right.body;\n    } else if (node.type === 'VariableDeclaration') {\n      for (const decl of node.declarations) {\n        if (\n          decl.type === 'VariableDeclarator' &&\n          _optionalChain$2([\n            decl,\n            'access',\n            (_) => _.init,\n            'optionalAccess',\n            (_2) => _2.type,\n          ]) === 'FunctionExpression' &&\n          _optionalChain$2([\n            decl,\n            'access',\n            (_3) => _3.init,\n            'optionalAccess',\n            (_4) => _4.params,\n            'access',\n            (_5) => _5.length,\n          ]) === 3\n        ) {\n          block = decl.init.body;\n          break;\n        }\n      }\n    } else if (node.type === 'FunctionDeclaration') {\n      block = node.body;\n    } else {\n      return null;\n    }\n    const relevantExpression = _optionalChain$2([\n      block,\n      'optionalAccess',\n      (_6) => _6.body,\n      'access',\n      (_7) => _7.at,\n      'call',\n      (_8) => _8(-2),\n    ]);\n    let call = null;\n    if (matchesStructure(relevantExpression, logicalExpression)) {\n      if (\n        _optionalChain$2([\n          relevantExpression,\n          'optionalAccess',\n          (_9) => _9.type,\n        ]) !== 'ExpressionStatement' ||\n        relevantExpression.expression.type !== 'LogicalExpression' ||\n        relevantExpression.expression.right.type !== 'SequenceExpression' ||\n        relevantExpression.expression.right.expressions[0].type !==\n          'AssignmentExpression' ||\n        relevantExpression.expression.right.expressions[0].right.type !==\n          'CallExpression'\n      ) {\n        return null;\n      }\n      call = relevantExpression.expression.right.expressions[0].right;\n    } else if (\n      _optionalChain$2([\n        relevantExpression,\n        'optionalAccess',\n        (_10) => _10.type,\n      ]) === 'IfStatement' &&\n      relevantExpression.consequent.type === 'BlockStatement'\n    ) {\n      for (const n of relevantExpression.consequent.body) {\n        if (!matchesStructure(n, nsigExpression)) {\n          continue;\n        }\n        if (\n          n.type !== 'VariableDeclaration' ||\n          _optionalChain$2([\n            n,\n            'access',\n            (_11) => _11.declarations,\n            'access',\n            (_12) => _12[0],\n            'access',\n            (_13) => _13.init,\n            'optionalAccess',\n            (_14) => _14.type,\n          ]) !== 'CallExpression'\n        ) {\n          continue;\n        }\n        call = n.declarations[0].init;\n        break;\n      }\n    }\n    if (call === null) {\n      return null;\n    }\n    return {\n      type: 'ArrowFunctionExpression',\n      params: [{ type: 'Identifier', name: 'sig' }],\n      body: {\n        type: 'CallExpression',\n        callee: { type: 'Identifier', name: call.callee.name },\n        arguments:\n          call.arguments.length === 1\n            ? [{ type: 'Identifier', name: 'sig' }]\n            : [call.arguments[0], { type: 'Identifier', name: 'sig' }],\n        optional: false,\n      },\n      async: false,\n      expression: false,\n      generator: false,\n    };\n  }\n  function _optionalChain$1(ops) {\n    let lastAccessLHS = undefined;\n    let value = ops[0];\n    let i = 1;\n    while (i < ops.length) {\n      const op = ops[i];\n      const fn = ops[i + 1];\n      i += 2;\n      if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) {\n        return undefined;\n      }\n      if (op === 'access' || op === 'optionalAccess') {\n        lastAccessLHS = value;\n        value = fn(value);\n      } else if (op === 'call' || op === 'optionalCall') {\n        value = fn((...args) => value.call(lastAccessLHS, ...args));\n        lastAccessLHS = undefined;\n      }\n    }\n    return value;\n  }\n  const identifier = {\n    or: [\n      {\n        type: 'VariableDeclaration',\n        kind: 'var',\n        declarations: {\n          anykey: [\n            {\n              type: 'VariableDeclarator',\n              id: { type: 'Identifier' },\n              init: {\n                type: 'ArrayExpression',\n                elements: [{ type: 'Identifier' }],\n              },\n            },\n          ],\n        },\n      },\n      {\n        type: 'ExpressionStatement',\n        expression: {\n          type: 'AssignmentExpression',\n          left: { type: 'Identifier' },\n          operator: '=',\n          right: {\n            type: 'ArrayExpression',\n            elements: [{ type: 'Identifier' }],\n          },\n        },\n      },\n    ],\n  };\n  const catchBlockBody = [\n    {\n      type: 'ReturnStatement',\n      argument: {\n        type: 'BinaryExpression',\n        left: {\n          type: 'MemberExpression',\n          object: { type: 'Identifier' },\n          computed: true,\n          property: { type: 'Literal' },\n          optional: false,\n        },\n        right: { type: 'Identifier' },\n        operator: '+',\n      },\n    },\n  ];\n  function extract(node) {\n    if (!matchesStructure(node, identifier)) {\n      let name = null;\n      let block = null;\n      switch (node.type) {\n        case 'ExpressionStatement': {\n          if (\n            node.expression.type === 'AssignmentExpression' &&\n            node.expression.left.type === 'Identifier' &&\n            node.expression.right.type === 'FunctionExpression' &&\n            node.expression.right.params.length === 1\n          ) {\n            name = node.expression.left.name;\n            block = node.expression.right.body;\n          }\n          break;\n        }\n        case 'FunctionDeclaration': {\n          if (node.params.length === 1) {\n            name = _optionalChain$1([\n              node,\n              'access',\n              (_) => _.id,\n              'optionalAccess',\n              (_2) => _2.name,\n            ]);\n            block = node.body;\n          }\n          break;\n        }\n      }\n      if (!block || !name) {\n        return null;\n      }\n      const tryNode = block.body.at(-2);\n      if (\n        _optionalChain$1([tryNode, 'optionalAccess', (_3) => _3.type]) !==\n          'TryStatement' ||\n        _optionalChain$1([\n          tryNode,\n          'access',\n          (_4) => _4.handler,\n          'optionalAccess',\n          (_5) => _5.type,\n        ]) !== 'CatchClause'\n      ) {\n        return null;\n      }\n      const catchBody = tryNode.handler.body.body;\n      if (matchesStructure(catchBody, catchBlockBody)) {\n        return makeSolverFuncFromName(name);\n      }\n      return null;\n    }\n    if (node.type === 'VariableDeclaration') {\n      for (const declaration of node.declarations) {\n        if (\n          declaration.type !== 'VariableDeclarator' ||\n          !declaration.init ||\n          declaration.init.type !== 'ArrayExpression' ||\n          declaration.init.elements.length !== 1\n        ) {\n          continue;\n        }\n        const [firstElement] = declaration.init.elements;\n        if (firstElement && firstElement.type === 'Identifier') {\n          return makeSolverFuncFromName(firstElement.name);\n        }\n      }\n    } else if (node.type === 'ExpressionStatement') {\n      const expr = node.expression;\n      if (\n        expr.type === 'AssignmentExpression' &&\n        expr.left.type === 'Identifier' &&\n        expr.operator === '=' &&\n        expr.right.type === 'ArrayExpression' &&\n        expr.right.elements.length === 1\n      ) {\n        const [firstElement] = expr.right.elements;\n        if (firstElement && firstElement.type === 'Identifier') {\n          return makeSolverFuncFromName(firstElement.name);\n        }\n      }\n    }\n    return null;\n  }\n  function makeSolverFuncFromName(name) {\n    return {\n      type: 'ArrowFunctionExpression',\n      params: [{ type: 'Identifier', name: 'n' }],\n      body: {\n        type: 'CallExpression',\n        callee: { type: 'Identifier', name: name },\n        arguments: [{ type: 'Identifier', name: 'n' }],\n        optional: false,\n      },\n      async: false,\n      expression: false,\n      generator: false,\n    };\n  }\n  const setupNodes = meriyah.parse(\n    `\\nif (typeof globalThis.XMLHttpRequest === \"undefined\") {\\n    globalThis.XMLHttpRequest = { prototype: {} };\\n}\\nconst window = Object.create(null);\\nif (typeof URL === \"undefined\") {\\n    window.location = {\\n        hash: \"\",\\n        host: \"www.youtube.com\",\\n        hostname: \"www.youtube.com\",\\n        href: \"https://www.youtube.com/watch?v=yt-dlp-wins\",\\n        origin: \"https://www.youtube.com\",\\n        password: \"\",\\n        pathname: \"/watch\",\\n        port: \"\",\\n        protocol: \"https:\",\\n        search: \"?v=yt-dlp-wins\",\\n        username: \"\",\\n    };\\n} else {\\n    window.location = new URL(\"https://www.youtube.com/watch?v=yt-dlp-wins\");\\n}\\nif (typeof globalThis.document === \"undefined\") {\\n    globalThis.document = Object.create(null);\\n}\\nif (typeof globalThis.navigator === \"undefined\") {\\n    globalThis.navigator = Object.create(null);\\n}\\nif (typeof globalThis.self === \"undefined\") {\\n    globalThis.self = globalThis;\\n}\\n`,\n  ).body;\n  function _optionalChain(ops) {\n    let lastAccessLHS = undefined;\n    let value = ops[0];\n    let i = 1;\n    while (i < ops.length) {\n      const op = ops[i];\n      const fn = ops[i + 1];\n      i += 2;\n      if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) {\n        return undefined;\n      }\n      if (op === 'access' || op === 'optionalAccess') {\n        lastAccessLHS = value;\n        value = fn(value);\n      } else if (op === 'call' || op === 'optionalCall') {\n        value = fn((...args) => value.call(lastAccessLHS, ...args));\n        lastAccessLHS = undefined;\n      }\n    }\n    return value;\n  }\n  function preprocessPlayer(data) {\n    const ast = meriyah.parse(data);\n    const body = ast.body;\n    const block = (() => {\n      switch (body.length) {\n        case 1: {\n          const func = body[0];\n          if (\n            _optionalChain([func, 'optionalAccess', (_) => _.type]) ===\n              'ExpressionStatement' &&\n            func.expression.type === 'CallExpression' &&\n            func.expression.callee.type === 'MemberExpression' &&\n            func.expression.callee.object.type === 'FunctionExpression'\n          ) {\n            return func.expression.callee.object.body;\n          }\n          break;\n        }\n        case 2: {\n          const func = body[1];\n          if (\n            _optionalChain([func, 'optionalAccess', (_2) => _2.type]) ===\n              'ExpressionStatement' &&\n            func.expression.type === 'CallExpression' &&\n            func.expression.callee.type === 'FunctionExpression'\n          ) {\n            const block = func.expression.callee.body;\n            block.body.splice(0, 1);\n            return block;\n          }\n          break;\n        }\n      }\n      throw 'unexpected structure';\n    })();\n    const found = { n: [], sig: [] };\n    const plainExpressions = block.body.filter((node) => {\n      const n = extract(node);\n      if (n) {\n        found.n.push(n);\n      }\n      const sig = extract$1(node);\n      if (sig) {\n        found.sig.push(sig);\n      }\n      if (node.type === 'ExpressionStatement') {\n        if (node.expression.type === 'AssignmentExpression') {\n          return true;\n        }\n        return node.expression.type === 'Literal';\n      }\n      return true;\n    });\n    block.body = plainExpressions;\n    for (const [name, options] of Object.entries(found)) {\n      const unique = new Set(options.map((x) => JSON.stringify(x)));\n      if (unique.size !== 1) {\n        const message = `found ${unique.size} ${name} function possibilities`;\n        throw (\n          message +\n          (unique.size\n            ? `: ${options.map((x) => astring.generate(x)).join(', ')}`\n            : '')\n        );\n      }\n      plainExpressions.push({\n        type: 'ExpressionStatement',\n        expression: {\n          type: 'AssignmentExpression',\n          operator: '=',\n          left: {\n            type: 'MemberExpression',\n            computed: false,\n            object: { type: 'Identifier', name: '_result' },\n            property: { type: 'Identifier', name: name },\n          },\n          right: options[0],\n        },\n      });\n    }\n    ast.body.splice(0, 0, ...setupNodes);\n    return astring.generate(ast);\n  }\n  function getFromPrepared(code) {\n    const resultObj = { n: null, sig: null };\n    Function('_result', code)(resultObj);\n    return resultObj;\n  }\n  function main(input) {\n    const preprocessedPlayer =\n      input.type === 'player'\n        ? preprocessPlayer(input.player)\n        : input.preprocessed_player;\n    const solvers = getFromPrepared(preprocessedPlayer);\n    const responses = input.requests.map((input) => {\n      if (!isOneOf(input.type, 'n', 'sig')) {\n        return { type: 'error', error: `Unknown request type: ${input.type}` };\n      }\n      const solver = solvers[input.type];\n      if (!solver) {\n        return {\n          type: 'error',\n          error: `Failed to extract ${input.type} function`,\n        };\n      }\n      try {\n        return {\n          type: 'result',\n          data: Object.fromEntries(\n            input.challenges.map((challenge) => [challenge, solver(challenge)]),\n          ),\n        };\n      } catch (error) {\n        return {\n          type: 'error',\n          error:\n            error instanceof Error\n              ? `${error.message}\\n${error.stack}`\n              : `${error}`,\n        };\n      }\n    });\n    const output = { type: 'result', responses: responses };\n    if (input.type === 'player' && input.output_preprocessed) {\n      output.preprocessed_player = preprocessedPlayer;\n    }\n    return output;\n  }\n  return main;\n})(meriyah, astring);\n"
  },
  {
    "path": "app/src/main/kotlin/com/dpi/ActivityLifecycleManager.kt",
    "content": "package com.dpi\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.app.Application\nimport android.os.Bundle\nimport android.os.Handler\nimport android.os.Looper\nimport timber.log.Timber\nimport java.util.Collections\nimport java.util.concurrent.ConcurrentHashMap\n\n/**\n * Manages activity lifecycle events and associated logic.\n * Provides hooks for monitoring and responding to activity lifecycle changes.\n */\nabstract class ActivityLifecycleManager : BaseLifecycleContentProvider() {\n\n    private val activeActivities: MutableSet<Activity> =\n        Collections.newSetFromMap(ConcurrentHashMap())\n\n    private val handler = Handler(Looper.getMainLooper())\n\n    private val activityTimerRunnable: Runnable = object : Runnable {\n        override fun run() {\n            try {\n                activeActivities.forEach { activity ->\n                    try {\n                        onActivityTimer(activity)\n                    } catch (e: Exception) {\n                        Timber.tag(TAG).w(e, \"Error in activity timer\")\n                    }\n                }\n                handler.postDelayed(this, activityTimerDelayMillis.toLong())\n            } catch (e: Exception) {\n                Timber.tag(TAG).w(e, \"Error in activity timer runnable\")\n            }\n        }\n    }\n\n    protected open val activityTimerDelayMillis: Int\n        get() = 3000\n\n    protected open fun onActivityTimer(activity: Activity) {}\n\n    override fun onCreate(): Boolean {\n        val application = getApplication() ?: return true\n\n        if (!onInit(application)) {\n            return true\n        }\n\n        application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {\n            override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {\n                this@ActivityLifecycleManager.onActivityCreated(activity)\n            }\n\n            override fun onActivityStarted(activity: Activity) {\n                this@ActivityLifecycleManager.onActivityStarted(activity)\n            }\n\n            override fun onActivityResumed(activity: Activity) {\n                activeActivities.add(activity)\n                handler.removeCallbacksAndMessages(null)\n                handler.post(activityTimerRunnable)\n                this@ActivityLifecycleManager.onActivityResumed(activity)\n            }\n\n            override fun onActivityPaused(activity: Activity) {\n                activeActivities.remove(activity)\n                this@ActivityLifecycleManager.onActivityPaused(activity)\n            }\n\n            override fun onActivityStopped(activity: Activity) {\n                this@ActivityLifecycleManager.onActivityStopped(activity)\n            }\n\n            override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}\n\n            override fun onActivityDestroyed(activity: Activity) {\n                this@ActivityLifecycleManager.onActivityDestroyed(activity)\n            }\n        })\n\n        return true\n    }\n\n    protected open fun onInit(application: Application): Boolean = true\n\n    protected open fun onActivityCreated(activity: Activity) {}\n    protected open fun onActivityStarted(activity: Activity) {}\n    protected open fun onActivityResumed(activity: Activity) {}\n    protected open fun onActivityPaused(activity: Activity) {}\n    protected open fun onActivityStopped(activity: Activity) {}\n    protected open fun onActivityDestroyed(activity: Activity) {}\n\n    companion object {\n        private val TAG = ActivityLifecycleManager::class.java.simpleName\n\n        @SuppressLint(\"PrivateApi\")\n        private fun getApplication(): Application? {\n            return try {\n                val activityThreadClass = Class.forName(\"android.app.ActivityThread\")\n                val activityThread = activityThreadClass\n                    .getMethod(\"currentActivityThread\")\n                    .invoke(null)\n                activityThreadClass\n                    .getMethod(\"getApplication\")\n                    .invoke(activityThread) as? Application\n            } catch (e: Exception) {\n                Timber.tag(\"AppUtils\").w(e, \"Failed to get Application instance\")\n                null\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/dpi/BaseLifecycleContentProvider.kt",
    "content": "package com.dpi\n\nimport android.content.ContentProvider\nimport android.content.ContentValues\nimport android.database.Cursor\nimport android.net.Uri\n\n/**\n * Base class for lifecycle management ContentProvider with default implementations.\n * This class exists solely to leverage ContentProvider's early initialization lifecycle.\n */\nabstract class BaseLifecycleContentProvider : ContentProvider() {\n\n    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int = 0\n\n    override fun getType(uri: Uri): String? = null\n\n    override fun insert(uri: Uri, values: ContentValues?): Uri? = null\n\n    override fun onCreate(): Boolean = true\n\n    override fun query(\n        uri: Uri,\n        projection: Array<String>?,\n        selection: String?,\n        selectionArgs: Array<String>?,\n        sortOrder: String?\n    ): Cursor? = null\n\n    override fun update(\n        uri: Uri,\n        values: ContentValues?,\n        selection: String?,\n        selectionArgs: Array<String>?\n    ): Int = 0\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/dpi/DensityConfiguration.kt",
    "content": "package com.dpi\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.Context\nimport android.content.res.Configuration\nimport android.content.res.Resources\nimport android.util.Log\nimport timber.log.Timber\nimport kotlin.math.roundToInt\n\n/**\n * Configuration class for adjusting screen density dynamically.\n * Applies density scaling to the entire application and maintains it across activity lifecycle events.\n */\ninternal class DensityConfiguration(\n    private val densityScale: Float\n) : ActivityLifecycleManager() {\n\n    private var originalDensityDpi: Int = 0\n\n    /**\n     * Applies the density scaling to the application context.\n     * This method should be called once during initialization.\n     */\n    @SuppressLint(\"LogNotTimber\")\n    fun applyDensityScaling(context: Context) {\n        if (densityScale == 1.0f) return\n\n        try {\n            onCreate()\n            val resources = context.resources\n            val config = Configuration(resources.configuration)\n            originalDensityDpi = config.densityDpi\n            updateDensityDpi(config, resources)\n        } catch (e: Exception) {\n            Log.w(TAG, \"Failed to apply configuration\", e)\n        }\n    }\n\n    /**\n     * Updates the density DPI in the configuration and applies it to resources.\n     */\n    private fun updateDensityDpi(config: Configuration, resources: Resources) {\n        val newDensityDpi = (originalDensityDpi * densityScale).roundToInt()\n        config.densityDpi = newDensityDpi\n        Timber.tag(TAG).i(\"Updated densityDpi to: $newDensityDpi\")\n        @Suppress(\"DEPRECATION\")\n        resources.updateConfiguration(config, resources.displayMetrics)\n    }\n\n    /**\n     * Reapply density scaling when an activity is created.\n     */\n    override fun onActivityCreated(activity: Activity) {\n        applyDensityToActivity(activity)\n    }\n\n    /**\n     * Reapply density scaling when an activity is resumed.\n     */\n    override fun onActivityResumed(activity: Activity) {\n        applyDensityToActivity(activity)\n    }\n\n    /**\n     * Reapply density scaling when an activity is started.\n     */\n    override fun onActivityStarted(activity: Activity) {\n        applyDensityToActivity(activity)\n    }\n\n    /**\n     * Applies the density configuration to a specific activity's resources.\n     */\n    private fun applyDensityToActivity(activity: Activity) {\n        try {\n            updateDensityDpi(activity.resources.configuration, activity.resources)\n        } catch (e: Exception) {\n            Timber.tag(TAG).w(e, \"Failed to update density for activity\")\n        }\n    }\n\n    companion object {\n        private val TAG = DensityConfiguration::class.java.simpleName\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/dpi/DensityScaler.kt",
    "content": "package com.dpi\n\nimport android.content.Context\nimport timber.log.Timber\n\n/**\n * DensityScaler - Main entry point for screen density scaling.\n *\n * Reads scale factor from user preferences with default of 1.0f (100% native).\n *\n * Supported scale factors:\n * - 1.0f (100%) - Native density (default)\n * - 0.75f (75%) - Compact\n * - 0.65f (65%) - Very Compact\n * - 0.55f (55%) - Ultra Compact\n */\nclass DensityScaler : BaseLifecycleContentProvider() {\n\n    override fun onCreate(): Boolean {\n        val context = context ?: return false\n        val scaleFactor = getScaleFactorFromPreferences(context)\n        DensityConfiguration(scaleFactor).applyDensityScaling(context)\n        return true\n    }\n\n    companion object {\n        private const val PREFS_NAME = \"metrolist_settings\"\n        private const val KEY_DENSITY_SCALE = \"density_scale_factor\"\n        private const val DEFAULT_SCALE_FACTOR = 1.0f\n\n        /**\n         * Reads the density scale factor from SharedPreferences.\n         * Uses SharedPreferences instead of DataStore for synchronous access during ContentProvider initialization.\n         */\n        private fun getScaleFactorFromPreferences(context: Context): Float {\n            return try {\n                val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)\n                prefs.getFloat(KEY_DENSITY_SCALE, DEFAULT_SCALE_FACTOR)\n            } catch (e: Exception) {\n                Timber.tag(\"DensityScaler\").w(e, \"Failed to read scale factor from preferences\")\n                DEFAULT_SCALE_FACTOR\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/App.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music\n\nimport android.app.Application\nimport android.app.NotificationChannel\nimport android.app.NotificationManager\nimport android.content.Context\nimport android.os.Build\nimport android.widget.Toast\nimport androidx.datastore.preferences.core.edit\nimport coil3.ImageLoader\nimport coil3.PlatformContext\nimport coil3.SingletonImageLoader\nimport coil3.disk.DiskCache\nimport coil3.disk.directory\nimport coil3.memory.MemoryCache\nimport coil3.request.CachePolicy\nimport coil3.request.allowHardware\nimport coil3.request.crossfade\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.YouTubeLocale\nimport com.metrolist.kugou.KuGou\nimport com.metrolist.lastfm.LastFM\nimport com.metrolist.music.BuildConfig\nimport com.metrolist.music.constants.*\nimport com.metrolist.music.di.ApplicationScope\nimport com.metrolist.music.extensions.toEnum\nimport com.metrolist.music.extensions.toInetSocketAddress\nimport com.metrolist.music.utils.CrashHandler\nimport com.metrolist.music.utils.cipher.CipherDeobfuscator\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.reportException\nimport dagger.hilt.android.HiltAndroidApp\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.withContext\nimport okhttp3.Credentials\nimport timber.log.Timber\nimport java.net.Authenticator\nimport java.net.PasswordAuthentication\nimport java.net.Proxy\nimport java.util.Locale\nimport javax.inject.Inject\n\n@HiltAndroidApp\nclass App :\n    Application(),\n    SingletonImageLoader.Factory {\n    @Inject\n    @ApplicationScope\n    lateinit var applicationScope: CoroutineScope\n\n    override fun onCreate() {\n        super.onCreate()\n\n        // Install crash handler first\n        CrashHandler.install(this)\n\n        // Initialize cipher deobfuscator for WEB_REMIX streaming\n        CipherDeobfuscator.initialize(this)\n\n        Timber.plant(Timber.DebugTree())\n\n        // تهيئة إعدادات التطبيق عند الإقلاع\n        applicationScope.launch {\n            initializeSettings()\n            observeSettingsChanges()\n        }\n    }\n\n    private suspend fun initializeSettings() {\n        val settings = dataStore.data.first()\n        val locale = Locale.getDefault()\n        val languageTag = locale.language\n\n        YouTube.locale =\n            YouTubeLocale(\n                gl =\n                    settings[ContentCountryKey]?.takeIf { it != SYSTEM_DEFAULT }\n                        ?: locale.country.takeIf { it in CountryCodeToName }\n                        ?: \"US\",\n                hl =\n                    settings[ContentLanguageKey]?.takeIf { it != SYSTEM_DEFAULT }\n                        ?: locale.language.takeIf { it in LanguageCodeToName }\n                        ?: languageTag.takeIf { it in LanguageCodeToName }\n                        ?: \"en\",\n            )\n\n        if (languageTag == \"zh-TW\") {\n            KuGou.useTraditionalChinese = true\n        }\n\n        // Initialize LastFM with API keys from BuildConfig (GitHub Secrets)\n        LastFM.initialize(\n            apiKey = BuildConfig.LASTFM_API_KEY.takeIf { it.isNotEmpty() } ?: \"\",\n            secret = BuildConfig.LASTFM_SECRET.takeIf { it.isNotEmpty() } ?: \"\",\n        )\n\n        if (settings[ProxyEnabledKey] == true) {\n            val username = settings[ProxyUsernameKey].orEmpty()\n            val password = settings[ProxyPasswordKey].orEmpty()\n            val type = settings[ProxyTypeKey].toEnum(defaultValue = Proxy.Type.HTTP)\n\n            if (username.isNotEmpty() || password.isNotEmpty()) {\n                if (type == Proxy.Type.HTTP) {\n                    YouTube.proxyAuth = Credentials.basic(username, password)\n                } else {\n                    Authenticator.setDefault(\n                        object : Authenticator() {\n                            override fun getPasswordAuthentication(): PasswordAuthentication =\n                                PasswordAuthentication(username, password.toCharArray())\n                        },\n                    )\n                }\n            }\n            try {\n                settings[ProxyUrlKey]?.let {\n                    YouTube.proxy = Proxy(type, it.toInetSocketAddress())\n                }\n            } catch (e: Exception) {\n                withContext(Dispatchers.Main) {\n                    Toast.makeText(this@App, getString(R.string.failed_to_parse_proxy), Toast.LENGTH_SHORT).show()\n                }\n                reportException(e)\n            }\n        }\n\n        YouTube.useLoginForBrowse = settings[UseLoginForBrowse] ?: true\n\n        val channel =\n            NotificationChannel(\n                \"updates\",\n                getString(R.string.update_channel_name),\n                NotificationManager.IMPORTANCE_DEFAULT,\n            ).apply {\n                description = getString(R.string.update_channel_desc)\n            }\n        val nm = getSystemService(NotificationManager::class.java)\n        nm.createNotificationChannel(channel)\n    }\n\n    private fun observeSettingsChanges() {\n        applicationScope.launch(Dispatchers.IO) {\n            dataStore.data\n                .map { it[VisitorDataKey] }\n                .distinctUntilChanged()\n                .collect { visitorData ->\n                    YouTube.visitorData = visitorData?.takeIf { it != \"null\" }\n                        ?: YouTube.visitorData().getOrNull()?.also { newVisitorData ->\n                            dataStore.edit { settings ->\n                                settings[VisitorDataKey] = newVisitorData\n                            }\n                        }\n                }\n        }\n\n        applicationScope.launch(Dispatchers.IO) {\n            dataStore.data\n                .map { it[DataSyncIdKey] }\n                .distinctUntilChanged()\n                .collect { dataSyncId ->\n                    YouTube.dataSyncId =\n                        dataSyncId?.let {\n                            it.takeIf { !it.contains(\"||\") }\n                                ?: it.takeIf { it.endsWith(\"||\") }?.substringBefore(\"||\")\n                                ?: it.substringAfter(\"||\")\n                        }\n                }\n        }\n\n        applicationScope.launch(Dispatchers.IO) {\n            dataStore.data\n                .map { it[InnerTubeCookieKey] }\n                .distinctUntilChanged()\n                .collect { cookie ->\n                    try {\n                        YouTube.cookie = cookie\n                    } catch (e: Exception) {\n                        Timber.e(e, \"Could not parse cookie. Clearing existing cookie.\")\n                        forgetAccount(this@App)\n                    }\n                }\n        }\n\n        applicationScope.launch(Dispatchers.IO) {\n            dataStore.data\n                .map { it[LastFMSessionKey] }\n                .distinctUntilChanged()\n                .collect { session ->\n                    try {\n                        LastFM.sessionKey = session\n                    } catch (e: Exception) {\n                        Timber.e(\"Error while loading last.fm session key. %s\", e.message)\n                    }\n                }\n        }\n\n        applicationScope.launch(Dispatchers.IO) {\n            dataStore.data\n                .map { Triple(it[ContentCountryKey], it[ContentLanguageKey], it[AppLanguageKey]) }\n                .distinctUntilChanged()\n                .collect { (contentCountry, contentLanguage, appLanguage) ->\n                    val systemLocale = Locale.getDefault()\n                    val effectiveAppLocale =\n                        appLanguage\n                            ?.takeUnless { it == SYSTEM_DEFAULT }\n                            ?.let { Locale.forLanguageTag(it) }\n                            ?: systemLocale\n\n                    YouTube.locale =\n                        YouTubeLocale(\n                            gl =\n                                contentCountry?.takeIf { it != SYSTEM_DEFAULT }\n                                    ?: effectiveAppLocale.country.takeIf { it in CountryCodeToName }\n                                    ?: systemLocale.country.takeIf { it in CountryCodeToName }\n                                    ?: \"US\",\n                            hl =\n                                contentLanguage?.takeIf { it != SYSTEM_DEFAULT }\n                                    ?: effectiveAppLocale.toLanguageTag().takeIf { it in LanguageCodeToName }\n                                    ?: effectiveAppLocale.language.takeIf { it in LanguageCodeToName }\n                                    ?: \"en\",\n                        )\n                }\n        }\n    }\n\n    override fun newImageLoader(context: PlatformContext): ImageLoader {\n        val cacheSize =\n            runBlocking {\n                dataStore.data.map { it[MaxImageCacheSizeKey] ?: 512 }.first()\n            }\n        return ImageLoader\n            .Builder(this)\n            .apply {\n                crossfade(true)\n                allowHardware(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)\n                // Memory cache for fast image loading (prevents network requests on recomposition)\n                memoryCache {\n                    MemoryCache\n                        .Builder()\n                        .maxSizePercent(context, 0.25)\n                        .build()\n                }\n                if (cacheSize == 0) {\n                    diskCachePolicy(CachePolicy.DISABLED)\n                } else {\n                    diskCache(\n                        DiskCache\n                            .Builder()\n                            .directory(cacheDir.resolve(\"coil\"))\n                            .maxSizeBytes(cacheSize * 1024 * 1024L)\n                            .build(),\n                    )\n                    // Allow reading from disk cache as fallback when network is unavailable\n                    networkCachePolicy(CachePolicy.ENABLED)\n                }\n            }.build()\n    }\n\n    companion object {\n        suspend fun forgetAccount(context: Context) {\n            Timber.d(\"forgetAccount: Starting logout process\")\n\n            // Clear DataStore preferences\n            Timber.d(\"forgetAccount: Clearing DataStore preferences\")\n            context.dataStore.edit { settings ->\n                settings.remove(InnerTubeCookieKey)\n                settings.remove(VisitorDataKey)\n                settings.remove(DataSyncIdKey)\n                settings.remove(AccountNameKey)\n                settings.remove(AccountEmailKey)\n                settings.remove(AccountChannelHandleKey)\n            }\n            Timber.d(\"forgetAccount: DataStore preferences cleared\")\n\n            // Immediately clear YouTube object's auth state\n            Timber.d(\"forgetAccount: Clearing YouTube object auth state\")\n            Timber.d(\n                \"forgetAccount: Before - cookie=${YouTube.cookie?.take(\n                    50,\n                )}, visitorData=${YouTube.visitorData?.take(20)}, dataSyncId=${YouTube.dataSyncId?.take(20)}\",\n            )\n            YouTube.cookie = null\n            YouTube.visitorData = null\n            YouTube.dataSyncId = null\n            Timber.d(\n                \"forgetAccount: After - cookie=${YouTube.cookie}, visitorData=${YouTube.visitorData}, dataSyncId=${YouTube.dataSyncId}\",\n            )\n\n            // Clear WebView cookies to prevent auto-relogin\n            Timber.d(\"forgetAccount: Clearing WebView CookieManager\")\n            withContext(Dispatchers.Main) {\n                android.webkit.CookieManager.getInstance().apply {\n                    removeAllCookies { removed ->\n                        Timber.d(\"forgetAccount: CookieManager.removeAllCookies callback: removed=$removed\")\n                    }\n                    flush()\n                }\n            }\n            Timber.d(\"forgetAccount: Logout process complete\")\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/MainActivity.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music\n\nimport android.Manifest\nimport android.annotation.SuppressLint\nimport android.app.PendingIntent\nimport android.content.ComponentName\nimport android.content.Intent\nimport android.content.ServiceConnection\nimport android.content.pm.PackageManager\nimport android.os.Build\nimport android.os.Bundle\nimport android.os.IBinder\nimport android.view.View\nimport android.view.WindowManager\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.slideInHorizontally\nimport androidx.compose.animation.slideOutHorizontally\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.add\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.displayCutout\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.AlertDialogDefaults\nimport androidx.compose.material3.Badge\nimport androidx.compose.material3.BadgedBox\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.contentColorFor\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalFocusManager\nimport androidx.compose.ui.platform.LocalWindowInfo\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.TextRange\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.util.fastAny\nimport androidx.compose.ui.window.Dialog\nimport androidx.compose.ui.window.DialogProperties\nimport androidx.core.app.ActivityCompat\nimport androidx.core.app.NotificationCompat\nimport androidx.core.app.NotificationManagerCompat\nimport androidx.core.content.ContextCompat\nimport androidx.core.net.toUri\nimport androidx.core.util.Consumer\nimport androidx.core.view.WindowCompat\nimport androidx.datastore.preferences.core.edit\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.lifecycle.coroutineScope\nimport androidx.lifecycle.lifecycleScope\nimport androidx.media3.common.MediaItem\nimport androidx.media3.common.Player\nimport androidx.navigation.NavHostController\nimport androidx.navigation.compose.NavHost\nimport androidx.navigation.compose.currentBackStackEntryAsState\nimport androidx.navigation.compose.rememberNavController\nimport coil3.compose.AsyncImage\nimport coil3.imageLoader\nimport coil3.request.CachePolicy\nimport coil3.request.ImageRequest\nimport coil3.request.allowHardware\nimport coil3.request.crossfade\nimport coil3.toBitmap\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.WatchEndpoint\nimport com.metrolist.music.constants.AppBarHeight\nimport com.metrolist.music.constants.AppLanguageKey\nimport com.metrolist.music.constants.CheckForUpdatesKey\nimport com.metrolist.music.constants.DarkModeKey\nimport com.metrolist.music.constants.DefaultOpenTabKey\nimport com.metrolist.music.constants.DisableScreenshotKey\nimport com.metrolist.music.constants.DynamicThemeKey\nimport com.metrolist.music.constants.EnableHighRefreshRateKey\nimport com.metrolist.music.constants.LastSeenVersionKey\nimport com.metrolist.music.constants.ListenTogetherInTopBarKey\nimport com.metrolist.music.constants.ListenTogetherUsernameKey\nimport com.metrolist.music.constants.MiniPlayerBottomSpacing\nimport com.metrolist.music.constants.MiniPlayerHeight\nimport com.metrolist.music.constants.NavigationBarAnimationSpec\nimport com.metrolist.music.constants.NavigationBarHeight\nimport com.metrolist.music.constants.PauseListenHistoryKey\nimport com.metrolist.music.constants.PauseSearchHistoryKey\nimport com.metrolist.music.constants.PureBlackKey\nimport com.metrolist.music.constants.SYSTEM_DEFAULT\nimport com.metrolist.music.constants.SelectedThemeColorKey\nimport com.metrolist.music.constants.SlimNavBarHeight\nimport com.metrolist.music.constants.SlimNavBarKey\nimport com.metrolist.music.constants.StopMusicOnTaskClearKey\nimport com.metrolist.music.constants.UpdateNotificationsEnabledKey\nimport com.metrolist.music.constants.UseNewMiniPlayerDesignKey\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.db.entities.SearchHistory\nimport com.metrolist.music.extensions.toEnum\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.playback.DownloadUtil\nimport com.metrolist.music.playback.MusicService\nimport com.metrolist.music.playback.MusicService.MusicBinder\nimport com.metrolist.music.playback.PlayerConnection\nimport com.metrolist.music.playback.queues.YouTubeQueue\nimport com.metrolist.music.ui.component.AccountSettingsDialog\nimport com.metrolist.music.ui.component.AppNavigationBar\nimport com.metrolist.music.ui.component.AppNavigationRail\nimport com.metrolist.music.ui.component.BottomSheetMenu\nimport com.metrolist.music.ui.component.BottomSheetPage\nimport com.metrolist.music.ui.component.LocalBottomSheetPageState\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.rememberBottomSheetState\nimport com.metrolist.music.ui.component.shimmer.ShimmerTheme\nimport com.metrolist.music.ui.menu.YouTubeSongMenu\nimport com.metrolist.music.ui.player.BottomSheetPlayer\nimport com.metrolist.music.ui.screens.Screens\nimport com.metrolist.music.ui.screens.navigationBuilder\nimport com.metrolist.music.ui.screens.settings.ChangelogScreen\nimport com.metrolist.music.ui.screens.settings.DarkMode\nimport com.metrolist.music.ui.screens.settings.NavigationTab\nimport com.metrolist.music.ui.theme.ColorSaver\nimport com.metrolist.music.ui.theme.DefaultThemeColor\nimport com.metrolist.music.ui.theme.MetrolistTheme\nimport com.metrolist.music.ui.theme.extractThemeColor\nimport com.metrolist.music.ui.utils.appBarScrollBehavior\nimport com.metrolist.music.ui.utils.resetHeightOffset\nimport com.metrolist.music.utils.SyncUtils\nimport com.metrolist.music.utils.Updater\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.utils.reportException\nimport com.metrolist.music.utils.setAppLocale\nimport com.metrolist.music.viewmodels.HomeViewModel\nimport com.valentinilk.shimmer.LocalShimmerTheme\nimport dagger.hilt.android.AndroidEntryPoint\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport timber.log.Timber\nimport java.net.URLDecoder\nimport java.net.URLEncoder\nimport java.util.Locale\nimport javax.inject.Inject\n\n@Suppress(\"DEPRECATION\", \"ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE\")\n@AndroidEntryPoint\nclass MainActivity : ComponentActivity() {\n    companion object {\n        private const val ACTION_SEARCH = \"com.metrolist.music.action.SEARCH\"\n        private const val ACTION_LIBRARY = \"com.metrolist.music.action.LIBRARY\"\n        const val ACTION_RECOGNITION = \"com.metrolist.music.action.RECOGNITION\"\n        const val EXTRA_AUTO_START_RECOGNITION = \"auto_start_recognition\"\n    }\n\n    @Inject\n    lateinit var database: MusicDatabase\n\n    @Inject\n    lateinit var downloadUtil: DownloadUtil\n\n    @Inject\n    lateinit var syncUtils: SyncUtils\n\n    @Inject\n    lateinit var listenTogetherManager: com.metrolist.music.listentogether.ListenTogetherManager\n\n    private lateinit var navController: NavHostController\n    private var pendingIntent: Intent? = null\n    private var latestVersionName by mutableStateOf(BuildConfig.VERSION_NAME)\n\n    private var playerConnection by mutableStateOf<PlayerConnection?>(null)\n    private var isServiceBound = false\n\n    private val serviceConnection =\n        object : ServiceConnection {\n            override fun onServiceConnected(\n                name: ComponentName?,\n                service: IBinder?,\n            ) {\n                if (service is MusicBinder) {\n                    try {\n                        playerConnection = PlayerConnection(this@MainActivity, service, database, lifecycleScope)\n                        Timber.tag(\"MainActivity\").d(\"PlayerConnection created successfully\")\n                        // Connect Listen Together manager to player\n                        listenTogetherManager.setPlayerConnection(playerConnection)\n                    } catch (e: Exception) {\n                        Timber.tag(\"MainActivity\").e(e, \"Failed to create PlayerConnection\")\n                        // Retry after a delay of 500ms\n                        lifecycleScope.launch {\n                            delay(500)\n                            try {\n                                playerConnection = PlayerConnection(this@MainActivity, service, database, lifecycleScope)\n                                listenTogetherManager.setPlayerConnection(playerConnection)\n                            } catch (e2: Exception) {\n                                Timber.tag(\"MainActivity\").e(e2, \"Failed to create PlayerConnection on retry\")\n                            }\n                        }\n                    }\n                }\n            }\n\n            override fun onServiceDisconnected(name: ComponentName?) {\n                // Disconnect Listen Together manager\n                listenTogetherManager.setPlayerConnection(null)\n                playerConnection?.dispose()\n                playerConnection = null\n            }\n        }\n\n    private fun safeUnbindService(source: String) {\n        if (!isServiceBound) return\n        try {\n            unbindService(serviceConnection)\n        } catch (e: IllegalArgumentException) {\n            Timber.tag(\"MainActivity\").w(e, \"Service was not bound when attempting to unbind in $source\")\n        } finally {\n            isServiceBound = false\n            listenTogetherManager.setPlayerConnection(null)\n            playerConnection?.dispose()\n            playerConnection = null\n        }\n    }\n\n    override fun onStart() {\n        super.onStart()\n        // Request notification permission on Android 13+\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n            if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {\n                ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1000)\n            }\n        }\n\n        // Explicitly start the service so it becomes an \"explicitly started\" service.\n        // Without this, the service only exists while a client is bound (BIND_AUTO_CREATE).\n        // When onStop() releases the binding (e.g. screen off, app backgrounded), Media3's\n        // MediaNotificationManager tries to call startForegroundService() to keep the service\n        // alive — but this is blocked on Android 12+ when the app is in the background,\n        // causing ForegroundServiceStartNotAllowedException. Starting the service explicitly\n        // here ensures it persists independently of binding state, so Media3 never needs to\n        // re-start it from a background context.\n        startService(Intent(this, MusicService::class.java))\n        bindService(\n            Intent(this, MusicService::class.java),\n            serviceConnection,\n            BIND_AUTO_CREATE,\n        )\n        isServiceBound = true\n    }\n\n    override fun onStop() {\n        safeUnbindService(\"onStop()\")\n        super.onStop()\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        if (dataStore.get(StopMusicOnTaskClearKey, false) &&\n            playerConnection?.isPlaying?.value == true &&\n            isFinishing\n        ) {\n            stopService(Intent(this, MusicService::class.java))\n        }\n        safeUnbindService(\"onDestroy()\")\n    }\n\n    override fun onNewIntent(intent: Intent) {\n        super.onNewIntent(intent)\n        if (::navController.isInitialized) {\n            handleDeepLinkIntent(intent, navController)\n        } else {\n            pendingIntent = intent\n        }\n    }\n\n    @SuppressLint(\"UnusedMaterial3ScaffoldPaddingParameter\")\n    @OptIn(ExperimentalMaterial3Api::class)\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        window.decorView.layoutDirection = View.LAYOUT_DIRECTION_LTR\n        WindowCompat.setDecorFitsSystemWindows(window, false)\n\n        // Initialize Listen Together manager\n        listenTogetherManager.initialize()\n\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {\n            val locale =\n                dataStore[AppLanguageKey]\n                    ?.takeUnless { it == SYSTEM_DEFAULT }\n                    ?.let { Locale.forLanguageTag(it) }\n                    ?: Locale.getDefault()\n            setAppLocale(this, locale)\n        }\n\n        lifecycleScope.launch {\n            dataStore.data\n                .map { it[DisableScreenshotKey] ?: false }\n                .distinctUntilChanged()\n                .collectLatest {\n                    if (it) {\n                        window.setFlags(\n                            WindowManager.LayoutParams.FLAG_SECURE,\n                            WindowManager.LayoutParams.FLAG_SECURE,\n                        )\n                    } else {\n                        window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)\n                    }\n                }\n        }\n\n        setContent {\n            MetrolistApp(\n                latestVersionName = latestVersionName,\n                onLatestVersionNameChange = { latestVersionName = it },\n                playerConnection = playerConnection,\n                database = database,\n                downloadUtil = downloadUtil,\n                syncUtils = syncUtils,\n            )\n        }\n    }\n\n    @SuppressLint(\"UnusedMaterial3ScaffoldPaddingParameter\")\n    @OptIn(ExperimentalMaterial3Api::class)\n    @Composable\n    private fun MetrolistApp(\n        latestVersionName: String,\n        onLatestVersionNameChange: (String) -> Unit,\n        playerConnection: PlayerConnection?,\n        database: MusicDatabase,\n        downloadUtil: DownloadUtil,\n        syncUtils: SyncUtils,\n    ) {\n        val checkForUpdates by rememberPreference(CheckForUpdatesKey, defaultValue = true)\n\n        if (BuildConfig.UPDATER_AVAILABLE) {\n            LaunchedEffect(checkForUpdates) {\n                if (checkForUpdates) {\n                    withContext(Dispatchers.IO) {\n                        val updatesEnabled = dataStore.get(CheckForUpdatesKey, true)\n                        val notifEnabled = dataStore.get(UpdateNotificationsEnabledKey, true)\n                        if (!updatesEnabled) return@withContext\n\n                        Updater.checkForUpdate().onSuccess { (releaseInfo, hasUpdate) ->\n                            if (releaseInfo != null) {\n                                onLatestVersionNameChange(releaseInfo.versionName)\n                                if (hasUpdate && notifEnabled) {\n                                    val downloadUrl = Updater.getDownloadUrlForCurrentVariant(releaseInfo)\n                                    if (downloadUrl != null) {\n                                        val intent = Intent(Intent.ACTION_VIEW, downloadUrl.toUri())\n\n                                        val flags =\n                                            PendingIntent.FLAG_UPDATE_CURRENT or\n                                                (PendingIntent.FLAG_IMMUTABLE)\n                                        val pending = PendingIntent.getActivity(this@MainActivity, 1001, intent, flags)\n\n                                        val notif =\n                                            NotificationCompat\n                                                .Builder(this@MainActivity, \"updates\")\n                                                .setSmallIcon(R.drawable.update)\n                                                .setContentTitle(getString(R.string.update_available_title))\n                                                .setContentText(releaseInfo.versionName)\n                                                .setContentIntent(pending)\n                                                .setAutoCancel(true)\n                                                .build()\n\n                                        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU ||\n                                            ContextCompat.checkSelfPermission(this@MainActivity, Manifest.permission.POST_NOTIFICATIONS) ==\n                                            PackageManager.PERMISSION_GRANTED\n                                        ) {\n                                            NotificationManagerCompat.from(this@MainActivity).notify(1001, notif)\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                } else {\n                    onLatestVersionNameChange(BuildConfig.VERSION_NAME)\n                }\n            }\n        }\n\n        val enableDynamicTheme by rememberPreference(DynamicThemeKey, defaultValue = true)\n        val enableHighRefreshRate by rememberPreference(EnableHighRefreshRateKey, defaultValue = true)\n\n        LaunchedEffect(enableHighRefreshRate) {\n            val window = this@MainActivity.window\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {\n                val layoutParams = window.attributes\n                if (enableHighRefreshRate) {\n                    layoutParams.preferredDisplayModeId = 0\n                } else {\n                    val modes = window.windowManager.defaultDisplay.supportedModes\n                    val mode60 =\n                        modes.firstOrNull { kotlin.math.abs(it.refreshRate - 60f) < 1f }\n                            ?: modes.minByOrNull { kotlin.math.abs(it.refreshRate - 60f) }\n\n                    if (mode60 != null) {\n                        layoutParams.preferredDisplayModeId = mode60.modeId\n                    }\n                }\n                window.attributes = layoutParams\n            } else {\n                val params = window.attributes\n                if (enableHighRefreshRate) {\n                    params.preferredRefreshRate = 0f\n                } else {\n                    params.preferredRefreshRate = 60f\n                }\n                window.attributes = params\n            }\n        }\n\n        val darkTheme by rememberEnumPreference(DarkModeKey, defaultValue = DarkMode.AUTO)\n        val isSystemInDarkTheme = isSystemInDarkTheme()\n        val useDarkTheme =\n            remember(darkTheme, isSystemInDarkTheme) {\n                if (darkTheme == DarkMode.AUTO) isSystemInDarkTheme else darkTheme == DarkMode.ON\n            }\n\n        LaunchedEffect(useDarkTheme) {\n            setSystemBarAppearance(useDarkTheme)\n        }\n\n        val pureBlackEnabled by rememberPreference(PureBlackKey, defaultValue = false)\n        val pureBlack =\n            remember(pureBlackEnabled, useDarkTheme) {\n                pureBlackEnabled && useDarkTheme\n            }\n\n        val (selectedThemeColorInt) = rememberPreference(SelectedThemeColorKey, defaultValue = DefaultThemeColor.toArgb())\n        val selectedThemeColor = Color(selectedThemeColorInt)\n\n        val showChangelog = rememberSaveable { mutableStateOf(false) }\n\n        var themeColor by rememberSaveable(stateSaver = ColorSaver) {\n            mutableStateOf(selectedThemeColor)\n        }\n\n        LaunchedEffect(selectedThemeColor) {\n            if (!enableDynamicTheme) {\n                themeColor = selectedThemeColor\n            }\n        }\n\n        LaunchedEffect(playerConnection, enableDynamicTheme, selectedThemeColor) {\n            val playerConnection = playerConnection\n            if (!enableDynamicTheme || playerConnection == null) {\n                themeColor = selectedThemeColor\n                return@LaunchedEffect\n            }\n\n            playerConnection.service.currentMediaMetadata.collectLatest { song ->\n                if (song?.thumbnailUrl != null) {\n                    withContext(Dispatchers.IO) {\n                        try {\n                            val result =\n                                imageLoader.execute(\n                                    ImageRequest\n                                        .Builder(this@MainActivity)\n                                        .data(song.thumbnailUrl)\n                                        .allowHardware(false)\n                                        .memoryCachePolicy(CachePolicy.ENABLED)\n                                        .diskCachePolicy(CachePolicy.ENABLED)\n                                        .networkCachePolicy(CachePolicy.ENABLED)\n                                        .crossfade(false)\n                                        .build(),\n                                )\n                            themeColor = result.image?.toBitmap()?.extractThemeColor() ?: selectedThemeColor\n                        } catch (e: Exception) {\n                            // Fallback to default on error\n                            themeColor = selectedThemeColor\n                        }\n                    }\n                } else {\n                    themeColor = selectedThemeColor\n                }\n            }\n        }\n\n        MetrolistTheme(\n            darkTheme = useDarkTheme,\n            pureBlack = pureBlack,\n            themeColor = themeColor,\n        ) {\n            BoxWithConstraints(\n                modifier =\n                    Modifier\n                        .fillMaxSize()\n                        .background(if (pureBlack) Color.Black else MaterialTheme.colorScheme.surface),\n            ) {\n                val focusManager = LocalFocusManager.current\n                val density = LocalDensity.current\n                val configuration = LocalWindowInfo.current\n                val cutoutInsets = WindowInsets.displayCutout\n                val windowsInsets = WindowInsets.systemBars\n                val bottomInset = with(density) { windowsInsets.getBottom(density).toDp() }\n                val bottomInsetDp = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()\n\n                val navController = rememberNavController()\n\n                LaunchedEffect(Unit) {\n                    val lastSeenVersion = dataStore.data.first()[LastSeenVersionKey] ?: \"\"\n                    val currentVersion = BuildConfig.VERSION_NAME\n                    if (lastSeenVersion != currentVersion) {\n                        showChangelog.value = true\n                    }\n                    dataStore.edit { settings ->\n                        settings[LastSeenVersionKey] = currentVersion\n                    }\n                }\n\n                val homeViewModel: HomeViewModel = hiltViewModel()\n                val accountImageUrl by homeViewModel.accountImageUrl.collectAsState()\n                val navBackStackEntry by navController.currentBackStackEntryAsState()\n                val (previousTab, setPreviousTab) = rememberSaveable { mutableStateOf(\"home\") }\n\n                val (listenTogetherInTopBar) = rememberPreference(ListenTogetherInTopBarKey, defaultValue = true)\n                val navigationItems =\n                    remember(listenTogetherInTopBar) {\n                        if (listenTogetherInTopBar) {\n                            Screens.MainScreens.filter { it != Screens.ListenTogether }\n                        } else {\n                            Screens.MainScreens\n                        }\n                    }\n                val (slimNav) = rememberPreference(SlimNavBarKey, defaultValue = false)\n                val (useNewMiniPlayerDesign) = rememberPreference(UseNewMiniPlayerDesignKey, defaultValue = true)\n                val defaultOpenTab =\n                    remember {\n                        dataStore[DefaultOpenTabKey].toEnum(defaultValue = NavigationTab.HOME)\n                    }\n                val tabOpenedFromShortcut =\n                    remember {\n                        when (intent?.action) {\n                            ACTION_SEARCH -> NavigationTab.LIBRARY\n                            ACTION_LIBRARY -> NavigationTab.SEARCH\n                            else -> null\n                        }\n                    }\n\n                val topLevelScreens =\n                    remember {\n                        listOf(\n                            Screens.Home.route,\n                            Screens.Library.route,\n                            Screens.ListenTogether.route,\n                            \"settings\",\n                        )\n                    }\n\n                val (query, onQueryChange) =\n                    rememberSaveable(stateSaver = TextFieldValue.Saver) {\n                        mutableStateOf(TextFieldValue())\n                    }\n\n                val onSearch: (String) -> Unit =\n                    remember {\n                        { searchQuery ->\n                            if (searchQuery.isNotEmpty()) {\n                                navController.navigate(\"search/${URLEncoder.encode(searchQuery, \"UTF-8\")}\")\n\n                                if (dataStore[PauseSearchHistoryKey] != true) {\n                                    lifecycleScope.launch(Dispatchers.IO) {\n                                        database.query {\n                                            insert(SearchHistory(query = searchQuery))\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n\n                val currentRoute by remember {\n                    derivedStateOf { navBackStackEntry?.destination?.route }\n                }\n\n                val inSearchScreen by remember {\n                    derivedStateOf { currentRoute?.startsWith(\"search/\") == true }\n                }\n                val navigationItemRoutes =\n                    remember(navigationItems) {\n                        navigationItems.map { it.route }.toSet()\n                    }\n\n                val shouldShowNavigationBar =\n                    remember(currentRoute, navigationItemRoutes) {\n                        currentRoute == null ||\n                            navigationItemRoutes.contains(currentRoute) ||\n                            currentRoute!!.startsWith(\"search/\")\n                    }\n\n                val isLandscape = configuration.containerDpSize.width > configuration.containerDpSize.height\n\n                val showRail = isLandscape && !inSearchScreen\n\n                val navPadding =\n                    if (shouldShowNavigationBar && !showRail) {\n                        if (slimNav) SlimNavBarHeight else NavigationBarHeight\n                    } else {\n                        0.dp\n                    }\n\n                val navigationBarHeight by animateDpAsState(\n                    targetValue = if (shouldShowNavigationBar && !showRail) NavigationBarHeight else 0.dp,\n                    animationSpec = NavigationBarAnimationSpec,\n                    label = \"navBarHeight\",\n                )\n\n                val playerBottomSheetState =\n                    rememberBottomSheetState(\n                        dismissedBound = 0.dp,\n                        collapsedBound =\n                            bottomInset +\n                                (if (!showRail && shouldShowNavigationBar) navPadding else 0.dp) +\n                                (if (useNewMiniPlayerDesign) MiniPlayerBottomSpacing else 0.dp) +\n                                MiniPlayerHeight,\n                        expandedBound = maxHeight,\n                    )\n\n                val playerAwareWindowInsets =\n                    remember(\n                        bottomInset,\n                        shouldShowNavigationBar,\n                        playerBottomSheetState.isDismissed,\n                        showRail,\n                    ) {\n                        var bottom = bottomInset\n                        if (shouldShowNavigationBar && !showRail) {\n                            bottom += NavigationBarHeight\n                        }\n                        if (!playerBottomSheetState.isDismissed) bottom += MiniPlayerHeight\n                        windowsInsets\n                            .only(WindowInsetsSides.Horizontal + WindowInsetsSides.Top)\n                            .add(WindowInsets(top = AppBarHeight, bottom = bottom))\n                    }\n                appBarScrollBehavior(\n                    canScroll = {\n                        !inSearchScreen &&\n                            (playerBottomSheetState.isCollapsed || playerBottomSheetState.isDismissed)\n                    },\n                )\n\n                val topAppBarScrollBehavior =\n                    appBarScrollBehavior(\n                        canScroll = {\n                            !inSearchScreen &&\n                                (playerBottomSheetState.isCollapsed || playerBottomSheetState.isDismissed)\n                        },\n                    )\n\n                // Navigation tracking\n                LaunchedEffect(navBackStackEntry) {\n                    if (inSearchScreen) {\n                        val searchQuery =\n                            withContext(Dispatchers.IO) {\n                                val rawQuery = navBackStackEntry?.arguments?.getString(\"query\")!!\n                                try {\n                                    URLDecoder.decode(rawQuery, \"UTF-8\")\n                                } catch (e: IllegalArgumentException) {\n                                    rawQuery\n                                }\n                            }\n                        onQueryChange(\n                            TextFieldValue(\n                                searchQuery,\n                                TextRange(searchQuery.length),\n                            ),\n                        )\n                    } else if (navigationItems.fastAny { it.route == navBackStackEntry?.destination?.route }) {\n                        onQueryChange(TextFieldValue())\n                    }\n\n                    // Reset scroll behavior for main navigation items\n                    if (navigationItems.fastAny { it.route == navBackStackEntry?.destination?.route }) {\n                        if (navigationItems.fastAny { it.route == previousTab }) {\n                            topAppBarScrollBehavior.state.resetHeightOffset()\n                        }\n                    }\n\n                    topAppBarScrollBehavior.state.resetHeightOffset()\n\n                    // Track previous tab for animations\n                    navController.currentBackStackEntry?.destination?.route?.let {\n                        setPreviousTab(it)\n                    }\n                }\n\n                LaunchedEffect(playerConnection) {\n                    val player = playerConnection?.player ?: return@LaunchedEffect\n                    if (player.currentMediaItem == null) {\n                        if (!playerBottomSheetState.isDismissed) {\n                            playerBottomSheetState.dismiss()\n                        }\n                    } else {\n                        if (playerBottomSheetState.isDismissed) {\n                            playerBottomSheetState.collapseSoft()\n                        }\n                    }\n                }\n\n                DisposableEffect(playerConnection, playerBottomSheetState) {\n                    val player = playerConnection?.player ?: return@DisposableEffect onDispose { }\n                    val listener =\n                        object : Player.Listener {\n                            override fun onMediaItemTransition(\n                                mediaItem: MediaItem?,\n                                reason: Int,\n                            ) {\n                                if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED &&\n                                    mediaItem != null &&\n                                    playerBottomSheetState.isDismissed\n                                ) {\n                                    playerBottomSheetState.collapseSoft()\n                                }\n                            }\n                        }\n                    player.addListener(listener)\n                    onDispose {\n                        player.removeListener(listener)\n                    }\n                }\n\n                var shouldShowTopBar by rememberSaveable { mutableStateOf(false) }\n\n                LaunchedEffect(navBackStackEntry, listenTogetherInTopBar) {\n                    val currentRoute = navBackStackEntry?.destination?.route\n                    val isListenTogetherScreen =\n                        currentRoute == Screens.ListenTogether.route ||\n                            currentRoute == \"listen_together_from_topbar\"\n                    shouldShowTopBar = currentRoute in topLevelScreens &&\n                        currentRoute != \"settings\" &&\n                        !(isListenTogetherScreen && listenTogetherInTopBar)\n                }\n\n                val coroutineScope = rememberCoroutineScope()\n                var sharedSong: SongItem? by remember {\n                    mutableStateOf(null)\n                }\n                val snackbarHostState = remember { SnackbarHostState() }\n\n                LaunchedEffect(Unit) {\n                    if (pendingIntent != null) {\n                        handleRecognitionIntent(pendingIntent!!, navController)\n                        handleDeepLinkIntent(pendingIntent!!, navController)\n                        pendingIntent = null\n                    } else {\n                        handleRecognitionIntent(intent, navController)\n                        handleDeepLinkIntent(intent, navController)\n                    }\n                }\n\n                DisposableEffect(Unit) {\n                    val listener =\n                        Consumer<Intent> { intent ->\n                            handleRecognitionIntent(intent, navController)\n                            handleDeepLinkIntent(intent, navController)\n                        }\n\n                    addOnNewIntentListener(listener)\n                    onDispose { removeOnNewIntentListener(listener) }\n                }\n\n                val currentTitleRes =\n                    remember(navBackStackEntry) {\n                        when (navBackStackEntry?.destination?.route) {\n                            Screens.Home.route -> R.string.home\n                            Screens.Search.route -> R.string.search\n                            Screens.Library.route -> R.string.filter_library\n                            Screens.ListenTogether.route -> R.string.together\n                            else -> null\n                        }\n                    }\n\n                var showAccountDialog by remember { mutableStateOf(false) }\n\n                val pauseListenHistory by rememberPreference(PauseListenHistoryKey, defaultValue = false)\n                val eventCount by database.eventCount().collectAsState(initial = 0)\n                val showHistoryButton =\n                    remember(pauseListenHistory, eventCount) {\n                        !(pauseListenHistory && eventCount == 0)\n                    }\n\n                val baseBg = if (pureBlack) Color.Black else MaterialTheme.colorScheme.surfaceContainer\n\n                CompositionLocalProvider(\n                    LocalDatabase provides database,\n                    LocalContentColor provides if (pureBlack) Color.White else contentColorFor(MaterialTheme.colorScheme.surface),\n                    LocalPlayerConnection provides playerConnection,\n                    LocalPlayerAwareWindowInsets provides playerAwareWindowInsets,\n                    LocalDownloadUtil provides downloadUtil,\n                    LocalShimmerTheme provides ShimmerTheme,\n                    LocalSyncUtils provides syncUtils,\n                    LocalListenTogetherManager provides listenTogetherManager,\n                    LocalChangelogState provides showChangelog,\n                ) {\n                    if (showChangelog.value) {\n                        ChangelogScreen(onDismiss = { showChangelog.value = false })\n                    }\n\n                    Scaffold(\n                        snackbarHost = { SnackbarHost(snackbarHostState) },\n                        topBar = {\n                            AnimatedVisibility(\n                                visible = shouldShowTopBar,\n                                enter = fadeIn(animationSpec = tween(durationMillis = 300)),\n                                exit = fadeOut(animationSpec = tween(durationMillis = 200)),\n                            ) {\n                                Row {\n                                    TopAppBar(\n                                        title = {\n                                            Text(\n                                                text = currentTitleRes?.let { stringResource(it) } ?: \"\",\n                                                style = MaterialTheme.typography.titleLarge,\n                                            )\n                                        },\n                                        actions = {\n                                            if (showHistoryButton) {\n                                                IconButton(onClick = { navController.navigate(\"history\") }) {\n                                                    Icon(\n                                                        painter = painterResource(R.drawable.history),\n                                                        contentDescription = stringResource(R.string.history),\n                                                    )\n                                                }\n                                            }\n                                            IconButton(onClick = { navController.navigate(\"stats\") }) {\n                                                Icon(\n                                                    painter = painterResource(R.drawable.stats),\n                                                    contentDescription = stringResource(R.string.stats),\n                                                )\n                                            }\n                                            if (listenTogetherInTopBar) {\n                                                IconButton(onClick = { navController.navigate(\"listen_together_from_topbar\") }) {\n                                                    Icon(\n                                                        painter = painterResource(R.drawable.group_outlined),\n                                                        contentDescription = stringResource(R.string.together),\n                                                    )\n                                                }\n                                            }\n                                            IconButton(onClick = { showAccountDialog = true }) {\n                                                BadgedBox(badge = {\n                                                    if (latestVersionName != BuildConfig.VERSION_NAME) {\n                                                        Badge()\n                                                    }\n                                                }) {\n                                                    if (accountImageUrl != null) {\n                                                        AsyncImage(\n                                                            model = accountImageUrl,\n                                                            contentDescription = stringResource(R.string.account),\n                                                            modifier =\n                                                                Modifier\n                                                                    .size(24.dp)\n                                                                    .clip(CircleShape),\n                                                        )\n                                                    } else {\n                                                        Icon(\n                                                            painter = painterResource(R.drawable.account),\n                                                            contentDescription = stringResource(R.string.account),\n                                                            modifier = Modifier.size(24.dp),\n                                                        )\n                                                    }\n                                                }\n                                            }\n                                        },\n                                        scrollBehavior = topAppBarScrollBehavior,\n                                        colors =\n                                            TopAppBarDefaults.topAppBarColors(\n                                                containerColor = if (pureBlack) Color.Black else MaterialTheme.colorScheme.surfaceContainer,\n                                                scrolledContainerColor = if (pureBlack) Color.Black else MaterialTheme.colorScheme.surfaceContainer,\n                                                titleContentColor = MaterialTheme.colorScheme.onSurface,\n                                                actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant,\n                                                navigationIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant,\n                                            ),\n                                        modifier =\n                                            Modifier.windowInsetsPadding(\n                                                if (showRail) {\n                                                    WindowInsets(left = NavigationBarHeight)\n                                                        .add(cutoutInsets.only(WindowInsetsSides.Start))\n                                                } else {\n                                                    cutoutInsets.only(WindowInsetsSides.Start + WindowInsetsSides.End)\n                                                },\n                                            ),\n                                    )\n                                }\n                            }\n                        },\n                        bottomBar = {\n                            val onNavItemClick: (Screens, Boolean) -> Unit =\n                                remember(navController, coroutineScope, topAppBarScrollBehavior, playerBottomSheetState) {\n                                    { screen: Screens, isSelected: Boolean ->\n                                        if (playerBottomSheetState.isExpanded) {\n                                            playerBottomSheetState.collapseSoft()\n                                        }\n\n                                        if (isSelected) {\n                                            navController.currentBackStackEntry?.savedStateHandle?.set(\"scrollToTop\", true)\n                                            coroutineScope.launch {\n                                                topAppBarScrollBehavior.state.resetHeightOffset()\n                                            }\n                                        } else {\n                                            navController.navigate(screen.route) {\n                                                popUpTo(navController.graph.startDestinationId) {\n                                                    saveState = true\n                                                }\n                                                launchSingleTop = true\n                                                restoreState = true\n                                            }\n                                        }\n                                    }\n                                }\n\n                            val onSearchLongClick: () -> Unit =\n                                remember(navController) {\n                                    {\n                                        navController.navigate(\"recognition\") {\n                                            launchSingleTop = true\n                                        }\n                                    }\n                                }\n\n                            // Pre-calculate values for graphicsLayer to avoid reading state during composition\n                            val navBarTotalHeight = bottomInset + NavigationBarHeight\n\n                            if (!showRail && currentRoute != \"wrapped\") {\n                                Box {\n                                    BottomSheetPlayer(\n                                        state = playerBottomSheetState,\n                                        navController = navController,\n                                        pureBlack = pureBlack,\n                                    )\n\n                                    AppNavigationBar(\n                                        navigationItems = navigationItems,\n                                        currentRoute = currentRoute,\n                                        onItemClick = onNavItemClick,\n                                        pureBlack = pureBlack,\n                                        slimNav = slimNav,\n                                        onSearchLongClick = onSearchLongClick,\n                                        modifier =\n                                            Modifier\n                                                .align(Alignment.BottomCenter)\n                                                .height(bottomInset + navPadding)\n                                                // Use graphicsLayer instead of offset to avoid recomposition\n                                                // graphicsLayer runs during draw phase, not composition phase\n                                                .graphicsLayer {\n                                                    val navBarHeightPx = navigationBarHeight.toPx()\n                                                    val totalHeightPx = navBarTotalHeight.toPx()\n\n                                                    translationY =\n                                                        if (navBarHeightPx == 0f) {\n                                                            totalHeightPx\n                                                        } else {\n                                                            // Read progress only during draw phase\n                                                            val progress = playerBottomSheetState.progress.coerceIn(0f, 1f)\n                                                            val slideOffset = totalHeightPx * progress\n                                                            val hideOffset =\n                                                                totalHeightPx * (1 - navBarHeightPx / NavigationBarHeight.toPx())\n                                                            slideOffset + hideOffset\n                                                        }\n                                                },\n                                    )\n\n                                    Box(\n                                        modifier =\n                                            Modifier\n                                                .fillMaxWidth()\n                                                .align(Alignment.BottomCenter)\n                                                .height(bottomInsetDp)\n                                                // Use graphicsLayer for background color changes\n                                                .graphicsLayer {\n                                                    val progress = playerBottomSheetState.progress\n                                                    alpha =\n                                                        if (progress > 0f ||\n                                                            (useNewMiniPlayerDesign && !shouldShowNavigationBar)\n                                                        ) {\n                                                            0f\n                                                        } else {\n                                                            1f\n                                                        }\n                                                }.background(baseBg),\n                                    )\n                                }\n                            } else {\n                                if (currentRoute != \"wrapped\") {\n                                    BottomSheetPlayer(\n                                        state = playerBottomSheetState,\n                                        navController = navController,\n                                        pureBlack = pureBlack,\n                                    )\n                                }\n\n                                Box(\n                                    modifier =\n                                        Modifier\n                                            .fillMaxWidth()\n                                            .align(Alignment.BottomCenter)\n                                            .height(bottomInsetDp)\n                                            // Use graphicsLayer for background color changes\n                                            .graphicsLayer {\n                                                val progress = playerBottomSheetState.progress\n                                                alpha =\n                                                    if (progress > 0f || (useNewMiniPlayerDesign && !shouldShowNavigationBar)) 0f else 1f\n                                            }.background(baseBg),\n                                )\n                            }\n                        },\n                        modifier =\n                            Modifier\n                                .fillMaxSize()\n                                .nestedScroll(topAppBarScrollBehavior.nestedScrollConnection),\n                    ) {\n                        Row(Modifier.fillMaxSize()) {\n                            val onRailItemClick: (Screens, Boolean) -> Unit =\n                                remember(navController, coroutineScope, topAppBarScrollBehavior, playerBottomSheetState) {\n                                    { screen: Screens, isSelected: Boolean ->\n                                        if (playerBottomSheetState.isExpanded) {\n                                            playerBottomSheetState.collapseSoft()\n                                        }\n\n                                        if (isSelected) {\n                                            navController.currentBackStackEntry?.savedStateHandle?.set(\"scrollToTop\", true)\n                                            coroutineScope.launch {\n                                                topAppBarScrollBehavior.state.resetHeightOffset()\n                                            }\n                                        } else {\n                                            navController.navigate(screen.route) {\n                                                popUpTo(navController.graph.startDestinationId) {\n                                                    saveState = true\n                                                }\n                                                launchSingleTop = true\n                                                restoreState = true\n                                            }\n                                        }\n                                    }\n                                }\n\n                            val onRailSearchLongClick: () -> Unit =\n                                remember(navController) {\n                                    {\n                                        navController.navigate(\"recognition\") {\n                                            launchSingleTop = true\n                                        }\n                                    }\n                                }\n\n                            if (showRail && currentRoute != \"wrapped\") {\n                                AppNavigationRail(\n                                    navigationItems = navigationItems,\n                                    currentRoute = currentRoute,\n                                    onItemClick = onRailItemClick,\n                                    pureBlack = pureBlack,\n                                    onSearchLongClick = onRailSearchLongClick,\n                                )\n                            }\n                            Box(Modifier.weight(1f)) {\n                                // NavHost with animations (Material 3 Expressive style)\n                                NavHost(\n                                    navController = navController,\n                                    startDestination =\n                                        when (tabOpenedFromShortcut ?: defaultOpenTab) {\n                                            NavigationTab.HOME -> Screens.Home\n                                            NavigationTab.LIBRARY -> Screens.Library\n                                            else -> Screens.Home\n                                        }.route,\n                                    // Enter Transition - smoother with smaller offset and longer duration\n                                    enterTransition = {\n                                        val currentRouteIndex =\n                                            navigationItems.indexOfFirst {\n                                                it.route == targetState.destination.route\n                                            }\n                                        val previousRouteIndex =\n                                            navigationItems.indexOfFirst {\n                                                it.route == initialState.destination.route\n                                            }\n\n                                        if (currentRouteIndex == -1 || currentRouteIndex > previousRouteIndex) {\n                                            slideInHorizontally { it / 8 } + fadeIn(tween(200))\n                                        } else {\n                                            slideInHorizontally { -it / 8 } + fadeIn(tween(200))\n                                        }\n                                    },\n                                    // Exit Transition - smoother with smaller offset and longer duration\n                                    exitTransition = {\n                                        val currentRouteIndex =\n                                            navigationItems.indexOfFirst {\n                                                it.route == initialState.destination.route\n                                            }\n                                        val targetRouteIndex =\n                                            navigationItems.indexOfFirst {\n                                                it.route == targetState.destination.route\n                                            }\n\n                                        if (targetRouteIndex == -1 || targetRouteIndex > currentRouteIndex) {\n                                            slideOutHorizontally { -it / 8 } + fadeOut(tween(200))\n                                        } else {\n                                            slideOutHorizontally { it / 8 } + fadeOut(tween(200))\n                                        }\n                                    },\n                                    // Pop Enter Transition - smoother with smaller offset and longer duration\n                                    popEnterTransition = {\n                                        val currentRouteIndex =\n                                            navigationItems.indexOfFirst {\n                                                it.route == targetState.destination.route\n                                            }\n                                        val previousRouteIndex =\n                                            navigationItems.indexOfFirst {\n                                                it.route == initialState.destination.route\n                                            }\n\n                                        if (previousRouteIndex != -1 && previousRouteIndex < currentRouteIndex) {\n                                            slideInHorizontally { it / 8 } + fadeIn(tween(200))\n                                        } else {\n                                            slideInHorizontally { -it / 8 } + fadeIn(tween(200))\n                                        }\n                                    },\n                                    // Pop Exit Transition - smoother with smaller offset and longer duration\n                                    popExitTransition = {\n                                        val currentRouteIndex =\n                                            navigationItems.indexOfFirst {\n                                                it.route == initialState.destination.route\n                                            }\n                                        val targetRouteIndex =\n                                            navigationItems.indexOfFirst {\n                                                it.route == targetState.destination.route\n                                            }\n\n                                        if (currentRouteIndex != -1 && currentRouteIndex < targetRouteIndex) {\n                                            slideOutHorizontally { -it / 8 } + fadeOut(tween(200))\n                                        } else {\n                                            slideOutHorizontally { it / 8 } + fadeOut(tween(200))\n                                        }\n                                    },\n                                    modifier = Modifier.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection),\n                                ) {\n                                    navigationBuilder(\n                                        navController = navController,\n                                        scrollBehavior = topAppBarScrollBehavior,\n                                        latestVersionName = latestVersionName,\n                                        activity = this@MainActivity,\n                                        snackbarHostState = snackbarHostState,\n                                    )\n                                }\n                            }\n                        }\n                    }\n\n                    BottomSheetMenu(\n                        state = LocalMenuState.current,\n                        modifier = Modifier.align(Alignment.BottomCenter),\n                    )\n\n                    BottomSheetPage(\n                        state = LocalBottomSheetPageState.current,\n                        modifier = Modifier.align(Alignment.BottomCenter),\n                    )\n\n                    if (showAccountDialog) {\n                        AccountSettingsDialog(\n                            navController = navController,\n                            onDismiss = {\n                                showAccountDialog = false\n                                homeViewModel.refresh()\n                            },\n                            latestVersionName = latestVersionName,\n                        )\n                    }\n\n                    sharedSong?.let { song ->\n                        playerConnection?.let {\n                            Dialog(\n                                onDismissRequest = { sharedSong = null },\n                                properties = DialogProperties(usePlatformDefaultWidth = false),\n                            ) {\n                                Surface(\n                                    modifier = Modifier.padding(24.dp),\n                                    shape = RoundedCornerShape(16.dp),\n                                    color = AlertDialogDefaults.containerColor,\n                                    tonalElevation = AlertDialogDefaults.TonalElevation,\n                                ) {\n                                    Column(\n                                        horizontalAlignment = Alignment.CenterHorizontally,\n                                    ) {\n                                        YouTubeSongMenu(\n                                            song = song,\n                                            navController = navController,\n                                            onDismiss = { sharedSong = null },\n                                        )\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Handles the ACTION_RECOGNITION intent sent from the Music Recognizer Widget.\n     * Always navigates to the recognition screen to show the result.\n     */\n    private fun handleRecognitionIntent(\n        intent: Intent,\n        navController: NavHostController,\n    ) {\n        if (intent.action != ACTION_RECOGNITION) return\n        val autoStart = intent.getBooleanExtra(EXTRA_AUTO_START_RECOGNITION, false)\n        intent.action = null\n        intent.removeExtra(EXTRA_AUTO_START_RECOGNITION)\n        navController.navigate(if (autoStart) \"recognition?autoStart=true\" else \"recognition\") {\n            launchSingleTop = true\n        }\n    }\n\n    private fun handleDeepLinkIntent(\n        intent: Intent,\n        navController: NavHostController,\n    ) {\n        val uri = intent.data ?: intent.extras?.getString(Intent.EXTRA_TEXT)?.toUri() ?: return\n        intent.data = null\n        intent.removeExtra(Intent.EXTRA_TEXT)\n        val coroutineScope = lifecycle.coroutineScope\n\n        val listenCode =\n            uri.getQueryParameter(\"code\")\n                ?: uri.getQueryParameter(\"room\")\n                ?: uri.pathSegments.getOrNull(1)\n        val isListenLink = uri.pathSegments.firstOrNull() == \"listen\" || uri.host?.equals(\"listen\", ignoreCase = true) == true\n        if (!listenCode.isNullOrBlank() && isListenLink) {\n            val username = dataStore.get(ListenTogetherUsernameKey, \"\").ifBlank { \"Guest\" }\n            listenTogetherManager.joinRoom(listenCode, username)\n            return\n        }\n\n        when (val path = uri.pathSegments.firstOrNull()) {\n            \"playlist\" -> {\n                uri.getQueryParameter(\"list\")?.let { playlistId ->\n                    if (playlistId.startsWith(\"OLAK5uy_\")) {\n                        coroutineScope.launch(Dispatchers.IO) {\n                            YouTube\n                                .albumSongs(playlistId)\n                                .onSuccess { songs ->\n                                    songs.firstOrNull()?.album?.id?.let { browseId ->\n                                        withContext(Dispatchers.Main) {\n                                            navController.navigate(\"album/$browseId\")\n                                        }\n                                    }\n                                }.onFailure { reportException(it) }\n                        }\n                    } else {\n                        navController.navigate(\"online_playlist/$playlistId\")\n                    }\n                }\n            }\n\n            \"browse\" -> {\n                uri.lastPathSegment?.let { browseId ->\n                    navController.navigate(\"album/$browseId\")\n                }\n            }\n\n            \"channel\", \"c\" -> {\n                uri.lastPathSegment?.let { artistId ->\n                    navController.navigate(\"artist/$artistId\")\n                }\n            }\n\n            \"search\" -> {\n                uri.getQueryParameter(\"q\")?.let {\n                    navController.navigate(\"search/${URLEncoder.encode(it, \"UTF-8\")}\")\n                }\n            }\n\n            else -> {\n                val videoId =\n                    when {\n                        path == \"watch\" -> uri.getQueryParameter(\"v\")\n                        uri.host == \"youtu.be\" -> uri.pathSegments.firstOrNull()\n                        else -> null\n                    }\n\n                val playlistId = uri.getQueryParameter(\"list\")\n\n                if (videoId != null) {\n                    coroutineScope.launch(Dispatchers.IO) {\n                        YouTube\n                            .queue(listOf(videoId), playlistId)\n                            .onSuccess { queue ->\n                                withContext(Dispatchers.Main) {\n                                    playerConnection?.playQueue(\n                                        YouTubeQueue(\n                                            WatchEndpoint(videoId = queue.firstOrNull()?.id, playlistId = playlistId),\n                                            queue.firstOrNull()?.toMediaMetadata(),\n                                        ),\n                                    )\n                                }\n                            }.onFailure {\n                                reportException(it)\n                            }\n                    }\n                } else if (playlistId != null) {\n                    coroutineScope.launch(Dispatchers.IO) {\n                        YouTube\n                            .queue(null, playlistId)\n                            .onSuccess { queue ->\n                                val firstItem = queue.firstOrNull()\n                                withContext(Dispatchers.Main) {\n                                    playerConnection?.playQueue(\n                                        YouTubeQueue(\n                                            WatchEndpoint(videoId = firstItem?.id, playlistId = playlistId),\n                                            firstItem?.toMediaMetadata(),\n                                        ),\n                                    )\n                                }\n                            }.onFailure {\n                                reportException(it)\n                            }\n                    }\n                }\n            }\n        }\n    }\n\n    @SuppressLint(\"ObsoleteSdkInt\")\n    private fun setSystemBarAppearance(isDark: Boolean) {\n        WindowCompat.getInsetsController(window, window.decorView.rootView).apply {\n            isAppearanceLightStatusBars = !isDark\n            isAppearanceLightNavigationBars = !isDark\n        }\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {\n            window.statusBarColor = (if (isDark) Color.Transparent else Color.Black.copy(alpha = 0.2f)).toArgb()\n        }\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {\n            window.navigationBarColor = (if (isDark) Color.Transparent else Color.Black.copy(alpha = 0.2f)).toArgb()\n        }\n    }\n}\n\nval LocalDatabase = staticCompositionLocalOf<MusicDatabase> { error(\"No database provided\") }\nval LocalPlayerConnection = staticCompositionLocalOf<PlayerConnection?> { error(\"No PlayerConnection provided\") }\nval LocalPlayerAwareWindowInsets = compositionLocalOf<WindowInsets> { error(\"No WindowInsets provided\") }\nval LocalDownloadUtil = staticCompositionLocalOf<DownloadUtil> { error(\"No DownloadUtil provided\") }\nval LocalSyncUtils = staticCompositionLocalOf<SyncUtils> { error(\"No SyncUtils provided\") }\nval LocalListenTogetherManager = staticCompositionLocalOf<com.metrolist.music.listentogether.ListenTogetherManager?> { null }\nval LocalChangelogState = staticCompositionLocalOf<MutableState<Boolean>> { error(\"No LocalChangelogState provided\") }\nval LocalIsPlayerExpanded = compositionLocalOf { false }\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/api/DeepLService.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.api\n\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport okhttp3.MediaType.Companion.toMediaType\nimport okhttp3.OkHttpClient\nimport okhttp3.Request\nimport okhttp3.RequestBody.Companion.toRequestBody\nimport org.json.JSONArray\nimport org.json.JSONObject\nimport java.util.concurrent.TimeUnit\n\nobject DeepLService {\n    private val client = OkHttpClient.Builder()\n        .connectTimeout(30, TimeUnit.SECONDS)\n        .readTimeout(90, TimeUnit.SECONDS)\n        .writeTimeout(30, TimeUnit.SECONDS)\n        .build()\n    private val JSON = \"application/json; charset=utf-8\".toMediaType()\n\n    suspend fun translate(\n        text: String,\n        targetLanguage: String,\n        apiKey: String,\n        formality: String = \"default\",\n        maxRetries: Int = 3\n    ): Result<List<String>> = withContext(Dispatchers.IO) {\n        var currentAttempt = 0\n        \n        // Validate input\n        if (text.isBlank()) {\n            return@withContext Result.failure(Exception(\"Input text is empty\"))\n        }\n        \n        val lines = text.lines()\n        val lineCount = lines.size\n        \n        // DeepL language codes (uppercase)\n        val deeplLangCode = when (targetLanguage.lowercase()) {\n            \"zh\", \"zh-cn\", \"zh-hans\" -> \"ZH\"\n            \"zh-tw\", \"zh-hant\" -> \"ZH\"\n            \"en\", \"en-us\" -> \"EN-US\"\n            \"en-gb\" -> \"EN-GB\"\n            \"pt\", \"pt-pt\" -> \"PT-PT\"\n            \"pt-br\" -> \"PT-BR\"\n            else -> targetLanguage.uppercase().take(2)\n        }\n        \n        // Determine if using free or pro API\n        val baseUrl = if (apiKey.endsWith(\":fx\")) {\n            \"https://api-free.deepl.com/v2/translate\"\n        } else {\n            \"https://api.deepl.com/v2/translate\"\n        }\n        \n        while (currentAttempt < maxRetries) {\n            try {\n                val jsonBody = JSONObject().apply {\n                    put(\"text\", JSONArray().apply {\n                        lines.forEach { put(it) }\n                    })\n                    put(\"target_lang\", deeplLangCode)\n                    if (formality != \"default\") {\n                        put(\"formality\", formality)\n                    }\n                    put(\"preserve_formatting\", true)\n                }\n\n                val request = Request.Builder()\n                    .url(baseUrl)\n                    .addHeader(\"Authorization\", \"DeepL-Auth-Key ${apiKey.trim()}\")\n                    .addHeader(\"Content-Type\", \"application/json\")\n                    .post(jsonBody.toString().toRequestBody(JSON))\n                    .build()\n\n                val response = client.newCall(request).execute()\n                val responseBody = response.body?.string()\n\n                if (!response.isSuccessful) {\n                    // Retry on server errors (5xx)\n                    if (response.code >= 500) {\n                        currentAttempt++\n                        kotlinx.coroutines.delay(1000L * currentAttempt)\n                        continue\n                    }\n                    \n                    val errorMsg = try {\n                        JSONObject(responseBody ?: \"\").optString(\"message\") \n                            ?: \"HTTP ${response.code}: ${response.message}\"\n                    } catch (e: Exception) {\n                        \"HTTP ${response.code}: ${response.message}\"\n                    }\n                    return@withContext Result.failure(Exception(\"Translation failed: $errorMsg\"))\n                }\n\n                if (responseBody == null) {\n                    currentAttempt++\n                    continue\n                }\n\n                val jsonResponse = JSONObject(responseBody)\n                val translations = jsonResponse.optJSONArray(\"translations\")\n                if (translations != null && translations.length() > 0) {\n                    val translatedLines = (0 until translations.length()).map { i ->\n                        translations.getJSONObject(i).optString(\"text\", \"\")\n                    }\n                    \n                    if (translatedLines.size == lineCount) {\n                        return@withContext Result.success(translatedLines)\n                    } else if (translatedLines.size > lineCount) {\n                        return@withContext Result.success(translatedLines.take(lineCount))\n                    } else {\n                        val paddedLines = translatedLines.toMutableList()\n                        while (paddedLines.size < lineCount) {\n                            paddedLines.add(\"\")\n                        }\n                        return@withContext Result.success(paddedLines)\n                    }\n                }\n            } catch (e: Exception) {\n                if (currentAttempt == maxRetries - 1) {\n                    return@withContext Result.failure(e)\n                }\n            }\n            currentAttempt++\n            kotlinx.coroutines.delay(1000L * currentAttempt)\n        }\n        return@withContext Result.failure(Exception(\"Max retries exceeded\"))\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/api/MistralService.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.api\n\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport okhttp3.MediaType.Companion.toMediaType\nimport okhttp3.OkHttpClient\nimport okhttp3.Request\nimport okhttp3.RequestBody.Companion.toRequestBody\nimport org.json.JSONArray\nimport org.json.JSONObject\nimport java.util.concurrent.TimeUnit\n\nobject MistralService {\n    private val client =\n        OkHttpClient\n            .Builder()\n            .connectTimeout(30, TimeUnit.SECONDS)\n            .readTimeout(90, TimeUnit.SECONDS)\n            .writeTimeout(30, TimeUnit.SECONDS)\n            .build()\n    private val JSON = \"application/json; charset=utf-8\".toMediaType()\n\n    suspend fun translate(\n        text: String,\n        targetLanguage: String,\n        apiKey: String,\n        model: String,\n        mode: String,\n        maxRetries: Int = 3,\n        sourceLanguage: String? = null,\n        customSystemPrompt: String = \"\",\n    ): Result<List<String>> =\n        withContext(Dispatchers.IO) {\n            var currentAttempt = 0\n\n            // Validate input\n            if (text.isBlank()) {\n                return@withContext Result.failure(Exception(\"Input text is empty\"))\n            }\n\n            val lines = text.lines()\n            val lineCount = lines.size\n\n            while (currentAttempt < maxRetries) {\n                try {\n                    // Use custom system prompt if provided, otherwise use the default\n                    val systemPrompt =\n                        if (customSystemPrompt.isNotBlank()) {\n                            customSystemPrompt.replace(\"{lineCount}\", lineCount.toString())\n                        } else {\n                            \"\"\"You are a precise lyrics translation assistant. Your output must ALWAYS be a valid JSON array of strings.\n\nCRITICAL RULES:\n1. Output ONLY a JSON array: [\"line1\", \"line2\", \"line3\"]\n2. NO explanations, NO questions, NO additional text\n3. Each input line maps to exactly one output line\n4. Preserve empty lines as empty strings \"\"\n5. Return EXACTLY $lineCount items in the array\n6. If uncertain, provide best approximation but maintain line count\"\"\"\n                        }\n\n                    val userPrompt =\n                        when (mode) {\n                            \"Romanized\" -> {\n                                \"\"\"Romanize/transliterate the following $lineCount lines into simple Latin script using ONLY basic English letters (a-z, A-Z).\n\nCRITICAL REQUIREMENTS:\n- Use ONLY simple ASCII characters (a-z, A-Z, 0-9, basic punctuation)\n- NO special characters like ā, ī, ū, ñ, ç, etc.\n- NO diacritics or accent marks\n- If text is already in Latin script, return it UNCHANGED\n- For non-Latin scripts (Hindi, Chinese, Japanese, Korean, Cyrillic, etc.), provide simple romanization\n- DO NOT translate meaning, only convert script to simple English letters\n- Keep all punctuation and formatting\n- Preserve line-by-line structure exactly\n\nExamples of correct simple romanization:\n- Sanskrit/Hindi \"आ\" → \"aa\" (not \"ā\")\n- Japanese \"東京\" → \"toukyou\" or \"tokyo\" (not \"tōkyō\")\n- Korean \"서울\" → \"seoul\" (not \"sŏul\")\n\nInput ($lineCount lines):\n$text\n\nOutput MUST be a JSON array with EXACTLY $lineCount strings using ONLY simple ASCII characters.\"\"\"\n                            }\n\n                            \"Transcribed\" -> {\n                                \"\"\"Transcribe/transliterate the following $lineCount lines phonetically into $targetLanguage script.\n\nCRITICAL REQUIREMENTS:\n- Convert the SOUND/PRONUNCIATION of the original text into $targetLanguage script\n- DO NOT translate the meaning - only represent how the original words SOUND\n- Use the native script of $targetLanguage (e.g., Devanagari for Hindi, Hangul for Korean, etc.)\n- Preserve the original pronunciation as closely as possible in the target script\n- Keep punctuation and formatting\n- Preserve line-by-line structure exactly\n- If text is already in $targetLanguage script, return it UNCHANGED\n\nExamples:\n- Japanese \"こんにちは\" to Hindi → \"कोन्निचिवा\" (phonetic, not translation)\n- English \"Hello\" to Hindi → \"हेलो\" (phonetic)\n- Korean \"안녕하세요\" to Hindi → \"अन्न्योंग हासेयो\" (phonetic)\n\nInput ($lineCount lines):\n$text\n\nOutput MUST be a JSON array with EXACTLY $lineCount strings in $targetLanguage script.\"\"\"\n                            }\n\n                            else -> {\n                                \"\"\"Translate the following $lineCount lines to $targetLanguage.\n\nIMPORTANT:\n- Provide natural, accurate translation\n- Maintain poetic flow and meaning\n- Keep punctuation appropriate for target language\n- Preserve line-by-line structure exactly\n- For song lyrics, prioritize singability\n\nInput ($lineCount lines):\n$text\n\nOutput MUST be a JSON array with EXACTLY $lineCount strings.\"\"\"\n                            }\n                        }\n\n                    val messages =\n                        JSONArray().apply {\n                            // Include system prompt when a custom one is provided\n                            if (customSystemPrompt.isNotBlank()) {\n                                put(\n                                    JSONObject().apply {\n                                        put(\"role\", \"system\")\n                                        put(\"content\", systemPrompt)\n                                    },\n                                )\n                            }\n                            put(\n                                JSONObject().apply {\n                                    put(\"role\", \"user\")\n                                    put(\"content\", userPrompt)\n                                },\n                            )\n                        }\n\n                    val jsonBody =\n                        JSONObject().apply {\n                            if (model.isNotBlank()) {\n                                put(\"model\", model)\n                            } else {\n                                put(\"model\", \"mistral-small-latest\")\n                            }\n                            put(\"messages\", messages)\n                            put(\"temperature\", 0.3) // Lower temperature for more consistent output\n                            put(\"max_tokens\", lineCount * 100) // Adequate tokens for translation\n                        }\n\n                    val request =\n                        Request\n                            .Builder()\n                            .url(\"https://api.mistral.ai/v1/chat/completions\")\n                            .apply {\n                                if (apiKey.isNotBlank()) {\n                                    addHeader(\"Authorization\", \"Bearer ${apiKey.trim()}\")\n                                }\n                            }.addHeader(\"Content-Type\", \"application/json\")\n                            .post(jsonBody.toString().toRequestBody(JSON))\n                            .build()\n\n                    val response = client.newCall(request).execute()\n                    val responseBody = response.body?.string()\n\n                    if (!response.isSuccessful) {\n                        // Retry on server errors (5xx)\n                        if (response.code >= 500) {\n                            currentAttempt++\n                            kotlinx.coroutines.delay(1000L * currentAttempt)\n                            continue\n                        }\n\n                        val errorMsg =\n                            try {\n                                JSONObject(responseBody ?: \"\").optJSONObject(\"error\")?.optString(\"message\")\n                                    ?: \"HTTP ${response.code}: ${response.message}\"\n                            } catch (e: Exception) {\n                                \"HTTP ${response.code}: ${response.message}\"\n                            }\n                        return@withContext Result.failure(Exception(\"Translation failed: $errorMsg\"))\n                    }\n\n                    if (responseBody == null) {\n                        currentAttempt++\n                        continue\n                    }\n\n                    val jsonResponse = JSONObject(responseBody)\n                    val choices = jsonResponse.optJSONArray(\"choices\")\n                    if (choices != null && choices.length() > 0) {\n                        val message = choices.getJSONObject(0).optJSONObject(\"message\")\n                        var content = message?.optString(\"content\")?.trim()\n\n                        if (!content.isNullOrBlank()) {\n                            // Enhanced JSON extraction with multiple fallback strategies\n                            var translatedLines: List<String>? = null\n\n                            // Strategy 1: Try direct JSON parsing\n                            try {\n                                val jsonArray = JSONArray(content)\n                                translatedLines = (0 until jsonArray.length()).map { jsonArray.optString(it) }\n                            } catch (e: Exception) {\n                                // Strategy 2: Extract JSON from markdown code blocks\n                                content = content.replace(\"```json\", \"\").replace(\"```\", \"\").trim()\n\n                                try {\n                                    val jsonArray = JSONArray(content)\n                                    translatedLines = (0 until jsonArray.length()).map { jsonArray.optString(it) }\n                                } catch (e2: Exception) {\n                                    // Strategy 3: Find first [ and last ]\n                                    val startIdx = content.indexOf('[')\n                                    val endIdx = content.lastIndexOf(']')\n\n                                    if (startIdx != -1 && endIdx != -1 && endIdx > startIdx) {\n                                        val jsonString = content.substring(startIdx, endIdx + 1)\n                                        try {\n                                            val jsonArray = JSONArray(jsonString)\n                                            translatedLines = (0 until jsonArray.length()).map { jsonArray.optString(it) }\n                                        } catch (e3: Exception) {\n                                            // Strategy 4: Manual line-by-line parsing as last resort\n                                            translatedLines =\n                                                content\n                                                    .lines()\n                                                    .filter { it.trim().isNotEmpty() }\n                                                    .map { it.trim().removeSurrounding(\"\\\"\").removeSurrounding(\"'\") }\n                                        }\n                                    }\n                                }\n                            }\n\n                            if (translatedLines != null) {\n                                // Validate line count matches\n                                if (translatedLines.size == lineCount) {\n                                    return@withContext Result.success(translatedLines)\n                                } else if (translatedLines.size > lineCount) {\n                                    // If we got more lines, take first N\n                                    return@withContext Result.success(translatedLines.take(lineCount))\n                                } else {\n                                    // If we got fewer lines, pad with empty strings\n                                    val paddedLines = translatedLines.toMutableList()\n                                    while (paddedLines.size < lineCount) {\n                                        paddedLines.add(\"\")\n                                    }\n                                    return@withContext Result.success(paddedLines)\n                                }\n                            }\n                        }\n                    }\n                } catch (e: Exception) {\n                    if (currentAttempt == maxRetries - 1) {\n                        return@withContext Result.failure(e)\n                    }\n                }\n                currentAttempt++\n                kotlinx.coroutines.delay(1000L * currentAttempt)\n            }\n            return@withContext Result.failure(Exception(\"Max retries exceeded\"))\n        }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/api/OpenRouterService.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.api\n\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport okhttp3.MediaType.Companion.toMediaType\nimport okhttp3.OkHttpClient\nimport okhttp3.Request\nimport okhttp3.RequestBody.Companion.toRequestBody\nimport org.json.JSONArray\nimport org.json.JSONObject\nimport java.util.concurrent.TimeUnit\n\nobject OpenRouterService {\n    private val client =\n        OkHttpClient\n            .Builder()\n            .connectTimeout(30, TimeUnit.SECONDS)\n            .readTimeout(90, TimeUnit.SECONDS)\n            .writeTimeout(30, TimeUnit.SECONDS)\n            .build()\n    private val JSON = \"application/json; charset=utf-8\".toMediaType()\n\n    suspend fun translate(\n        text: String,\n        targetLanguage: String,\n        apiKey: String,\n        baseUrl: String,\n        model: String,\n        mode: String,\n        maxRetries: Int = 3,\n        sourceLanguage: String? = null,\n        customSystemPrompt: String = \"\",\n    ): Result<List<String>> =\n        withContext(Dispatchers.IO) {\n            var currentAttempt = 0\n\n            // Validate input\n            if (text.isBlank()) {\n                return@withContext Result.failure(Exception(\"Input text is empty\"))\n            }\n\n            val lines = text.lines()\n            val lineCount = lines.size\n\n            while (currentAttempt < maxRetries) {\n                try {\n                    // Use custom system prompt if provided, otherwise use the default\n                    val systemPrompt =\n                        if (customSystemPrompt.isNotBlank()) {\n                            customSystemPrompt.replace(\"{lineCount}\", lineCount.toString())\n                        } else {\n                            \"\"\"You are a precise lyrics translation assistant. Your output must ALWAYS be a valid JSON array of strings.\n\nCRITICAL RULES:\n1. Output ONLY a JSON array: [\"line1\", \"line2\", \"line3\"]\n2. NO explanations, NO questions, NO additional text\n3. Each input line maps to exactly one output line\n4. Preserve empty lines as empty strings \"\"\n5. Return EXACTLY $lineCount items in the array\n6. If uncertain, provide best approximation but maintain line count\"\"\"\n                        }\n\n                    val userPrompt =\n                        when (mode) {\n                            \"Romanized\" -> {\n                                \"\"\"Romanize/transliterate the following $lineCount lines into simple Latin script using ONLY basic English letters (a-z, A-Z).\n\nCRITICAL REQUIREMENTS:\n- Use ONLY simple ASCII characters (a-z, A-Z, 0-9, basic punctuation)\n- NO special characters like ā, ī, ū, ñ, ç, etc.\n- NO diacritics or accent marks\n- If text is already in Latin script, return it UNCHANGED\n- For non-Latin scripts (Hindi, Chinese, Japanese, Korean, Cyrillic, etc.), provide simple romanization\n- DO NOT translate meaning, only convert script to simple English letters\n- Keep all punctuation and formatting\n- Preserve line-by-line structure exactly\n\nExamples of correct simple romanization:\n- Sanskrit/Hindi \"आ\" → \"aa\" (not \"ā\")\n- Japanese \"東京\" → \"toukyou\" or \"tokyo\" (not \"tōkyō\")\n- Korean \"서울\" → \"seoul\" (not \"sŏul\")\n\nInput ($lineCount lines):\n$text\n\nOutput MUST be a JSON array with EXACTLY $lineCount strings using ONLY simple ASCII characters.\"\"\"\n                            }\n\n                            \"Transcribed\" -> {\n                                \"\"\"Transcribe/transliterate the following $lineCount lines phonetically into $targetLanguage script.\n\nCRITICAL REQUIREMENTS:\n- Convert the SOUND/PRONUNCIATION of the original text into $targetLanguage script\n- DO NOT translate the meaning - only represent how the original words SOUND\n- Use the native script of $targetLanguage (e.g., Devanagari for Hindi, Hangul for Korean, etc.)\n- Preserve the original pronunciation as closely as possible in the target script\n- Keep punctuation and formatting\n- Preserve line-by-line structure exactly\n- If text is already in $targetLanguage script, return it UNCHANGED\n\nExamples:\n- Japanese \"こんにちは\" to Hindi → \"कोन्निचिवा\" (phonetic, not translation)\n- English \"Hello\" to Hindi → \"हेलो\" (phonetic)\n- Korean \"안녕하세요\" to Hindi → \"अन्न्योंग हासेयो\" (phonetic)\n\nInput ($lineCount lines):\n$text\n\nOutput MUST be a JSON array with EXACTLY $lineCount strings in $targetLanguage script.\"\"\"\n                            }\n\n                            else -> {\n                                \"\"\"Translate the following $lineCount lines to $targetLanguage.\n\nIMPORTANT:\n- Provide natural, accurate translation\n- Maintain poetic flow and meaning\n- Keep punctuation appropriate for target language\n- Preserve line-by-line structure exactly\n- For song lyrics, prioritize singability\n\nInput ($lineCount lines):\n$text\n\nOutput MUST be a JSON array with EXACTLY $lineCount strings.\"\"\"\n                            }\n                        }\n\n                    val messages =\n                        JSONArray().apply {\n                            put(\n                                JSONObject().apply {\n                                    put(\"role\", \"system\")\n                                    put(\"content\", systemPrompt)\n                                },\n                            )\n                            put(\n                                JSONObject().apply {\n                                    put(\"role\", \"user\")\n                                    put(\"content\", userPrompt)\n                                },\n                            )\n                        }\n\n                    val jsonBody =\n                        JSONObject().apply {\n                            if (model.isNotBlank()) {\n                                put(\"model\", model)\n                            }\n                            put(\"messages\", messages)\n                            put(\"temperature\", 0.3) // Lower temperature for more consistent output\n                            put(\"max_tokens\", lineCount * 100) // Adequate tokens for translation\n                        }\n\n                    val request =\n                        Request\n                            .Builder()\n                            .url(baseUrl.ifBlank { \"https://openrouter.ai/api/v1/chat/completions\" })\n                            .apply {\n                                if (apiKey.isNotBlank()) {\n                                    addHeader(\"Authorization\", \"Bearer ${apiKey.trim()}\")\n                                }\n                            }.addHeader(\"Content-Type\", \"application/json\")\n                            .addHeader(\"HTTP-Referer\", \"https://github.com/MetrolistGroup/Metrolist\")\n                            .addHeader(\"X-Title\", \"Metrolist\")\n                            .post(jsonBody.toString().toRequestBody(JSON))\n                            .build()\n\n                    val response = client.newCall(request).execute()\n                    val responseBody = response.body?.string()\n\n                    if (!response.isSuccessful) {\n                        // Retry on server errors (5xx)\n                        if (response.code >= 500) {\n                            currentAttempt++\n                            kotlinx.coroutines.delay(1000L * currentAttempt)\n                            continue\n                        }\n\n                        val errorMsg =\n                            try {\n                                JSONObject(responseBody ?: \"\").optJSONObject(\"error\")?.optString(\"message\")\n                                    ?: \"HTTP ${response.code}: ${response.message}\"\n                            } catch (e: Exception) {\n                                \"HTTP ${response.code}: ${response.message}\"\n                            }\n                        return@withContext Result.failure(Exception(\"Translation failed: $errorMsg\"))\n                    }\n\n                    if (responseBody == null) {\n                        currentAttempt++\n                        continue\n                    }\n\n                    val jsonResponse = JSONObject(responseBody)\n                    val choices = jsonResponse.optJSONArray(\"choices\")\n                    if (choices != null && choices.length() > 0) {\n                        val message = choices.getJSONObject(0).optJSONObject(\"message\")\n                        var content = message?.optString(\"content\")?.trim()\n\n                        if (!content.isNullOrBlank()) {\n                            // Enhanced JSON extraction with multiple fallback strategies\n                            var translatedLines: List<String>? = null\n\n                            // Strategy 1: Try direct JSON parsing\n                            try {\n                                val jsonArray = JSONArray(content)\n                                translatedLines = (0 until jsonArray.length()).map { jsonArray.optString(it) }\n                            } catch (e: Exception) {\n                                // Strategy 2: Extract JSON from markdown code blocks\n                                content = content.replace(\"```json\", \"\").replace(\"```\", \"\").trim()\n\n                                try {\n                                    val jsonArray = JSONArray(content)\n                                    translatedLines = (0 until jsonArray.length()).map { jsonArray.optString(it) }\n                                } catch (e2: Exception) {\n                                    // Strategy 3: Find first [ and last ]\n                                    val startIdx = content.indexOf('[')\n                                    val endIdx = content.lastIndexOf(']')\n\n                                    if (startIdx != -1 && endIdx != -1 && endIdx > startIdx) {\n                                        val jsonString = content.substring(startIdx, endIdx + 1)\n                                        try {\n                                            val jsonArray = JSONArray(jsonString)\n                                            translatedLines = (0 until jsonArray.length()).map { jsonArray.optString(it) }\n                                        } catch (e3: Exception) {\n                                            // Strategy 4: Manual line-by-line parsing as last resort\n                                            translatedLines =\n                                                content\n                                                    .lines()\n                                                    .filter { it.trim().isNotEmpty() }\n                                                    .map { it.trim().removeSurrounding(\"\\\"\").removeSurrounding(\"'\") }\n                                        }\n                                    }\n                                }\n                            }\n\n                            if (translatedLines != null) {\n                                // Validate line count matches\n                                if (translatedLines.size == lineCount) {\n                                    return@withContext Result.success(translatedLines)\n                                } else if (translatedLines.size > lineCount) {\n                                    // If we got more lines, take first N\n                                    return@withContext Result.success(translatedLines.take(lineCount))\n                                } else {\n                                    // If we got fewer lines, pad with empty strings\n                                    val paddedLines = translatedLines.toMutableList()\n                                    while (paddedLines.size < lineCount) {\n                                        paddedLines.add(\"\")\n                                    }\n                                    return@withContext Result.success(paddedLines)\n                                }\n                            }\n                        }\n                    }\n                } catch (e: Exception) {\n                    if (currentAttempt == maxRetries - 1) {\n                        return@withContext Result.failure(e)\n                    }\n                }\n                currentAttempt++\n                kotlinx.coroutines.delay(1000L * currentAttempt)\n            }\n            return@withContext Result.failure(Exception(\"Max retries exceeded\"))\n        }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/api/OpenRouterStreamingService.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.api\n\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.flow\nimport kotlinx.coroutines.flow.flowOn\nimport kotlinx.serialization.json.*\nimport okhttp3.MediaType.Companion.toMediaType\nimport okhttp3.OkHttpClient\nimport okhttp3.Request\nimport okhttp3.RequestBody.Companion.toRequestBody\nimport org.json.JSONArray\nimport org.json.JSONObject\nimport timber.log.Timber\nimport java.io.BufferedReader\nimport java.io.InputStreamReader\nimport java.util.concurrent.TimeUnit\n\nobject OpenRouterStreamingService {\n    private val client =\n        OkHttpClient\n            .Builder()\n            .connectTimeout(30, TimeUnit.SECONDS)\n            .readTimeout(120, TimeUnit.SECONDS)\n            .writeTimeout(30, TimeUnit.SECONDS)\n            .build()\n    private val JSON = \"application/json; charset=utf-8\".toMediaType()\n    private val json = Json { ignoreUnknownKeys = true }\n\n    /**\n     * Stream translation from OpenRouter with real-time updates\n     */\n    fun streamTranslation(\n        text: String,\n        targetLanguage: String,\n        apiKey: String,\n        baseUrl: String,\n        model: String,\n        mode: String,\n        customSystemPrompt: String = \"\",\n    ): Flow<StreamChunk> =\n        flow {\n            if (text.isBlank()) {\n                emit(StreamChunk.Error(\"Input text is empty\"))\n                return@flow\n            }\n\n            val lines = text.lines()\n            val lineCount = lines.size\n\n            Timber.d(\"Starting streaming translation for $lineCount lines\")\n\n            try {\n                // Use custom system prompt if provided, otherwise use the default\n                val systemPrompt =\n                    if (customSystemPrompt.isNotBlank()) {\n                        customSystemPrompt.replace(\"{lineCount}\", lineCount.toString())\n                    } else {\n                        \"\"\"You are a precise lyrics translation assistant. Your output must ALWAYS be a valid JSON array of strings.\n\nCRITICAL RULES:\n1. Output ONLY a JSON array: [\"line1\", \"line2\", \"line3\"]\n2. NO explanations, NO questions, NO additional text\n3. Each input line maps to exactly one output line\n4. Preserve empty lines as empty strings \"\"\n5. Return EXACTLY $lineCount items in the array\n6. If uncertain, provide best approximation but maintain line count\"\"\"\n                    }\n\n                val userPrompt =\n                    when (mode) {\n                        \"Transcribed\" -> {\n                            \"\"\"Transcribe/transliterate the following $lineCount lines phonetically into $targetLanguage script.\n\nCRITICAL REQUIREMENTS:\n- Convert the SOUND/PRONUNCIATION of the original text into $targetLanguage script\n- DO NOT translate the meaning - only represent how the original words SOUND\n- Use the native script of $targetLanguage\n- Preserve the original pronunciation as closely as possible\n- Keep punctuation and formatting\n\nInput ($lineCount lines):\n$text\n\nOutput MUST be a JSON array with EXACTLY $lineCount strings.\"\"\"\n                        }\n\n                        else -> {\n                            \"\"\"Translate the following $lineCount lines to $targetLanguage.\n\nIMPORTANT:\n- Provide natural, accurate translation\n- Maintain poetic flow and meaning\n- Keep punctuation appropriate for target language\n- Preserve line-by-line structure exactly\n\nInput ($lineCount lines):\n$text\n\nOutput MUST be a JSON array with EXACTLY $lineCount strings.\"\"\"\n                        }\n                    }\n\n                val messages =\n                    JSONArray().apply {\n                        put(\n                            JSONObject().apply {\n                                put(\"role\", \"system\")\n                                put(\"content\", systemPrompt)\n                            },\n                        )\n                        put(\n                            JSONObject().apply {\n                                put(\"role\", \"user\")\n                                put(\"content\", userPrompt)\n                            },\n                        )\n                    }\n\n                val jsonBody =\n                    JSONObject().apply {\n                        if (model.isNotBlank()) {\n                            put(\"model\", model)\n                        }\n                        put(\"messages\", messages)\n                        put(\"stream\", true)\n                        put(\"temperature\", 0.3)\n                        put(\"max_tokens\", lineCount * 100)\n                    }\n\n                val request =\n                    Request\n                        .Builder()\n                        .url(baseUrl.ifBlank { \"https://openrouter.ai/api/v1/chat/completions\" })\n                        .apply {\n                            if (apiKey.isNotBlank()) {\n                                addHeader(\"Authorization\", \"Bearer ${apiKey.trim()}\")\n                            }\n                        }.addHeader(\"Content-Type\", \"application/json\")\n                        .addHeader(\"HTTP-Referer\", \"https://github.com/MetrolistGroup/Metrolist\")\n                        .addHeader(\"X-Title\", \"Metrolist\")\n                        .post(jsonBody.toString().toRequestBody(JSON))\n                        .build()\n\n                client.newCall(request).execute().use { response ->\n                    Timber.d(\"Got streaming response: ${response.code}\")\n\n                    if (!response.isSuccessful) {\n                        val errorMsg =\n                            try {\n                                JSONObject(response.body?.string() ?: \"\")\n                                    .optJSONObject(\"error\")\n                                    ?.optString(\"message\")\n                                    ?: \"HTTP ${response.code}: ${response.message}\"\n                            } catch (e: Exception) {\n                                \"HTTP ${response.code}: ${response.message}\"\n                            }\n                        emit(StreamChunk.Error(\"Translation failed: $errorMsg\"))\n                        return@flow\n                    }\n\n                    val reader = BufferedReader(InputStreamReader(response.body?.byteStream()))\n                    var line: String?\n                    val contentBuilder = StringBuilder()\n                    var chunkCount = 0\n\n                    while (reader.readLine().also { line = it } != null) {\n                        line?.let { currentLine ->\n                            if (currentLine.startsWith(\"data: \")) {\n                                val data = currentLine.substring(6)\n                                if (data == \"[DONE]\") {\n                                    Timber.d(\"Streaming complete, received $chunkCount chunks\")\n                                    // Processing complete, parse the full content\n                                    val fullContent = contentBuilder.toString()\n                                    Timber.d(\"Full content length: ${fullContent.length}\")\n                                    val result = parseTranslationContent(fullContent, lineCount)\n                                    result\n                                        .onSuccess { translatedLines ->\n                                            Timber.d(\"Successfully parsed ${translatedLines.size} lines\")\n                                            emit(StreamChunk.Complete(translatedLines))\n                                        }.onFailure { error ->\n                                            Timber.e(error, \"Failed to parse translation\")\n                                            emit(StreamChunk.Error(error.message ?: \"Parsing failed\"))\n                                        }\n                                    return@flow\n                                }\n\n                                try {\n                                    val jsonObject = json.parseToJsonElement(data).jsonObject\n                                    val choices = jsonObject[\"choices\"]?.jsonArray\n                                    val delta =\n                                        choices\n                                            ?.get(0)\n                                            ?.jsonObject\n                                            ?.get(\"delta\")\n                                            ?.jsonObject\n                                    val content = delta?.get(\"content\")?.jsonPrimitive?.content\n\n                                    content?.let { chunk ->\n                                        contentBuilder.append(chunk)\n                                        chunkCount++\n                                        emit(StreamChunk.Content(chunk))\n                                    }\n                                } catch (e: Exception) {\n                                    // Ignore malformed JSON chunks\n                                    Timber.v(\"Ignored malformed chunk: ${e.message}\")\n                                }\n                            }\n                        }\n                    }\n\n                    // If we got here without seeing [DONE], try to parse what we have\n                    if (contentBuilder.isNotEmpty()) {\n                        Timber.w(\"Stream ended without [DONE] marker, attempting to parse content\")\n                        val fullContent = contentBuilder.toString()\n                        val result = parseTranslationContent(fullContent, lineCount)\n                        result\n                            .onSuccess { translatedLines ->\n                                emit(StreamChunk.Complete(translatedLines))\n                            }.onFailure { error ->\n                                emit(StreamChunk.Error(error.message ?: \"Parsing failed\"))\n                            }\n                    }\n                }\n            } catch (e: Exception) {\n                Timber.e(e, \"Streaming error\")\n                emit(StreamChunk.Error(e.message ?: \"Unknown error\"))\n            }\n        }.flowOn(Dispatchers.IO)\n\n    private fun parseTranslationContent(\n        content: String,\n        expectedLineCount: Int,\n    ): Result<List<String>> {\n        var translatedLines: List<String>? = null\n\n        // Strategy 1: Try direct JSON parsing\n        try {\n            val jsonArray = JSONArray(content.trim())\n            translatedLines = (0 until jsonArray.length()).map { jsonArray.optString(it) }\n        } catch (e: Exception) {\n            // Strategy 2: Extract JSON from markdown code blocks\n            var cleanedContent = content.replace(\"```json\", \"\").replace(\"```\", \"\").trim()\n\n            try {\n                val jsonArray = JSONArray(cleanedContent)\n                translatedLines = (0 until jsonArray.length()).map { jsonArray.optString(it) }\n            } catch (e2: Exception) {\n                // Strategy 3: Find first [ and last ]\n                val startIdx = cleanedContent.indexOf('[')\n                val endIdx = cleanedContent.lastIndexOf(']')\n\n                if (startIdx != -1 && endIdx != -1 && endIdx > startIdx) {\n                    val jsonString = cleanedContent.substring(startIdx, endIdx + 1)\n                    try {\n                        val jsonArray = JSONArray(jsonString)\n                        translatedLines = (0 until jsonArray.length()).map { jsonArray.optString(it) }\n                    } catch (e3: Exception) {\n                        // Strategy 4: Manual line-by-line parsing as last resort\n                        translatedLines =\n                            cleanedContent\n                                .lines()\n                                .filter { it.trim().isNotEmpty() }\n                                .map { it.trim().removeSurrounding(\"\\\"\").removeSurrounding(\"'\") }\n                    }\n                }\n            }\n        }\n\n        if (translatedLines == null) {\n            return Result.failure(Exception(\"Failed to parse translation\"))\n        }\n\n        // Adjust line count\n        return when {\n            translatedLines.size == expectedLineCount -> {\n                Result.success(translatedLines)\n            }\n\n            translatedLines.size > expectedLineCount -> {\n                Result.success(translatedLines.take(expectedLineCount))\n            }\n\n            else -> {\n                val paddedLines = translatedLines.toMutableList()\n                while (paddedLines.size < expectedLineCount) {\n                    paddedLines.add(\"\")\n                }\n                Result.success(paddedLines)\n            }\n        }\n    }\n\n    sealed class StreamChunk {\n        data class Content(\n            val text: String,\n        ) : StreamChunk()\n\n        data class Complete(\n            val translatedLines: List<String>,\n        ) : StreamChunk()\n\n        data class Error(\n            val message: String,\n        ) : StreamChunk()\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/constants/Dimensions.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.constants\n\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.animation.core.spring\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\nconst val CONTENT_TYPE_HEADER = 0\nconst val CONTENT_TYPE_LIST = 1\nconst val CONTENT_TYPE_SONG = 2\nconst val CONTENT_TYPE_ARTIST = 3\nconst val CONTENT_TYPE_ALBUM = 4\nconst val CONTENT_TYPE_PLAYLIST = 5\n\nval NavigationBarHeight = 80.dp\nval SlimNavBarHeight = 64.dp\nval MiniPlayerHeight = 64.dp\nval MinMiniPlayerHeight = 16.dp\nval MiniPlayerBottomSpacing = 8.dp // Space between MiniPlayer and NavigationBar\nval QueuePeekHeight = 64.dp\nval AppBarHeight = 64.dp\n\nval ListItemHeight = 64.dp\nval SuggestionItemHeight = 56.dp\nval SearchFilterHeight = 48.dp\nval ListThumbnailSize = 48.dp\nval SmallGridThumbnailHeight = 104.dp\nval GridThumbnailHeight = 128.dp\nval AlbumThumbnailSize = 144.dp\n\nval ThumbnailCornerRadius = 3.dp\n\nval PlayerHorizontalPadding = 32.dp\n\nval NavigationBarAnimationSpec = spring<Dp>(\n    dampingRatio = Spring.DampingRatioNoBouncy,\n    stiffness = Spring.StiffnessLow\n)\n\nval BottomSheetAnimationSpec = spring<Dp>(\n    dampingRatio = Spring.DampingRatioNoBouncy,\n    stiffness = Spring.StiffnessMediumLow\n)\n\nval BottomSheetSoftAnimationSpec = spring<Dp>(\n    dampingRatio = Spring.DampingRatioNoBouncy,\n    stiffness = Spring.StiffnessLow\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/constants/HistorySource.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.constants\n\nenum class HistorySource {\n    LOCAL, REMOTE\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/constants/LibraryFilter.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.constants\n\nenum class LibraryFilter {\n    SONGS,\n    ARTISTS,\n    ALBUMS,\n    PLAYLISTS,\n    PODCASTS,\n    LIBRARY,\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/constants/MediaSessionConstants.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.constants\n\nimport android.os.Bundle\nimport androidx.media3.session.SessionCommand\n\nobject MediaSessionConstants {\n    const val ACTION_TOGGLE_LIBRARY = \"TOGGLE_LIBRARY\"\n    const val ACTION_TOGGLE_START_RADIO = \"TOGGLE_START_RADIO\"\n    const val ACTION_TOGGLE_LIKE = \"TOGGLE_LIKE\"\n    const val ACTION_TOGGLE_SHUFFLE = \"TOGGLE_SHUFFLE\"\n    const val ACTION_TOGGLE_REPEAT_MODE = \"TOGGLE_REPEAT_MODE\"\n    const val ACTION_ADD_TO_TARGET_PLAYLIST = \"ADD_TO_TARGET_PLAYLIST\"\n\n    val CommandToggleLibrary = SessionCommand(ACTION_TOGGLE_LIBRARY, Bundle.EMPTY)\n    val CommandToggleLike = SessionCommand(ACTION_TOGGLE_LIKE, Bundle.EMPTY)\n    val CommandToggleStartRadio = SessionCommand(ACTION_TOGGLE_START_RADIO, Bundle.EMPTY)\n    val CommandToggleShuffle = SessionCommand(ACTION_TOGGLE_SHUFFLE, Bundle.EMPTY)\n    val CommandToggleRepeatMode = SessionCommand(ACTION_TOGGLE_REPEAT_MODE, Bundle.EMPTY)\n    val CommandAddToTargetPlaylist = SessionCommand(ACTION_ADD_TO_TARGET_PLAYLIST, Bundle.EMPTY)\n\n    const val TARGET_PLAYLIST_AUTO = \"auto\"\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/constants/PreferenceKeys.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.constants\n\nimport androidx.datastore.preferences.core.booleanPreferencesKey\nimport androidx.datastore.preferences.core.floatPreferencesKey\nimport androidx.datastore.preferences.core.intPreferencesKey\nimport androidx.datastore.preferences.core.longPreferencesKey\nimport androidx.datastore.preferences.core.stringPreferencesKey\nimport java.time.LocalDateTime\nimport java.time.ZoneOffset\n\nval EnableDynamicIconKey = booleanPreferencesKey(\"enableDynamicIcon\")\nval EnableHighRefreshRateKey = booleanPreferencesKey(\"enableHighRefreshRate\")\nval DynamicThemeKey = booleanPreferencesKey(\"dynamicTheme\")\nval SelectedThemeColorKey = intPreferencesKey(\"selectedThemeColor\")\nval DarkModeKey = stringPreferencesKey(\"darkMode\")\nval PureBlackKey = booleanPreferencesKey(\"pureBlack\")\nval PureBlackMiniPlayerKey = booleanPreferencesKey(\"pureBlackMiniPlayer\")\nval MiniPlayerOutlineKey = booleanPreferencesKey(\"miniPlayerOutline\")\nval DensityScaleKey = floatPreferencesKey(\"density_scale_factor\")\nval CustomDensityScaleKey = floatPreferencesKey(\"custom_density_scale_value\")\n\nenum class DensityScale(\n    val value: Float,\n    val label: String,\n) {\n    NATIVE(1.0f, \"Native (100%)\"),\n    SLIGHTLY_COMPACT(0.85f, \"Slightly Compact (85%)\"),\n    COMPACT(0.75f, \"Compact (75%)\"),\n    VERY_COMPACT(0.65f, \"Very Compact (65%)\"),\n    ULTRA_COMPACT(0.55f, \"Ultra Compact (55%)\"),\n    ;\n\n    companion object {\n        fun fromValue(value: Float): DensityScale = entries.find { it.value == value } ?: NATIVE\n    }\n}\n\nval DefaultOpenTabKey = stringPreferencesKey(\"defaultOpenTab\")\nval SlimNavBarKey = booleanPreferencesKey(\"slimNavBar\")\nval GridItemsSizeKey = stringPreferencesKey(\"gridItemSize\")\nval SliderStyleKey = stringPreferencesKey(\"sliderStyle\")\nval SquigglySliderKey = booleanPreferencesKey(\"squigglySlider\")\nval SwipeToSongKey = booleanPreferencesKey(\"SwipeToSong\")\nval SwipeToRemoveSongKey = booleanPreferencesKey(\"SwipeToRemoveSong\")\nval UseNewPlayerDesignKey = booleanPreferencesKey(\"useNewPlayerDesign\")\nval UseNewMiniPlayerDesignKey = booleanPreferencesKey(\"useNewMiniPlayerDesign\")\nval HidePlayerThumbnailKey = booleanPreferencesKey(\"hidePlayerThumbnail\")\nval CropAlbumArtKey = booleanPreferencesKey(\"cropAlbumArt\")\nval SeekExtraSeconds = booleanPreferencesKey(\"seekExtraSeconds\")\nval PauseOnMute = booleanPreferencesKey(\"pauseOnMute\")\nval ResumeOnBluetoothConnectKey = booleanPreferencesKey(\"resumeOnBluetoothConnect\")\nval KeepScreenOn = booleanPreferencesKey(\"keepScreenOn\")\nval AlarmEnabledKey = booleanPreferencesKey(\"alarmEnabled\")\nval AlarmHourKey = intPreferencesKey(\"alarmHour\")\nval AlarmMinuteKey = intPreferencesKey(\"alarmMinute\")\nval AlarmPlaylistIdKey = stringPreferencesKey(\"alarmPlaylistId\")\nval AlarmRandomSongKey = booleanPreferencesKey(\"alarmRandomSong\")\nval AlarmNextTriggerAtKey = longPreferencesKey(\"alarmNextTriggerAt\")\nval AlarmEntriesKey = stringPreferencesKey(\"alarmEntries\")\nval DeveloperModeKey = booleanPreferencesKey(\"developerMode\")\n\nenum class SliderStyle {\n    DEFAULT,\n    WAVY,\n    SLIM,\n}\n\nconst val SYSTEM_DEFAULT = \"SYSTEM_DEFAULT\"\nval AppLanguageKey = stringPreferencesKey(\"appLanguage\")\nval ContentLanguageKey = stringPreferencesKey(\"contentLanguage\")\nval ContentCountryKey = stringPreferencesKey(\"contentCountry\")\nval EnableKugouKey = booleanPreferencesKey(\"enableKugou\")\nval EnableLrcLibKey = booleanPreferencesKey(\"enableLrclib\")\nval EnableBetterLyricsKey = booleanPreferencesKey(\"enableBetterLyrics\")\nval EnableSimpMusicKey = booleanPreferencesKey(\"enableSimpMusic\")\nval EnableLyricsPlus = booleanPreferencesKey(\"enableLyricsPlus\")\nval HideExplicitKey = booleanPreferencesKey(\"hideExplicit\")\nval HideVideoSongsKey = booleanPreferencesKey(\"hideVideoSongs\")\nval HideYoutubeShortsKey = booleanPreferencesKey(\"hideYoutubeShorts\")\nval ShowArtistDescriptionKey = booleanPreferencesKey(\"showArtistDescription\")\nval ShowArtistSubscriberCountKey = booleanPreferencesKey(\"showArtistSubscriberCount\")\nval ShowMonthlyListenersKey = booleanPreferencesKey(\"showMonthlyListeners\")\nval ProxyEnabledKey = booleanPreferencesKey(\"proxyEnabled\")\nval ProxyUrlKey = stringPreferencesKey(\"proxyUrl\")\nval ProxyTypeKey = stringPreferencesKey(\"proxyType\")\nval ProxyUsernameKey = stringPreferencesKey(\"proxyUsername\")\nval ProxyPasswordKey = stringPreferencesKey(\"proxyPassword\")\nval YtmSyncKey = booleanPreferencesKey(\"ytmSync\")\nval SelectedYtmPlaylistsKey = stringPreferencesKey(\"selectedYtmPlaylists\")\nval CheckForUpdatesKey = booleanPreferencesKey(\"checkForUpdates\")\nval UpdateNotificationsEnabledKey = booleanPreferencesKey(\"updateNotifications\")\nval LastUpdateCheckTimeKey = longPreferencesKey(\"lastUpdateCheckTime\")\n\nval AudioQualityKey = stringPreferencesKey(\"audioQuality\")\n\nenum class AudioQuality {\n    AUTO,\n    HIGH,\n    LOW,\n}\n\nval AudioOffload = booleanPreferencesKey(\"enableOffload\")\n\nval PersistentQueueKey = booleanPreferencesKey(\"persistentQueue\")\nval PersistentShuffleAcrossQueuesKey = booleanPreferencesKey(\"persistentShuffleAcrossQueues\")\nval RememberShuffleAndRepeatKey = booleanPreferencesKey(\"rememberShuffleAndRepeat\")\nval ShuffleModeKey = booleanPreferencesKey(\"shuffleMode\")\nval SkipSilenceKey = booleanPreferencesKey(\"skipSilence\")\nval SkipSilenceInstantKey = booleanPreferencesKey(\"skipSilenceInstant\")\nval AudioNormalizationKey = booleanPreferencesKey(\"audioNormalization\")\nval AutoLoadMoreKey = booleanPreferencesKey(\"autoLoadMore\")\nval DisableLoadMoreWhenRepeatAllKey = booleanPreferencesKey(\"disableLoadMoreWhenRepeatAll\")\nval AutoDownloadOnLikeKey = booleanPreferencesKey(\"autoDownloadOnLike\")\nval SimilarContent = booleanPreferencesKey(\"similarContent\")\nval AutoSkipNextOnErrorKey = booleanPreferencesKey(\"autoSkipNextOnError\")\nval StopMusicOnTaskClearKey = booleanPreferencesKey(\"stopMusicOnTaskClear\")\nval ShufflePlaylistFirstKey = booleanPreferencesKey(\"shufflePlaylistFirst\")\nval PreventDuplicateTracksInQueueKey = booleanPreferencesKey(\"preventDuplicateTracksInQueue\")\nval CrossfadeEnabledKey = booleanPreferencesKey(\"crossfadeEnabled\")\nval CrossfadeDurationKey = floatPreferencesKey(\"crossfadeDurationFloat\")\nval CrossfadeGaplessKey = booleanPreferencesKey(\"crossfadeGapless\")\n\nval MaxImageCacheSizeKey = intPreferencesKey(\"maxImageCacheSize\")\nval MaxSongCacheSizeKey = intPreferencesKey(\"maxSongCacheSize\")\nval EnableSongCacheKey = booleanPreferencesKey(\"enableSongCache\")\n\nval PauseListenHistoryKey = booleanPreferencesKey(\"pauseListenHistory\")\nval PauseSearchHistoryKey = booleanPreferencesKey(\"pauseSearchHistory\")\nval DisableScreenshotKey = booleanPreferencesKey(\"disableScreenshot\")\n\nval DiscordTokenKey = stringPreferencesKey(\"discordToken\")\nval DiscordInfoDismissedKey = booleanPreferencesKey(\"discordInfoDismissed\")\nval DiscordUsernameKey = stringPreferencesKey(\"discordUsername\")\nval DiscordNameKey = stringPreferencesKey(\"discordName\")\nval EnableDiscordRPCKey = booleanPreferencesKey(\"discordRPCEnable\")\nval DiscordUseDetailsKey = booleanPreferencesKey(\"discordUseDetails\")\nval DiscordAvatarKey = stringPreferencesKey(\"discordAvatar\")\nval DiscordStatusKey = stringPreferencesKey(\"discordStatus\")\nval DiscordButton1TextKey = stringPreferencesKey(\"discordButton1Text\")\nval DiscordButton1VisibleKey = booleanPreferencesKey(\"discordButton1Visible\")\nval DiscordButton2TextKey = stringPreferencesKey(\"discordButton2Text\")\nval DiscordButton2VisibleKey = booleanPreferencesKey(\"discordButton2Visible\")\nval DiscordActivityTypeKey = stringPreferencesKey(\"discordActivityType\")\nval DiscordActivityNameKey = stringPreferencesKey(\"discordActivityName\")\nval DiscordAdvancedModeKey = booleanPreferencesKey(\"discordAdvancedMode\")\n\n// Google Cast\nval EnableGoogleCastKey = booleanPreferencesKey(\"enableGoogleCast\")\n\n// Listen Together\nval ListenTogetherServerUrlKey = stringPreferencesKey(\"listenTogetherServerUrl\")\nval ListenTogetherUsernameKey = stringPreferencesKey(\"listenTogetherUsername\")\nval EnableListenTogetherKey = booleanPreferencesKey(\"enableListenTogether\")\nval ListenTogetherAutoApprovalKey = booleanPreferencesKey(\"listenTogetherAutoApproval\")\nval ListenTogetherAutoApproveSuggestionsKey = booleanPreferencesKey(\"listenTogetherAutoApproveSuggestions\")\nval ListenTogetherSyncVolumeKey = booleanPreferencesKey(\"listenTogetherSyncVolume\")\nval ListenTogetherBlockedUsersKey = stringPreferencesKey(\"listenTogetherBlockedUsers\")\nval ListenTogetherInTopBarKey = booleanPreferencesKey(\"listenTogetherInTopBar\")\n\n// Session persistence for reconnection\nval ListenTogetherSessionTokenKey = stringPreferencesKey(\"listenTogetherSessionToken\")\nval ListenTogetherRoomCodeKey = stringPreferencesKey(\"listenTogetherRoomCode\")\nval ListenTogetherUserIdKey = stringPreferencesKey(\"listenTogetherUserId\")\nval ListenTogetherIsHostKey = booleanPreferencesKey(\"listenTogetherIsHost\")\nval ListenTogetherSessionTimestampKey = longPreferencesKey(\"listenTogetherSessionTimestamp\")\n\nval LastFMSessionKey = stringPreferencesKey(\"lastfmSession\")\nval LastFMUsernameKey = stringPreferencesKey(\"lastfmUsername\")\nval EnableLastFMScrobblingKey = booleanPreferencesKey(\"lastfmScrobblingEnable\")\nval LastFMUseNowPlaying = booleanPreferencesKey(\"lastfmUseNowPlaying\")\n\nval LastFMUseSendLikes = booleanPreferencesKey(\"lastfmUseSendLikes\")\n\nval ScrobbleDelayPercentKey = floatPreferencesKey(\"scrobbleDelayPercent\")\nval ScrobbleMinSongDurationKey = intPreferencesKey(\"scrobbleMinSongDuration\")\nval ScrobbleDelaySecondsKey = intPreferencesKey(\"scrobbleDelaySeconds\")\n\nval ChipSortTypeKey = stringPreferencesKey(\"chipSortType\")\nval SongSortTypeKey = stringPreferencesKey(\"songSortType\")\nval SongSortDescendingKey = booleanPreferencesKey(\"songSortDescending\")\nval PlaylistSongSortTypeKey = stringPreferencesKey(\"playlistSongSortType\")\nval PlaylistSongSortDescendingKey = booleanPreferencesKey(\"playlistSongSortDescending\")\nval AutoPlaylistSongSortTypeKey = stringPreferencesKey(\"autoPlaylistSongSortType\")\nval AutoPlaylistSongSortDescendingKey = booleanPreferencesKey(\"autoPlaylistSongSortDescending\")\nval ArtistSortTypeKey = stringPreferencesKey(\"artistSortType\")\nval ArtistSortDescendingKey = booleanPreferencesKey(\"artistSortDescending\")\nval AlbumSortTypeKey = stringPreferencesKey(\"albumSortType\")\nval AlbumSortDescendingKey = booleanPreferencesKey(\"albumSortDescending\")\nval PlaylistSortTypeKey = stringPreferencesKey(\"playlistSortType\")\nval PlaylistSortDescendingKey = booleanPreferencesKey(\"playlistSortDescending\")\nval AddToPlaylistSortTypeKey = stringPreferencesKey(\"addToPlaylistSortType\")\nval AddToPlaylistSortDescendingKey = booleanPreferencesKey(\"addToPlaylistSortDescending\")\nval ArtistSongSortTypeKey = stringPreferencesKey(\"artistSongSortType\")\nval ArtistSongSortDescendingKey = booleanPreferencesKey(\"artistSongSortDescending\")\nval MixSortTypeKey = stringPreferencesKey(\"mixSortType\")\nval MixSortDescendingKey = booleanPreferencesKey(\"albumSortDescending\")\n\nval SongFilterKey = stringPreferencesKey(\"songFilter\")\nval ArtistFilterKey = stringPreferencesKey(\"artistFilter\")\nval AlbumFilterKey = stringPreferencesKey(\"albumFilter\")\nval PodcastFilterKey = stringPreferencesKey(\"podcastFilter\")\n\nval LastLikeSongSyncKey = longPreferencesKey(\"last_like_song_sync\")\nval LastLibSongSyncKey = longPreferencesKey(\"last_library_song_sync\")\nval LastAlbumSyncKey = longPreferencesKey(\"last_album_sync\")\nval LastArtistSyncKey = longPreferencesKey(\"last_artist_sync\")\nval LastPlaylistSyncKey = longPreferencesKey(\"last_playlist_sync\")\nval LastFullSyncKey = longPreferencesKey(\"last_full_sync\")\nval LastWeeklyMostPlaylistSyncKey = longPreferencesKey(\"last_weekly_most_playlist_sync\")\nval LastMonthlyMostPlaylistSyncKey = longPreferencesKey(\"last_monthly_most_playlist_sync\")\n\n// Sync cooldown in seconds (30 minutes)\nconst val SYNC_COOLDOWN = 30 * 60L\n\nval ArtistViewTypeKey = stringPreferencesKey(\"artistViewType\")\nval AlbumViewTypeKey = stringPreferencesKey(\"albumViewType\")\nval PlaylistViewTypeKey = stringPreferencesKey(\"playlistViewType\")\n\nval PlaylistEditLockKey = booleanPreferencesKey(\"playlistEditLock\")\nval QuickPicksKey = stringPreferencesKey(\"discover\")\nval PreferredLyricsProviderKey = stringPreferencesKey(\"lyricsProvider\")\nval LyricsProviderOrderKey = stringPreferencesKey(\"lyricsProviderOrder\")\nval QueueEditLockKey = booleanPreferencesKey(\"queueEditLock\")\nval ShowWrappedCardKey = booleanPreferencesKey(\"show_wrapped_card\")\nval WrappedSeenKey = booleanPreferencesKey(\"wrapped_seen\")\nval LastSeenVersionKey = stringPreferencesKey(\"lastSeenVersion\")\nval RandomizeHomeOrderKey = booleanPreferencesKey(\"randomizeHomeOrder\")\n\nval ShowLikedPlaylistKey = booleanPreferencesKey(\"show_liked_playlist\")\nval ShowDownloadedPlaylistKey = booleanPreferencesKey(\"show_downloaded_playlist\")\nval ShowTopPlaylistKey = booleanPreferencesKey(\"show_top_playlist\")\nval ShowCachedPlaylistKey = booleanPreferencesKey(\"show_cached_playlist\")\nval ShowUploadedPlaylistKey = booleanPreferencesKey(\"show_uploaded_playlist\")\n\nenum class LibraryViewType {\n    LIST,\n    GRID,\n    ;\n\n    fun toggle() =\n        when (this) {\n            LIST -> GRID\n            GRID -> LIST\n        }\n}\n\nenum class SongFilter {\n    LIBRARY,\n    LIKED,\n    DOWNLOADED,\n    UPLOADED,\n}\n\nenum class ArtistFilter {\n    LIBRARY,\n    LIKED,\n}\n\nenum class AlbumFilter {\n    LIBRARY,\n    LIKED,\n    UPLOADED,\n}\n\nenum class PodcastFilter {\n    EPISODES,\n    CHANNELS,\n    DOWNLOADED,\n}\n\nenum class SongSortType {\n    CREATE_DATE,\n    NAME,\n    ARTIST,\n    PLAY_TIME,\n}\n\nenum class PlaylistSongSortType {\n    CUSTOM,\n    CREATE_DATE,\n    NAME,\n    ARTIST,\n    PLAY_TIME,\n}\n\nenum class AutoPlaylistSongSortType {\n    CREATE_DATE,\n    NAME,\n    ARTIST,\n    PLAY_TIME,\n}\n\nenum class ArtistSortType {\n    CREATE_DATE,\n    NAME,\n    SONG_COUNT,\n    PLAY_TIME,\n}\n\nenum class ArtistSongSortType {\n    CREATE_DATE,\n    NAME,\n    PLAY_TIME,\n}\n\nenum class AlbumSortType {\n    CREATE_DATE,\n    NAME,\n    ARTIST,\n    YEAR,\n    SONG_COUNT,\n    LENGTH,\n    PLAY_TIME,\n}\n\nenum class PlaylistSortType {\n    CREATE_DATE,\n    NAME,\n    SONG_COUNT,\n    LAST_UPDATED,\n}\n\nenum class MixSortType {\n    CREATE_DATE,\n    NAME,\n    LAST_UPDATED,\n}\n\nenum class GridItemSize {\n    BIG,\n    SMALL,\n}\n\nenum class MyTopFilter {\n    ALL_TIME,\n    DAY,\n    WEEK,\n    MONTH,\n    YEAR,\n    ;\n\n    fun toTimeMillis(): Long =\n        when (this) {\n            DAY -> {\n                LocalDateTime\n                    .now()\n                    .minusDays(1)\n                    .toInstant(ZoneOffset.UTC)\n                    .toEpochMilli()\n            }\n\n            WEEK -> {\n                LocalDateTime\n                    .now()\n                    .minusWeeks(1)\n                    .toInstant(ZoneOffset.UTC)\n                    .toEpochMilli()\n            }\n\n            MONTH -> {\n                LocalDateTime\n                    .now()\n                    .minusMonths(1)\n                    .toInstant(ZoneOffset.UTC)\n                    .toEpochMilli()\n            }\n\n            YEAR -> {\n                LocalDateTime\n                    .now()\n                    .minusMonths(12)\n                    .toInstant(ZoneOffset.UTC)\n                    .toEpochMilli()\n            }\n\n            ALL_TIME -> {\n                0\n            }\n        }\n}\n\nenum class QuickPicks {\n    QUICK_PICKS,\n    LAST_LISTEN,\n}\n\nenum class PreferredLyricsProvider {\n    LRCLIB,\n    KUGOU,\n    BETTER_LYRICS,\n    SIMPMUSIC,\n}\n\nenum class PlayerButtonsStyle {\n    DEFAULT,\n    PRIMARY,\n    TERTIARY,\n}\n\nenum class PlayerBackgroundStyle {\n    DEFAULT,\n    GRADIENT,\n    BLUR,\n}\n\nval TopSize = stringPreferencesKey(\"topSize\")\nval HistoryDuration = floatPreferencesKey(\"historyDuration\")\n\nval PlayerButtonsStyleKey = stringPreferencesKey(\"player_buttons_style\")\nval PlayerBackgroundStyleKey = stringPreferencesKey(\"playerBackgroundStyle\")\nval ShowLyricsKey = booleanPreferencesKey(\"showLyrics\")\nval LyricsTextPositionKey = stringPreferencesKey(\"lyricsTextPosition\")\nval LyricsClickKey = booleanPreferencesKey(\"lyricsClick\")\nval LyricsScrollKey = booleanPreferencesKey(\"lyricsScrollKey\")\nval LyricsRomanizeAsMainKey = booleanPreferencesKey(\"lyricsRomanizeAsMain\")\nval LyricsRomanizeCyrillicByLineKey = booleanPreferencesKey(\"lyricsRomanizeCyrillicByLine\")\nval OpenRouterApiKey = stringPreferencesKey(\"openRouterApiKey\")\nval AiProviderKey = stringPreferencesKey(\"aiProvider\")\nval OpenRouterBaseUrlKey = stringPreferencesKey(\"openRouterBaseUrl\")\nval OpenRouterModelKey = stringPreferencesKey(\"openRouterModel\")\nval TranslateModeKey = stringPreferencesKey(\"translateMode\")\nval TranslateLanguageKey = stringPreferencesKey(\"translateLanguage\")\nval DeeplApiKey = stringPreferencesKey(\"deeplApiKey\")\nval DeeplFormalityKey = stringPreferencesKey(\"deeplFormality\")\nval AiSystemPromptKey = stringPreferencesKey(\"aiSystemPrompt\")\n\nconst val DEFAULT_AI_SYSTEM_PROMPT = \"\"\"You are a precise lyrics translation assistant. Your output must ALWAYS be a valid JSON array of strings.\n\nCRITICAL RULES:\n1. Output ONLY a JSON array: [\"line1\", \"line2\", \"line3\"]\n2. NO explanations, NO questions, NO additional text\n3. Each input line maps to exactly one output line\n4. Preserve empty lines as empty strings \"\"\n5. Return EXACTLY {lineCount} items in the array\n6. If uncertain, provide best approximation but maintain line count\"\"\"\nval LyricsGlowEffectKey = booleanPreferencesKey(\"lyricsGlowEffect\")\n\nval LyricsRomanizeList = stringPreferencesKey(\"lyricsRomanizeList\")\nval LyricsAnimationStyleKey = stringPreferencesKey(\"lyricsAnimationStyle\")\n\nenum class LyricsAnimationStyle {\n    NONE,\n    FADE,\n    GLOW,\n    SLIDE,\n    KARAOKE,\n    APPLE,\n}\n\nval LyricsTextSizeKey = floatPreferencesKey(\"lyricsTextSize\")\nval LyricsLineSpacingKey = floatPreferencesKey(\"lyricsLineSpacing\")\n\nval PlayerVolumeKey = floatPreferencesKey(\"playerVolume\")\nval SleepTimerDefaultKey = floatPreferencesKey(\"sleepTimerDefault\")\nval SleepTimerStopAfterCurrentSongKey = booleanPreferencesKey(\"sleepTimerStopAfterCurrentSong\")\nval SleepTimerFadeOutKey = booleanPreferencesKey(\"sleepTimerFadeOut\")\nval RepeatModeKey = intPreferencesKey(\"repeatMode\")\n\nval SearchSourceKey = stringPreferencesKey(\"searchSource\")\nval SwipeThumbnailKey = booleanPreferencesKey(\"swipeThumbnail\")\nval SwipeSensitivityKey = floatPreferencesKey(\"swipeSensitivity\")\nval SleepTimerEnabledKey = booleanPreferencesKey(\"sleepTimerEnabled\")\nval SleepTimerRepeatKey = stringPreferencesKey(\"sleepTimerRepeat\")\nval SleepTimerStartTimeKey = stringPreferencesKey(\"sleepTimerStartTime\")\nval SleepTimerEndTimeKey = stringPreferencesKey(\"sleepTimerEndTime\")\nval SleepTimerCustomDaysKey = stringPreferencesKey(\"sleepTimerCustomDays\")\nval SleepTimerDayTimesKey = stringPreferencesKey(\"sleepTimerDayTimes\")\n\nenum class SearchSource {\n    LOCAL,\n    ONLINE,\n    ;\n\n    fun toggle() =\n        when (this) {\n            LOCAL -> ONLINE\n            ONLINE -> LOCAL\n        }\n}\n\nval VisitorDataKey = stringPreferencesKey(\"visitorData\")\nval DataSyncIdKey = stringPreferencesKey(\"dataSyncId\")\nval AndroidAutoYouTubePlaylistsKey = booleanPreferencesKey(\"androidAutoYoutubePlaylists\")\nval AndroidAutoSectionsOrderKey = stringPreferencesKey(\"androidAutoSectionsOrder\")\nval AndroidAutoTargetPlaylistKey = stringPreferencesKey(\"androidAutoTargetPlaylist\")\nval InnerTubeCookieKey = stringPreferencesKey(\"innerTubeCookie\")\nval AccountNameKey = stringPreferencesKey(\"accountName\")\nval AccountEmailKey = stringPreferencesKey(\"accountEmail\")\nval AccountChannelHandleKey = stringPreferencesKey(\"accountChannelHandle\")\nval UseLoginForBrowse = booleanPreferencesKey(\"useLoginForBrowse\")\n\nval LanguageCodeToName =\n    mapOf(\n        \"af\" to \"Afrikaans\",\n        \"az\" to \"Azərbaycan\",\n        \"id\" to \"Bahasa Indonesia\",\n        \"ms\" to \"Bahasa Malaysia\",\n        \"ca\" to \"Català\",\n        \"cs\" to \"Čeština\",\n        \"da\" to \"Dansk\",\n        \"de\" to \"Deutsch\",\n        \"et\" to \"Eesti\",\n        \"en-GB\" to \"English (UK)\",\n        \"en\" to \"English (US)\",\n        \"es\" to \"Español (España)\",\n        \"es-419\" to \"Español (Latinoamérica)\",\n        \"eu\" to \"Euskara\",\n        \"fil\" to \"Filipino\",\n        \"fr\" to \"Français\",\n        \"fr-CA\" to \"Français (Canada)\",\n        \"gl\" to \"Galego\",\n        \"hr\" to \"Hrvatski\",\n        \"zu\" to \"IsiZulu\",\n        \"is\" to \"Íslenska\",\n        \"it\" to \"Italiano\",\n        \"sw\" to \"Kiswahili\",\n        \"lt\" to \"Lietuvių\",\n        \"hu\" to \"Magyar\",\n        \"nl\" to \"Nederlands\",\n        \"no\" to \"Norsk\",\n        \"or\" to \"Odia\",\n        \"uz\" to \"O‘zbe\",\n        \"pl\" to \"Polski\",\n        \"pt-PT\" to \"Português\",\n        \"pt\" to \"Português (Brasil)\",\n        \"ro\" to \"Română\",\n        \"sq\" to \"Shqip\",\n        \"sk\" to \"Slovenčina\",\n        \"sl\" to \"Slovenščina\",\n        \"fi\" to \"Suomi\",\n        \"sv\" to \"Svenska\",\n        \"bo\" to \"Tibetan བོད་སྐད།\",\n        \"vi\" to \"Tiếng Việt\",\n        \"tr\" to \"Türkçe\",\n        \"bg\" to \"Български\",\n        \"ky\" to \"Кыргызча\",\n        \"kk\" to \"Қазақ Тілі\",\n        \"mk\" to \"Македонски\",\n        \"mn\" to \"Монгол\",\n        \"ru\" to \"Русский\",\n        \"sr\" to \"Српски\",\n        \"uk\" to \"Українська\",\n        \"el\" to \"Ελληνικά\",\n        \"hy\" to \"Հայերեն\",\n        \"iw\" to \"עברית\",\n        \"ur\" to \"اردو\",\n        \"ar\" to \"العربية\",\n        \"fa\" to \"فارسی\",\n        \"ne\" to \"नेपाली\",\n        \"mr\" to \"मराठी\",\n        \"hi\" to \"हिन्दी\",\n        \"bn\" to \"বাংলা\",\n        \"pa\" to \"ਪੰਜਾਬੀ\",\n        \"gu\" to \"ગુજરાતી\",\n        \"ta\" to \"தமிழ்\",\n        \"te\" to \"తెలుగు\",\n        \"kn\" to \"ಕನ್ನಡ\",\n        \"ml\" to \"മലയാളം\",\n        \"si\" to \"සිංහල\",\n        \"th\" to \"ภาษาไทย\",\n        \"lo\" to \"ລາວ\",\n        \"my\" to \"ဗမာ\",\n        \"ka\" to \"ქართული\",\n        \"am\" to \"አማርኛ\",\n        \"km\" to \"ខ្មែរ\",\n        \"zh-CN\" to \"中文 (简体)\",\n        \"zh-TW\" to \"中文 (繁體)\",\n        \"zh-HK\" to \"中文 (香港)\",\n        \"ja\" to \"日本語\",\n        \"ko\" to \"한국어\",\n    )\n\nval CountryCodeToName =\n    mapOf(\n        \"DZ\" to \"Algeria\",\n        \"AR\" to \"Argentina\",\n        \"AU\" to \"Australia\",\n        \"AT\" to \"Austria\",\n        \"AZ\" to \"Azerbaijan\",\n        \"BH\" to \"Bahrain\",\n        \"BD\" to \"Bangladesh\",\n        \"BY\" to \"Belarus\",\n        \"BE\" to \"Belgium\",\n        \"BO\" to \"Bolivia\",\n        \"BA\" to \"Bosnia and Herzegovina\",\n        \"BR\" to \"Brazil\",\n        \"BG\" to \"Bulgaria\",\n        \"KH\" to \"Cambodia\",\n        \"CA\" to \"Canada\",\n        \"CL\" to \"Chile\",\n        \"HK\" to \"Hong Kong\",\n        \"CO\" to \"Colombia\",\n        \"CR\" to \"Costa Rica\",\n        \"HR\" to \"Croatia\",\n        \"CY\" to \"Cyprus\",\n        \"CZ\" to \"Czech Republic\",\n        \"DK\" to \"Denmark\",\n        \"DO\" to \"Dominican Republic\",\n        \"EC\" to \"Ecuador\",\n        \"EG\" to \"Egypt\",\n        \"SV\" to \"El Salvador\",\n        \"EE\" to \"Estonia\",\n        \"FI\" to \"Finland\",\n        \"FR\" to \"France\",\n        \"GE\" to \"Georgia\",\n        \"DE\" to \"Germany\",\n        \"GH\" to \"Ghana\",\n        \"GR\" to \"Greece\",\n        \"GT\" to \"Guatemala\",\n        \"HN\" to \"Honduras\",\n        \"HU\" to \"Hungary\",\n        \"IS\" to \"Iceland\",\n        \"IN\" to \"India\",\n        \"ID\" to \"Indonesia\",\n        \"IQ\" to \"Iraq\",\n        \"IE\" to \"Ireland\",\n        \"IL\" to \"Israel\",\n        \"IT\" to \"Italy\",\n        \"JM\" to \"Jamaica\",\n        \"JP\" to \"Japan\",\n        \"JO\" to \"Jordan\",\n        \"KZ\" to \"Kazakhstan\",\n        \"KE\" to \"Kenya\",\n        \"KR\" to \"South Korea\",\n        \"KW\" to \"Kuwait\",\n        \"LA\" to \"Lao\",\n        \"LV\" to \"Latvia\",\n        \"LB\" to \"Lebanon\",\n        \"LY\" to \"Libya\",\n        \"LI\" to \"Liechtenstein\",\n        \"LT\" to \"Lithuania\",\n        \"LU\" to \"Luxembourg\",\n        \"MK\" to \"Macedonia\",\n        \"MY\" to \"Malaysia\",\n        \"MT\" to \"Malta\",\n        \"MX\" to \"Mexico\",\n        \"ME\" to \"Montenegro\",\n        \"MA\" to \"Morocco\",\n        \"NP\" to \"Nepal\",\n        \"NL\" to \"Netherlands\",\n        \"NZ\" to \"New Zealand\",\n        \"NI\" to \"Nicaragua\",\n        \"NG\" to \"Nigeria\",\n        \"NO\" to \"Norway\",\n        \"OM\" to \"Oman\",\n        \"PK\" to \"Pakistan\",\n        \"PA\" to \"Panama\",\n        \"PG\" to \"Papua New Guinea\",\n        \"PY\" to \"Paraguay\",\n        \"PE\" to \"Peru\",\n        \"PH\" to \"Philippines\",\n        \"PL\" to \"Poland\",\n        \"PT\" to \"Portugal\",\n        \"PR\" to \"Puerto Rico\",\n        \"QA\" to \"Qatar\",\n        \"RO\" to \"Romania\",\n        \"RU\" to \"Russian Federation\",\n        \"SA\" to \"Saudi Arabia\",\n        \"SN\" to \"Senegal\",\n        \"RS\" to \"Serbia\",\n        \"SG\" to \"Singapore\",\n        \"SK\" to \"Slovakia\",\n        \"SI\" to \"Slovenia\",\n        \"ZA\" to \"South Africa\",\n        \"ES\" to \"Spain\",\n        \"LK\" to \"Sri Lanka\",\n        \"SE\" to \"Sweden\",\n        \"CH\" to \"Switzerland\",\n        \"TW\" to \"Taiwan\",\n        \"TZ\" to \"Tanzania\",\n        \"TH\" to \"Thailand\",\n        \"TN\" to \"Tunisia\",\n        \"TR\" to \"Turkey\",\n        \"UG\" to \"Uganda\",\n        \"UA\" to \"Ukraine\",\n        \"AE\" to \"United Arab Emirates\",\n        \"GB\" to \"United Kingdom\",\n        \"US\" to \"United States\",\n        \"UY\" to \"Uruguay\",\n        \"VE\" to \"Venezuela (Bolivarian Republic)\",\n        \"VN\" to \"Vietnam\",\n        \"YE\" to \"Yemen\",\n        \"ZW\" to \"Zimbabwe\",\n    )\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/constants/StatPeriod.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.constants\n\nimport com.metrolist.music.ui.screens.OptionStats\nimport java.time.LocalDateTime\nimport java.time.ZoneOffset\n\nenum class StatPeriod {\n    WEEK_1,\n    MONTH_1,\n    MONTH_3,\n    MONTH_6,\n    YEAR_1,\n    ALL,\n    ;\n\n    fun toTimeMillis(): Long =\n        when (this) {\n            WEEK_1 ->\n                LocalDateTime\n                    .now()\n                    .minusWeeks(1)\n                    .toInstant(ZoneOffset.UTC)\n                    .toEpochMilli()\n\n            MONTH_1 ->\n                LocalDateTime\n                    .now()\n                    .minusMonths(1)\n                    .toInstant(ZoneOffset.UTC)\n                    .toEpochMilli()\n\n            MONTH_3 ->\n                LocalDateTime\n                    .now()\n                    .minusMonths(3)\n                    .toInstant(ZoneOffset.UTC)\n                    .toEpochMilli()\n\n            MONTH_6 ->\n                LocalDateTime\n                    .now()\n                    .minusMonths(6)\n                    .toInstant(ZoneOffset.UTC)\n                    .toEpochMilli()\n\n            YEAR_1 ->\n                LocalDateTime\n                    .now()\n                    .minusMonths(12)\n                    .toInstant(ZoneOffset.UTC)\n                    .toEpochMilli()\n\n            ALL -> 0\n        }\n}\n\nfun statToPeriod(\n    selection: OptionStats,\n    test: Int,\n): Long =\n    when (selection) {\n        OptionStats.WEEKS -> {\n            LocalDateTime\n                .now()\n                .minusWeeks(test.toLong())\n                .minusDays(1)\n                .toInstant(ZoneOffset.UTC)\n                .toEpochMilli()\n        }\n\n        OptionStats.MONTHS -> {\n            LocalDateTime\n                .now()\n                .withDayOfMonth(1)\n                .minusMonths(test.toLong())\n                .toInstant(ZoneOffset.UTC)\n                .toEpochMilli()\n        }\n\n        OptionStats.YEARS -> {\n            LocalDateTime\n                .now()\n                .withDayOfMonth(1)\n                .withMonth(1)\n                .minusYears(test.toLong())\n                .toInstant(\n                    ZoneOffset.UTC,\n                ).toEpochMilli()\n        }\n\n        OptionStats.CONTINUOUS -> {\n            val index = if (test >= StatPeriod.entries.size) 0 else test\n            StatPeriod.entries[index].toTimeMillis()\n        }\n    }\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/Converters.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db\n\nimport androidx.room.TypeConverter\nimport java.time.Instant\nimport java.time.LocalDateTime\nimport java.time.ZoneOffset\n\nclass Converters {\n    @TypeConverter\n    fun fromTimestamp(value: Long?): LocalDateTime? =\n        if (value != null) {\n            LocalDateTime.ofInstant(Instant.ofEpochMilli(value), ZoneOffset.UTC)\n        } else {\n            null\n        }\n\n    @TypeConverter\n    fun dateToTimestamp(date: LocalDateTime?): Long? =\n        date?.atZone(ZoneOffset.UTC)?.toInstant()?.toEpochMilli()\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/DatabaseDao.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db\n\nimport androidx.room.Dao\nimport androidx.room.Delete\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.Query\nimport androidx.room.RawQuery\nimport androidx.room.RewriteQueriesToDropUnusedColumns\nimport androidx.room.RoomWarnings\nimport androidx.room.Transaction\nimport androidx.room.Update\nimport androidx.room.Upsert\nimport androidx.sqlite.db.SupportSQLiteQuery\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.pages.AlbumPage\nimport com.metrolist.innertube.pages.ArtistPage\nimport com.metrolist.music.constants.AlbumSortType\nimport com.metrolist.music.constants.ArtistSongSortType\nimport com.metrolist.music.constants.ArtistSortType\nimport com.metrolist.music.constants.PlaylistSortType\nimport com.metrolist.music.constants.SongSortType\nimport com.metrolist.music.db.entities.Album\nimport com.metrolist.music.db.entities.AlbumArtistMap\nimport com.metrolist.music.db.entities.AlbumEntity\nimport com.metrolist.music.db.entities.AlbumWithSongs\nimport com.metrolist.music.db.entities.Artist\nimport com.metrolist.music.db.entities.ArtistEntity\nimport com.metrolist.music.db.entities.Event\nimport com.metrolist.music.db.entities.EventWithSong\nimport com.metrolist.music.db.entities.FormatEntity\nimport com.metrolist.music.db.entities.LyricsEntity\nimport com.metrolist.music.db.entities.PlayCountEntity\nimport com.metrolist.music.db.entities.Playlist\nimport com.metrolist.music.db.entities.PlaylistEntity\nimport com.metrolist.music.db.entities.PlaylistSong\nimport com.metrolist.music.db.entities.PlaylistSongMap\nimport com.metrolist.music.db.entities.PodcastEntity\nimport com.metrolist.music.db.entities.RecognitionHistory\nimport com.metrolist.music.db.entities.RelatedSongMap\nimport com.metrolist.music.db.entities.SearchHistory\nimport com.metrolist.music.db.entities.SetVideoIdEntity\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.db.entities.SongAlbumMap\nimport com.metrolist.music.db.entities.SongArtistMap\nimport com.metrolist.music.db.entities.SongEntity\nimport com.metrolist.music.db.entities.SongWithStats\nimport com.metrolist.music.extensions.reversed\nimport com.metrolist.music.extensions.toSQLiteQuery\nimport com.metrolist.music.models.MediaMetadata\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.ui.utils.resize\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.runBlocking\nimport java.text.Collator\nimport java.time.LocalDateTime\nimport java.time.ZoneOffset\nimport java.util.Locale\n\n@Dao\ninterface DatabaseDao {\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE inLibrary IS NOT NULL ORDER BY rowId\")\n    fun songsByRowIdAsc(): Flow<List<Song>>\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE inLibrary IS NOT NULL ORDER BY inLibrary\")\n    fun songsByCreateDateAsc(): Flow<List<Song>>\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE inLibrary IS NOT NULL ORDER BY title\")\n    fun songsByNameAsc(): Flow<List<Song>>\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE inLibrary IS NOT NULL ORDER BY totalPlayTime\")\n    fun songsByPlayTimeAsc(): Flow<List<Song>>\n\n\n    fun songs(\n        sortType: SongSortType,\n        descending: Boolean,\n    ) = when (sortType) {\n        SongSortType.CREATE_DATE -> songsByCreateDateAsc()\n        SongSortType.NAME ->\n            songsByNameAsc().map { songs ->\n                val collator = Collator.getInstance(Locale.getDefault())\n                collator.strength = Collator.PRIMARY\n                songs.sortedWith(compareBy(collator) { it.song.title })\n            }\n\n        SongSortType.ARTIST ->\n            songsByRowIdAsc().map { songs ->\n                val collator = Collator.getInstance(Locale.getDefault())\n                collator.strength = Collator.PRIMARY\n                songs\n                    .sortedWith(\n                        compareBy(collator) { song ->\n                            song.orderedArtists.joinToString(\"\") { it.name }\n                        },\n                    ).groupBy { it.album?.title }\n                    .flatMap { (_, songsByAlbum) ->\n                        songsByAlbum.sortedBy { album ->\n                            album.orderedArtists.joinToString(\n                                \"\",\n                            ) { it.name }\n                        }\n                    }\n            }\n\n        SongSortType.PLAY_TIME -> songsByPlayTimeAsc()\n    }.map { it.reversed(descending) }\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE liked ORDER BY rowId\")\n    fun likedSongsByRowIdAsc(): Flow<List<Song>>\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE liked ORDER BY likedDate\")\n    fun likedSongsByCreateDateAsc(): Flow<List<Song>>\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE liked ORDER BY title\")\n    fun likedSongsByNameAsc(): Flow<List<Song>>\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE liked ORDER BY totalPlayTime\")\n    fun likedSongsByPlayTimeAsc(): Flow<List<Song>>\n\n    fun likedSongs(\n        sortType: SongSortType,\n        descending: Boolean,\n    ) = when (sortType) {\n        SongSortType.CREATE_DATE -> likedSongsByCreateDateAsc()\n        SongSortType.NAME ->\n            likedSongsByNameAsc().map { songs ->\n                val collator = Collator.getInstance(Locale.getDefault())\n                collator.strength = Collator.PRIMARY\n                songs.sortedWith(compareBy(collator) { it.song.title })\n            }\n\n        SongSortType.ARTIST ->\n            likedSongsByRowIdAsc().map { songs ->\n                val collator = Collator.getInstance(Locale.getDefault())\n                collator.strength = Collator.PRIMARY\n                songs\n                    .sortedWith(\n                        compareBy(collator) { song ->\n                            song.orderedArtists.joinToString(\"\") { it.name }\n                        },\n                    ).groupBy { it.album?.title }\n                    .flatMap { (_, songsByAlbum) ->\n                        songsByAlbum.sortedBy { album ->\n                            album.orderedArtists.joinToString(\n                                \"\",\n                            ) { it.name }\n                        }\n                    }\n            }\n\n        SongSortType.PLAY_TIME -> likedSongsByPlayTimeAsc()\n    }.map { it.reversed(descending) }\n\n    @Transaction\n    @Query(\"SELECT COUNT(1) FROM song WHERE liked\")\n    fun likedSongsCount(): Flow<Int>\n\n    @Transaction\n    @Query(\"SELECT song.* FROM song JOIN song_album_map ON song.id = song_album_map.songId WHERE song_album_map.albumId = :albumId\")\n    fun albumSongs(albumId: String): Flow<List<Song>>\n\n    @Transaction\n    @Query(\"SELECT * FROM playlist_song_map WHERE playlistId = :playlistId ORDER BY position\")\n    fun playlistSongs(playlistId: String): Flow<List<PlaylistSong>>\n\n    @Transaction\n    @Query(\n        \"SELECT song.* FROM song_artist_map JOIN song ON song_artist_map.songId = song.id WHERE artistId = :artistId AND inLibrary IS NOT NULL ORDER BY inLibrary\",\n    )\n    fun artistSongsByCreateDateAsc(artistId: String): Flow<List<Song>>\n\n    @Transaction\n    @Query(\n        \"SELECT song.* FROM song_artist_map JOIN song ON song_artist_map.songId = song.id WHERE artistId = :artistId AND inLibrary IS NOT NULL ORDER BY title\",\n    )\n    fun artistSongsByNameAsc(artistId: String): Flow<List<Song>>\n\n    @Transaction\n    @Query(\n        \"SELECT song.* FROM song_artist_map JOIN song ON song_artist_map.songId = song.id WHERE artistId = :artistId AND inLibrary IS NOT NULL ORDER BY totalPlayTime\",\n    )\n    fun artistSongsByPlayTimeAsc(artistId: String): Flow<List<Song>>\n\n    fun artistSongs(\n        artistId: String,\n        sortType: ArtistSongSortType,\n        descending: Boolean,\n        fromTimeStamp: Long? = null,\n        toTimeStamp: Long? = null,\n        limit: Int = -1\n    ): Flow<List<Song>> {\n        val songsFlow = when (sortType) {\n            ArtistSongSortType.CREATE_DATE -> artistSongsByCreateDateAsc(artistId)\n            ArtistSongSortType.NAME ->\n                artistSongsByNameAsc(artistId).map { artistSongs ->\n                    val collator = Collator.getInstance(Locale.getDefault())\n                    collator.strength = Collator.PRIMARY\n                    artistSongs.sortedWith(compareBy(collator) { it.song.title })\n                }\n\n            ArtistSongSortType.PLAY_TIME -> {\n                if (fromTimeStamp != null && toTimeStamp != null) {\n                    mostPlayedSongsByArtist(artistId, fromTimeStamp, toTimeStamp)\n                } else {\n                    artistSongsByPlayTimeAsc(artistId)\n                }\n            }\n        }\n\n        return songsFlow.map { songs ->\n            val limitedSongs = if (limit > 0) songs.take(limit) else songs\n            limitedSongs.reversed(descending)\n        }\n    }\n\n    @Transaction\n    @RewriteQueriesToDropUnusedColumns\n    @Query(\n        \"\"\"\n        SELECT s.*\n        FROM song s\n        JOIN (\n            SELECT e.songId, SUM(e.playTime) as totalPlayTime\n            FROM event e\n            JOIN song_artist_map sam ON e.songId = sam.songId\n            WHERE sam.artistId = :artistId AND e.timestamp >= :fromTimeStamp AND e.timestamp <= :toTimeStamp\n            GROUP BY e.songId\n        ) AS play_times ON s.id = play_times.songId\n        ORDER BY play_times.totalPlayTime DESC\n        \"\"\"\n    )\n    fun mostPlayedSongsByArtist(artistId: String, fromTimeStamp: Long, toTimeStamp: Long): Flow<List<Song>>\n\n    @Transaction\n    @Query(\n        \"SELECT song.* FROM song_artist_map JOIN song ON song_artist_map.songId = song.id WHERE artistId = :artistId AND inLibrary IS NOT NULL LIMIT :previewSize\",\n    )\n    fun artistSongsPreview(\n        artistId: String,\n        previewSize: Int = 3,\n    ): Flow<List<Song>>\n\n    @Transaction\n    @Query(\n        \"\"\"\n        SELECT song.*\n        FROM (SELECT *, COUNT(1) AS referredCount\n              FROM related_song_map\n              GROUP BY relatedSongId) map\n                 JOIN song ON song.id = map.relatedSongId\n        WHERE songId IN (SELECT songId\n                         FROM (SELECT songId\n                               FROM event\n                               ORDER BY ROWID DESC\n                               LIMIT 5)\n                         UNION\n                         SELECT songId\n                         FROM (SELECT songId\n                               FROM event\n                               WHERE timestamp > :now - 86400000 * 7\n                               GROUP BY songId\n                               ORDER BY SUM(playTime) DESC\n                               LIMIT 5)\n                         UNION\n                         SELECT id\n                         FROM (SELECT id\n                               FROM song\n                               ORDER BY totalPlayTime DESC\n                               LIMIT 10))\n        ORDER BY referredCount DESC\n        LIMIT 100\n    \"\"\",\n    )\n    fun quickPicks(now: Long = System.currentTimeMillis()): Flow<List<Song>>\n\n    @Transaction\n    @Query(\n        \"\"\"\n        SELECT\n            song.*\n        FROM\n            event\n        JOIN\n            song ON event.songId = song.id\n        WHERE\n            event.timestamp > (:now - 86400000 * 7 * 2)\n        GROUP BY\n            song.albumId\n        HAVING\n            song.albumId IS NOT NULL\n        ORDER BY\n            sum(event.playTime) DESC\n        LIMIT :limit\n        OFFSET :offset\n\n        \"\"\",\n    )\n    fun getRecommendationAlbum(\n        now: Long = System.currentTimeMillis(),\n        limit: Int = 5,\n        offset: Int = 0,\n    ): Flow<List<Song>>\n\n    @Transaction\n    @Query(\n        \"\"\"\n        SELECT s.id, s.title, s.thumbnailUrl, s.isVideo,\n               (SELECT name FROM artist WHERE id = sam.artistId) as artistName,\n               (SELECT COUNT(1)\n                FROM event\n                WHERE songId = s.id\n                  AND timestamp > :fromTimeStamp AND timestamp <= :toTimeStamp) AS songCountListened,\n               (SELECT SUM(event.playTime)\n                FROM event\n                WHERE songId = s.id\n                  AND timestamp > :fromTimeStamp AND timestamp <= :toTimeStamp) AS timeListened\n        FROM song s\n        LEFT JOIN song_artist_map sam ON s.id = sam.songId\n        JOIN (SELECT songId\n              FROM event\n              WHERE timestamp > :fromTimeStamp\n                AND timestamp <= :toTimeStamp\n              GROUP BY songId\n              ORDER BY SUM(playTime) DESC\n              LIMIT :limit) AS top_songs ON s.id = top_songs.songId\n        GROUP BY s.id\n        ORDER BY timeListened DESC\n        LIMIT :limit OFFSET :offset\n        \"\"\",\n    )\n    fun mostPlayedSongsStats(\n        fromTimeStamp: Long,\n        limit: Int = 6,\n        offset: Int = 0,\n        toTimeStamp: Long? = LocalDateTime.now().toInstant(ZoneOffset.UTC).toEpochMilli(),\n    ): Flow<List<SongWithStats>>\n\n    // Time Transfer\n    @Query(\"UPDATE event SET songId = :toSongId WHERE songId = :fromSongId\")\n    suspend fun transferEvents(fromSongId: String, toSongId: String): Int\n\n    // 1) Load source rows\n    @Query(\"SELECT * FROM playCount WHERE song = :fromSongId\")\n    suspend fun getPlayCountsForSong(fromSongId: String): List<PlayCountEntity>\n\n    // 2) Try to add into existing target row\n    @Query(\n        \"\"\"\n    UPDATE playCount\n    SET count = count + :delta\n    WHERE song = :toSongId AND year = :year AND month = :month\n    \"\"\",\n    )\n    suspend fun addToPlayCountRow(toSongId: String, year: Int, month: Int, delta: Int): Int\n\n    // 3) Insert new target row if none existed\n    @androidx.room.Insert(onConflict = androidx.room.OnConflictStrategy.IGNORE)\n    suspend fun insertPlayCountRow(row: PlayCountEntity): Long\n\n\n    @Query(\"DELETE FROM playCount WHERE song = :fromSongId\")\n    suspend fun deletePlayCountsForSong(fromSongId: String): Int\n\n    @Transaction\n    suspend fun transferSongStats(fromSongId: String, toSongId: String) {\n        require(fromSongId != toSongId) { \"fromSongId and toSongId must differ\" }\n\n        val movedPlayTime = getTotalPlayTimeForSong(fromSongId) ?: 0L\n\n        // 1) move events (source loses them)\n        transferEvents(fromSongId, toSongId)\n\n        // 2) merge playCount rows into target and remove source rows\n        val rows = getPlayCountsForSong(fromSongId)\n        for (r in rows) {\n            val updated = addToPlayCountRow(toSongId, r.year, r.month, r.count)\n            if (updated == 0) {\n                // no target row existed -> create it\n                insertPlayCountRow(\n                    PlayCountEntity(\n                        song = toSongId,\n                        year = r.year,\n                        month = r.month,\n                        count = r.count,\n                    ),\n                )\n            }\n        }\n        deletePlayCountsForSong(fromSongId)\n\n        if (movedPlayTime != 0L) {\n            incrementTotalPlayTime(toSongId, movedPlayTime)\n            incrementTotalPlayTime(fromSongId, -movedPlayTime)\n        }\n    }\n    // Time Transfer\n\n    @Transaction\n    @RewriteQueriesToDropUnusedColumns\n    @Query(\n        \"\"\"\n        SELECT song.*,\n               (SELECT COUNT(1)\n                FROM event\n                WHERE songId = song.id\n                  AND timestamp > :fromTimeStamp AND timestamp <= :toTimeStamp) AS songCountListened,\n               (SELECT SUM(event.playTime)\n                FROM event\n                WHERE songId = song.id\n                  AND timestamp > :fromTimeStamp AND timestamp <= :toTimeStamp) AS timeListened\n        FROM song\n        JOIN (SELECT songId\n                     FROM event\n                     WHERE timestamp > :fromTimeStamp\n                     AND timestamp <= :toTimeStamp\n                     GROUP BY songId\n                     ORDER BY SUM(playTime) DESC\n                     LIMIT :limit)\n        ON song.id = songId\n        LIMIT :limit\n        OFFSET :offset\n    \"\"\",\n    )\n    fun mostPlayedSongs(\n        fromTimeStamp: Long,\n        limit: Int = 6,\n        offset: Int = 0,\n        toTimeStamp: Long? = LocalDateTime.now().toInstant(ZoneOffset.UTC).toEpochMilli(),\n    ): Flow<List<Song>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\n        \"\"\"\n        SELECT artist.*,\n               (SELECT COUNT(1)\n                FROM song_artist_map\n                         JOIN event ON song_artist_map.songId = event.songId\n                WHERE artistId = artist.id\n                  AND timestamp > :fromTimeStamp AND timestamp <= :toTimeStamp) AS songCount,\n               (SELECT SUM(event.playTime)\n                FROM song_artist_map\n                         JOIN event ON song_artist_map.songId = event.songId\n                WHERE artistId = artist.id\n                  AND timestamp > :fromTimeStamp AND timestamp <= :toTimeStamp) AS timeListened\n        FROM artist\n                 JOIN(SELECT artistId, SUM(songTotalPlayTime) AS totalPlayTime\n                      FROM song_artist_map\n                               JOIN (SELECT songId, SUM(playTime) AS songTotalPlayTime\n                                     FROM event\n                                     WHERE timestamp > :fromTimeStamp\n                                     AND timestamp <= :toTimeStamp\n                                     GROUP BY songId) AS e\n                                    ON song_artist_map.songId = e.songId\n                      GROUP BY artistId\n                      ORDER BY totalPlayTime DESC\n                      LIMIT :limit\n                      OFFSET :offset)\n                     ON artist.id = artistId\n    \"\"\",\n    )\n    fun mostPlayedArtists(\n        fromTimeStamp: Long,\n        limit: Int = 6,\n        offset: Int = 0,\n        toTimeStamp: Long? = LocalDateTime.now().toInstant(ZoneOffset.UTC).toEpochMilli(),\n    ): Flow<List<Artist>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\n        \"\"\"\n    SELECT album.*,\n           COUNT(DISTINCT song_album_map.songId) as downloadCount,\n           (SELECT COUNT(1)\n            FROM song_album_map\n                     JOIN event e ON song_album_map.songId = e.songId\n            WHERE albumId = album.id\n              AND e.timestamp > :fromTimeStamp\n              AND e.timestamp <= :toTimeStamp) AS songCountListened,\n           (SELECT SUM(e.playTime)\n            FROM song_album_map\n                     JOIN event e ON song_album_map.songId = e.songId\n            WHERE albumId = album.id\n              AND e.timestamp > :fromTimeStamp\n              AND e.timestamp <= :toTimeStamp) AS timeListened\n    FROM album\n    JOIN song_album_map ON album.id = song_album_map.albumId\n    WHERE album.id IN (\n        SELECT sam.albumId\n        FROM event\n                 JOIN song_album_map sam ON event.songId = sam.songId\n        WHERE event.timestamp > :fromTimeStamp\n          AND event.timestamp <= :toTimeStamp\n        GROUP BY sam.albumId\n        HAVING sam.albumId IS NOT NULL\n    )\n    GROUP BY album.id\n    ORDER BY timeListened DESC\n    LIMIT :limit OFFSET :offset\n    \"\"\"\n    )\n    fun mostPlayedAlbums(\n        fromTimeStamp: Long,\n        limit: Int = 6,\n        offset: Int = 0,\n        toTimeStamp: Long? = LocalDateTime.now().toInstant(ZoneOffset.UTC).toEpochMilli(),\n    ): Flow<List<Album>>\n\n    @Query(\"SELECT SUM(playTime) FROM event WHERE timestamp >= :fromTimeStamp AND timestamp <= :toTimeStamp\")\n    fun getTotalPlayTimeInRange(fromTimeStamp: Long, toTimeStamp: Long): Flow<Long?>\n\n    @Query(\"SELECT SUM(playTime) FROM event WHERE songId = :songId\")\n    fun getTotalPlayTimeForSong(songId: String): Long?\n\n    @Query(\"SELECT COUNT(DISTINCT songId) FROM event WHERE timestamp >= :fromTimeStamp AND timestamp <= :toTimeStamp\")\n    fun getUniqueSongCountInRange(fromTimeStamp: Long, toTimeStamp: Long): Flow<Int>\n\n    @Query(\n        \"\"\"\n        SELECT COUNT(DISTINCT artistId)\n        FROM event\n        JOIN song_artist_map ON event.songId = song_artist_map.songId\n        WHERE timestamp >= :fromTimeStamp AND timestamp <= :toTimeStamp\n    \"\"\"\n    )\n    fun getUniqueArtistCountInRange(fromTimeStamp: Long, toTimeStamp: Long): Flow<Int>\n\n    @Query(\n        \"\"\"\n        SELECT COUNT(DISTINCT albumId)\n        FROM event\n        JOIN song ON event.songId = song.id\n        WHERE timestamp >= :fromTimeStamp AND timestamp <= :toTimeStamp\n    \"\"\"\n    )\n    fun getUniqueAlbumCountInRange(fromTimeStamp: Long, toTimeStamp: Long): Flow<Int>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\n        \"\"\"\n        SELECT album.*, count(song.dateDownload) downloadCount\n        FROM album_artist_map\n            JOIN album ON album_artist_map.albumId = album.id\n            JOIN song ON album_artist_map.albumId = song.albumId\n        WHERE artistId = :artistId\n        GROUP BY album.id\n        LIMIT :previewSize\n    \"\"\"\n    )\n    fun artistAlbumsPreview(artistId: String, previewSize: Int = 6): Flow<List<Album>>\n\n    @Query(\"SELECT sum(count) from playCount WHERE song = :songId\")\n    fun getLifetimePlayCount(songId: String?): Flow<Int>\n\n    @Query(\"SELECT sum(count) from playCount WHERE song = :songId AND year = :year\")\n    fun getPlayCountByYear(songId: String?, year: Int): Flow<Int>\n\n    @Query(\"SELECT count from playCount WHERE song = :songId AND year = :year AND month = :month\")\n    fun getPlayCountByMonth(songId: String?, year: Int, month: Int): Flow<Int>\n\n    @Transaction\n    @Query(\n        \"\"\"\n        SELECT song.*\n        FROM (SELECT n.songId      AS eid,\n                     SUM(playTime) AS oldPlayTime,\n                     newPlayTime\n              FROM event\n                       JOIN\n                   (SELECT songId, SUM(playTime) AS newPlayTime\n                    FROM event\n                    WHERE timestamp > (:now - 86400000 * 30 * 1)\n                    GROUP BY songId\n                    ORDER BY newPlayTime) as n\n                   ON event.songId = n.songId\n              WHERE timestamp < (:now - 86400000 * 30 * 1)\n              GROUP BY n.songId\n              ORDER BY oldPlayTime) AS t\n                 JOIN song on song.id = t.eid\n        WHERE 0.2 * t.oldPlayTime > t.newPlayTime\n        LIMIT 100\n    \"\"\"\n    )\n    fun forgottenFavorites(now: Long = System.currentTimeMillis()): Flow<List<Song>>\n\n    @Transaction\n    @Query(\n        \"\"\"\n        SELECT song.*\n        FROM event\n                 JOIN\n             song ON event.songId = song.id\n        WHERE event.timestamp > (:now - 86400000 * 7 * 2)\n        GROUP BY song.albumId\n        HAVING song.albumId IS NOT NULL\n        ORDER BY sum(event.playTime) DESC\n        LIMIT :limit\n        OFFSET :offset\n        \"\"\",\n    )\n    fun recommendedAlbum(\n        now: Long = System.currentTimeMillis(),\n        limit: Int = 5,\n        offset: Int = 0,\n    ): Flow<List<Song>>\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE id = :songId\")\n    fun song(songId: String?): Flow<Song?>\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE id = :songId LIMIT 1\")\n    suspend fun getSongById(songId: String): Song?\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE id = :songId LIMIT 1\")\n    fun getSongByIdBlocking(songId: String): Song?\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE id IN (:songIds)\")\n    suspend fun getSongsByIds(songIds: List<String>): List<Song>\n\n\n    @Transaction\n    @Query(\"SELECT * FROM song_artist_map WHERE songId = :songId\")\n    fun songArtistMap(songId: String): List<SongArtistMap>\n\n    @Transaction\n    @Query(\"SELECT * FROM song\")\n    fun allSongs(): Flow<List<Song>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\n        \"\"\"\n        SELECT DISTINCT artist.*,\n               (SELECT COUNT(1)\n                FROM song_artist_map\n                         JOIN event ON song_artist_map.songId = event.songId\n                WHERE artistId = artist.id) AS songCount\n        FROM artist\n                 LEFT JOIN(SELECT artistId, SUM(songTotalPlayTime) AS totalPlayTime\n                      FROM song_artist_map\n                               JOIN (SELECT songId, SUM(playTime) AS songTotalPlayTime\n                                     FROM event\n                                     GROUP BY songId) AS e\n                                    ON song_artist_map.songId = e.songId\n                      GROUP BY artistId\n                      ORDER BY totalPlayTime DESC) AS artistTotalPlayTime\n                     ON artist.id = artistId\n                     OR artist.bookmarkedAt IS NOT NULL\n                     ORDER BY\n                      CASE\n                        WHEN artistTotalPlayTime.artistId IS NULL THEN 1\n                        ELSE 0\n                      END,\n                      artistTotalPlayTime.totalPlayTime DESC\n    \"\"\",\n    )\n    fun allArtistsByPlayTime(): Flow<List<Artist>>\n\n    @Query(\"SELECT * FROM set_video_id WHERE videoId = :videoId\")\n    suspend fun getSetVideoId(videoId: String): SetVideoIdEntity?\n\n    @Transaction\n    @Query(\"SELECT * FROM format WHERE id = :id\")\n    fun format(id: String?): Flow<FormatEntity?>\n\n    @Transaction\n    @Query(\"SELECT * FROM lyrics WHERE id = :id\")\n    fun lyrics(id: String?): Flow<LyricsEntity?>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT *, (SELECT COUNT(1) FROM song_artist_map JOIN song ON song_artist_map.songId = song.id WHERE artistId = artist.id AND song.inLibrary IS NOT NULL) AS songCount FROM artist WHERE songCount > 0 ORDER BY rowId\")\n    fun artistsByCreateDateAsc(): Flow<List<Artist>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT *, (SELECT COUNT(1) FROM song_artist_map JOIN song ON song_artist_map.songId = song.id WHERE artistId = artist.id AND song.inLibrary IS NOT NULL) AS songCount FROM artist WHERE songCount > 0 ORDER BY name\")\n    fun artistsByNameAsc(): Flow<List<Artist>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT *, (SELECT COUNT(1) FROM song_artist_map JOIN song ON song_artist_map.songId = song.id WHERE artistId = artist.id AND song.inLibrary IS NOT NULL) AS songCount FROM artist WHERE songCount > 0 ORDER BY songCount\")\n    fun artistsBySongCountAsc(): Flow<List<Artist>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\n        \"\"\"\n        SELECT artist.*,\n               (SELECT COUNT(1)\n                FROM song_artist_map\n                         JOIN song ON song_artist_map.songId = song.id\n                WHERE artistId = artist.id\n                  AND song.inLibrary IS NOT NULL) AS songCount\n        FROM artist\n                 JOIN(SELECT artistId, SUM(totalPlayTime) AS totalPlayTime\n                      FROM song_artist_map\n                               JOIN song\n                                    ON song_artist_map.songId = song.id\n                      GROUP BY artistId\n                      ORDER BY totalPlayTime)\n                     ON artist.id = artistId\n        WHERE songCount > 0\n    \"\"\"\n    )\n    fun artistsByPlayTimeAsc(): Flow<List<Artist>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT *, (SELECT COUNT(1) FROM song_artist_map JOIN song ON song_artist_map.songId = song.id WHERE artistId = artist.id AND song.inLibrary IS NOT NULL) AS songCount FROM artist WHERE bookmarkedAt IS NOT NULL ORDER BY bookmarkedAt\")\n    fun artistsBookmarkedByCreateDateAsc(): Flow<List<Artist>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT *, (SELECT COUNT(1) FROM song_artist_map JOIN song ON song_artist_map.songId = song.id WHERE artistId = artist.id AND song.inLibrary IS NOT NULL) AS songCount FROM artist WHERE bookmarkedAt IS NOT NULL ORDER BY name\")\n    fun artistsBookmarkedByNameAsc(): Flow<List<Artist>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT *, (SELECT COUNT(1) FROM song_artist_map JOIN song ON song_artist_map.songId = song.id WHERE artistId = artist.id AND song.inLibrary IS NOT NULL) AS songCount FROM artist WHERE bookmarkedAt IS NOT NULL ORDER BY songCount\")\n    fun artistsBookmarkedBySongCountAsc(): Flow<List<Artist>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\n        \"\"\"\n        SELECT artist.*,\n               (SELECT COUNT(1)\n                FROM song_artist_map\n                         JOIN song ON song_artist_map.songId = song.id\n                WHERE artistId = artist.id\n                  AND song.inLibrary IS NOT NULL) AS songCount\n        FROM artist\n                 JOIN(SELECT artistId, SUM(totalPlayTime) AS totalPlayTime\n                      FROM song_artist_map\n                               JOIN song\n                                    ON song_artist_map.songId = song.id\n                      GROUP BY artistId\n                      ORDER BY totalPlayTime)\n                     ON artist.id = artistId\n        WHERE bookmarkedAt IS NOT NULL\n    \"\"\"\n    )\n    fun artistsBookmarkedByPlayTimeAsc(): Flow<List<Artist>>\n\n    fun artists(sortType: ArtistSortType, descending: Boolean) =\n        when (sortType) {\n            ArtistSortType.CREATE_DATE -> artistsByCreateDateAsc()\n            ArtistSortType.NAME -> artistsByNameAsc()\n            ArtistSortType.SONG_COUNT -> artistsBySongCountAsc()\n            ArtistSortType.PLAY_TIME -> artistsByPlayTimeAsc()\n        }.map { artists ->\n            artists\n                .filter { it.artist.isYouTubeArtist || it.artist.isLocal } // TODO: add ui to filter by local or remote or something idk\n                .reversed(descending)\n        }\n\n    fun artistsBookmarked(sortType: ArtistSortType, descending: Boolean) =\n        when (sortType) {\n            ArtistSortType.CREATE_DATE -> artistsBookmarkedByCreateDateAsc()\n            ArtistSortType.NAME -> artistsBookmarkedByNameAsc()\n            ArtistSortType.SONG_COUNT -> artistsBookmarkedBySongCountAsc()\n            ArtistSortType.PLAY_TIME -> artistsBookmarkedByPlayTimeAsc()\n        }.map { artists ->\n            artists\n                .filter { it.artist.isYouTubeArtist || it.artist.isLocal } // TODO: add ui to filter by local or remote or something idk\n                .reversed(descending)\n        }\n\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT *, (SELECT COUNT(1) FROM song_artist_map JOIN song ON song_artist_map.songId = song.id WHERE artistId = artist.id AND song.inLibrary IS NOT NULL) AS songCount FROM artist WHERE id = :id\")\n    fun artist(id: String): Flow<Artist?>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT * FROM album WHERE EXISTS(SELECT * FROM song WHERE song.albumId = album.id AND song.inLibrary IS NOT NULL) ORDER BY rowId\")\n    fun albumsByCreateDateAsc(): Flow<List<Album>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT * FROM album WHERE EXISTS(SELECT * FROM song WHERE song.albumId = album.id AND song.inLibrary IS NOT NULL) ORDER BY title\")\n    fun albumsByNameAsc(): Flow<List<Album>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT * FROM album WHERE EXISTS(SELECT * FROM song WHERE song.albumId = album.id AND song.inLibrary IS NOT NULL) ORDER BY year\")\n    fun albumsByYearAsc(): Flow<List<Album>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT * FROM album WHERE EXISTS(SELECT * FROM song WHERE song.albumId = album.id AND song.inLibrary IS NOT NULL) ORDER BY songCount\")\n    fun albumsBySongCountAsc(): Flow<List<Album>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT * FROM album WHERE EXISTS(SELECT * FROM song WHERE song.albumId = album.id AND song.inLibrary IS NOT NULL) ORDER BY duration\")\n    fun albumsByLengthAsc(): Flow<List<Album>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\n        \"\"\"\n        SELECT album.*\n        FROM album\n                 JOIN song\n                      ON song.albumId = album.id\n        WHERE EXISTS(SELECT * FROM song WHERE song.albumId = album.id AND song.inLibrary IS NOT NULL)\n        GROUP BY album.id\n        ORDER BY SUM(song.totalPlayTime)\n    \"\"\",\n    )\n    fun albumsByPlayTimeAsc(): Flow<List<Album>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT * FROM album WHERE bookmarkedAt IS NOT NULL ORDER BY rowId\")\n    fun albumsLikedByCreateDateAsc(): Flow<List<Album>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT * FROM album WHERE bookmarkedAt IS NOT NULL ORDER BY title\")\n    fun albumsLikedByNameAsc(): Flow<List<Album>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT * FROM album WHERE bookmarkedAt IS NOT NULL ORDER BY year\")\n    fun albumsLikedByYearAsc(): Flow<List<Album>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT * FROM album WHERE bookmarkedAt IS NOT NULL ORDER BY songCount\")\n    fun albumsLikedBySongCountAsc(): Flow<List<Album>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT * FROM album WHERE bookmarkedAt IS NOT NULL ORDER BY duration\")\n    fun albumsLikedByLengthAsc(): Flow<List<Album>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\n        \"\"\"\n        SELECT album.*\n        FROM album\n                 JOIN song\n                      ON song.albumId = album.id\n        WHERE bookmarkedAt IS NOT NULL\n        GROUP BY album.id\n        ORDER BY SUM(song.totalPlayTime)\n    \"\"\"\n    )\n    fun albumsLikedByPlayTimeAsc(): Flow<List<Album>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT * FROM album WHERE isUploaded = 1 ORDER BY rowId\")\n    fun albumsUploadedByCreateDateAsc(): Flow<List<Album>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT * FROM album WHERE bookmarkedAt IS NOT NULL ORDER BY title\")\n    fun albumsUploadedByNameAsc(): Flow<List<Album>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT * FROM album WHERE bookmarkedAt IS NOT NULL ORDER BY year\")\n    fun albumsUploadedByYearAsc(): Flow<List<Album>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT * FROM album WHERE bookmarkedAt IS NOT NULL ORDER BY songCount\")\n    fun albumsUploadedBySongCountAsc(): Flow<List<Album>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT * FROM album WHERE bookmarkedAt IS NOT NULL ORDER BY duration\")\n    fun albumsUploadedByLengthAsc(): Flow<List<Album>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\n        \"\"\"\n        SELECT album.*\n        FROM album\n                 JOIN song\n                      ON song.albumId = album.id\n        WHERE bookmarkedAt IS NOT NULL\n        GROUP BY album.id\n        ORDER BY SUM(song.totalPlayTime)\n    \"\"\"\n    )\n    fun albumsUploadedByPlayTimeAsc(): Flow<List<Album>>\n\n    fun albums(\n        sortType: AlbumSortType,\n        descending: Boolean,\n    ) = when (sortType) {\n        AlbumSortType.CREATE_DATE -> albumsByCreateDateAsc()\n        AlbumSortType.NAME ->\n            albumsByNameAsc().map { albums ->\n                val collator = Collator.getInstance(Locale.getDefault())\n                collator.strength = Collator.PRIMARY\n                albums.sortedWith(compareBy(collator) { it.album.title })\n            }\n\n        AlbumSortType.ARTIST ->\n            albumsByCreateDateAsc().map { albums ->\n                val collator = Collator.getInstance(Locale.getDefault())\n                collator.strength = Collator.PRIMARY\n                albums.sortedWith(compareBy(collator) { album -> album.artists.joinToString(\"\") { it.name } })\n            }\n\n        AlbumSortType.YEAR -> albumsByYearAsc()\n        AlbumSortType.SONG_COUNT -> albumsBySongCountAsc()\n        AlbumSortType.LENGTH -> albumsByLengthAsc()\n        AlbumSortType.PLAY_TIME -> albumsByPlayTimeAsc()\n    }.map { it.reversed(descending) }\n\n    fun albumsLiked(\n        sortType: AlbumSortType,\n        descending: Boolean,\n    ) = when (sortType) {\n        AlbumSortType.CREATE_DATE -> albumsLikedByCreateDateAsc()\n        AlbumSortType.NAME ->\n            albumsLikedByNameAsc().map { albums ->\n                val collator = Collator.getInstance(Locale.getDefault())\n                collator.strength = Collator.PRIMARY\n                albums.sortedWith(compareBy(collator) { it.album.title })\n            }\n\n        AlbumSortType.ARTIST ->\n            albumsLikedByCreateDateAsc().map { albums ->\n                val collator = Collator.getInstance(Locale.getDefault())\n                collator.strength = Collator.PRIMARY\n                albums.sortedWith(compareBy(collator) { album -> album.artists.joinToString(\"\") { it.name } })\n            }\n\n        AlbumSortType.YEAR -> albumsLikedByYearAsc()\n        AlbumSortType.SONG_COUNT -> albumsLikedBySongCountAsc()\n        AlbumSortType.LENGTH -> albumsLikedByLengthAsc()\n        AlbumSortType.PLAY_TIME -> albumsLikedByPlayTimeAsc()\n    }.map { it.reversed(descending) }\n\n    fun albumsUploaded(\n        sortType: AlbumSortType,\n        descending: Boolean,\n    ) = when (sortType) {\n        AlbumSortType.CREATE_DATE -> albumsUploadedByCreateDateAsc()\n        AlbumSortType.NAME ->\n            albumsUploadedByNameAsc().map { albums ->\n                val collator = Collator.getInstance(Locale.getDefault())\n                collator.strength = Collator.PRIMARY\n                albums.sortedWith(compareBy(collator) { it.album.title })\n            }\n\n        AlbumSortType.ARTIST ->\n            albumsUploadedByCreateDateAsc().map { albums ->\n                val collator = Collator.getInstance(Locale.getDefault())\n                collator.strength = Collator.PRIMARY\n                albums.sortedWith(compareBy(collator) { album -> album.artists.joinToString(\"\") { it.name } })\n            }\n\n        AlbumSortType.YEAR -> albumsUploadedByYearAsc()\n        AlbumSortType.SONG_COUNT -> albumsUploadedBySongCountAsc()\n        AlbumSortType.LENGTH -> albumsUploadedByLengthAsc()\n        AlbumSortType.PLAY_TIME -> albumsUploadedByPlayTimeAsc()\n    }.map { it.reversed(descending) }\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"SELECT * FROM album WHERE id = :id\")\n    fun album(id: String): Flow<Album?>\n\n    @Transaction\n    @Query(\"SELECT * FROM album WHERE id = :albumId\")\n    fun albumWithSongs(albumId: String): Flow<AlbumWithSongs?>\n\n    @Transaction\n    @Query(\"SELECT * FROM album_artist_map WHERE albumId = :albumId\")\n    fun albumArtistMaps(albumId: String): List<AlbumArtistMap>\n\n    @Transaction\n    @Query(\"SELECT *, (SELECT COUNT(*) FROM playlist_song_map WHERE playlistId = playlist.id) AS songCount FROM playlist WHERE bookmarkedAt IS NOT NULL ORDER BY rowId\")\n    fun playlistsByCreateDateAsc(): Flow<List<Playlist>>\n\n    @Transaction\n    @Query(\n        \"SELECT *, (SELECT COUNT(*) FROM playlist_song_map WHERE playlistId = playlist.id) AS songCount FROM playlist WHERE bookmarkedAt IS NOT NULL ORDER BY lastUpdateTime\",\n    )\n    fun playlistsByUpdatedDateAsc(): Flow<List<Playlist>>\n\n    @Transaction\n    @Query(\"SELECT *, (SELECT COUNT(*) FROM playlist_song_map WHERE playlistId = playlist.id) AS songCount FROM playlist WHERE bookmarkedAt IS NOT NULL ORDER BY name\")\n    fun playlistsByNameAsc(): Flow<List<Playlist>>\n\n    @Transaction\n    @Query(\"SELECT *, (SELECT COUNT(*) FROM playlist_song_map WHERE playlistId = playlist.id) AS songCount FROM playlist WHERE isEditable AND bookmarkedAt IS NOT NULL ORDER BY name\")\n    fun editablePlaylistsByNameAsc(): Flow<List<Playlist>>\n\n    @Transaction\n    @Query(\"SELECT *, (SELECT COUNT(*) FROM playlist_song_map WHERE playlistId = playlist.id) AS songCount FROM playlist WHERE bookmarkedAt IS NOT NULL ORDER BY songCount\")\n    fun playlistsBySongCountAsc(): Flow<List<Playlist>>\n\n    fun playlists(\n        sortType: PlaylistSortType,\n        descending: Boolean,\n    ) = when (sortType) {\n        PlaylistSortType.CREATE_DATE -> playlistsByCreateDateAsc()\n        PlaylistSortType.NAME ->\n            playlistsByNameAsc().map { playlists ->\n                val collator = Collator.getInstance(Locale.getDefault())\n                collator.strength = Collator.PRIMARY\n                playlists.sortedWith(compareBy(collator) { it.playlist.name })\n            }\n\n        PlaylistSortType.SONG_COUNT -> playlistsBySongCountAsc()\n        PlaylistSortType.LAST_UPDATED -> playlistsByUpdatedDateAsc()\n    }.map { it.reversed(descending) }\n\n    @Transaction\n    @Query(\"SELECT *, (SELECT COUNT(*) FROM playlist_song_map WHERE playlistId = playlist.id) AS songCount FROM playlist WHERE id = :playlistId\")\n    fun playlist(playlistId: String): Flow<Playlist?>\n\n    @Transaction\n    @Query(\"SELECT *, (SELECT COUNT(*) FROM playlist_song_map WHERE playlistId = playlist.id) AS songCount FROM playlist WHERE isEditable AND bookmarkedAt IS NOT NULL ORDER BY rowId\")\n    fun editablePlaylistsByCreateDateAsc(): Flow<List<Playlist>>\n\n    @Transaction\n    @Query(\"SELECT *, (SELECT COUNT(*) FROM playlist_song_map WHERE playlistId = playlist.id) AS songCount FROM playlist WHERE browseId = :browseId\")\n    fun playlistByBrowseId(browseId: String): Flow<Playlist?>\n\n    @Transaction\n    @Query(\"SELECT COUNT(*) from playlist_song_map WHERE playlistId = :playlistId AND songId = :songId LIMIT 1\")\n    fun checkInPlaylist(\n        playlistId: String,\n        songId: String,\n    ): Int\n\n    @Query(\"SELECT songId from playlist_song_map WHERE playlistId = :playlistId AND songId IN (:songIds)\")\n    fun playlistDuplicates(\n        playlistId: String,\n        songIds: List<String>,\n    ): List<String>\n\n    @Query(\"UPDATE playlist SET lastUpdateTime = :now WHERE id = :playlistId\")\n    fun updatePlaylistLastUpdated(\n        playlistId: String,\n        now: LocalDateTime = LocalDateTime.now(),\n    )\n    @Transaction\n    fun addSongToPlaylist(playlist: Playlist, songIds: List<String>) {\n        var position = playlist.songCount\n        songIds.forEach { id ->\n            insert(\n                PlaylistSongMap(\n                    songId = id,\n                    playlistId = playlist.id,\n                    position = position++\n                )\n            )\n        }\n        updatePlaylistLastUpdated(playlist.id)\n    }\n\n    fun downloadedSongs(\n        sortType: SongSortType,\n        descending: Boolean\n    ): Flow<List<Song>> = when (sortType) {\n        SongSortType.CREATE_DATE -> downloadedSongsByCreateDateAsc()\n        SongSortType.NAME -> downloadedSongsByNameAsc().map { songs ->\n            val collator = Collator.getInstance(Locale.getDefault())\n            collator.strength = Collator.PRIMARY\n            songs.sortedWith(compareBy(collator) { it.song.title })\n        }\n\n        SongSortType.ARTIST -> downloadedSongsByNameAsc().map { songs ->\n            val collator = Collator.getInstance(Locale.getDefault())\n            collator.strength = Collator.PRIMARY\n            songs.sortedWith(compareBy(collator) { song ->\n                song.orderedArtists.joinToString(\"\") { it.name }\n            })\n        }\n\n        SongSortType.PLAY_TIME -> downloadedSongsByPlayTimeAsc()\n    }.map { it.reversed(descending) }\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE (isDownloaded = 1 OR isCached = 1) AND (isEpisode = 0 OR isEpisode IS NULL) ORDER BY dateDownload\")\n    fun downloadedSongsByCreateDateAsc(): Flow<List<Song>>\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE (isDownloaded = 1 OR isCached = 1) AND (isEpisode = 0 OR isEpisode IS NULL) ORDER BY title\")\n    fun downloadedSongsByNameAsc(): Flow<List<Song>>\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE (isDownloaded = 1 OR isCached = 1) AND (isEpisode = 0 OR isEpisode IS NULL) ORDER BY totalPlayTime\")\n    fun downloadedSongsByPlayTimeAsc(): Flow<List<Song>>\n\n    @Query(\"UPDATE song SET isDownloaded = :downloaded, dateDownload = :date WHERE id = :songId\")\n    fun updateDownloadedInfo(songId: String, downloaded: Boolean, date: LocalDateTime?)\n\n    @Query(\"UPDATE song SET isCached = :cached WHERE id = :songId\")\n    fun updateCachedInfo(songId: String, cached: Boolean)\n\n    @Query(\"UPDATE song SET isCached = 1 WHERE id IN (:songIds)\")\n    fun updateCachedInfoMany(songIds: List<String>)\n\n    @Query(\"UPDATE song SET playbackPosition = :position WHERE id = :songId\")\n    fun updatePlaybackPosition(songId: String, position: Long?)\n\n    @Query(\"SELECT playbackPosition FROM song WHERE id = :songId\")\n    fun getPlaybackPosition(songId: String): Long?\n\n    @Query(\"SELECT playbackPosition FROM song WHERE id = :songId\")\n    fun playbackPositionFlow(songId: String): Flow<Long?>\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE isUploaded = 1 ORDER BY dateDownload\")\n    fun uploadedSongsByCreateDateAsc(): Flow<List<Song>>\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE isUploaded = 1 ORDER BY title\")\n    fun uploadedSongsByNameAsc(): Flow<List<Song>>\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE isUploaded = 1 ORDER BY totalPlayTime\")\n    fun uploadedSongsByPlayTimeAsc(): Flow<List<Song>>\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE isUploaded = 1 ORDER BY rowId\")\n    fun uploadedSongsByRowIdAsc(): Flow<List<Song>>\n\n    fun uploadedSongs(\n        sortType: SongSortType,\n        descending: Boolean,\n    ) = when (sortType) {\n        SongSortType.CREATE_DATE -> uploadedSongsByCreateDateAsc()\n        SongSortType.NAME ->\n            uploadedSongsByNameAsc().map { songs ->\n                val collator = Collator.getInstance(Locale.getDefault())\n                collator.strength = Collator.PRIMARY\n                songs.sortedWith(compareBy(collator) { it.song.title })\n            }\n\n        SongSortType.ARTIST ->\n            uploadedSongsByRowIdAsc().map { songs ->\n                val collator = Collator.getInstance(Locale.getDefault())\n                collator.strength = Collator.PRIMARY\n                songs\n                    .sortedWith(\n                        compareBy(collator) { song ->\n                            song.orderedArtists.joinToString(\"\") { it.name }\n                        },\n                    ).groupBy { it.album?.title }\n                    .flatMap { (_, songsByAlbum) ->\n                        songsByAlbum.sortedBy { album ->\n                            album.orderedArtists.joinToString(\n                                \"\",\n                            ) { it.name }\n                        }\n                    }\n            }\n\n        SongSortType.PLAY_TIME -> uploadedSongsByPlayTimeAsc()\n    }.map { it.reversed(descending) }\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE isEpisode = 1 ORDER BY inLibrary\")\n    fun podcastEpisodesByCreateDateAsc(): Flow<List<Song>>\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE isEpisode = 1 ORDER BY title\")\n    fun podcastEpisodesByNameAsc(): Flow<List<Song>>\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE isEpisode = 1 ORDER BY totalPlayTime\")\n    fun podcastEpisodesByPlayTimeAsc(): Flow<List<Song>>\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE isEpisode = 1 ORDER BY rowId\")\n    fun podcastEpisodesByRowIdAsc(): Flow<List<Song>>\n\n    fun podcastEpisodes(\n        sortType: SongSortType,\n        descending: Boolean,\n    ) = when (sortType) {\n        SongSortType.CREATE_DATE -> podcastEpisodesByCreateDateAsc()\n        SongSortType.NAME ->\n            podcastEpisodesByNameAsc().map { songs ->\n                val collator = Collator.getInstance(Locale.getDefault())\n                collator.strength = Collator.PRIMARY\n                songs.sortedWith(compareBy(collator) { it.song.title })\n            }\n\n        SongSortType.ARTIST ->\n            podcastEpisodesByRowIdAsc().map { songs ->\n                val collator = Collator.getInstance(Locale.getDefault())\n                collator.strength = Collator.PRIMARY\n                songs\n                    .sortedWith(\n                        compareBy(collator) { song ->\n                            song.orderedArtists.joinToString(\"\") { it.name }\n                        },\n                    ).groupBy { it.album?.title }\n                    .flatMap { (_, songsByAlbum) ->\n                        songsByAlbum.sortedBy { album ->\n                            album.orderedArtists.joinToString(\n                                \"\",\n                            ) { it.name }\n                        }\n                    }\n            }\n\n        SongSortType.PLAY_TIME -> podcastEpisodesByPlayTimeAsc()\n    }.map { it.reversed(descending) }\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE isEpisode = 1 AND (isDownloaded = 1 OR isCached = 1) ORDER BY dateDownload\")\n    fun downloadedPodcastEpisodesByCreateDateAsc(): Flow<List<Song>>\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE isEpisode = 1 AND (isDownloaded = 1 OR isCached = 1) ORDER BY title\")\n    fun downloadedPodcastEpisodesByNameAsc(): Flow<List<Song>>\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE isEpisode = 1 AND (isDownloaded = 1 OR isCached = 1) ORDER BY totalPlayTime\")\n    fun downloadedPodcastEpisodesByPlayTimeAsc(): Flow<List<Song>>\n\n    // Saved episodes (in library but not necessarily downloaded)\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE isEpisode = 1 AND inLibrary IS NOT NULL ORDER BY inLibrary DESC\")\n    fun savedPodcastEpisodesByCreateDateAsc(): Flow<List<Song>>\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE isEpisode = 1 AND inLibrary IS NOT NULL ORDER BY title\")\n    fun savedPodcastEpisodesByNameAsc(): Flow<List<Song>>\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE isEpisode = 1 AND inLibrary IS NOT NULL ORDER BY totalPlayTime\")\n    fun savedPodcastEpisodesByPlayTimeAsc(): Flow<List<Song>>\n\n    fun savedPodcastEpisodes(\n        sortType: SongSortType,\n        descending: Boolean,\n    ) = when (sortType) {\n        SongSortType.CREATE_DATE -> savedPodcastEpisodesByCreateDateAsc()\n        SongSortType.NAME ->\n            savedPodcastEpisodesByNameAsc().map { songs ->\n                val collator = Collator.getInstance(Locale.getDefault())\n                collator.strength = Collator.PRIMARY\n                songs.sortedWith(compareBy(collator) { it.song.title })\n            }\n        SongSortType.ARTIST ->\n            savedPodcastEpisodesByNameAsc().map { songs ->\n                val collator = Collator.getInstance(Locale.getDefault())\n                collator.strength = Collator.PRIMARY\n                songs.sortedWith(compareBy(collator) { song ->\n                    song.orderedArtists.joinToString(\"\") { it.name }\n                })\n            }\n        SongSortType.PLAY_TIME -> savedPodcastEpisodesByPlayTimeAsc()\n    }.map { it.reversed(descending) }\n\n    fun downloadedPodcastEpisodes(\n        sortType: SongSortType,\n        descending: Boolean,\n    ) = when (sortType) {\n        SongSortType.CREATE_DATE -> downloadedPodcastEpisodesByCreateDateAsc()\n        SongSortType.NAME ->\n            downloadedPodcastEpisodesByNameAsc().map { songs ->\n                val collator = Collator.getInstance(Locale.getDefault())\n                collator.strength = Collator.PRIMARY\n                songs.sortedWith(compareBy(collator) { it.song.title })\n            }\n        SongSortType.ARTIST ->\n            downloadedPodcastEpisodesByNameAsc().map { songs ->\n                val collator = Collator.getInstance(Locale.getDefault())\n                collator.strength = Collator.PRIMARY\n                songs.sortedWith(compareBy(collator) { song ->\n                    song.orderedArtists.joinToString(\"\") { it.name }\n                })\n            }\n        SongSortType.PLAY_TIME -> downloadedPodcastEpisodesByPlayTimeAsc()\n    }.map { it.reversed(descending) }\n\n    @Transaction\n    @Query(\"SELECT * FROM song WHERE title LIKE '%' || :query || '%' AND inLibrary IS NOT NULL LIMIT :previewSize\")\n    fun searchSongs(\n        query: String,\n        previewSize: Int = Int.MAX_VALUE,\n    ): Flow<List<Song>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\n        \"SELECT *, (SELECT COUNT(1) FROM song_artist_map JOIN song ON song_artist_map.songId = song.id WHERE artistId = artist.id AND song.inLibrary IS NOT NULL) AS songCount FROM artist WHERE name LIKE '%' || :query || '%' AND songCount > 0 LIMIT :previewSize\",\n    )\n    fun searchArtists(\n        query: String,\n        previewSize: Int = Int.MAX_VALUE,\n    ): Flow<List<Artist>>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\n        \"SELECT * FROM album WHERE title LIKE '%' || :query || '%' AND EXISTS(SELECT * FROM song WHERE song.albumId = album.id AND song.inLibrary IS NOT NULL) LIMIT :previewSize\",\n    )\n    fun searchAlbums(\n        query: String,\n        previewSize: Int = Int.MAX_VALUE,\n    ): Flow<List<Album>>\n\n    @Transaction\n    @Query(\n        \"SELECT *, (SELECT COUNT(*) FROM playlist_song_map WHERE playlistId = playlist.id) AS songCount FROM playlist WHERE name LIKE '%' || :query || '%' LIMIT :previewSize\",\n    )\n    fun searchPlaylists(\n        query: String,\n        previewSize: Int = Int.MAX_VALUE,\n    ): Flow<List<Playlist>>\n\n    @Transaction\n    @Query(\"SELECT * FROM event ORDER BY rowId DESC\")\n    fun events(): Flow<List<EventWithSong>>\n\n    @Transaction\n    @Query(\"SELECT * FROM event ORDER BY rowId ASC LIMIT 1\")\n    fun firstEvent(): Flow<EventWithSong?>\n\n    @Query(\"SELECT COUNT(*) FROM event\")\n    fun eventCount(): Flow<Int>\n\n    @Transaction\n    @Query(\"DELETE FROM event\")\n    fun clearListenHistory()\n\n    @Transaction\n    @Query(\"SELECT * FROM search_history WHERE `query` LIKE :query || '%' ORDER BY id DESC\")\n    fun searchHistory(query: String = \"\"): Flow<List<SearchHistory>>\n\n    @Transaction\n    @Query(\"DELETE FROM search_history\")\n    fun clearSearchHistory()\n\n    // Recognition History\n    @Transaction\n    @Query(\"SELECT * FROM recognition_history ORDER BY recognizedAt DESC\")\n    fun recognitionHistory(): Flow<List<RecognitionHistory>>\n\n    @Transaction\n    @Query(\"SELECT * FROM recognition_history WHERE id = :id\")\n    fun recognitionHistoryById(id: Long): Flow<RecognitionHistory?>\n\n    @Transaction\n    @Query(\"SELECT * FROM recognition_history WHERE title LIKE '%' || :query || '%' OR artist LIKE '%' || :query || '%' ORDER BY recognizedAt DESC\")\n    fun searchRecognitionHistory(query: String): Flow<List<RecognitionHistory>>\n\n    @Transaction\n    @Query(\"DELETE FROM recognition_history\")\n    fun clearRecognitionHistory()\n\n    @Transaction\n    @Query(\"DELETE FROM recognition_history WHERE id = :id\")\n    fun deleteRecognitionHistoryById(id: Long)\n\n    @Transaction\n    @Query(\"UPDATE recognition_history SET liked = :liked WHERE id = :id\")\n    fun updateRecognitionHistoryLiked(id: Long, liked: Boolean)\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    fun insert(recognitionHistory: RecognitionHistory): Long\n\n    @Delete\n    fun delete(recognitionHistory: RecognitionHistory)\n\n    @Query(\"UPDATE song SET totalPlayTime = totalPlayTime + :playTime WHERE id = :songId\")\n    fun incrementTotalPlayTime(songId: String, playTime: Long)\n\n    @Query(\"UPDATE playCount SET count = count + 1 WHERE song = :songId AND year = :year AND month = :month\")\n    fun incrementPlayCount(songId: String, year: Int, month: Int)\n\n    /**\n     * Increment by one the play count with today's year and month.\n     */\n    fun incrementPlayCount(songId: String) {\n        val time = LocalDateTime.now().atOffset(ZoneOffset.UTC)\n        var oldCount: Int\n        runBlocking {\n            oldCount = getPlayCountByMonth(songId, time.year, time.monthValue).first()\n        }\n\n        // add new\n        if (oldCount <= 0) {\n            insert(PlayCountEntity(songId, time.year, time.monthValue, 0))\n        }\n        incrementPlayCount(songId, time.year, time.monthValue)\n    }\n\n    @Transaction\n    @Query(\"UPDATE song SET inLibrary = :inLibrary WHERE id = :songId\")\n    fun inLibrary(\n        songId: String,\n        inLibrary: LocalDateTime?,\n    )\n\n    @Transaction\n    @Query(\"UPDATE song SET libraryAddToken = :libraryAddToken, libraryRemoveToken = :libraryRemoveToken WHERE id = :songId\")\n    fun addLibraryTokens(\n        songId: String,\n        libraryAddToken: String?,\n        libraryRemoveToken: String?,\n    )\n\n    @Transaction\n    @Query(\"SELECT COUNT(1) FROM related_song_map WHERE songId = :songId LIMIT 1\")\n    fun hasRelatedSongs(songId: String): Boolean\n\n    @Transaction\n    @Query(\n        \"SELECT song.* FROM (SELECT * from related_song_map GROUP BY relatedSongId) map JOIN song ON song.id = map.relatedSongId where songId = :songId\",\n    )\n    fun getRelatedSongs(songId: String): Flow<List<Song>>\n\n    @Transaction\n    @Query(\n        \"\"\"\n        SELECT song.*\n        FROM (SELECT *\n              FROM related_song_map\n              GROUP BY relatedSongId) map\n                 JOIN\n             song\n             ON song.id = map.relatedSongId\n        WHERE songId = :songId\n        \"\"\"\n    )\n    fun relatedSongs(songId: String): List<Song>\n\n    @Transaction\n    @Query(\n        \"\"\"\n        UPDATE playlist_song_map SET position =\n            CASE\n                WHEN position < :fromPosition THEN position + 1\n                WHEN position > :fromPosition THEN position - 1\n                ELSE :toPosition\n            END\n        WHERE playlistId = :playlistId AND position BETWEEN MIN(:fromPosition, :toPosition) AND MAX(:fromPosition, :toPosition)\n    \"\"\",\n    )\n    fun move(\n        playlistId: String,\n        fromPosition: Int,\n        toPosition: Int,\n    )\n\n    @Transaction\n    @Query(\"DELETE FROM playlist_song_map WHERE playlistId = :playlistId\")\n    fun clearPlaylist(playlistId: String)\n\n    @Transaction\n    @Query(\"SELECT * FROM artist WHERE name = :name\")\n    fun artistByName(name: String): ArtistEntity?\n\n    @Query(\"SELECT * FROM artist WHERE id = :id LIMIT 1\")\n    fun getArtistById(id: String): ArtistEntity?\n\n    @Insert(onConflict = OnConflictStrategy.IGNORE)\n    fun insert(song: SongEntity): Long\n\n    @Insert(onConflict = OnConflictStrategy.IGNORE)\n    fun insert(artist: ArtistEntity)\n\n    @Insert(onConflict = OnConflictStrategy.IGNORE)\n    fun insert(album: AlbumEntity): Long\n\n    @Insert(onConflict = OnConflictStrategy.IGNORE)\n    fun insert(playlist: PlaylistEntity)\n\n    @Insert(onConflict = OnConflictStrategy.IGNORE)\n    fun insert(map: SongArtistMap)\n\n    @Insert(onConflict = OnConflictStrategy.IGNORE)\n    fun insert(map: SongAlbumMap)\n\n    @Insert(onConflict = OnConflictStrategy.IGNORE)\n    fun insert(map: AlbumArtistMap)\n\n    @Insert(onConflict = OnConflictStrategy.IGNORE)\n    fun insert(map: PlaylistSongMap)\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    fun insert(searchHistory: SearchHistory)\n\n    @Insert(onConflict = OnConflictStrategy.IGNORE)\n    fun insert(event: Event)\n\n    @Insert(onConflict = OnConflictStrategy.IGNORE)\n    fun insert(map: RelatedSongMap)\n\n    @Insert(onConflict = OnConflictStrategy.IGNORE)\n    fun insert(playCountEntity: PlayCountEntity): Long\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    fun insert(setVideoIdEntity: SetVideoIdEntity)\n\n    @Transaction\n    fun insert(\n        mediaMetadata: MediaMetadata,\n        block: (SongEntity) -> SongEntity = { it },\n    ) {\n        if (insert(mediaMetadata.toSongEntity().let(block)) == -1L) return\n\n        mediaMetadata.artists.forEachIndexed { index, artist ->\n            val artistId = artist.id ?: artistByName(artist.name)?.id ?: ArtistEntity.generateArtistId()\n\n            insert(\n                ArtistEntity(\n                    id = artistId,\n                    name = artist.name,\n                    channelId = artist.id,\n                )\n            )\n\n            insert(\n                SongArtistMap(\n                    songId = mediaMetadata.id,\n                    artistId = artistId,\n                    position = index,\n                )\n            )\n        }\n    }\n\n    @Transaction\n    fun insert(albumPage: AlbumPage) {\n        if (insert(\n                AlbumEntity(\n                    id = albumPage.album.browseId,\n                    playlistId = albumPage.album.playlistId,\n                    title = albumPage.album.title,\n                    year = albumPage.album.year,\n                    thumbnailUrl = albumPage.album.thumbnail,\n                    songCount = albumPage.songs.size,\n                    duration = albumPage.songs.sumOf { it.duration ?: 0 },\n                    explicit = albumPage.album.explicit || albumPage.songs.any { it.explicit },\n                ),\n            ) == -1L\n        ) {\n            return\n        }\n        albumPage.songs\n            .map(SongItem::toMediaMetadata)\n            .onEach(::insert)\n            .onEach {\n                val existingSong = getSongByIdBlocking(it.id)\n                if (existingSong != null) {\n                    update(existingSong, it)\n                }\n            }.mapIndexed { index, song ->\n                SongAlbumMap(\n                    songId = song.id,\n                    albumId = albumPage.album.browseId,\n                    index = index,\n                )\n            }.forEach(::upsert)\n        albumPage.album.artists\n            ?.map { artist ->\n                ArtistEntity(\n                    id = artist.id ?: artistByName(artist.name)?.id\n                    ?: ArtistEntity.generateArtistId(),\n                    name = artist.name,\n                )\n            }?.onEach(::insert)\n            ?.mapIndexed { index, artist ->\n                AlbumArtistMap(\n                    albumId = albumPage.album.browseId,\n                    artistId = artist.id,\n                    order = index,\n                )\n            }?.forEach(::insert)\n    }\n\n    @Transaction\n    fun update(\n        song: Song,\n        mediaMetadata: MediaMetadata,\n    ) {\n        update(\n            song.song.copy(\n                title = mediaMetadata.title,\n                duration = mediaMetadata.duration,\n                thumbnailUrl = mediaMetadata.thumbnailUrl,\n                albumId = mediaMetadata.album?.id,\n                albumName = mediaMetadata.album?.title,\n                libraryAddToken = mediaMetadata.libraryAddToken,\n                libraryRemoveToken = mediaMetadata.libraryRemoveToken\n            ),\n        )\n        songArtistMap(song.id).forEach(::delete)\n        mediaMetadata.artists.forEachIndexed { index, artist ->\n            val artistId = artist.id ?: artistByName(artist.name)?.id ?: ArtistEntity.generateArtistId()\n\n            insert(\n                ArtistEntity(\n                    id = artistId,\n                    name = artist.name,\n                    channelId = artist.id,\n                ),\n            )\n            insert(\n                SongArtistMap(\n                    songId = song.id,\n                    artistId = artistId,\n                    position = index,\n                ),\n            )\n        }\n    }\n\n    @Update\n    fun update(song: SongEntity)\n\n    @Update\n    fun update(artist: ArtistEntity)\n\n    @Update\n    fun update(album: AlbumEntity)\n\n    @Update\n    fun update(playlist: PlaylistEntity)\n\n    @Update\n    fun update(map: PlaylistSongMap)\n\n    @Transaction\n    fun update(\n        artist: ArtistEntity,\n        artistPage: ArtistPage\n    ) {\n        update(\n            artist.copy(\n                name = artistPage.artist.title,\n                thumbnailUrl = artistPage.artist.thumbnail?.resize(544, 544),\n                lastUpdateTime = LocalDateTime.now()\n            )\n        )\n    }\n\n    @Transaction\n    fun update(\n        album: AlbumEntity,\n        albumPage: AlbumPage,\n        artists: List<ArtistEntity>? = emptyList(),\n    ) {\n        update(\n            album.copy(\n                id = albumPage.album.browseId,\n                playlistId = albumPage.album.playlistId,\n                title = albumPage.album.title,\n                year = albumPage.album.year,\n                thumbnailUrl = albumPage.album.thumbnail,\n                songCount = albumPage.songs.size,\n                duration = albumPage.songs.sumOf { it.duration ?: 0 },\n                explicit = albumPage.album.explicit || albumPage.songs.any { it.explicit },\n            ),\n        )\n        if (artists?.size != albumPage.album.artists?.size) {\n            artists?.forEach(::delete)\n        }\n        albumPage.songs\n            .map(SongItem::toMediaMetadata)\n            .onEach(::insert)\n            .onEach {\n                val existingSong = getSongByIdBlocking(it.id)\n                if (existingSong != null) {\n                    update(existingSong, it)\n                }\n            }.mapIndexed { index, song ->\n                SongAlbumMap(\n                    songId = song.id,\n                    albumId = albumPage.album.browseId,\n                    index = index,\n                )\n            }.forEach(::upsert)\n\n        albumPage.album.artists?.let { artists ->\n            // Recreate album artists\n            albumArtistMaps(album.id).forEach(::delete)\n            artists\n                .map { artist ->\n                    ArtistEntity(\n                        id = artist.id ?: artistByName(artist.name)?.id\n                        ?: ArtistEntity.generateArtistId(),\n                        name = artist.name,\n                    )\n                }.onEach(::insert)\n                .mapIndexed { index, artist ->\n                    AlbumArtistMap(\n                        albumId = albumPage.album.browseId,\n                        artistId = artist.id,\n                        order = index,\n                    )\n                }.forEach(::insert)\n        }\n    }\n\n    @Update\n    fun update(playlistEntity: PlaylistEntity, playlistItem: PlaylistItem) {\n        update(\n            playlistEntity.copy(\n                name = playlistItem.title,\n                browseId = playlistItem.id,\n                thumbnailUrl = playlistItem.thumbnail,\n                isEditable = playlistItem.isEditable,\n                remoteSongCount = playlistItem.songCountText?.let { Regex(\"\"\"\\d+\"\"\").find(it)?.value?.toIntOrNull() },\n                playEndpointParams = playlistItem.playEndpoint?.params,\n                shuffleEndpointParams = playlistItem.shuffleEndpoint?.params,\n                radioEndpointParams = playlistItem.radioEndpoint?.params\n            )\n        )\n    }\n\n    @Upsert\n    fun upsert(map: SongAlbumMap)\n\n    @Upsert\n    fun upsert(lyrics: LyricsEntity)\n\n    @Upsert\n    fun upsert(format: FormatEntity)\n\n    @Upsert\n    fun upsert(song: SongEntity)\n\n    @Delete\n    fun delete(song: SongEntity)\n\n    @Delete\n    fun delete(songArtistMap: SongArtistMap)\n\n    @Delete\n    fun delete(artist: ArtistEntity)\n\n    @Delete\n    fun delete(album: AlbumEntity)\n\n    @Delete\n    fun delete(albumArtistMap: AlbumArtistMap)\n\n    @Delete\n    fun delete(playlist: PlaylistEntity)\n\n    @Delete\n    fun delete(playlistSongMap: PlaylistSongMap)\n\n    @Query(\"DELETE FROM playlist WHERE browseId = :browseId\")\n    fun deletePlaylistById(browseId: String)\n\n    @Delete\n    fun delete(lyrics: LyricsEntity)\n\n    @Delete\n    fun delete(searchHistory: SearchHistory)\n\n    @Delete\n    fun delete(event: Event)\n\n    @Transaction\n    @Query(\"SELECT * FROM playlist_song_map WHERE songId = :songId\")\n    fun playlistSongMaps(songId: String): List<PlaylistSongMap>\n\n    @Transaction\n    @Query(\"SELECT * FROM playlist_song_map WHERE playlistId = :playlistId AND position >= :from ORDER BY position\")\n    fun playlistSongMaps(\n        playlistId: String,\n        from: Int,\n    ): List<PlaylistSongMap>\n\n    @RawQuery\n    fun raw(supportSQLiteQuery: SupportSQLiteQuery): Int\n\n    fun checkpoint() {\n        raw(\"PRAGMA wal_checkpoint(FULL)\".toSQLiteQuery())\n    }\n\n    // Podcast methods\n\n    @Query(\"SELECT * FROM podcast WHERE bookmarkedAt IS NOT NULL ORDER BY bookmarkedAt DESC\")\n    fun subscribedPodcasts(): Flow<List<PodcastEntity>>\n\n    @Query(\"SELECT * FROM podcast WHERE id = :id\")\n    fun podcast(id: String): Flow<PodcastEntity?>\n\n    @Query(\"SELECT EXISTS(SELECT 1 FROM podcast WHERE channelId = :channelId AND bookmarkedAt IS NOT NULL)\")\n    fun hasSubscribedPodcastByChannelId(channelId: String): Flow<Boolean>\n\n    @Transaction\n    @SuppressWarnings(RoomWarnings.QUERY_MISMATCH)\n    @Query(\"\"\"\n        SELECT *, (SELECT COUNT(1) FROM song_artist_map JOIN song ON song_artist_map.songId = song.id WHERE artistId = artist.id AND song.inLibrary IS NOT NULL) AS songCount\n        FROM artist\n        WHERE artist.bookmarkedAt IS NOT NULL\n        AND artist.isPodcastChannel = 1\n        ORDER BY artist.name COLLATE NOCASE ASC\n    \"\"\")\n    fun bookmarkedPodcastChannels(): Flow<List<Artist>>\n\n    @Query(\"SELECT * FROM podcast WHERE channelId = :channelId\")\n    fun podcastsByChannelId(channelId: String): Flow<List<PodcastEntity>>\n\n    @Insert(onConflict = OnConflictStrategy.IGNORE)\n    fun insert(podcast: PodcastEntity): Long\n\n    @Update\n    fun update(podcast: PodcastEntity)\n\n    @Upsert\n    fun upsert(podcast: PodcastEntity)\n\n    @Delete\n    fun delete(podcast: PodcastEntity)\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/MusicDatabase.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.database.sqlite.SQLiteDatabase\nimport androidx.core.content.contentValuesOf\nimport androidx.room.AutoMigration\nimport androidx.room.Database\nimport androidx.room.DeleteColumn\nimport androidx.room.DeleteTable\nimport androidx.room.RenameColumn\nimport androidx.room.Room\nimport androidx.room.RoomDatabase\nimport androidx.room.TypeConverters\nimport androidx.room.migration.AutoMigrationSpec\nimport androidx.room.migration.Migration\nimport androidx.sqlite.db.SupportSQLiteDatabase\nimport androidx.sqlite.db.SupportSQLiteOpenHelper\nimport com.metrolist.music.db.daos.SpeedDialDao\nimport com.metrolist.music.db.entities.AlbumArtistMap\nimport com.metrolist.music.db.entities.AlbumEntity\nimport com.metrolist.music.db.entities.ArtistEntity\nimport com.metrolist.music.db.entities.Event\nimport com.metrolist.music.db.entities.FormatEntity\nimport com.metrolist.music.db.entities.LyricsEntity\nimport com.metrolist.music.db.entities.PlayCountEntity\nimport com.metrolist.music.db.entities.PlaylistEntity\nimport com.metrolist.music.db.entities.PlaylistSongMap\nimport com.metrolist.music.db.entities.PlaylistSongMapPreview\nimport com.metrolist.music.db.entities.PodcastEntity\nimport com.metrolist.music.db.entities.RecognitionHistory\nimport com.metrolist.music.db.entities.RelatedSongMap\nimport com.metrolist.music.db.entities.SearchHistory\nimport com.metrolist.music.db.entities.SetVideoIdEntity\nimport com.metrolist.music.db.entities.SongAlbumMap\nimport com.metrolist.music.db.entities.SongArtistMap\nimport com.metrolist.music.db.entities.SongEntity\nimport com.metrolist.music.db.entities.SpeedDialItem\nimport com.metrolist.music.db.entities.SortedSongAlbumMap\nimport com.metrolist.music.db.entities.SortedSongArtistMap\nimport com.metrolist.music.extensions.toSQLiteQuery\nimport timber.log.Timber\nimport java.time.Instant\nimport java.time.LocalDateTime\nimport java.time.ZoneOffset\nimport java.util.Date\n\nclass MusicDatabase(\n    private val delegate: InternalDatabase,\n) : DatabaseDao by delegate.dao {\n    val speedDialDao: SpeedDialDao\n        get() = delegate.speedDialDao\n\n    val openHelper: SupportSQLiteOpenHelper\n        get() = delegate.openHelper\n\n    fun query(block: MusicDatabase.() -> Unit) =\n        with(delegate) {\n            queryExecutor.execute {\n                block(this@MusicDatabase)\n            }\n        }\n\n    fun transaction(block: MusicDatabase.() -> Unit) =\n        with(delegate) {\n            transactionExecutor.execute {\n                runInTransaction {\n                    block(this@MusicDatabase)\n                }\n            }\n        }\n\n    suspend fun withTransaction(block: suspend MusicDatabase.() -> Unit) =\n        with(delegate) {\n            kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) {\n                runInTransaction {\n                    kotlinx.coroutines.runBlocking {\n                        block(this@MusicDatabase)\n                    }\n                }\n            }\n        }\n\n    fun close() = delegate.close()\n}\n\n@Database(\n    entities = [\n        SongEntity::class,\n        ArtistEntity::class,\n        AlbumEntity::class,\n        PlaylistEntity::class,\n        SongArtistMap::class,\n        SongAlbumMap::class,\n        AlbumArtistMap::class,\n        PlaylistSongMap::class,\n        SearchHistory::class,\n        FormatEntity::class,\n        LyricsEntity::class,\n        Event::class,\n        RelatedSongMap::class,\n        SetVideoIdEntity::class,\n        PlayCountEntity::class,\n        RecognitionHistory::class,\n        SpeedDialItem::class,\n        PodcastEntity::class\n    ],\n    views = [\n        SortedSongArtistMap::class,\n        SortedSongAlbumMap::class,\n        PlaylistSongMapPreview::class,\n    ],\n    version = 36,\n    exportSchema = true,\n    autoMigrations = [\n        AutoMigration(from = 2, to = 3),\n        AutoMigration(from = 3, to = 4),\n        AutoMigration(from = 4, to = 5),\n        AutoMigration(from = 5, to = 6, spec = Migration5To6::class),\n        AutoMigration(from = 6, to = 7, spec = Migration6To7::class),\n        AutoMigration(from = 7, to = 8, spec = Migration7To8::class),\n        AutoMigration(from = 8, to = 9),\n        AutoMigration(from = 9, to = 10, spec = Migration9To10::class),\n        AutoMigration(from = 10, to = 11, spec = Migration10To11::class),\n        AutoMigration(from = 11, to = 12, spec = Migration11To12::class),\n        AutoMigration(from = 12, to = 13, spec = Migration12To13::class),\n        AutoMigration(from = 13, to = 14, spec = Migration13To14::class),\n        AutoMigration(from = 14, to = 15),\n        AutoMigration(from = 15, to = 16),\n        AutoMigration(from = 16, to = 17, spec = Migration16To17::class),\n        AutoMigration(from = 17, to = 18),\n        AutoMigration(from = 18, to = 19, spec = Migration18To19::class),\n        AutoMigration(from = 19, to = 20, spec = Migration19To20::class),\n        AutoMigration(from = 20, to = 21, spec = Migration20To21::class),\n        AutoMigration(from = 21, to = 22, spec = Migration21To22::class),\n        AutoMigration(from = 22, to = 23, spec = Migration22To23::class),\n        AutoMigration(from = 23, to = 24, spec = Migration23To24::class),\n        AutoMigration(from = 24, to = 25),\n        AutoMigration(from = 25, to = 26),\n        AutoMigration(from = 26, to = 27),\n        AutoMigration(from = 27, to = 28),\n        AutoMigration(from = 28, to = 29),\n        AutoMigration(from = 29, to = 30, spec = Migration29To30::class),\n        AutoMigration(from = 30, to = 31),\n        AutoMigration(from = 31, to = 32),\n        AutoMigration(from = 32, to = 33),\n        AutoMigration(from = 33, to = 34),\n        AutoMigration(from = 34, to = 35),\n        AutoMigration(from = 35, to = 36, spec = Migration35To36::class),\n    ],\n)\n@TypeConverters(Converters::class)\nabstract class InternalDatabase : RoomDatabase() {\n    abstract val dao: DatabaseDao\n    abstract val speedDialDao: SpeedDialDao\n\n    companion object {\n        const val DB_NAME = \"song.db\"\n\n        fun newInstance(context: Context): MusicDatabase =\n            MusicDatabase(\n                delegate =\n                Room\n                    .databaseBuilder(context, InternalDatabase::class.java, DB_NAME)\n                    .addMigrations(\n                        MIGRATION_1_2,\n                        MIGRATION_21_24,\n                        MIGRATION_22_24,\n                        MIGRATION_24_25,\n                    )\n                    .fallbackToDestructiveMigration(dropAllTables = true)\n                    .setJournalMode(RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING)\n                    .setTransactionExecutor(java.util.concurrent.Executors.newFixedThreadPool(4))\n                    .setQueryExecutor(java.util.concurrent.Executors.newFixedThreadPool(4))\n                    .addCallback(object : RoomDatabase.Callback() {\n                        override fun onOpen(db: SupportSQLiteDatabase) {\n                            super.onOpen(db)\n                            try {\n                                db.query(\"PRAGMA busy_timeout = 60000\").close()\n                                db.query(\"PRAGMA cache_size = -16000\").close()\n                                db.query(\"PRAGMA wal_autocheckpoint = 1000\").close()\n                                db.query(\"PRAGMA synchronous = NORMAL\").close()\n                            } catch (e: Exception) {\n                                Timber.tag(\"MusicDatabase\").e(e, \"Failed to set PRAGMA settings\")\n                            }\n                        }\n                    })\n                    .build(),\n            )\n    }\n}\n\n// ===== Migrations =====\n\nval MIGRATION_1_2 =\n    object : Migration(1, 2) {\n        override fun migrate(db: SupportSQLiteDatabase) {\n            data class OldSongEntity(\n                val id: String,\n                val title: String,\n                val duration: Int = -1,\n                val thumbnailUrl: String? = null,\n                val albumId: String? = null,\n                val albumName: String? = null,\n                val liked: Boolean = false,\n                val totalPlayTime: Long = 0,\n                val downloadState: Int = 0,\n                val createDate: LocalDateTime = LocalDateTime.now(),\n                val modifyDate: LocalDateTime = LocalDateTime.now(),\n            )\n\n            val converters = Converters()\n            val artistMap = mutableMapOf<Int, String>()\n            val artists = mutableListOf<ArtistEntity>()\n            db.query(\"SELECT * FROM artist\".toSQLiteQuery()).use { cursor ->\n                while (cursor.moveToNext()) {\n                    val oldId = cursor.getInt(0)\n                    val newId = ArtistEntity.generateArtistId()\n                    artistMap[oldId] = newId\n                    artists.add(\n                        ArtistEntity(\n                            id = newId,\n                            name = cursor.getString(1),\n                        ),\n                    )\n                }\n            }\n\n            val playlistMap = mutableMapOf<Int, String>()\n            val playlists = mutableListOf<PlaylistEntity>()\n            db.query(\"SELECT * FROM playlist\".toSQLiteQuery()).use { cursor ->\n                while (cursor.moveToNext()) {\n                    val oldId = cursor.getInt(0)\n                    val newId = PlaylistEntity.generatePlaylistId()\n                    playlistMap[oldId] = newId\n                    playlists.add(\n                        PlaylistEntity(\n                            id = newId,\n                            name = cursor.getString(1),\n                        ),\n                    )\n                }\n            }\n            val playlistSongMaps = mutableListOf<PlaylistSongMap>()\n            db.query(\"SELECT * FROM playlist_song\".toSQLiteQuery()).use { cursor ->\n                while (cursor.moveToNext()) {\n                    playlistSongMaps.add(\n                        PlaylistSongMap(\n                            playlistId = playlistMap[cursor.getInt(1)]!!,\n                            songId = cursor.getString(2),\n                            position = cursor.getInt(3),\n                        ),\n                    )\n                }\n            }\n            playlistSongMaps.sortBy { it.position }\n            val playlistSongCount = mutableMapOf<String, Int>()\n            playlistSongMaps.map { map ->\n                if (map.playlistId !in playlistSongCount) playlistSongCount[map.playlistId] = 0\n                map.copy(position = playlistSongCount[map.playlistId]!!).also {\n                    playlistSongCount[map.playlistId] = playlistSongCount[map.playlistId]!! + 1\n                }\n            }\n            val songs = mutableListOf<OldSongEntity>()\n            val songArtistMaps = mutableListOf<SongArtistMap>()\n            db.query(\"SELECT * FROM song\".toSQLiteQuery()).use { cursor ->\n                while (cursor.moveToNext()) {\n                    val songId = cursor.getString(0)\n                    songs.add(\n                        OldSongEntity(\n                            id = songId,\n                            title = cursor.getString(1),\n                            duration = cursor.getInt(3),\n                            liked = cursor.getInt(4) == 1,\n                            createDate = Instant.ofEpochMilli(Date(cursor.getLong(8)).time)\n                                .atZone(ZoneOffset.UTC).toLocalDateTime(),\n                            modifyDate = Instant.ofEpochMilli(Date(cursor.getLong(9)).time)\n                                .atZone(ZoneOffset.UTC).toLocalDateTime(),\n                        ),\n                    )\n                    songArtistMaps.add(\n                        SongArtistMap(\n                            songId = songId,\n                            artistId = artistMap[cursor.getInt(2)]!!,\n                            position = 0,\n                        ),\n                    )\n                }\n            }\n            db.execSQL(\"DROP TABLE IF EXISTS song\")\n            db.execSQL(\"DROP TABLE IF EXISTS artist\")\n            db.execSQL(\"DROP TABLE IF EXISTS playlist\")\n            db.execSQL(\"DROP TABLE IF EXISTS playlist_song\")\n            db.execSQL(\n                \"CREATE TABLE IF NOT EXISTS `song` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `duration` INTEGER NOT NULL, `thumbnailUrl` TEXT, `albumId` TEXT, `albumName` TEXT, `liked` INTEGER NOT NULL, `totalPlayTime` INTEGER NOT NULL, `isTrash` INTEGER NOT NULL, `download_state` INTEGER NOT NULL, `create_date` INTEGER NOT NULL, `modify_date` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n            )\n            db.execSQL(\n                \"CREATE TABLE IF NOT EXISTS `artist` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `bannerUrl` TEXT, `description` TEXT, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n            )\n            db.execSQL(\n                \"CREATE TABLE IF NOT EXISTS `album` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `year` INTEGER, `thumbnailUrl` TEXT, `songCount` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n            )\n            db.execSQL(\n                \"CREATE TABLE IF NOT EXISTS `playlist` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT, `authorId` TEXT, `year` INTEGER, `thumbnailUrl` TEXT, `createDate` INTEGER NOT NULL, `lastUpdateTime` INTEGER NOT NULL, PRIMARY KEY(`id`))\",\n            )\n            db.execSQL(\n                \"CREATE TABLE IF NOT EXISTS `song_artist_map` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n            )\n            db.execSQL(\"CREATE INDEX IF NOT EXISTS `index_song_artist_map_songId` ON `song_artist_map` (`songId`)\")\n            db.execSQL(\"CREATE INDEX IF NOT EXISTS `index_song_artist_map_artistId` ON `song_artist_map` (`artistId`)\")\n            db.execSQL(\n                \"CREATE TABLE IF NOT EXISTS `song_album_map` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `index` INTEGER, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n            )\n            db.execSQL(\"CREATE INDEX IF NOT EXISTS `index_song_album_map_songId` ON `song_album_map` (`songId`)\")\n            db.execSQL(\"CREATE INDEX IF NOT EXISTS `index_song_album_map_albumId` ON `song_album_map` (`albumId`)\")\n            db.execSQL(\n                \"CREATE TABLE IF NOT EXISTS `album_artist_map` (`albumId` TEXT NOT NULL, `artistId` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`albumId`, `artistId`), FOREIGN KEY(`albumId`) REFERENCES `album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n            )\n            db.execSQL(\"CREATE INDEX IF NOT EXISTS `index_album_artist_map_albumId` ON `album_artist_map` (`albumId`)\")\n            db.execSQL(\"CREATE INDEX IF NOT EXISTS `index_album_artist_map_artistId` ON `album_artist_map` (`artistId`)\")\n            db.execSQL(\n                \"CREATE TABLE IF NOT EXISTS `playlist_song_map` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `playlistId` TEXT NOT NULL, `songId` TEXT NOT NULL, `position` INTEGER NOT NULL, FOREIGN KEY(`playlistId`) REFERENCES `playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`songId`) REFERENCES `song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )\",\n            )\n            db.execSQL(\"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_playlistId` ON `playlist_song_map` (`playlistId`)\")\n            db.execSQL(\"CREATE INDEX IF NOT EXISTS `index_playlist_song_map_songId` ON `playlist_song_map` (`songId`)\")\n            db.execSQL(\"CREATE TABLE IF NOT EXISTS `download` (`id` INTEGER NOT NULL, `songId` TEXT NOT NULL, PRIMARY KEY(`id`))\")\n            db.execSQL(\n                \"CREATE TABLE IF NOT EXISTS `search_history` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)\",\n            )\n            db.execSQL(\"CREATE UNIQUE INDEX IF NOT EXISTS `index_search_history_query` ON `search_history` (`query`)\")\n            db.execSQL(\"CREATE VIEW `sorted_song_artist_map` AS SELECT * FROM song_artist_map ORDER BY position\")\n            db.execSQL(\n                \"CREATE VIEW `playlist_song_map_preview` AS SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\",\n            )\n            artists.forEach { artist ->\n                db.insert(\n                    \"artist\",\n                    SQLiteDatabase.CONFLICT_ABORT,\n                    contentValuesOf(\n                        \"id\" to artist.id,\n                        \"name\" to artist.name,\n                        \"createDate\" to converters.dateToTimestamp(artist.lastUpdateTime),\n                        \"lastUpdateTime\" to converters.dateToTimestamp(artist.lastUpdateTime),\n                    ),\n                )\n            }\n            songs.forEach { song ->\n                db.insert(\n                    \"song\",\n                    SQLiteDatabase.CONFLICT_ABORT,\n                    contentValuesOf(\n                        \"id\" to song.id,\n                        \"title\" to song.title,\n                        \"duration\" to song.duration,\n                        \"liked\" to song.liked,\n                        \"totalPlayTime\" to song.totalPlayTime,\n                        \"isTrash\" to false,\n                        \"download_state\" to song.downloadState,\n                        \"create_date\" to converters.dateToTimestamp(song.createDate),\n                        \"modify_date\" to converters.dateToTimestamp(song.modifyDate),\n                    ),\n                )\n            }\n            songArtistMaps.forEach { songArtistMap ->\n                db.insert(\n                    \"song_artist_map\",\n                    SQLiteDatabase.CONFLICT_ABORT,\n                    contentValuesOf(\n                        \"songId\" to songArtistMap.songId,\n                        \"artistId\" to songArtistMap.artistId,\n                        \"position\" to songArtistMap.position,\n                    ),\n                )\n            }\n            playlists.forEach { playlist ->\n                db.insert(\n                    \"playlist\",\n                    SQLiteDatabase.CONFLICT_ABORT,\n                    contentValuesOf(\n                        \"id\" to playlist.id,\n                        \"name\" to playlist.name,\n                        \"createDate\" to converters.dateToTimestamp(LocalDateTime.now()),\n                        \"lastUpdateTime\" to converters.dateToTimestamp(LocalDateTime.now()),\n                    ),\n                )\n            }\n            playlistSongMaps.forEach { playlistSongMap ->\n                db.insert(\n                    \"playlist_song_map\",\n                    SQLiteDatabase.CONFLICT_ABORT,\n                    contentValuesOf(\n                        \"playlistId\" to playlistSongMap.playlistId,\n                        \"songId\" to playlistSongMap.songId,\n                        \"position\" to playlistSongMap.position,\n                    ),\n                )\n            }\n        }\n    }\n\nval MIGRATION_21_24 =\n    object : Migration(21, 24) {\n        override fun migrate(db: SupportSQLiteDatabase) {\n            // Combine all changes from 21→22→23→24\n\n            // From 21→22: Add columns\n            try {\n                db.execSQL(\"ALTER TABLE song ADD COLUMN libraryAddToken TEXT DEFAULT ''\")\n            } catch (e: Exception) {\n                Timber.tag(\"Migration\").w(\"Column libraryAddToken may already exist\")\n            }\n            try {\n                db.execSQL(\"ALTER TABLE song ADD COLUMN libraryRemoveToken TEXT DEFAULT ''\")\n            } catch (e: Exception) {\n                Timber.tag(\"Migration\").w(\"Column libraryRemoveToken may already exist\")\n            }\n            try {\n                db.execSQL(\"ALTER TABLE song ADD COLUMN romanizeLyrics INTEGER NOT NULL DEFAULT 1\")\n            } catch (e: Exception) {\n                Timber.tag(\"Migration\").w(\"Column romanizeLyrics may already exist\")\n            }\n            try {\n                db.execSQL(\"ALTER TABLE song ADD COLUMN isDownloaded INTEGER NOT NULL DEFAULT 0\")\n            } catch (e: Exception) {\n                Timber.tag(\"Migration\").w(\"Column isDownloaded may already exist\")\n            }\n\n            // From 23→24: Add isUploaded\n            var hasIsUploaded = false\n            db.query(\"PRAGMA table_info('song')\").use { cursor ->\n                val nameIndex = cursor.getColumnIndex(\"name\")\n                while (cursor.moveToNext()) {\n                    val colName = if (nameIndex >= 0) cursor.getString(nameIndex) else null\n                    if (colName == \"isUploaded\") {\n                        hasIsUploaded = true\n                        break\n                    }\n                }\n            }\n\n            if (!hasIsUploaded) {\n                db.execSQL(\"ALTER TABLE `song` ADD COLUMN `isUploaded` INTEGER NOT NULL DEFAULT 0\")\n            }\n        }\n    }\n\nval MIGRATION_22_24 =\n    object : Migration(22, 24) {\n        override fun migrate(db: SupportSQLiteDatabase) {\n            // From 23→24: Add isUploaded\n            var hasIsUploaded = false\n            db.query(\"PRAGMA table_info('song')\").use { cursor ->\n                val nameIndex = cursor.getColumnIndex(\"name\")\n                while (cursor.moveToNext()) {\n                    val colName = if (nameIndex >= 0) cursor.getString(nameIndex) else null\n                    if (colName == \"isUploaded\") {\n                        hasIsUploaded = true\n                        break\n                    }\n                }\n            }\n\n            if (!hasIsUploaded) {\n                db.execSQL(\"ALTER TABLE `song` ADD COLUMN `isUploaded` INTEGER NOT NULL DEFAULT 0\")\n            }\n        }\n    }\n\n// ===== AutoMigration Specs =====\n\n@DeleteColumn.Entries(\n    DeleteColumn(tableName = \"song\", columnName = \"isTrash\"),\n    DeleteColumn(tableName = \"playlist\", columnName = \"author\"),\n    DeleteColumn(tableName = \"playlist\", columnName = \"authorId\"),\n    DeleteColumn(tableName = \"playlist\", columnName = \"year\"),\n    DeleteColumn(tableName = \"playlist\", columnName = \"thumbnailUrl\"),\n    DeleteColumn(tableName = \"playlist\", columnName = \"createDate\"),\n    DeleteColumn(tableName = \"playlist\", columnName = \"lastUpdateTime\"),\n)\n@RenameColumn.Entries(\n    RenameColumn(\n        tableName = \"song\",\n        fromColumnName = \"download_state\",\n        toColumnName = \"downloadState\"\n    ),\n    RenameColumn(tableName = \"song\", fromColumnName = \"create_date\", toColumnName = \"createDate\"),\n    RenameColumn(tableName = \"song\", fromColumnName = \"modify_date\", toColumnName = \"modifyDate\"),\n)\nclass Migration5To6 : AutoMigrationSpec {\n    override fun onPostMigrate(db: SupportSQLiteDatabase) {\n        db.query(\"SELECT id FROM playlist WHERE id NOT LIKE 'LP%'\").use { cursor ->\n            while (cursor.moveToNext()) {\n                db.execSQL(\n                    \"UPDATE playlist SET browseId = '${cursor.getString(0)}' WHERE id = '${cursor.getString(0)}'\"\n                )\n            }\n        }\n    }\n}\n\nclass Migration6To7 : AutoMigrationSpec {\n    override fun onPostMigrate(db: SupportSQLiteDatabase) {\n        db.query(\"SELECT id, createDate FROM song\").use { cursor ->\n            while (cursor.moveToNext()) {\n                db.execSQL(\n                    \"UPDATE song SET inLibrary = ${cursor.getLong(1)} WHERE id = '${cursor.getString(0)}'\"\n                )\n            }\n        }\n    }\n}\n\n@DeleteColumn.Entries(\n    DeleteColumn(tableName = \"song\", columnName = \"createDate\"),\n    DeleteColumn(tableName = \"song\", columnName = \"modifyDate\"),\n)\nclass Migration7To8 : AutoMigrationSpec\n\n@DeleteTable.Entries(\n    DeleteTable(tableName = \"download\"),\n)\nclass Migration9To10 : AutoMigrationSpec\n\n@DeleteColumn.Entries(\n    DeleteColumn(tableName = \"song\", columnName = \"downloadState\"),\n    DeleteColumn(tableName = \"artist\", columnName = \"bannerUrl\"),\n    DeleteColumn(tableName = \"artist\", columnName = \"description\"),\n    DeleteColumn(tableName = \"artist\", columnName = \"createDate\"),\n)\nclass Migration10To11 : AutoMigrationSpec\n\n@DeleteColumn.Entries(\n    DeleteColumn(tableName = \"album\", columnName = \"createDate\"),\n)\nclass Migration11To12 : AutoMigrationSpec {\n    override fun onPostMigrate(db: SupportSQLiteDatabase) {\n        db.execSQL(\"UPDATE album SET bookmarkedAt = lastUpdateTime\")\n        db.query(\"SELECT DISTINCT albumId, albumName FROM song\").use { cursor ->\n            while (cursor.moveToNext()) {\n                val albumId = cursor.getString(0)\n                val albumName = cursor.getString(1)\n                db.insert(\n                    table = \"album\",\n                    conflictAlgorithm = SQLiteDatabase.CONFLICT_IGNORE,\n                    values =\n                    contentValuesOf(\n                        \"id\" to albumId,\n                        \"title\" to albumName,\n                        \"songCount\" to 0,\n                        \"duration\" to 0,\n                        \"lastUpdateTime\" to 0,\n                    ),\n                )\n            }\n        }\n        db.query(\"CREATE INDEX IF NOT EXISTS `index_song_albumId` ON `song` (`albumId`)\")\n    }\n}\n\nclass Migration12To13 : AutoMigrationSpec {\n    override fun onPostMigrate(db: SupportSQLiteDatabase) {\n    }\n}\n\nclass Migration13To14 : AutoMigrationSpec {\n    @SuppressLint(\"Range\")\n    override fun onPostMigrate(db: SupportSQLiteDatabase) {\n        db.execSQL(\"UPDATE playlist SET createdAt = '${Converters().dateToTimestamp(LocalDateTime.now())}'\")\n        db.execSQL(\n            \"UPDATE playlist SET lastUpdateTime = '${Converters().dateToTimestamp(LocalDateTime.now())}'\"\n        )\n    }\n}\n\n@DeleteColumn.Entries(\n    DeleteColumn(tableName = \"song\", columnName = \"isLocal\"),\n    DeleteColumn(tableName = \"song\", columnName = \"localPath\"),\n    DeleteColumn(tableName = \"artist\", columnName = \"isLocal\"),\n    DeleteColumn(tableName = \"playlist\", columnName = \"isLocal\"),\n)\nclass Migration16To17 : AutoMigrationSpec {\n    override fun onPostMigrate(db: SupportSQLiteDatabase) {\n        db.execSQL(\"UPDATE playlist SET bookmarkedAt = lastUpdateTime\")\n        db.execSQL(\"UPDATE playlist SET isEditable = 1 WHERE browseId IS NOT NULL\")\n    }\n}\n\nclass Migration18To19 : AutoMigrationSpec {\n    override fun onPostMigrate(db: SupportSQLiteDatabase) {\n        db.execSQL(\"UPDATE song SET explicit = 0 WHERE explicit IS NULL\")\n    }\n}\n\nclass Migration19To20 : AutoMigrationSpec {\n    override fun onPostMigrate(db: SupportSQLiteDatabase) {\n        db.execSQL(\"UPDATE song SET explicit = 0 WHERE explicit IS NULL\")\n    }\n}\n\n@DeleteColumn.Entries(\n    DeleteColumn(\n        tableName = \"song\",\n        columnName = \"artistName\"\n    )\n)\nclass Migration20To21 : AutoMigrationSpec\n\nclass Migration21To22 : AutoMigrationSpec {\n    override fun onPostMigrate(db: SupportSQLiteDatabase) {\n        try {\n            db.execSQL(\"ALTER TABLE song ADD COLUMN libraryAddToken TEXT DEFAULT ''\")\n        } catch (e: Exception) {\n            Timber.tag(\"Migration21To22\").w(e, \"Column may already exist\")\n        }\n        try {\n            db.execSQL(\"ALTER TABLE song ADD COLUMN libraryRemoveToken TEXT DEFAULT ''\")\n        } catch (e: Exception) {\n            Timber.tag(\"Migration21To22\").w(e, \"Column may already exist\")\n        }\n        try {\n            db.execSQL(\"ALTER TABLE song ADD COLUMN romanizeLyrics INTEGER NOT NULL DEFAULT 1\")\n        } catch (e: Exception) {\n            Timber.tag(\"Migration21To22\").w(e, \"Column may already exist\")\n        }\n        try {\n            db.execSQL(\"ALTER TABLE song ADD COLUMN isDownloaded INTEGER NOT NULL DEFAULT 0\")\n        } catch (e: Exception) {\n            Timber.tag(\"Migration21To22\").w(e, \"Column may already exist\")\n        }\n    }\n}\n\nclass Migration22To23 : AutoMigrationSpec {\n    override fun onPostMigrate(db: SupportSQLiteDatabase) {\n        // No changes needed for 22→23\n    }\n}\n\nclass Migration23To24: AutoMigrationSpec {\n    override fun onPostMigrate(db: SupportSQLiteDatabase) {\n        var hasIsUploaded = false\n        db.query(\"PRAGMA table_info('song')\").use { cursor ->\n            val nameIndex = cursor.getColumnIndex(\"name\")\n            while (cursor.moveToNext()) {\n                val colName = if (nameIndex >= 0) cursor.getString(nameIndex) else null\n                if (colName == \"isUploaded\") {\n                    hasIsUploaded = true\n                    break\n                }\n            }\n        }\n\n        if (!hasIsUploaded) {\n            db.execSQL(\"ALTER TABLE `song` ADD COLUMN `isUploaded` INTEGER NOT NULL DEFAULT 0\")\n        }\n    }\n}\n\nval MIGRATION_24_25 =\n    object : Migration(24, 25) {\n        override fun migrate(db: SupportSQLiteDatabase) {\n            // Add perceptualLoudnessDb column to format table for improved audio normalization\n            var columnExists = false\n            db.query(\"PRAGMA table_info(format)\").use { cursor ->\n                val nameIndex = cursor.getColumnIndex(\"name\")\n                while (cursor.moveToNext()) {\n                    if (cursor.getString(nameIndex) == \"perceptualLoudnessDb\") {\n                        columnExists = true\n                        break\n                    }\n                }\n            }\n\n            if (!columnExists) {\n                // Add the column allowing NULL values (since existing rows won't have this data)\n                db.execSQL(\"ALTER TABLE format ADD COLUMN perceptualLoudnessDb REAL DEFAULT NULL\")\n            }\n        }\n    }\n\nclass Migration29To30 : AutoMigrationSpec {\n    override fun onPostMigrate(db: SupportSQLiteDatabase) {\n        // Ensure isVideo column exists (safeguard)\n        var hasIsVideo = false\n        db.query(\"PRAGMA table_info('song')\").use { cursor ->\n            val nameIndex = cursor.getColumnIndex(\"name\")\n            while (cursor.moveToNext()) {\n                val colName = if (nameIndex >= 0) cursor.getString(nameIndex) else null\n                if (colName == \"isVideo\") {\n                    hasIsVideo = true\n                    break\n                }\n            }\n        }\n        if (!hasIsVideo) {\n            db.execSQL(\"ALTER TABLE song ADD COLUMN isVideo INTEGER NOT NULL DEFAULT 0\")\n        }\n\n        // Ensure provider column exists in lyrics table\n        var hasProvider = false\n        db.query(\"PRAGMA table_info('lyrics')\").use { cursor ->\n            val nameIndex = cursor.getColumnIndex(\"name\")\n            while (cursor.moveToNext()) {\n                val colName = if (nameIndex >= 0) cursor.getString(nameIndex) else null\n                if (colName == \"provider\") {\n                    hasProvider = true\n                    break\n                }\n            }\n        }\n        if (!hasProvider) {\n            db.execSQL(\"ALTER TABLE lyrics ADD COLUMN provider TEXT NOT NULL DEFAULT 'Unknown'\")\n        }\n    }\n}\n\nclass Migration35To36 : AutoMigrationSpec {\n    override fun onPostMigrate(db: SupportSQLiteDatabase) {\n        var hasIsCached = false\n        db.query(\"PRAGMA table_info('song')\").use { cursor ->\n            val nameIndex = cursor.getColumnIndex(\"name\")\n            while (cursor.moveToNext()) {\n                val colName = if (nameIndex >= 0) cursor.getString(nameIndex) else null\n                if (colName == \"isCached\") {\n                    hasIsCached = true\n                    break\n                }\n            }\n        }\n        if (!hasIsCached) {\n            db.execSQL(\"ALTER TABLE song ADD COLUMN isCached INTEGER NOT NULL DEFAULT 0\")\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/daos/SpeedDialDao.kt",
    "content": "package com.metrolist.music.db.daos\n\nimport androidx.room.Dao\nimport androidx.room.Insert\nimport androidx.room.OnConflictStrategy\nimport androidx.room.Query\nimport com.metrolist.music.db.entities.SpeedDialItem\nimport kotlinx.coroutines.flow.Flow\n\n@Dao\ninterface SpeedDialDao {\n    @Query(\"SELECT * FROM speed_dial_item ORDER BY createDate ASC\")\n    fun getAll(): Flow<List<SpeedDialItem>>\n\n    @Insert(onConflict = OnConflictStrategy.REPLACE)\n    suspend fun insert(item: SpeedDialItem)\n\n    @Query(\"DELETE FROM speed_dial_item WHERE id = :id\")\n    suspend fun delete(id: String)\n\n    @Query(\"SELECT EXISTS(SELECT * FROM speed_dial_item WHERE id = :id)\")\n    fun isPinned(id: String): Flow<Boolean>\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/Album.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.compose.runtime.Immutable\nimport androidx.room.Embedded\nimport androidx.room.Junction\nimport androidx.room.Relation\n\n@Immutable\ndata class Album(\n    @Embedded\n    val album: AlbumEntity,\n    @Relation(\n        entity = ArtistEntity::class,\n        entityColumn = \"id\",\n        parentColumn = \"id\",\n        associateBy =\n        Junction(\n            value = AlbumArtistMap::class,\n            parentColumn = \"albumId\",\n            entityColumn = \"artistId\",\n        ),\n    )\n    val artists: List<ArtistEntity> = emptyList(),\n    val songCountListened: Int? = 0,\n    val timeListened: Long? = 0\n) : LocalItem() {\n    override val id: String\n        get() = album.id\n    override val title: String\n        get() = album.title\n    override val thumbnailUrl: String?\n        get() = album.thumbnailUrl\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/AlbumArtistMap.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.ForeignKey\n\n@Entity(\n    tableName = \"album_artist_map\",\n    primaryKeys = [\"albumId\", \"artistId\"],\n    foreignKeys = [\n        ForeignKey(\n            entity = AlbumEntity::class,\n            parentColumns = [\"id\"],\n            childColumns = [\"albumId\"],\n            onDelete = ForeignKey.CASCADE,\n        ),\n        ForeignKey(\n            entity = ArtistEntity::class,\n            parentColumns = [\"id\"],\n            childColumns = [\"artistId\"],\n            onDelete = ForeignKey.CASCADE,\n        ),\n    ]\n)\ndata class AlbumArtistMap(\n    @ColumnInfo(index = true) val albumId: String,\n    @ColumnInfo(index = true) val artistId: String,\n    val order: Int,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/AlbumEntity.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.compose.runtime.Immutable\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\nimport com.metrolist.innertube.YouTube\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport java.time.LocalDateTime\n\n@Immutable\n@Entity(tableName = \"album\")\ndata class AlbumEntity(\n    @PrimaryKey val id: String,\n    val playlistId: String? = null,\n    val title: String,\n    val year: Int? = null,\n    val thumbnailUrl: String? = null,\n    val themeColor: Int? = null,\n    val songCount: Int,\n    val duration: Int,\n    @ColumnInfo(defaultValue = \"0\")\n    val explicit: Boolean = false,\n    val lastUpdateTime: LocalDateTime = LocalDateTime.now(),\n    val bookmarkedAt: LocalDateTime? = null,\n    val likedDate: LocalDateTime? = null,\n    val inLibrary: LocalDateTime? = null,\n    @ColumnInfo(name = \"isLocal\", defaultValue = false.toString())\n    val isLocal: Boolean = false,\n    @ColumnInfo(name = \"isUploaded\", defaultValue = false.toString())\n    val isUploaded: Boolean = false\n) {\n    fun localToggleLike() = copy(\n        bookmarkedAt = if (bookmarkedAt != null) null else LocalDateTime.now()\n    )\n\n    fun toggleUploaded() = copy(\n        isUploaded = !isUploaded\n    )\n\n    fun toggleLibrary() = copy(\n        inLibrary = if (inLibrary != null) null else LocalDateTime.now()\n    )\n\n    fun toggleLike() = localToggleLike().also {\n        CoroutineScope(Dispatchers.IO).launch {\n            if (playlistId != null)\n                YouTube.likePlaylist(playlistId, bookmarkedAt == null)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/AlbumWithSongs.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.compose.runtime.Immutable\nimport androidx.room.Embedded\nimport androidx.room.Junction\nimport androidx.room.Relation\n\n@Immutable\ndata class AlbumWithSongs(\n    @Embedded\n    val album: AlbumEntity,\n    @Relation(\n        entity = ArtistEntity::class,\n        entityColumn = \"id\",\n        parentColumn = \"id\",\n        associateBy =\n        Junction(\n            value = AlbumArtistMap::class,\n            parentColumn = \"albumId\",\n            entityColumn = \"artistId\",\n        ),\n    )\n    val artists: List<ArtistEntity>,\n    @Relation(\n        entity = SongEntity::class,\n        entityColumn = \"id\",\n        parentColumn = \"id\",\n        associateBy =\n        Junction(\n            value = SortedSongAlbumMap::class,\n            parentColumn = \"albumId\",\n            entityColumn = \"songId\",\n        ),\n    )\n    val songs: List<Song>,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/Artist.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.compose.runtime.Immutable\nimport androidx.room.Embedded\n\n@Immutable\ndata class Artist(\n    @Embedded\n    val artist: ArtistEntity,\n    val songCount: Int,\n    val timeListened: Int? = 0,\n) : LocalItem() {\n    override val id: String\n        get() = artist.id\n    override val title: String\n        get() = artist.name\n    override val thumbnailUrl: String?\n        get() = artist.thumbnailUrl\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/ArtistEntity.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.compose.runtime.Immutable\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\nimport com.metrolist.innertube.YouTube\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport org.apache.commons.lang3.RandomStringUtils\nimport java.time.LocalDateTime\n\n@Immutable\n@Entity(tableName = \"artist\")\ndata class ArtistEntity(\n    @PrimaryKey val id: String,\n    val name: String,\n    val thumbnailUrl: String? = null,\n    val channelId: String? = null,\n    val lastUpdateTime: LocalDateTime = LocalDateTime.now(),\n    val bookmarkedAt: LocalDateTime? = null,\n    @ColumnInfo(name = \"isLocal\", defaultValue = false.toString())\n    val isLocal: Boolean = false,\n    @ColumnInfo(name = \"isPodcastChannel\", defaultValue = false.toString())\n    val isPodcastChannel: Boolean = false\n) {\n    val isYouTubeArtist: Boolean\n        get() = id.startsWith(\"UC\") || id.startsWith(\"FEmusic_library_privately_owned_artist\")\n\n    val isPrivatelyOwnedArtist: Boolean\n        get() = id.startsWith(\"FEmusic_library_privately_owned_artist\")\n\n    fun localToggleLike() = copy(\n        bookmarkedAt = if (bookmarkedAt != null) null else LocalDateTime.now(),\n    )\n\n    fun toggleLike() = localToggleLike().also {\n        CoroutineScope(Dispatchers.IO).launch {\n            val targetChannelId = channelId ?: YouTube.getChannelId(id)\n            if (targetChannelId.isNotEmpty()) {\n                YouTube.subscribeChannel(targetChannelId, bookmarkedAt == null)\n            }\n        }\n    }\n\n    companion object {\n        fun generateArtistId() = \"LA\" + RandomStringUtils.insecure().next(8, true, false)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/Event.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.compose.runtime.Immutable\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.ForeignKey\nimport androidx.room.PrimaryKey\nimport java.time.LocalDateTime\n\n@Immutable\n@Entity(\n    tableName = \"event\",\n    foreignKeys = [\n        ForeignKey(\n            entity = SongEntity::class,\n            parentColumns = [\"id\"],\n            childColumns = [\"songId\"],\n            onDelete = ForeignKey.CASCADE,\n        ),\n    ],\n)\ndata class Event(\n    @PrimaryKey(autoGenerate = true) val id: Long = 0,\n    @ColumnInfo(index = true) val songId: String,\n    val timestamp: LocalDateTime,\n    val playTime: Long,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/EventWithSong.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.compose.runtime.Immutable\nimport androidx.room.Embedded\nimport androidx.room.Relation\n\n@Immutable\ndata class EventWithSong(\n    @Embedded\n    val event: Event,\n    @Relation(\n        entity = SongEntity::class,\n        parentColumn = \"songId\",\n        entityColumn = \"id\",\n    )\n    val song: Song,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/FormatEntity.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\n\n@Entity(tableName = \"format\")\ndata class FormatEntity(\n    @PrimaryKey val id: String,\n    val itag: Int,\n    val mimeType: String,\n    val codecs: String,\n    val bitrate: Int,\n    val sampleRate: Int?,\n    val contentLength: Long,\n    val loudnessDb: Double?,\n    val perceptualLoudnessDb: Double? = null,\n    @Deprecated(\"playbackTrackingUrl should be retrieved from a fresh player request\")\n    val playbackUrl: String?\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/LocalItem.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nsealed class LocalItem {\n    abstract val id: String\n    abstract val title: String\n    abstract val thumbnailUrl: String?\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/LyricsEntity.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\n\n@Entity(tableName = \"lyrics\")\ndata class LyricsEntity(\n    @PrimaryKey val id: String,\n    val lyrics: String,\n    @ColumnInfo(defaultValue = \"Unknown\") val provider: String = \"Unknown\",\n    @ColumnInfo(defaultValue = \"\") val translatedLyrics: String = \"\",\n    @ColumnInfo(defaultValue = \"\") val translationLanguage: String = \"\",\n    @ColumnInfo(defaultValue = \"\") val translationMode: String = \"\",\n) {\n    companion object {\n        const val LYRICS_NOT_FOUND = \"LYRICS_NOT_FOUND\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/PlayCountEntity.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.compose.runtime.Immutable\nimport androidx.room.Entity\n\n@Immutable\n@Entity(\n    tableName = \"playCount\",\n    primaryKeys = [\"song\", \"year\", \"month\"]\n)\nclass PlayCountEntity(\n    val song: String, // song id\n    val year: Int = -1,\n    val month: Int = -1,\n    val count: Int = -1,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/Playlist.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.compose.runtime.Immutable\nimport androidx.room.Embedded\nimport androidx.room.Junction\nimport androidx.room.Relation\n\n@Immutable\ndata class Playlist(\n    @Embedded\n    val playlist: PlaylistEntity,\n    val songCount: Int,\n    @Relation(\n        entity = SongEntity::class,\n        entityColumn = \"id\",\n        parentColumn = \"id\",\n        projection = [\"thumbnailUrl\"],\n        associateBy =\n        Junction(\n            value = PlaylistSongMapPreview::class,\n            parentColumn = \"playlistId\",\n            entityColumn = \"songId\",\n        ),\n    )\n    val songThumbnails: List<String?>,\n) : LocalItem() {\n    override val id: String\n        get() = playlist.id\n    override val title: String\n        get() = playlist.name\n    override val thumbnailUrl: String?\n        get() = null\n    \n    val thumbnails: List<String>\n        get() {\n            return if (playlist.thumbnailUrl != null)\n                listOf(playlist.thumbnailUrl)\n            else songThumbnails.filterNotNull()\n        }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/PlaylistEntity.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.compose.runtime.Immutable\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\nimport com.metrolist.innertube.YouTube\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport org.apache.commons.lang3.RandomStringUtils\nimport java.time.LocalDateTime\n\n@Immutable\n@Entity(tableName = \"playlist\")\ndata class PlaylistEntity(\n    @PrimaryKey val id: String = generatePlaylistId(),\n    val name: String,\n    val browseId: String? = null,\n    val createdAt: LocalDateTime? = LocalDateTime.now(),\n    val lastUpdateTime: LocalDateTime? = LocalDateTime.now(),\n    @ColumnInfo(name = \"isEditable\", defaultValue = true.toString())\n    val isEditable: Boolean = true,\n    val bookmarkedAt: LocalDateTime? = null,\n    val remoteSongCount: Int? = null,\n    val playEndpointParams: String? = null,\n    val thumbnailUrl: String? = null,\n    val shuffleEndpointParams: String? = null,\n    val radioEndpointParams: String? = null,\n    @ColumnInfo(name = \"isLocal\", defaultValue = false.toString())\n    val isLocal: Boolean = false,\n    @ColumnInfo(name = \"isAutoSync\", defaultValue = false.toString())\n    val isAutoSync: Boolean = false\n) {\n    companion object {\n        const val LIKED_PLAYLIST_ID = \"LP_LIKED\"\n        const val DOWNLOADED_PLAYLIST_ID = \"LP_DOWNLOADED\"\n        const val WEEKLY_MOST_PLAYLIST_ID = \"LP_WEEKLY_MOST\"\n        const val MONTHLY_MOST_PLAYLIST_ID = \"LP_MONTHLY_MOST\"\n\n        fun generatePlaylistId() = \"LP\" + RandomStringUtils.insecure().next(8, true, false)\n    }\n\n    val shareLink: String?\n        get() {\n            return if (browseId != null)\n                \"https://music.youtube.com/playlist?list=$browseId\"\n            else null\n        }\n\n    fun localToggleLike() = copy(\n        bookmarkedAt = if (bookmarkedAt != null) null else LocalDateTime.now()\n    )\n\n    fun toggleLike() = localToggleLike().also {\n        CoroutineScope(Dispatchers.IO).launch {\n            if (browseId != null)\n                YouTube.likePlaylist(browseId, bookmarkedAt == null)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/PlaylistSong.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.room.Embedded\nimport androidx.room.Relation\n\ndata class PlaylistSong(\n    @Embedded val map: PlaylistSongMap,\n    @Relation(\n        parentColumn = \"songId\",\n        entityColumn = \"id\",\n        entity = SongEntity::class,\n    )\n    val song: Song,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/PlaylistSongMap.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.ForeignKey\nimport androidx.room.PrimaryKey\n\n@Entity(\n    tableName = \"playlist_song_map\",\n    foreignKeys = [\n        ForeignKey(\n            entity = PlaylistEntity::class,\n            parentColumns = [\"id\"],\n            childColumns = [\"playlistId\"],\n            onDelete = ForeignKey.CASCADE,\n        ),\n        ForeignKey(\n            entity = SongEntity::class,\n            parentColumns = [\"id\"],\n            childColumns = [\"songId\"],\n            onDelete = ForeignKey.CASCADE,\n        ),\n    ],\n)\ndata class PlaylistSongMap(\n    @PrimaryKey(autoGenerate = true) val id: Int = 0,\n    @ColumnInfo(index = true) val playlistId: String,\n    @ColumnInfo(index = true) val songId: String,\n    val position: Int = 0,\n    val setVideoId: String? = null,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/PlaylistSongMapPreview.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.room.ColumnInfo\nimport androidx.room.DatabaseView\n\n@DatabaseView(\n    viewName = \"playlist_song_map_preview\",\n    value = \"SELECT * FROM playlist_song_map WHERE position <= 3 ORDER BY position\",\n)\ndata class PlaylistSongMapPreview(\n    @ColumnInfo(index = true) val playlistId: String,\n    @ColumnInfo(index = true) val songId: String,\n    val idInPlaylist: Int = 0,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/PodcastEntity.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.compose.runtime.Immutable\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\nimport java.time.LocalDateTime\n\n/**\n * Podcast library entries with YTM \"Save to Library\" sync support.\n *\n * Podcasts are saved using the likePlaylist API (like/like endpoint with playlistId).\n * The podcast ID format is \"MPSP<playlistId>\", e.g., \"MPSPPLxxx...\" where the\n * playlistId is extracted by removing the \"MPSP\" prefix.\n *\n * Note: channelId, libraryAddToken, libraryRemoveToken are legacy fields kept\n * for backwards compatibility. The correct API is likePlaylist().\n */\n@Immutable\n@Entity(tableName = \"podcast\")\ndata class PodcastEntity(\n    @PrimaryKey val id: String,\n    val title: String,\n    val author: String? = null,\n    val thumbnailUrl: String? = null,\n    val channelId: String? = null,\n    val bookmarkedAt: LocalDateTime? = null,\n    val lastUpdateTime: LocalDateTime = LocalDateTime.now(),\n    val libraryAddToken: String? = null,\n    val libraryRemoveToken: String? = null,\n) {\n    fun toggleBookmark() = copy(\n        bookmarkedAt = if (bookmarkedAt != null) null else LocalDateTime.now(),\n        lastUpdateTime = LocalDateTime.now(),\n    )\n\n    val inLibrary: Boolean get() = bookmarkedAt != null\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/RecognitionHistory.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.room.Entity\nimport androidx.room.Index\nimport androidx.room.PrimaryKey\nimport java.time.LocalDateTime\n\n@Entity(\n    tableName = \"recognition_history\",\n    indices = [\n        Index(\n            value = [\"trackId\"],\n            unique = false,\n        ),\n    ],\n)\ndata class RecognitionHistory(\n    @PrimaryKey(autoGenerate = true) val id: Long = 0,\n    val trackId: String,\n    val title: String,\n    val artist: String,\n    val album: String? = null,\n    val coverArtUrl: String? = null,\n    val coverArtHqUrl: String? = null,\n    val genre: String? = null,\n    val releaseDate: String? = null,\n    val label: String? = null,\n    val shazamUrl: String? = null,\n    val appleMusicUrl: String? = null,\n    val spotifyUrl: String? = null,\n    val isrc: String? = null,\n    val youtubeVideoId: String? = null,\n    val recognizedAt: LocalDateTime = LocalDateTime.now(),\n    val liked: Boolean = false\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/RelatedSongMap.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.ForeignKey\nimport androidx.room.PrimaryKey\n\n@Entity(\n    tableName = \"related_song_map\",\n    foreignKeys = [\n        ForeignKey(\n            entity = SongEntity::class,\n            parentColumns = [\"id\"],\n            childColumns = [\"songId\"],\n            onDelete = ForeignKey.CASCADE,\n        ),\n        ForeignKey(\n            entity = SongEntity::class,\n            parentColumns = [\"id\"],\n            childColumns = [\"relatedSongId\"],\n            onDelete = ForeignKey.CASCADE,\n        ),\n    ],\n)\ndata class RelatedSongMap(\n    @PrimaryKey(autoGenerate = true) val id: Long = 0,\n    @ColumnInfo(index = true) val songId: String,\n    @ColumnInfo(index = true) val relatedSongId: String,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/SearchHistory.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.room.Entity\nimport androidx.room.Index\nimport androidx.room.PrimaryKey\n\n@Entity(\n    tableName = \"search_history\",\n    indices = [\n        Index(\n            value = [\"query\"],\n            unique = true,\n        ),\n    ],\n)\ndata class SearchHistory(\n    @PrimaryKey(autoGenerate = true) val id: Long = 0,\n    val query: String,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/SetVideoIdEntity.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\n\n@Entity(tableName = \"set_video_id\")\ndata class SetVideoIdEntity(\n    @PrimaryKey(autoGenerate = false)\n    val videoId: String = \"\",\n    val setVideoId: String? = null,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/Song.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.compose.runtime.Immutable\nimport androidx.room.Embedded\nimport androidx.room.Junction\nimport androidx.room.Relation\n\n@Immutable\ndata class Song\n@JvmOverloads\nconstructor(\n    @Embedded val song: SongEntity,\n\n    @Relation(\n        entity = ArtistEntity::class,\n        entityColumn = \"id\",\n        parentColumn = \"id\",\n        associateBy =\n            Junction(\n                value = SortedSongArtistMap::class,\n                parentColumn = \"songId\",\n                entityColumn = \"artistId\",\n            ),\n    )\n    val artists: List<ArtistEntity>,\n\n    @Relation(\n        parentColumn = \"id\",\n        entityColumn = \"songId\",\n    )\n    val artistMaps: List<SongArtistMap> = emptyList(),\n\n    @Relation(\n        entity = AlbumEntity::class,\n        entityColumn = \"id\",\n        parentColumn = \"id\",\n        associateBy =\n            Junction(\n                value = SongAlbumMap::class,\n                parentColumn = \"songId\",\n                entityColumn = \"albumId\",\n            ),\n    )\n    val album: AlbumEntity? = null,\n\n    @Relation(\n        parentColumn = \"id\",\n        entityColumn = \"id\"\n    )\n    val format: FormatEntity? = null,\n) : LocalItem() {\n    override val id: String\n        get() = song.id\n    override val title: String\n        get() = song.title\n    override val thumbnailUrl: String?\n        get() = song.thumbnailUrl\n    val romanizeLyrics: Boolean\n        get() = song.romanizeLyrics\n\n    val orderedArtists: List<ArtistEntity>\n        get() {\n            if (artistMaps.isEmpty()) return artists\n\n            val artistsById = artists.associateBy { it.id }\n            val sorted = artistMaps\n                .sortedBy { it.position }\n                .mapNotNull { map -> artistsById[map.artistId] }\n\n            return sorted.ifEmpty { artists }\n        }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/SongAlbumMap.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.ForeignKey\n\n@Entity(\n    tableName = \"song_album_map\",\n    primaryKeys = [\"songId\", \"albumId\"],\n    foreignKeys = [\n        ForeignKey(\n            entity = SongEntity::class,\n            parentColumns = [\"id\"],\n            childColumns = [\"songId\"],\n            onDelete = ForeignKey.CASCADE,\n        ),\n        ForeignKey(\n            entity = AlbumEntity::class,\n            parentColumns = [\"id\"],\n            childColumns = [\"albumId\"],\n            onDelete = ForeignKey.CASCADE,\n        ),\n    ],\n)\ndata class SongAlbumMap(\n    @ColumnInfo(index = true) val songId: String,\n    @ColumnInfo(index = true) val albumId: String,\n    val index: Int,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/SongArtistMap.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.ForeignKey\n\n@Entity(\n    tableName = \"song_artist_map\",\n    primaryKeys = [\"songId\", \"artistId\"],\n    foreignKeys = [\n        ForeignKey(\n            entity = SongEntity::class,\n            parentColumns = [\"id\"],\n            childColumns = [\"songId\"],\n            onDelete = ForeignKey.CASCADE,\n        ),\n        ForeignKey(\n            entity = ArtistEntity::class,\n            parentColumns = [\"id\"],\n            childColumns = [\"artistId\"],\n            onDelete = ForeignKey.CASCADE,\n        ),\n    ],\n)\ndata class SongArtistMap(\n    @ColumnInfo(index = true) val songId: String,\n    @ColumnInfo(index = true) val artistId: String,\n    val position: Int,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/SongEntity.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.compose.runtime.Immutable\nimport androidx.room.ColumnInfo\nimport androidx.room.Entity\nimport androidx.room.Index\nimport androidx.room.PrimaryKey\nimport com.metrolist.innertube.YouTube\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport java.time.LocalDateTime\n\n@Immutable\n@Entity(\n    tableName = \"song\",\n    indices = [\n        Index(\n            value = [\"albumId\"]\n        )\n    ]\n)\ndata class SongEntity(\n    @PrimaryKey val id: String,\n    val title: String,\n    val duration: Int = -1, // in seconds\n    val thumbnailUrl: String? = null,\n    val albumId: String? = null,\n    val albumName: String? = null,\n    @ColumnInfo(defaultValue = \"0\")\n    val explicit: Boolean = false,\n    val year: Int? = null,\n    val date: LocalDateTime? = null, // ID3 tag property\n    val dateModified: LocalDateTime? = null, // file property\n    val liked: Boolean = false,\n    val likedDate: LocalDateTime? = null,\n    val totalPlayTime: Long = 0, // in milliseconds\n    val inLibrary: LocalDateTime? = null,\n    val dateDownload: LocalDateTime? = null,\n    @ColumnInfo(name = \"isLocal\", defaultValue = false.toString())\n    val isLocal: Boolean = false,\n    val libraryAddToken: String? = null,\n    val libraryRemoveToken: String? = null,\n    @ColumnInfo(defaultValue = \"0\")\n    val lyricsOffset: Int = 0,\n    @ColumnInfo(defaultValue = true.toString())\n    val romanizeLyrics: Boolean = true,\n    @ColumnInfo(defaultValue = \"0\")\n    val isDownloaded: Boolean = false,\n    @ColumnInfo(name = \"isUploaded\", defaultValue = false.toString())\n    val isUploaded: Boolean = false,\n    @ColumnInfo(name = \"isVideo\", defaultValue = false.toString())\n    val isVideo: Boolean = false,\n    @ColumnInfo(name = \"isEpisode\", defaultValue = false.toString())\n    val isEpisode: Boolean = false,\n    @ColumnInfo(name = \"playbackPosition\", defaultValue = \"NULL\")\n    val playbackPosition: Long? = null,\n    @ColumnInfo(name = \"uploadEntityId\", defaultValue = \"NULL\")\n    val uploadEntityId: String? = null,\n    @ColumnInfo(name = \"isCached\", defaultValue = \"0\")\n    val isCached: Boolean = false\n) {\n    fun localToggleLike() = copy(\n        liked = !liked,\n        likedDate = if (!liked) LocalDateTime.now() else null,\n    )\n\n    fun toggleLike() = copy(\n        liked = !liked,\n        likedDate = if (!liked) LocalDateTime.now() else null,\n        inLibrary = if (!liked) inLibrary ?: LocalDateTime.now() else inLibrary\n    ).also {\n        CoroutineScope(Dispatchers.IO).launch {\n            YouTube.likeVideo(id, !liked)\n        }\n    }\n\n    fun toggleLibrary(syncToYouTube: Boolean = true) = copy(\n        liked = if (inLibrary == null) liked else false,\n        inLibrary = if (inLibrary == null) LocalDateTime.now() else null,\n        likedDate = if (inLibrary == null) likedDate else null\n    ).also {\n        if (syncToYouTube) {\n            CoroutineScope(Dispatchers.IO).launch {\n                // Use the new reliable method that fetches fresh tokens\n                val addToLibrary = inLibrary == null\n                YouTube.toggleSongLibrary(id, addToLibrary)\n            }\n        }\n    }\n\n    fun toggleUploaded() = copy(\n        isUploaded = !isUploaded\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/SongWithStats.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.compose.runtime.Immutable\nimport androidx.room.Embedded\nimport androidx.room.Junction\nimport androidx.room.Relation\n\n@Immutable\ndata class SongWithStats(\n    val id: String,\n    val title: String,\n    @Relation(\n        entity = ArtistEntity::class,\n        parentColumn = \"id\",               // Song's primary key column\n        entityColumn = \"id\",               // Artist's primary key column\n        associateBy = Junction(\n            value = SortedSongArtistMap::class,  // Junction table for the many-to-many relationship\n            parentColumn = \"songId\",            // Foreign key to the Song table\n            entityColumn = \"artistId\"           // Foreign key to the Artist table\n        )\n    )\n    val artists: List<ArtistEntity>,\n    val thumbnailUrl: String,\n    val artistName: String?,\n    val songCountListened: Int,\n    val timeListened: Long?,\n    val isVideo: Boolean = false,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/SortedSongAlbumMap.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.room.ColumnInfo\nimport androidx.room.DatabaseView\n\n@DatabaseView(\n    viewName = \"sorted_song_album_map\",\n    value = \"SELECT * FROM song_album_map ORDER BY `index`\",\n)\ndata class SortedSongAlbumMap(\n    @ColumnInfo(index = true) val songId: String,\n    @ColumnInfo(index = true) val albumId: String,\n    val index: Int,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/SortedSongArtistMap.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.db.entities\n\nimport androidx.room.ColumnInfo\nimport androidx.room.DatabaseView\n\n@DatabaseView(\n    viewName = \"sorted_song_artist_map\",\n    value = \"SELECT * FROM song_artist_map ORDER BY position\",\n)\ndata class SortedSongArtistMap(\n    @ColumnInfo(index = true) val songId: String,\n    @ColumnInfo(index = true) val artistId: String,\n    val position: Int,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/db/entities/SpeedDialItem.kt",
    "content": "package com.metrolist.music.db.entities\n\nimport androidx.room.Entity\nimport androidx.room.PrimaryKey\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.Artist\nimport com.metrolist.innertube.models.ArtistItem\nimport com.metrolist.innertube.models.EpisodeItem\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.PodcastItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.YTItem\n\n@Entity(tableName = \"speed_dial_item\")\ndata class SpeedDialItem(\n    @PrimaryKey val id: String,\n    val secondaryId: String? = null,\n    val title: String,\n    val subtitle: String? = null,\n    val thumbnailUrl: String? = null,\n    val type: String, // \"SONG\", \"ALBUM\", \"ARTIST\", \"PLAYLIST\", \"LOCAL_PLAYLIST\"\n    val explicit: Boolean = false,\n    val createDate: Long = System.currentTimeMillis()\n) {\n    fun toYTItem(): YTItem {\n        return when (type) {\n            \"SONG\" -> SongItem(\n                id = id,\n                title = title,\n                artists = subtitle?.split(\", \")?.map { Artist(name = it, id = null) } ?: emptyList(),\n                thumbnail = thumbnailUrl ?: \"\",\n                explicit = explicit\n            )\n            \"ALBUM\" -> AlbumItem(\n                browseId = id,\n                playlistId = secondaryId ?: \"\",\n                title = title,\n                artists = subtitle?.split(\", \")?.map { Artist(name = it, id = null) },\n                thumbnail = thumbnailUrl ?: \"\",\n                explicit = explicit\n            )\n            \"ARTIST\" -> ArtistItem(\n                id = id,\n                title = title,\n                thumbnail = thumbnailUrl,\n                shuffleEndpoint = null,\n                radioEndpoint = null\n            )\n            \"PLAYLIST\", \"LOCAL_PLAYLIST\" -> PlaylistItem(\n                id = id,\n                title = title,\n                author = subtitle?.let { Artist(name = it, id = null) },\n                songCountText = null,\n                thumbnail = thumbnailUrl,\n                playEndpoint = null,\n                shuffleEndpoint = null,\n                radioEndpoint = null\n            )\n            else -> throw IllegalArgumentException(\"Unknown type: $type\")\n        }\n    }\n\n    companion object {\n        fun fromYTItem(item: YTItem): SpeedDialItem {\n            return when (item) {\n                is SongItem -> SpeedDialItem(\n                    id = item.id,\n                    title = item.title,\n                    subtitle = item.artists.joinToString(\", \") { it.name },\n                    thumbnailUrl = item.thumbnail,\n                    type = \"SONG\",\n                    explicit = item.explicit\n                )\n                is AlbumItem -> SpeedDialItem(\n                    id = item.browseId,\n                    secondaryId = item.playlistId,\n                    title = item.title,\n                    subtitle = item.artists?.joinToString(\", \") { it.name },\n                    thumbnailUrl = item.thumbnail,\n                    type = \"ALBUM\",\n                    explicit = item.explicit\n                )\n                is ArtistItem -> SpeedDialItem(\n                    id = item.id,\n                    title = item.title,\n                    thumbnailUrl = item.thumbnail,\n                    type = \"ARTIST\"\n                )\n                is PlaylistItem -> SpeedDialItem(\n                    id = item.id,\n                    title = item.title,\n                    subtitle = item.author?.name,\n                    thumbnailUrl = item.thumbnail,\n                    type = \"PLAYLIST\"\n                )\n                is PodcastItem -> SpeedDialItem(\n                    id = item.id,\n                    title = item.title,\n                    subtitle = item.author?.name,\n                    thumbnailUrl = item.thumbnail,\n                    type = \"PLAYLIST\"\n                )\n                is EpisodeItem -> SpeedDialItem(\n                    id = item.id,\n                    title = item.title,\n                    subtitle = item.author?.name,\n                    thumbnailUrl = item.thumbnail,\n                    type = \"SONG\",\n                    explicit = item.explicit\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/di/AppModule.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.di\n\nimport android.content.Context\nimport androidx.media3.database.DatabaseProvider\nimport androidx.media3.database.StandaloneDatabaseProvider\nimport androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor\nimport androidx.media3.datasource.cache.NoOpCacheEvictor\nimport androidx.media3.datasource.cache.SimpleCache\nimport androidx.room.Room\nimport com.metrolist.music.constants.MaxSongCacheSizeKey\nimport com.metrolist.music.db.InternalDatabase\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.listentogether.ListenTogetherClient\nimport com.metrolist.music.listentogether.ListenTogetherManager\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\nimport dagger.Module\nimport dagger.Provides\nimport dagger.hilt.InstallIn\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport dagger.hilt.components.SingletonComponent\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.SupervisorJob\nimport javax.inject.Singleton\n\n@Module\n@InstallIn(SingletonComponent::class)\nobject AppModule {\n\n    @Provides\n    @Singleton\n    @ApplicationScope\n    fun provideApplicationScope(): CoroutineScope {\n        return CoroutineScope(SupervisorJob() + Dispatchers.Default)\n    }\n\n    @Singleton\n    @Provides\n    fun provideDao(\n        database: InternalDatabase,\n    ) = database.dao\n\n    @Singleton\n    @Provides\n    fun provideInternalDatabase(\n        @ApplicationContext context: Context,\n    ): InternalDatabase = Room\n        .databaseBuilder(context, InternalDatabase::class.java, InternalDatabase.DB_NAME)\n        .build()\n\n    @Singleton\n    @Provides\n    fun provideDatabase(\n        internalDatabase: InternalDatabase,\n    ): MusicDatabase = MusicDatabase(internalDatabase)\n\n    @Singleton\n    @Provides\n    fun provideDatabaseProvider(\n        @ApplicationContext context: Context,\n    ): DatabaseProvider = StandaloneDatabaseProvider(context)\n\n    @Singleton\n    @Provides\n    @PlayerCache\n    fun providePlayerCache(\n        @ApplicationContext context: Context,\n        databaseProvider: DatabaseProvider,\n        musicDatabase: MusicDatabase,\n    ): SimpleCache {\n        val cacheSize = context.dataStore[MaxSongCacheSizeKey] ?: 1024\n        return SimpleCache(\n            context.filesDir.resolve(\"exoplayer\"),\n            com.metrolist.music.playback.MetrolistCacheEvictor(\n                when (cacheSize) {\n                    -1 -> NoOpCacheEvictor()\n                    else -> LeastRecentlyUsedCacheEvictor(cacheSize * 1024 * 1024L)\n                },\n                musicDatabase\n            ),\n            databaseProvider,\n        )\n    }\n\n    @Singleton\n    @Provides\n    @DownloadCache\n    fun provideDownloadCache(\n        @ApplicationContext context: Context,\n        databaseProvider: DatabaseProvider,\n    ): SimpleCache {\n        return SimpleCache(\n            context.filesDir.resolve(\"download\"),\n            NoOpCacheEvictor(),\n            databaseProvider\n        )\n    }\n\n    @Singleton\n    @Provides\n    fun provideListenTogetherClient(\n        @ApplicationContext context: Context,\n    ): ListenTogetherClient = ListenTogetherClient(context)\n\n    @Singleton\n    @Provides\n    fun provideListenTogetherManager(\n        @ApplicationContext context: Context,\n        client: ListenTogetherClient,\n    ): ListenTogetherManager = ListenTogetherManager(client, context)\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/di/LyricsHelperEntryPoint.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.di\n\nimport com.metrolist.music.lyrics.LyricsHelper\nimport dagger.hilt.EntryPoint\nimport dagger.hilt.InstallIn\nimport dagger.hilt.components.SingletonComponent\n\n@EntryPoint\n@InstallIn(SingletonComponent::class)\ninterface LyricsHelperEntryPoint {\n    fun lyricsHelper(): LyricsHelper\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/di/NetworkModule.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.di\n\nimport android.content.Context\nimport com.metrolist.music.utils.NetworkConnectivityObserver\nimport dagger.Module\nimport dagger.Provides\nimport dagger.hilt.InstallIn\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport dagger.hilt.components.SingletonComponent\nimport javax.inject.Singleton\n\n@Module\n@InstallIn(SingletonComponent::class)\nobject NetworkModule {\n\n    @Provides\n    @Singleton\n    fun provideNetworkConnectivityObserver(@ApplicationContext context: Context): NetworkConnectivityObserver {\n        return NetworkConnectivityObserver(context)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/di/Qualifiers.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.di\n\nimport javax.inject.Qualifier\n\n@Qualifier\n@Retention(AnnotationRetention.BINARY)\nannotation class PlayerCache\n\n@Qualifier\n@Retention(AnnotationRetention.BINARY)\nannotation class DownloadCache\n\n@Qualifier\n@Retention(AnnotationRetention.BINARY)\nannotation class ApplicationScope\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/di/WrappedModule.kt",
    "content": "package com.metrolist.music.di\n\nimport android.content.Context\nimport com.metrolist.music.db.DatabaseDao\nimport com.metrolist.music.ui.screens.wrapped.WrappedAudioService\nimport com.metrolist.music.ui.screens.wrapped.WrappedManager\nimport dagger.Module\nimport dagger.Provides\nimport dagger.hilt.InstallIn\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport dagger.hilt.components.SingletonComponent\nimport javax.inject.Singleton\n\n@Module\n@InstallIn(SingletonComponent::class)\nobject WrappedModule {\n    @Provides\n    @Singleton\n    fun provideWrappedManager(\n        databaseDao: DatabaseDao,\n        @ApplicationContext context: Context,\n    ): WrappedManager = WrappedManager(databaseDao, context)\n\n    @Provides\n    @Singleton\n    fun provideWrappedAudioService(@ApplicationContext context: Context): WrappedAudioService = WrappedAudioService(context)\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/eq/EqualizerService.kt",
    "content": "package com.metrolist.music.eq\n\n\nimport android.annotation.SuppressLint\nimport androidx.annotation.OptIn\nimport androidx.media3.common.util.UnstableApi\nimport com.metrolist.music.eq.audio.CustomEqualizerAudioProcessor\nimport com.metrolist.music.eq.data.ParametricEQ\nimport com.metrolist.music.eq.data.SavedEQProfile\nimport timber.log.Timber\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Service for managing custom EQ using ExoPlayer's AudioProcessor\n * Supports 10+ band Parametric EQ format (APO)\n */\n@Singleton\nclass EqualizerService @Inject constructor() {\n\n    @SuppressLint(\"UnsafeOptInUsageError\")\n    private val audioProcessors = mutableListOf<CustomEqualizerAudioProcessor>()\n    private var pendingProfile: SavedEQProfile? = null\n    private var shouldDisable: Boolean = false\n\n    companion object {\n        private const val TAG = \"EqualizerService\"\n    }\n\n    /**\n     * Add an audio processor instance\n     * This should be called when ExoPlayer is initialized\n     */\n    @OptIn(UnstableApi::class)\n    fun addAudioProcessor(processor: CustomEqualizerAudioProcessor) {\n        audioProcessors.add(processor)\n        Timber.tag(TAG).d(\"Audio processor added. Total: ${audioProcessors.size}\")\n\n        // Apply pending profile if one was set before processor was available\n        if (shouldDisable) {\n            processor.disable()\n            // Don't clear shouldDisable here, as we might add more processors\n        } else if (pendingProfile != null) {\n            val profile = pendingProfile!!\n            applyProfileToProcessor(processor, profile)\n            // Don't clear pendingProfile here\n        }\n    }\n\n    /**\n     * Remove an audio processor instance\n     */\n    fun removeAudioProcessor(processor: CustomEqualizerAudioProcessor) {\n        audioProcessors.remove(processor)\n    }\n\n    /**\n     * Apply an EQ profile\n     * If audio processor is not set, stores as pending profile\n     */\n    @OptIn(UnstableApi::class)\n    fun applyProfile(profile: SavedEQProfile): Result<Unit> {\n        if (audioProcessors.isEmpty()) {\n            Timber.tag(TAG)\n                .w(\"No audio processors set yet. Storing profile as pending: ${profile.name}\")\n            pendingProfile = profile\n            shouldDisable = false\n            return Result.success(Unit)\n        }\n\n        pendingProfile = profile // Keep it for future processors\n        shouldDisable = false\n        \n        var success = true\n        var lastError: Exception? = null\n\n        audioProcessors.forEach { processor ->\n            try {\n                applyProfileToProcessor(processor, profile)\n            } catch (e: Exception) {\n                success = false\n                lastError = e\n            }\n        }\n\n        return if (success) Result.success(Unit) else Result.failure(lastError ?: Exception(\"Unknown error\"))\n    }\n\n    private fun applyProfileToProcessor(processor: CustomEqualizerAudioProcessor, profile: SavedEQProfile) {\n        val parametricEQ = ParametricEQ(\n            preamp = profile.preamp,\n            bands = profile.bands\n        )\n        processor.applyProfile(parametricEQ)\n    }\n\n    /**\n     * Disable the equalizer (flat response)\n     * If audio processor is not set, stores pending disable request\n     */\n    @OptIn(UnstableApi::class)\n    fun disable() {\n        if (audioProcessors.isEmpty()) {\n            Timber.tag(TAG).w(\"No audio processors set yet. Storing disable as pending\")\n            shouldDisable = true\n            pendingProfile = null\n            return\n        }\n\n        shouldDisable = true // Keep state\n        pendingProfile = null\n\n        audioProcessors.forEach { processor ->\n            try {\n                processor.disable()\n            } catch (e: Exception) {\n                Timber.tag(TAG).e(\"Failed to disable equalizer: ${e.message}\")\n            }\n        }\n        Timber.tag(TAG).d(\"Equalizer disabled on all processors\")\n    }\n\n    /**\n     * Check if audio processor is set\n     */\n    fun isInitialized(): Boolean {\n        return audioProcessors.isNotEmpty()\n    }\n\n    /**\n     * Check if equalizer is enabled\n     */\n    @OptIn(UnstableApi::class)\n    fun isEnabled(): Boolean {\n        return audioProcessors.any { it.isEnabled() }\n    }\n\n    /**\n     * Get information about the current EQ capabilities\n     */\n    fun getEqualizerInfo(): EqualizerInfo {\n        return EqualizerInfo(\n            supportsUnlimitedBands = true,\n            maxBands = Int.MAX_VALUE,\n            description = \"Custom ExoPlayer AudioProcessor with biquad filters\"\n        )\n    }\n\n    /**\n     * Release resources (not needed for AudioProcessor, but kept for API compatibility)\n     */\n    fun release() {\n        // AudioProcessor is managed by ExoPlayer, we just clear our reference\n        audioProcessors.clear()\n        Timber.tag(TAG).d(\"Audio processor references cleared\")\n    }\n}\n\n/**\n * Information about equalizer capabilities\n */\ndata class EqualizerInfo(\n    val supportsUnlimitedBands: Boolean,\n    val maxBands: Int,\n    val description: String\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/eq/audio/BiquadFilter.kt",
    "content": "package com.metrolist.music.eq.audio\n\nimport com.metrolist.music.eq.data.FilterType\nimport kotlin.math.PI\nimport kotlin.math.cos\nimport kotlin.math.pow\nimport kotlin.math.sin\nimport kotlin.math.sqrt\n\n/**\n * Biquad filter implementation for EQ\n * Supports peaking (PK), low-shelf (LSC), and high-shelf (HSC) filters\n * Based on Robert Bristow-Johnson's Audio EQ Cookbook\n */\nclass BiquadFilter(\n    private val sampleRate: Int,\n    private val frequency: Double,\n    private val gain: Double,\n    private val q: Double = 1.41,\n    private val filterType: FilterType = FilterType.PK\n) {\n    // Filter coefficients\n    private var a0 = 0.0\n    private var a1 = 0.0\n    private var a2 = 0.0\n    private var b0 = 0.0\n    private var b1 = 0.0\n    private var b2 = 0.0\n\n    // State variables for filtering (per channel)\n    private var x1L = 0.0\n    private var x2L = 0.0\n    private var y1L = 0.0\n    private var y2L = 0.0\n\n    private var x1R = 0.0\n    private var x2R = 0.0\n    private var y1R = 0.0\n    private var y2R = 0.0\n\n    init {\n        calculateCoefficients()\n    }\n\n    /**\n     * Calculate biquad filter coefficients based on filter type\n     * Based on Robert Bristow-Johnson's Audio EQ Cookbook\n     */\n    private fun calculateCoefficients() {\n        when (filterType) {\n            FilterType.PK -> calculatePeakingCoefficients()\n            FilterType.LSC -> calculateLowShelfCoefficients()\n            FilterType.HSC -> calculateHighShelfCoefficients()\n            else -> {\n                // Handle any unexpected filter type\n                calculatePeakingCoefficients()\n            }\n        }\n    }\n\n    /**\n     * Calculate peaking EQ coefficients (PK)\n     * Boosts or cuts around a center frequency\n     */\n    private fun calculatePeakingCoefficients() {\n        val A = 10.0.pow(gain / 40.0) // Gain in linear scale\n        val omega = 2.0 * PI * frequency / sampleRate\n        val sinOmega = sin(omega)\n        val cosOmega = cos(omega)\n        val alpha = sinOmega / (2.0 * q)\n\n        // Peaking EQ coefficients\n        b0 = 1.0 + alpha * A\n        b1 = -2.0 * cosOmega\n        b2 = 1.0 - alpha * A\n        a0 = 1.0 + alpha / A\n        a1 = -2.0 * cosOmega\n        a2 = 1.0 - alpha / A\n\n        // Normalize coefficients\n        b0 /= a0\n        b1 /= a0\n        b2 /= a0\n        a1 /= a0\n        a2 /= a0\n        a0 = 1.0\n    }\n\n    /**\n     * Calculate low-shelf coefficients (LSC)\n     * Boosts or cuts frequencies below the cutoff frequency\n     */\n    private fun calculateLowShelfCoefficients() {\n        val A = sqrt(10.0.pow(gain / 20.0)) // Gain amplitude\n        val omega = 2.0 * PI * frequency / sampleRate\n        val sinOmega = sin(omega)\n        val cosOmega = cos(omega)\n        val S = 1.0 // Shelf slope parameter (could be made adjustable)\n        val alpha = sinOmega / 2.0 * sqrt((A + 1.0 / A) * (1.0 / S - 1.0) + 2.0)\n        val sqrtA = sqrt(A)\n\n        // Low-shelf coefficients\n        val aPlusOne = A + 1.0\n        val aMinusOne = A - 1.0\n        val twoSqrtAAlpha = 2.0 * sqrtA * alpha\n\n        b0 = A * (aPlusOne - aMinusOne * cosOmega + twoSqrtAAlpha)\n        b1 = 2.0 * A * (aMinusOne - aPlusOne * cosOmega)\n        b2 = A * (aPlusOne - aMinusOne * cosOmega - twoSqrtAAlpha)\n        a0 = aPlusOne + aMinusOne * cosOmega + twoSqrtAAlpha\n        a1 = -2.0 * (aMinusOne + aPlusOne * cosOmega)\n        a2 = aPlusOne + aMinusOne * cosOmega - twoSqrtAAlpha\n\n        // Normalize coefficients\n        b0 /= a0\n        b1 /= a0\n        b2 /= a0\n        a1 /= a0\n        a2 /= a0\n        a0 = 1.0\n    }\n\n    /**\n     * Calculate high-shelf coefficients (HSC)\n     * Boosts or cuts frequencies above the cutoff frequency\n     */\n    private fun calculateHighShelfCoefficients() {\n        val A = sqrt(10.0.pow(gain / 20.0)) // Gain amplitude\n        val omega = 2.0 * PI * frequency / sampleRate\n        val sinOmega = sin(omega)\n        val cosOmega = cos(omega)\n        val S = 1.0 // Shelf slope parameter (could be made adjustable)\n        val alpha = sinOmega / 2.0 * sqrt((A + 1.0 / A) * (1.0 / S - 1.0) + 2.0)\n        val sqrtA = sqrt(A)\n\n        // High-shelf coefficients\n        val aPlusOne = A + 1.0\n        val aMinusOne = A - 1.0\n        val twoSqrtAAlpha = 2.0 * sqrtA * alpha\n\n        b0 = A * (aPlusOne + aMinusOne * cosOmega + twoSqrtAAlpha)\n        b1 = -2.0 * A * (aMinusOne + aPlusOne * cosOmega)\n        b2 = A * (aPlusOne + aMinusOne * cosOmega - twoSqrtAAlpha)\n        a0 = aPlusOne - aMinusOne * cosOmega + twoSqrtAAlpha\n        a1 = 2.0 * (aMinusOne - aPlusOne * cosOmega)\n        a2 = aPlusOne - aMinusOne * cosOmega - twoSqrtAAlpha\n\n        // Normalize coefficients\n        b0 /= a0\n        b1 /= a0\n        b2 /= a0\n        a1 /= a0\n        a2 /= a0\n        a0 = 1.0\n    }\n\n    /**\n     * Process a single sample (mono)\n     */\n    fun processSample(input: Double): Double {\n        val output = b0 * input + b1 * x1L + b2 * x2L - a1 * y1L - a2 * y2L\n\n        // Update state\n        x2L = x1L\n        x1L = input\n        y2L = y1L\n        y1L = output\n\n        return output\n    }\n\n    /**\n     * Process stereo samples (left and right channels)\n     */\n    fun processStereo(inputLeft: Double, inputRight: Double): Pair<Double, Double> {\n        // Left channel\n        val outputLeft = b0 * inputLeft + b1 * x1L + b2 * x2L - a1 * y1L - a2 * y2L\n        x2L = x1L\n        x1L = inputLeft\n        y2L = y1L\n        y1L = outputLeft\n\n        // Right channel\n        val outputRight = b0 * inputRight + b1 * x1R + b2 * x2R - a1 * y1R - a2 * y2R\n        x2R = x1R\n        x1R = inputRight\n        y2R = y1R\n        y1R = outputRight\n\n        return Pair(outputLeft, outputRight)\n    }\n\n    /**\n     * Reset filter state (clears history)\n     */\n    fun reset() {\n        x1L = 0.0\n        x2L = 0.0\n        y1L = 0.0\n        y2L = 0.0\n        x1R = 0.0\n        x2R = 0.0\n        y1R = 0.0\n        y2R = 0.0\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/eq/audio/CustomEqualizerAudioProcessor.kt",
    "content": "package com.metrolist.music.eq.audio\n\nimport androidx.media3.common.C\nimport androidx.media3.common.audio.AudioProcessor\nimport androidx.media3.common.util.UnstableApi\nimport com.metrolist.music.eq.data.ParametricEQ\nimport com.metrolist.music.eq.data.ParametricEQBand\nimport timber.log.Timber\nimport java.nio.ByteBuffer\nimport java.nio.ByteOrder\nimport kotlin.math.pow\n\n/**\n * Custom audio processor for ExoPlayer that applies parametric EQ using biquad filters\n * Uses ParametricEQ format from AutoEQ project\n */\n@UnstableApi\n@SuppressWarnings(\"Deprecated\")\nclass CustomEqualizerAudioProcessor : AudioProcessor {\n\n    private var sampleRate = 0\n    private var channelCount = 0\n    private var encoding = C.ENCODING_INVALID\n    private var isActive = false\n    private var equalizerEnabled = false\n\n    private var inputBuffer: ByteBuffer = EMPTY_BUFFER\n    private var outputBuffer: ByteBuffer = EMPTY_BUFFER\n    private var inputEnded = false\n\n    private var filters: List<BiquadFilter> = emptyList()\n    private var preampGain: Double = 1.0  // Linear preamp gain multiplier\n    private var pendingProfile: ParametricEQ? = null\n\n    companion object {\n        private const val TAG = \"CustomEqualizerAudioProcessor\"\n        private val EMPTY_BUFFER: ByteBuffer = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder())\n    }\n\n    /**\n     * Apply an EQ profile\n     */\n    @Synchronized\n    fun applyProfile(parametricEQ: ParametricEQ) {\n        if (sampleRate == 0) {\n            // Audio processor not configured yet, store as pending\n            Timber.tag(TAG)\n                .d(\"Audio processor not configured yet. Storing profile as pending with ${parametricEQ.bands.size} bands\")\n            pendingProfile = parametricEQ\n            return\n        }\n\n        // Convert preamp from dB to linear gain\n        preampGain = 10.0.pow(parametricEQ.preamp / 20.0)\n\n        createFilters(parametricEQ.bands)\n        equalizerEnabled = true\n\n        // Reset filter states to ensure clean transition\n        filters.forEach { it.reset() }\n\n        Timber.tag(TAG)\n            .d(\"Applied EQ profile with ${filters.size} bands and ${parametricEQ.preamp} dB preamp\")\n    }\n\n    /**\n     * Disable the equalizer\n     */\n    @Synchronized\n    fun disable() {\n        equalizerEnabled = false\n        filters = emptyList()\n        preampGain = 1.0\n        pendingProfile = null\n        Timber.tag(TAG).d(\"Equalizer disabled\")\n    }\n\n    /**\n     * Check if equalizer is enabled\n     */\n    fun isEnabled(): Boolean = equalizerEnabled\n\n    /**\n     * Create biquad filters from ParametricEQ bands\n     * Only creates filters for enabled bands below Nyquist frequency\n     * Supports PK (peaking), LSC (low-shelf), and HSC (high-shelf) filter types\n     */\n    private fun createFilters(bands: List<ParametricEQBand>) {\n        if (sampleRate == 0) {\n            Timber.tag(TAG).w(\"Cannot create filters: sample rate not set\")\n            return\n        }\n\n        // Filter out disabled bands and frequencies above Nyquist limit\n        filters = bands\n            .filter { it.enabled && it.frequency < sampleRate / 2.0 }\n            .map { band ->\n                BiquadFilter(\n                    sampleRate = sampleRate,\n                    frequency = band.frequency,\n                    gain = band.gain,\n                    q = band.q,\n                    filterType = band.filterType\n                )\n            }\n\n        Timber.tag(TAG)\n            .d(\"Created ${filters.size} biquad filters from ${bands.size} bands (PK/LSC/HSC)\")\n    }\n\n    override fun configure(inputAudioFormat: AudioProcessor.AudioFormat): AudioProcessor.AudioFormat {\n        sampleRate = inputAudioFormat.sampleRate\n        channelCount = inputAudioFormat.channelCount\n        encoding = inputAudioFormat.encoding\n\n        Timber.tag(TAG)\n            .d(\"Configured: sampleRate=$sampleRate, channels=$channelCount, encoding=$encoding\")\n\n        // Apply pending profile if one exists\n        pendingProfile?.let { profile ->\n            preampGain = 10.0.pow(profile.preamp / 20.0)\n            createFilters(profile.bands)\n            equalizerEnabled = true\n            pendingProfile = null\n            Timber.tag(TAG)\n                .d(\"Applied pending profile with ${filters.size} bands and ${profile.preamp} dB preamp\")\n        }\n\n        // Only support 16-bit PCM stereo/mono\n        if (encoding != C.ENCODING_PCM_16BIT || channelCount > 2) {\n            val exception = AudioProcessor.UnhandledAudioFormatException(inputAudioFormat)\n            throw exception // Rethrow, unsupported\n        }\n\n        isActive = true\n        return inputAudioFormat\n    }\n\n    override fun isActive(): Boolean = isActive\n\n    override fun queueInput(inputBuffer: ByteBuffer) {\n        if (!equalizerEnabled || filters.isEmpty()) {\n            // Passthrough mode - directly use input as output\n            val remaining = inputBuffer.remaining()\n            if (remaining == 0) return\n\n            // Ensure output buffer is large enough\n            if (outputBuffer.capacity() < remaining) {\n                outputBuffer = ByteBuffer.allocateDirect(remaining).order(ByteOrder.nativeOrder())\n            } else {\n                outputBuffer.clear()\n            }\n            outputBuffer.put(inputBuffer)\n            outputBuffer.flip()\n            return\n        }\n\n        val inputSize = inputBuffer.remaining()\n        if (inputSize == 0) {\n            return\n        }\n\n        // Ensure we have our own output buffer (reuse if possible to avoid allocations)\n        // Note: We MUST NOT use inputBuffer as outputBuffer if we modify it\n        if (outputBuffer === EMPTY_BUFFER || outputBuffer === inputBuffer) {\n            // Need new buffer - was empty or same as input\n            outputBuffer = ByteBuffer.allocateDirect(inputSize).order(ByteOrder.nativeOrder())\n        } else if (outputBuffer.capacity() < inputSize) {\n            // Need larger buffer\n            outputBuffer = ByteBuffer.allocateDirect(inputSize).order(ByteOrder.nativeOrder())\n        } else {\n            // Reuse existing buffer (most common path)\n            outputBuffer.clear()\n        }\n\n        // Process audio samples\n        when (encoding) {\n            C.ENCODING_PCM_16BIT -> {\n                // Ensure the output buffer is ready to receive data\n                // We don't set limit() here because putShort will advance position\n                processAudioBuffer16Bit(inputBuffer, outputBuffer)\n            }\n            else -> {\n                // Unsupported format, passthrough\n                outputBuffer.put(inputBuffer)\n            }\n        }\n\n        outputBuffer.flip()\n        // inputBuffer position is already updated by processAudioBuffer16Bit/put\n    }\n\n    /**\n     * Process 16-bit PCM audio through all biquad filters\n     */\n    private fun processAudioBuffer16Bit(input: ByteBuffer, output: ByteBuffer) {\n        // Ensure we are reading from the current position\n        // Input is ready to be read from position() to limit()\n        // Output is ready to be written to from position()\n\n        val sampleCount = input.remaining() / 2 // 2 bytes per 16-bit sample\n\n        repeat(sampleCount / channelCount) {\n            when (channelCount) {\n                1 -> {\n                    // Mono\n                    val sample = input.getShort().toDouble() / 32768.0 // Normalize to [-1, 1]\n                    var processed = sample\n\n                    // Apply all filters in series\n                    for (filter in filters) {\n                        processed = filter.processSample(processed)\n                    }\n\n                    // Apply preamp gain\n                    processed *= preampGain\n\n                    // Clamp and convert back to 16-bit\n                    val outputSample = (processed * 32768.0).coerceIn(-32768.0, 32767.0).toInt().toShort()\n                    output.putShort(outputSample)\n                }\n                2 -> {\n                    // Stereo\n                    val leftSample = input.getShort().toDouble() / 32768.0\n                    val rightSample = input.getShort().toDouble() / 32768.0\n\n                    var processedLeft = leftSample\n                    var processedRight = rightSample\n\n                    // Apply all filters in series\n                    for (filter in filters) {\n                        val (left, right) = filter.processStereo(processedLeft, processedRight)\n                        processedLeft = left\n                        processedRight = right\n                    }\n\n                    // Apply preamp gain\n                    processedLeft *= preampGain\n                    processedRight *= preampGain\n\n                    // Clamp and convert back to 16-bit\n                    val outputLeft = (processedLeft * 32768.0).coerceIn(-32768.0, 32767.0).toInt().toShort()\n                    val outputRight = (processedRight * 32768.0).coerceIn(-32768.0, 32767.0).toInt().toShort()\n\n                    output.putShort(outputLeft)\n                    output.putShort(outputRight)\n                }\n                else -> {\n                    // Should not happen as configure rejects > 2 channels\n                    repeat(channelCount) {\n                        output.putShort(input.getShort())\n                    }\n                }\n            }\n        }\n    }\n\n    override fun getOutput(): ByteBuffer {\n        // Return output buffer ready for reading (already flipped in queueInput)\n        val buffer = outputBuffer\n        outputBuffer = EMPTY_BUFFER\n        return buffer\n    }\n\n    override fun isEnded(): Boolean {\n        return inputEnded && outputBuffer.remaining() == 0\n    }\n\n    @Deprecated(\"Deprecated in Java\")\n    override fun flush() {\n        outputBuffer = EMPTY_BUFFER\n        inputEnded = false\n\n        // Reset filter states\n        filters.forEach { it.reset() }\n    }\n\n    override fun reset() {\n        @Suppress(\"DEPRECATION\")\n        flush()\n        inputBuffer = EMPTY_BUFFER\n        sampleRate = 0\n        channelCount = 0\n        encoding = C.ENCODING_INVALID\n        isActive = false\n        filters.forEach { it.reset() }\n    }\n\n    override fun queueEndOfStream() {\n        inputEnded = true\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/eq/data/EQProfileRepository.kt",
    "content": "package com.metrolist.music.eq.data\n\nimport android.content.Context\nimport android.content.SharedPreferences\nimport androidx.core.content.edit\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.withContext\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.Json\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Saved EQ Profile with metadata\n */\n@Serializable\ndata class SavedEQProfile(\n    val id: String,                       // Unique identifier\n    val name: String,                     // Display name\n    val deviceModel: String,              // e.g., \"Sony WH-1000XM4\"\n    val bands: List<ParametricEQBand>,    // EQ bands\n    val preamp: Double = 0.0,             // Preamp gain in dB\n    val isCustom: Boolean = false,        // Whether this is a custom imported profile\n    val isActive: Boolean = false,        // Whether this profile is currently active\n    val addedTimestamp: Long = System.currentTimeMillis()\n)\n\n/**\n * Repository for managing EQ profiles\n * Handles saving, loading, and activating EQ profiles\n */\n@Singleton\nclass EQProfileRepository @Inject constructor(\n    @ApplicationContext private val context: Context\n) {\n    private val prefs: SharedPreferences = context.getSharedPreferences(\n        \"nanosonic_eq_profiles\",\n        Context.MODE_PRIVATE\n    )\n\n    private val json = Json {\n        ignoreUnknownKeys = true\n        prettyPrint = true\n    }\n\n    private val _profiles = MutableStateFlow<List<SavedEQProfile>>(emptyList())\n    val profiles: StateFlow<List<SavedEQProfile>> = _profiles.asStateFlow()\n\n    private val _activeProfile = MutableStateFlow<SavedEQProfile?>(null)\n    val activeProfile: StateFlow<SavedEQProfile?> = _activeProfile.asStateFlow()\n\n    companion object {\n        private const val KEY_PROFILES = \"eq_profiles\"\n        private const val KEY_ACTIVE_PROFILE_ID = \"active_profile_id\"\n    }\n\n    init {\n        loadProfiles()\n    }\n\n    /**\n     * Load all saved profiles from SharedPreferences\n     */\n    private fun loadProfiles() {\n        try {\n            val profilesJson = prefs.getString(KEY_PROFILES, null)\n            if (profilesJson != null) {\n                val loadedProfiles = json.decodeFromString<List<SavedEQProfile>>(profilesJson)\n                _profiles.value = loadedProfiles\n\n                // Load active profile\n                val activeId = prefs.getString(KEY_ACTIVE_PROFILE_ID, null)\n                _activeProfile.value = loadedProfiles.find { it.id == activeId }\n            }\n        } catch (e: Exception) {\n            println(\"Error loading EQ profiles: ${e.message}\")\n            _profiles.value = emptyList()\n            _activeProfile.value = null\n        }\n    }\n\n    /**\n     * Save a new EQ profile\n     */\n    suspend fun saveProfile(profile: SavedEQProfile) = withContext(Dispatchers.IO) {\n        val currentProfiles = _profiles.value.toMutableList()\n\n        // Check if profile with same ID already exists\n        val existingIndex = currentProfiles.indexOfFirst { it.id == profile.id }\n\n        if (existingIndex >= 0) {\n            // Update existing profile\n            currentProfiles[existingIndex] = profile\n        } else {\n            // Add new profile\n            currentProfiles.add(profile)\n        }\n\n        // Save to SharedPreferences\n        val profilesJson = json.encodeToString<List<SavedEQProfile>>(currentProfiles)\n        prefs.edit { putString(KEY_PROFILES, profilesJson) }\n\n        _profiles.value = currentProfiles\n    }\n\n    /**\n     * Delete a profile\n     */\n    suspend fun deleteProfile(profileId: String) = withContext(Dispatchers.IO) {\n        val currentProfiles = _profiles.value.toMutableList()\n        currentProfiles.removeAll { it.id == profileId }\n\n        val profilesJson = json.encodeToString<List<SavedEQProfile>>(currentProfiles)\n        prefs.edit { putString(KEY_PROFILES, profilesJson) }\n\n        // If deleted profile was active, clear active profile\n        if (_activeProfile.value?.id == profileId) {\n            _activeProfile.value = null\n            prefs.edit { remove(KEY_ACTIVE_PROFILE_ID) }\n        }\n\n        _profiles.value = currentProfiles\n    }\n\n    /**\n     * Set a profile as active (only one profile can be active at a time)\n     * Pass null to deactivate all profiles\n     */\n    suspend fun setActiveProfile(profileId: String?) = withContext(Dispatchers.IO) {\n        val currentProfiles = _profiles.value\n\n        if (profileId == null) {\n            // Deactivate all profiles\n            _activeProfile.value = null\n            prefs.edit { remove(KEY_ACTIVE_PROFILE_ID) }\n        } else {\n            val profile = currentProfiles.find { it.id == profileId }\n            _activeProfile.value = profile\n            prefs.edit { putString(KEY_ACTIVE_PROFILE_ID, profileId) }\n        }\n    }\n\n    /**\n     * Get all saved profiles\n     */\n    fun getAllProfiles(): List<SavedEQProfile> {\n        return _profiles.value\n    }\n\n    /**\n     * Get active profile\n     */\n    fun getActiveProfile(): SavedEQProfile? {\n        return _activeProfile.value\n    }\n\n    /**\n     * Import a custom EQ profile from ParametricEQ data\n     */\n    suspend fun importCustomProfile(\n        name: String,\n        parametricEQ: ParametricEQ\n    ) = withContext(Dispatchers.IO) {\n        // Generate unique ID for custom profile\n        val id = \"custom_${System.currentTimeMillis()}_${name.hashCode()}\"\n\n        val customProfile = SavedEQProfile(\n            id = id,\n            name = name,\n            deviceModel = name,\n            bands = parametricEQ.bands,  // Already ParametricEQBand\n            preamp = parametricEQ.preamp,\n            isActive = false,\n            isCustom = true // Ensure this flag is set!\n        )\n\n        saveProfile(customProfile)\n    }\n\n    /**\n     * Get profiles sorted by type: AutoEQ first, then custom profiles\n     * Within each group, sort by timestamp (newest first)\n     */\n    fun getSortedProfiles(): List<SavedEQProfile> {\n        // Only custom profiles are supported now\n        return _profiles.value\n            .filter { it.isCustom }\n            .sortedByDescending { it.addedTimestamp }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/eq/data/FilterType.kt",
    "content": "package com.metrolist.music.eq.data\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\nenum class FilterType {\n    /** Peaking filter - boosts or cuts around a center frequency */\n    PK,\n    /** Low-shelf filter - affects frequencies below the cutoff */\n    LSC,\n    /** High-shelf filter - affects frequencies above the cutoff */\n    HSC,\n    /** Low-pass filter - attenuates frequencies above the cutoff */\n    LPQ,\n    /** High-pass filter - attenuates frequencies below the cutoff */\n    HPQ\n}"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/eq/data/ParametricEQ.kt",
    "content": "package com.metrolist.music.eq.data\n\nimport kotlinx.serialization.Serializable\n\n/**\n * Represents a single parametric EQ filter/band\n * Supports APO Parametric EQ filters\n */\n@Serializable\ndata class ParametricEQBand(\n    val frequency: Double,                      // Center frequency in Hz\n    val gain: Double,                           // Gain in dB\n    val q: Double = 1.41,                       // Q factor (bandwidth) - default to sqrt(2)\n    val filterType: FilterType = FilterType.PK, // Filter type\n    val enabled: Boolean = true                 // Whether this band is active\n)\n\n/**\n * Represents a complete parametric EQ configuration for a headphone\n * Parsed from AutoEQ preset files (...ParametricEQ.txt)\n */\n@Serializable\ndata class ParametricEQ(\n    val preamp: Double,                         // Preamp/gain in dB (to prevent clipping)\n    val bands: List<ParametricEQBand>,          // List of EQ bands\n    val metadata: Map<String, String> = emptyMap()  // Additional metadata from file\n) {\n    companion object {\n        const val MAX_BANDS = 20  // Maximum bands supported by the implementation\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/eq/data/ParametricEQParser.kt",
    "content": "package com.metrolist.music.eq.data\n\nimport java.io.File\n\n/**\n * Parser for AutoEq ParametricEQ.txt files.\n * These files contain parametric EQ settings that can be applied to audio devices.\n *\n * File format:\n *   Preamp: -5.2 dB\n *   Filter 1: ON LSC Fc 105 Hz Gain 8.8 dB Q 0.70\n *   Filter 2: ON PK Fc 70 Hz Gain -6.7 dB Q 0.29\n *   ...\n *\n * Where:\n *   - LSC = Low Shelf\n *   - HSC = High Shelf\n *   - PK = Peaking filter\n *   - LPQ = Low Pass\n *   - HPQ = High Pass\n */\nobject ParametricEQParser {\n\n    /**\n     * Parse a ParametricEQ file\n     */\n    fun parseFile(file: File): ParametricEQ {\n        if (!file.exists()) {\n            throw IllegalArgumentException(\"File does not exist: ${file.absolutePath}\")\n        }\n\n        return parseText(file.readText())\n    }\n\n    /**\n     * Parse a ParametricEQ file from a path string\n     */\n    fun parseFile(filePath: String): ParametricEQ {\n        return parseFile(File(filePath))\n    }\n\n    /**\n     * Parse ParametricEQ text content\n     */\n    fun parseText(content: String): ParametricEQ {\n        val lines = content.lines()\n        var preamp = 0.0\n        val bands = mutableListOf<ParametricEQBand>()\n        val metadata = mutableMapOf<String, String>()\n\n        for (line in lines) {\n            val trimmedLine = line.trim()\n            if (trimmedLine.isEmpty()) continue\n\n            when {\n                // Parse preamp line: \"Preamp: -5.2 dB\"\n                trimmedLine.startsWith(\"Preamp:\", ignoreCase = true) -> {\n                    preamp = parsePreamp(trimmedLine)\n                }\n\n                // Parse filter line: \"Filter 1: ON LSC Fc 105 Hz Gain 8.8 dB Q 0.70\"\n                trimmedLine.startsWith(\"Filter\", ignoreCase = true) -> {\n                    val band = parseFilterLine(trimmedLine)\n                    if (band != null) {\n                        bands.add(band)\n                    }\n                }\n\n                // Store other lines as metadata\n                else -> {\n                    val parts = trimmedLine.split(\":\", limit = 2)\n                    if (parts.size == 2) {\n                        metadata[parts[0].trim()] = parts[1].trim()\n                    }\n                }\n            }\n        }\n\n        return ParametricEQ(\n            preamp = preamp,\n            bands = bands,\n            metadata = metadata\n        )\n    }\n\n    /**\n     * Parse the preamp line\n     * Example: \"Preamp: -5.2 dB\"\n     */\n    private fun parsePreamp(line: String): Double {\n        val regex = Regex(\"\"\"Preamp:\\s*([-+]?\\d+\\.?\\d*)\\s*dB\"\"\", RegexOption.IGNORE_CASE)\n        val match = regex.find(line)\n        return match?.groupValues?.get(1)?.toDoubleOrNull() ?: 0.0\n    }\n\n    /**\n     * Parse a filter line\n     * Example: \"Filter 1: ON LSC Fc 105 Hz Gain 8.8 dB Q 0.70\"\n     */\n    private fun parseFilterLine(line: String): ParametricEQBand? {\n        try {\n            // Check if filter is ON\n            if (!line.contains(\"ON\", ignoreCase = true)) {\n                return null\n            }\n\n            // Extract filter type (LSC, HSC, PK, LPQ, HPQ)\n            val filterType = parseFilterType(line) ?: return null\n\n            // Extract frequency: \"Fc 105 Hz\"\n            val frequency = parseValue(line, \"Fc\", \"Hz\") ?: return null\n\n            // Extract gain: \"Gain 8.8 dB\"\n            val gain = parseValue(line, \"Gain\", \"dB\") ?: return null\n\n            // Extract Q factor: \"Q 0.70\"\n            val q = parseValue(line, \"Q\", null) ?: return null\n\n            return ParametricEQBand(\n                filterType = filterType,\n                frequency = frequency,\n                gain = gain,\n                q = q\n            )\n        } catch (e: Exception) {\n            println(\"Warning: Failed to parse filter line: $line\")\n            println(\"Error: ${e.message}\")\n            return null\n        }\n    }\n\n    /**\n     * Parse filter type from line\n     */\n    private fun parseFilterType(line: String): FilterType? {\n        return when {\n            line.contains(\"LSC\", ignoreCase = true) -> FilterType.LSC\n            line.contains(\"HSC\", ignoreCase = true) -> FilterType.HSC\n            line.contains(\"PK\", ignoreCase = true) -> FilterType.PK\n            line.contains(\"LPQ\", ignoreCase = true) -> FilterType.LPQ\n            line.contains(\"HPQ\", ignoreCase = true) -> FilterType.HPQ\n            else -> null\n        }\n    }\n\n    /**\n     * Parse a numeric value from the line\n     * Example: parseValue(\"... Fc 105 Hz ...\", \"Fc\", \"Hz\") -> 105.0\n     */\n    private fun parseValue(line: String, keyword: String, unit: String?): Double? {\n        val unitPattern = if (unit != null) \"\\\\s*$unit\" else \"\"\n        val regex = Regex(\"\"\"$keyword\\s+([-+]?\\d+\\.?\\d*)$unitPattern\"\"\", RegexOption.IGNORE_CASE)\n        val match = regex.find(line)\n        return match?.groupValues?.get(1)?.toDoubleOrNull()\n    }\n\n    /**\n     * Convert ParametricEQ to a human-readable string\n     */\n    fun toString(eq: ParametricEQ): String {\n        val sb = StringBuilder()\n        sb.appendLine(\"Preamp: ${eq.preamp} dB\")\n        eq.bands.forEachIndexed { index, band ->\n            sb.appendLine(\n                \"Filter ${index + 1}: ${band.filterType} Fc ${band.frequency} Hz \" +\n                        \"Gain ${band.gain} dB Q ${band.q}\"\n            )\n        }\n        return sb.toString()\n    }\n\n    /**\n     * Format ParametricEQ for export to file\n     */\n    fun toFileFormat(eq: ParametricEQ): String {\n        val sb = StringBuilder()\n        sb.appendLine(\"Preamp: ${eq.preamp} dB\")\n        eq.bands.forEachIndexed { index, band ->\n            sb.appendLine(\n                \"Filter ${index + 1}: ON ${band.filterType} \" +\n                        \"Fc ${band.frequency.toInt()} Hz \" +\n                        \"Gain ${band.gain} dB \" +\n                        \"Q ${String.format(\"%.2f\", band.q)}\"\n            )\n        }\n        return sb.toString()\n    }\n\n    /**\n     * Validate a ParametricEQ profile\n     * Returns a list of validation error messages (empty list if valid)\n     */\n    fun validate(eq: ParametricEQ): List<String> {\n        val errors = mutableListOf<String>()\n\n        // Validate preamp\n        if (eq.preamp < -50.0 || eq.preamp > 50.0) {\n            errors.add(\"Preamp value ${eq.preamp} dB is out of range (-50 to +50 dB)\")\n        }\n\n        // Validate bands exist\n        if (eq.bands.isEmpty()) {\n            errors.add(\"EQ profile must have at least one band\")\n        }\n\n        // Validate number of bands\n        if (eq.bands.size > ParametricEQ.MAX_BANDS) {\n            errors.add(\"EQ profile has ${eq.bands.size} bands, maximum is ${ParametricEQ.MAX_BANDS}\")\n        }\n\n        // Validate each band\n        eq.bands.forEachIndexed { index, band ->\n            // Validate frequency\n            if (band.frequency <= 0.0 || band.frequency > 100000.0) {\n                errors.add(\"Band ${index + 1}: Frequency ${band.frequency} Hz is out of range (1 to 100000 Hz)\")\n            }\n\n            // Validate gain\n            if (band.gain < -30.0 || band.gain > 30.0) {\n                errors.add(\"Band ${index + 1}: Gain ${band.gain} dB is out of range (-30 to +30 dB)\")\n            }\n\n            // Validate Q factor\n            if (band.q <= 0.0 || band.q > 20.0) {\n                errors.add(\"Band ${index + 1}: Q factor ${band.q} is out of range (0.01 to 20)\")\n            }\n        }\n\n        return errors\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/extensions/ContextExt.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.extensions\n\nimport android.content.Context\nimport android.net.ConnectivityManager\nimport android.net.NetworkCapabilities\nimport com.metrolist.innertube.utils.parseCookieString\nimport com.metrolist.music.constants.InnerTubeCookieKey\nimport com.metrolist.music.constants.YtmSyncKey\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\nimport kotlinx.coroutines.runBlocking\n\nfun Context.isSyncEnabled(): Boolean {\n    return runBlocking {\n        dataStore.get(YtmSyncKey, true) && isUserLoggedIn()\n    }\n}\n\nfun Context.isUserLoggedIn(): Boolean {\n    return runBlocking {\n        val cookie = dataStore[InnerTubeCookieKey] ?: \"\"\n        \"SAPISID\" in parseCookieString(cookie) && isInternetConnected()\n    }\n}\n\nfun Context.isInternetConnected(): Boolean {\n    val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager\n    val networkCapabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)\n    return networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/extensions/CoroutineExt.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.extensions\n\nimport kotlinx.coroutines.CoroutineExceptionHandler\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.launch\n\nfun <T> Flow<T>.collect(\n    scope: CoroutineScope,\n    action: suspend (value: T) -> Unit,\n) {\n    scope.launch {\n        collect(action)\n    }\n}\n\nfun <T> Flow<T>.collectLatest(\n    scope: CoroutineScope,\n    action: suspend (value: T) -> Unit,\n) {\n    scope.launch {\n        collectLatest(action)\n    }\n}\n\nval SilentHandler = CoroutineExceptionHandler { _, _ -> }\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/extensions/FileExt.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.extensions\n\nimport java.io.File\nimport java.io.InputStream\nimport java.io.OutputStream\nimport java.util.zip.ZipInputStream\nimport java.util.zip.ZipOutputStream\n\noperator fun File.div(child: String): File = File(this, child)\n\nfun InputStream.zipInputStream(): ZipInputStream = ZipInputStream(this)\n\nfun OutputStream.zipOutputStream(): ZipOutputStream = ZipOutputStream(this)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/extensions/ListExt.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.extensions\n\nimport com.metrolist.music.db.entities.Album\nimport com.metrolist.music.db.entities.Playlist\nimport com.metrolist.music.db.entities.Song\n\nfun <T> List<T>.reversed(reversed: Boolean) = if (reversed) asReversed() else this\n\nfun <T> MutableList<T>.move(\n    fromIndex: Int,\n    toIndex: Int,\n): MutableList<T> {\n    add(toIndex, removeAt(fromIndex))\n    return this\n}\n\nfun <T : Any> List<T>.mergeNearbyElements(\n    key: (T) -> Any = { it },\n    merge: (first: T, second: T) -> T = { first, _ -> first },\n): List<T> {\n    if (isEmpty()) return emptyList()\n\n    val mergedList = mutableListOf<T>()\n    var currentItem = this[0]\n\n    for (i in 1 until size) {\n        val nextItem = this[i]\n        if (key(currentItem) == key(nextItem)) {\n            currentItem = merge(currentItem, nextItem)\n        } else {\n            mergedList.add(currentItem)\n            currentItem = nextItem\n        }\n    }\n    mergedList.add(currentItem)\n\n    return mergedList\n}\n\n// Extension function to filter explicit content for local Song entities\nfun List<Song>.filterExplicit(enabled: Boolean = true) =\n    if (enabled) {\n        filter { !it.song.explicit }\n    } else {\n        this\n    }\n\n// Extension function to filter video songs for local Song entities\nfun List<Song>.filterVideoSongs(enabled: Boolean = true) =\n    if (enabled) {\n        filter { !it.song.isVideo }\n    } else {\n        this\n    }\n\n// Extension function to filter explicit content for local Album entities\nfun List<Album>.filterExplicitAlbums(enabled: Boolean = true) =\n    if (enabled) {\n        filter { !it.album.explicit }\n    } else {\n        this\n    }\n\n// Extension function to filter YouTube Shorts playlist\nfun List<Playlist>.filterYoutubeShorts(enabled: Boolean = false) =\n    if (enabled) {\n        filterNot { it.playlist.browseId?.startsWith(\"SS\") == true }\n    } else {\n        this\n    }\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/extensions/MediaItemExt.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.extensions\n\nimport android.os.Bundle\nimport androidx.core.net.toUri\nimport androidx.media3.common.MediaItem\nimport androidx.media3.common.MediaMetadata.MEDIA_TYPE_MUSIC\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.models.MediaMetadata\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.ui.utils.resize\n\nval MediaItem.metadata: MediaMetadata?\n    get() = localConfiguration?.tag as? MediaMetadata\n\nfun Song.toMediaItem() = MediaItem.Builder()\n    .setMediaId(song.id)\n    .setUri(song.id)\n    .setCustomCacheKey(song.id)\n    .setTag(toMediaMetadata())\n    .setMediaMetadata(\n        androidx.media3.common.MediaMetadata.Builder()\n            .setTitle(song.title)\n            .setSubtitle(orderedArtists.joinToString { it.name })\n            .setArtist(orderedArtists.joinToString { it.name })\n            .setArtworkUri(song.thumbnailUrl?.toUri())\n            .setAlbumTitle(song.albumName)\n            .setAlbumArtist(orderedArtists.firstOrNull()?.name)\n            .setDisplayTitle(song.title)\n            .setMediaType(MEDIA_TYPE_MUSIC)\n            .setIsBrowsable(false)\n            .setIsPlayable(true)\n            .setExtras(Bundle().apply {\n                putString(\"artwork_uri\", song.thumbnailUrl)\n            })\n            .build()\n    )\n    .build()\n\nfun SongItem.toMediaItem() = MediaItem.Builder()\n    .setMediaId(id)\n    .setUri(id)\n    .setCustomCacheKey(id)\n    .setTag(toMediaMetadata())\n    .setMediaMetadata(\n        androidx.media3.common.MediaMetadata.Builder()\n            .setTitle(title)\n            .setSubtitle(artists.joinToString { it.name })\n            .setArtist(artists.joinToString { it.name })\n            .setArtworkUri(thumbnail.resize(544, 544).toUri())\n            .setAlbumTitle(album?.name)\n            .setAlbumArtist(artists.firstOrNull()?.name)\n            .setDisplayTitle(title)\n            .setMediaType(MEDIA_TYPE_MUSIC)\n            .setIsBrowsable(false)\n            .setIsPlayable(true)\n            .setExtras(Bundle().apply {\n                putString(\"artwork_uri\", thumbnail.resize(544, 544))\n            })\n            .build()\n    )\n    .build()\n\nfun MediaMetadata.toMediaItem() = MediaItem.Builder()\n    .setMediaId(id)\n    .setUri(id)\n    .setCustomCacheKey(id)\n    .setTag(this)\n    .setMediaMetadata(\n        androidx.media3.common.MediaMetadata.Builder()\n            .setTitle(title)\n            .setSubtitle(artists.joinToString { it.name })\n            .setArtist(artists.joinToString { it.name })\n            .setArtworkUri(thumbnailUrl?.toUri())\n            .setAlbumTitle(album?.title)\n            .setAlbumArtist(artists.firstOrNull()?.name)\n            .setDisplayTitle(title)\n            .setMediaType(MEDIA_TYPE_MUSIC)\n            .setIsBrowsable(false)\n            .setIsPlayable(true)\n            .setExtras(Bundle().apply {\n                thumbnailUrl?.let { putString(\"artwork_uri\", it) }\n            })\n            .build()\n    )\n    .build()\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/extensions/PlayerExt.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.extensions\n\nimport androidx.media3.common.C\nimport androidx.media3.common.MediaItem\nimport androidx.media3.common.Player\nimport androidx.media3.common.Player.REPEAT_MODE_ALL\nimport androidx.media3.common.Player.REPEAT_MODE_OFF\nimport androidx.media3.common.Player.REPEAT_MODE_ONE\nimport androidx.media3.common.Timeline\nimport androidx.media3.common.TrackSelectionParameters\nimport com.metrolist.music.models.MediaMetadata\nimport java.util.ArrayDeque\n\nfun Player.togglePlayPause() {\n    if (!playWhenReady && playbackState == Player.STATE_IDLE) {\n        prepare()\n    }\n    playWhenReady = !playWhenReady\n}\n\nfun Player.toggleRepeatMode() {\n    repeatMode =\n        when (repeatMode) {\n            REPEAT_MODE_OFF -> REPEAT_MODE_ALL\n            REPEAT_MODE_ALL -> REPEAT_MODE_ONE\n            REPEAT_MODE_ONE -> REPEAT_MODE_OFF\n            else -> throw IllegalStateException()\n        }\n}\n\nfun Player.getQueueWindows(): List<Timeline.Window> {\n    val timeline = currentTimeline\n    if (timeline.isEmpty) {\n        return emptyList()\n    }\n    val queue = ArrayDeque<Timeline.Window>()\n    val queueSize = timeline.windowCount\n\n    val currentMediaItemIndex: Int = currentMediaItemIndex\n    queue.add(timeline.getWindow(currentMediaItemIndex, Timeline.Window()))\n\n    var firstMediaItemIndex = currentMediaItemIndex\n    var lastMediaItemIndex = currentMediaItemIndex\n    val shuffleModeEnabled = shuffleModeEnabled\n    while ((firstMediaItemIndex != C.INDEX_UNSET || lastMediaItemIndex != C.INDEX_UNSET) && queue.size < queueSize) {\n        if (lastMediaItemIndex != C.INDEX_UNSET) {\n            lastMediaItemIndex =\n                timeline.getNextWindowIndex(lastMediaItemIndex, REPEAT_MODE_OFF, shuffleModeEnabled)\n            if (lastMediaItemIndex != C.INDEX_UNSET) {\n                queue.add(timeline.getWindow(lastMediaItemIndex, Timeline.Window()))\n            }\n        }\n        if (firstMediaItemIndex != C.INDEX_UNSET && queue.size < queueSize) {\n            firstMediaItemIndex = timeline.getPreviousWindowIndex(\n                firstMediaItemIndex,\n                REPEAT_MODE_OFF,\n                shuffleModeEnabled\n            )\n            if (firstMediaItemIndex != C.INDEX_UNSET) {\n                queue.addFirst(timeline.getWindow(firstMediaItemIndex, Timeline.Window()))\n            }\n        }\n    }\n    return queue.toList()\n}\n\nfun Player.getCurrentQueueIndex(): Int {\n    if (currentTimeline.isEmpty) {\n        return -1\n    }\n    var index = 0\n    var currentMediaItemIndex = currentMediaItemIndex\n    while (currentMediaItemIndex != C.INDEX_UNSET) {\n        currentMediaItemIndex = currentTimeline.getPreviousWindowIndex(\n            currentMediaItemIndex,\n            REPEAT_MODE_OFF,\n            shuffleModeEnabled\n        )\n        if (currentMediaItemIndex != C.INDEX_UNSET) {\n            index++\n        }\n    }\n    return index\n}\n\nval Player.currentMetadata: MediaMetadata?\n    get() = currentMediaItem?.metadata\n\nval Player.mediaItems: List<MediaItem>\n    get() =\n        object : AbstractList<MediaItem>() {\n            override val size: Int\n                get() = mediaItemCount\n\n            override fun get(index: Int): MediaItem = getMediaItemAt(index)\n        }\n\nfun Player.findNextMediaItemById(mediaId: String): MediaItem? {\n    for (i in currentMediaItemIndex until mediaItemCount) {\n        if (getMediaItemAt(i).mediaId == mediaId) {\n            return getMediaItemAt(i)\n        }\n    }\n    return null\n}\n\nfun Player.setOffloadEnabled(enabled: Boolean) {\n    trackSelectionParameters = trackSelectionParameters.buildUpon()\n        .setAudioOffloadPreferences(\n            TrackSelectionParameters.AudioOffloadPreferences\n                .Builder()\n                .setAudioOffloadMode(\n                    if (enabled) {\n                        TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED\n                    } else {\n                        TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_DISABLED\n                    }\n                )\n                .build()\n        ).build()\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/extensions/QueueExt.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.extensions\n\nimport com.metrolist.music.models.MediaMetadata\nimport com.metrolist.music.models.PersistQueue\nimport com.metrolist.music.models.QueueData\nimport com.metrolist.music.models.QueueType\nimport com.metrolist.music.playback.queues.ListQueue\nimport com.metrolist.music.playback.queues.LocalAlbumRadio\nimport com.metrolist.music.playback.queues.Queue\nimport com.metrolist.music.playback.queues.YouTubeAlbumRadio\nimport com.metrolist.music.playback.queues.YouTubeQueue\n\nfun Queue.toPersistQueue(\n    title: String?,\n    items: List<MediaMetadata>,\n    mediaItemIndex: Int,\n    position: Long\n): PersistQueue {\n    return when (this) {\n        is ListQueue -> PersistQueue(\n            title = title,\n            items = items,\n            mediaItemIndex = mediaItemIndex,\n            position = position,\n            queueType = QueueType.LIST\n        )\n        is YouTubeQueue -> {\n            // Since endpoint is private, we'll store a simplified version\n            val endpoint = \"youtube_queue\"\n            PersistQueue(\n                title = title,\n                items = items,\n                mediaItemIndex = mediaItemIndex,\n                position = position,\n                queueType = QueueType.YOUTUBE,\n                queueData = QueueData.YouTubeData(endpoint = endpoint)\n            )\n        }\n        is YouTubeAlbumRadio -> {\n            // Since playlistId is private, we'll store a simplified version\n            PersistQueue(\n                title = title,\n                items = items,\n                mediaItemIndex = mediaItemIndex,\n                position = position,\n                queueType = QueueType.YOUTUBE_ALBUM_RADIO,\n                queueData = QueueData.YouTubeAlbumRadioData(\n                    playlistId = \"youtube_album_radio\"\n                )\n            )\n        }\n        is LocalAlbumRadio -> {\n            // Since albumWithSongs and startIndex are private, we'll store a simplified version\n            PersistQueue(\n                title = title,\n                items = items,\n                mediaItemIndex = mediaItemIndex,\n                position = position,\n                queueType = QueueType.LOCAL_ALBUM_RADIO,\n                queueData = QueueData.LocalAlbumRadioData(\n                    albumId = \"local_album_radio\",\n                    startIndex = 0\n                )\n            )\n        }\n        else -> PersistQueue(\n            title = title,\n            items = items,\n            mediaItemIndex = mediaItemIndex,\n            position = position,\n            queueType = QueueType.LIST\n        )\n    }\n}\n\nfun PersistQueue.toQueue(): Queue {\n    return when (queueType) {\n        is QueueType.LIST -> ListQueue(\n            title = title,\n            items = items.map { it.toMediaItem() },\n            startIndex = mediaItemIndex,\n            position = position\n        )\n        is QueueType.YOUTUBE -> {\n            // For now, fallback to ListQueue since we can't reconstruct YouTubeQueue properly\n            ListQueue(\n                title = title,\n                items = items.map { it.toMediaItem() },\n                startIndex = mediaItemIndex,\n                position = position\n            )\n        }\n        is QueueType.YOUTUBE_ALBUM_RADIO -> {\n            // For now, fallback to ListQueue since we can't reconstruct YouTubeAlbumRadio properly\n            ListQueue(\n                title = title,\n                items = items.map { it.toMediaItem() },\n                startIndex = mediaItemIndex,\n                position = position\n            )\n        }\n        is QueueType.LOCAL_ALBUM_RADIO -> {\n            // For now, fallback to ListQueue since we can't reconstruct LocalAlbumRadio properly\n            ListQueue(\n                title = title,\n                items = items.map { it.toMediaItem() },\n                startIndex = mediaItemIndex,\n                position = position\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/extensions/StringExt.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.extensions\n\nimport androidx.sqlite.db.SimpleSQLiteQuery\nimport java.net.InetSocketAddress\nimport java.net.InetSocketAddress.createUnresolved\n\ninline fun <reified T : Enum<T>> String?.toEnum(defaultValue: T): T =\n    if (this == null) {\n        defaultValue\n    } else {\n        try {\n            enumValueOf(this)\n        } catch (e: IllegalArgumentException) {\n            defaultValue\n        }\n    }\n\nfun String.toSQLiteQuery(): SimpleSQLiteQuery = SimpleSQLiteQuery(this)\n\nfun String.toInetSocketAddress(): InetSocketAddress {\n    val (host, port) = split(\":\")\n    return createUnresolved(host, port.toInt())\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/extensions/UtilExt.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.extensions\n\nfun <T> tryOrNull(block: () -> T): T? =\n    try {\n        block()\n    } catch (e: Exception) {\n        null\n    }\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/listentogether/ListenTogetherActionReceiver.kt",
    "content": "package com.metrolist.music.listentogether\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport androidx.core.app.NotificationManagerCompat\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\n\nclass ListenTogetherActionReceiver : BroadcastReceiver() {\n    override fun onReceive(context: Context, intent: Intent) {\n        val client = ListenTogetherClient.getInstance() ?: return\n        val notifId = intent.getIntExtra(ListenTogetherClient.EXTRA_NOTIFICATION_ID, 0)\n\n        // Cancel the notification immediately\n        NotificationManagerCompat.from(context).cancel(notifId)\n\n        when (intent.action) {\n            ListenTogetherClient.ACTION_APPROVE_JOIN -> {\n                val userId = intent.getStringExtra(ListenTogetherClient.EXTRA_USER_ID) ?: return\n                CoroutineScope(Dispatchers.IO).launch {\n                    client.approveJoin(userId)\n                }\n            }\n            ListenTogetherClient.ACTION_REJECT_JOIN -> {\n                val userId = intent.getStringExtra(ListenTogetherClient.EXTRA_USER_ID) ?: return\n                CoroutineScope(Dispatchers.IO).launch {\n                    client.rejectJoin(userId, null)\n                }\n            }\n            ListenTogetherClient.ACTION_APPROVE_SUGGESTION -> {\n                val suggestionId = intent.getStringExtra(ListenTogetherClient.EXTRA_SUGGESTION_ID) ?: return\n                CoroutineScope(Dispatchers.IO).launch {\n                    client.approveSuggestion(suggestionId)\n                }\n            }\n            ListenTogetherClient.ACTION_REJECT_SUGGESTION -> {\n                val suggestionId = intent.getStringExtra(ListenTogetherClient.EXTRA_SUGGESTION_ID) ?: return\n                CoroutineScope(Dispatchers.IO).launch {\n                    client.rejectSuggestion(suggestionId, null)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/listentogether/ListenTogetherClient.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.listentogether\n\nimport android.Manifest\nimport android.app.NotificationChannel\nimport android.app.NotificationManager\nimport android.app.PendingIntent\nimport android.content.Context\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.os.PowerManager\nimport android.widget.Toast\nimport androidx.annotation.RequiresPermission\nimport androidx.core.app.ActivityCompat\nimport androidx.core.app.NotificationCompat\nimport androidx.core.app.NotificationManagerCompat\nimport androidx.core.content.getSystemService\nimport androidx.datastore.preferences.core.edit\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.ListenTogetherAutoApprovalKey\nimport com.metrolist.music.constants.ListenTogetherAutoApproveSuggestionsKey\nimport com.metrolist.music.constants.ListenTogetherIsHostKey\nimport com.metrolist.music.constants.ListenTogetherRoomCodeKey\nimport com.metrolist.music.constants.ListenTogetherServerUrlKey\nimport com.metrolist.music.constants.ListenTogetherSessionTimestampKey\nimport com.metrolist.music.constants.ListenTogetherSessionTokenKey\nimport com.metrolist.music.constants.ListenTogetherUserIdKey\nimport com.metrolist.music.utils.NetworkConnectivityObserver\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.launch\nimport kotlinx.serialization.json.Json\nimport okhttp3.OkHttpClient\nimport okhttp3.Request\nimport okhttp3.Response\nimport okhttp3.WebSocket\nimport okhttp3.WebSocketListener\nimport timber.log.Timber\nimport java.time.LocalDateTime\nimport java.time.format.DateTimeFormatter\nimport java.util.concurrent.TimeUnit\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Connection state for the Listen Together feature\n */\nenum class ConnectionState {\n    DISCONNECTED,\n    CONNECTING,\n    CONNECTED,\n    RECONNECTING,\n    ERROR,\n}\n\n/**\n * Room role for the current user\n */\nenum class RoomRole {\n    HOST,\n    GUEST,\n    NONE,\n}\n\n/**\n * Log entry for debugging\n */\ndata class LogEntry(\n    val timestamp: String,\n    val level: LogLevel,\n    val message: String,\n    val details: String? = null,\n)\n\nenum class LogLevel {\n    INFO,\n    WARNING,\n    ERROR,\n    DEBUG,\n}\n\n/**\n * Pending action to execute when connected\n */\nsealed class PendingAction {\n    data class CreateRoom(\n        val username: String,\n    ) : PendingAction()\n\n    data class JoinRoom(\n        val roomCode: String,\n        val username: String,\n    ) : PendingAction()\n}\n\n/**\n * Event types for the Listen Together client\n */\nsealed class ListenTogetherEvent {\n    // Connection events\n    data class Connected(\n        val userId: String,\n    ) : ListenTogetherEvent()\n\n    data object Disconnected : ListenTogetherEvent()\n\n    data class ConnectionError(\n        val error: String,\n    ) : ListenTogetherEvent()\n\n    data class Reconnecting(\n        val attempt: Int,\n        val maxAttempts: Int,\n    ) : ListenTogetherEvent()\n\n    // Room events\n    data class RoomCreated(\n        val roomCode: String,\n        val userId: String,\n    ) : ListenTogetherEvent()\n\n    data class JoinRequestReceived(\n        val userId: String,\n        val username: String,\n    ) : ListenTogetherEvent()\n\n    data class JoinApproved(\n        val roomCode: String,\n        val userId: String,\n        val state: RoomState,\n    ) : ListenTogetherEvent()\n\n    data class JoinRejected(\n        val reason: String,\n    ) : ListenTogetherEvent()\n\n    data class UserJoined(\n        val userId: String,\n        val username: String,\n    ) : ListenTogetherEvent()\n\n    data class UserLeft(\n        val userId: String,\n        val username: String,\n    ) : ListenTogetherEvent()\n\n    data class HostChanged(\n        val newHostId: String,\n        val newHostName: String,\n    ) : ListenTogetherEvent()\n\n    data class Kicked(\n        val reason: String,\n    ) : ListenTogetherEvent()\n\n    data class Reconnected(\n        val roomCode: String,\n        val userId: String,\n        val state: RoomState,\n        val isHost: Boolean,\n    ) : ListenTogetherEvent()\n\n    data class UserReconnected(\n        val userId: String,\n        val username: String,\n    ) : ListenTogetherEvent()\n\n    data class UserDisconnected(\n        val userId: String,\n        val username: String,\n    ) : ListenTogetherEvent()\n\n    // Playback events\n    data class PlaybackSync(\n        val action: PlaybackActionPayload,\n    ) : ListenTogetherEvent()\n\n    data class BufferWait(\n        val trackId: String,\n        val waitingFor: List<String>,\n    ) : ListenTogetherEvent()\n\n    data class BufferComplete(\n        val trackId: String,\n    ) : ListenTogetherEvent()\n\n    data class SyncStateReceived(\n        val state: SyncStatePayload,\n    ) : ListenTogetherEvent()\n\n    // Error events\n    data class ServerError(\n        val code: String,\n        val message: String,\n    ) : ListenTogetherEvent()\n}\n\n/**\n * WebSocket client for Listen Together feature\n */\n@Singleton\nclass ListenTogetherClient\n    @Inject\n    constructor(\n        private val context: Context,\n    ) {\n        companion object {\n            private const val TAG = \"ListenTogether\"\n            private val DEFAULT_SERVER_URL = ListenTogetherServers.defaultServerUrl\n            private const val MAX_RECONNECT_ATTEMPTS = 15 // Increased from 5 to 15\n            private const val INITIAL_RECONNECT_DELAY_MS = 1000L // Start at 1 second\n            private const val MAX_RECONNECT_DELAY_MS = 120000L // Cap at 2 minutes\n            private const val PING_INTERVAL_MS = 25000L\n            private const val MAX_LOG_ENTRIES = 500\n            private const val SESSION_GRACE_PERIOD_MS = 10 * 60 * 1000L // 10 minutes\n\n            // Notification constants\n            private const val NOTIFICATION_CHANNEL_ID = \"listen_together_channel\"\n            const val ACTION_APPROVE_JOIN = \"com.metrolist.music.LISTEN_TOGETHER_APPROVE_JOIN\"\n            const val ACTION_REJECT_JOIN = \"com.metrolist.music.LISTEN_TOGETHER_REJECT_JOIN\"\n            const val ACTION_APPROVE_SUGGESTION = \"com.metrolist.music.LISTEN_TOGETHER_APPROVE_SUGGESTION\"\n            const val ACTION_REJECT_SUGGESTION = \"com.metrolist.music.LISTEN_TOGETHER_REJECT_SUGGESTION\"\n            const val EXTRA_USER_ID = \"extra_user_id\"\n            const val EXTRA_SUGGESTION_ID = \"extra_suggestion_id\"\n            const val EXTRA_NOTIFICATION_ID = \"extra_notification_id\"\n\n            @Volatile\n            private var instance: ListenTogetherClient? = null\n\n            fun getInstance(): ListenTogetherClient? = instance\n\n            fun setInstance(client: ListenTogetherClient) {\n                instance = client\n            }\n        }\n\n        // Initialize scope early before init block since it's used in observeNetworkChanges()\n        private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())\n\n        // State flows - initialized before init block to avoid NullPointerException when accessing log()\n        private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)\n        val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()\n\n        private val _roomState = MutableStateFlow<RoomState?>(null)\n        val roomState: StateFlow<RoomState?> = _roomState.asStateFlow()\n\n        private val _role = MutableStateFlow(RoomRole.NONE)\n        val role: StateFlow<RoomRole> = _role.asStateFlow()\n\n        private val _userId = MutableStateFlow<String?>(null)\n        val userId: StateFlow<String?> = _userId.asStateFlow()\n\n        private val _pendingJoinRequests = MutableStateFlow<List<JoinRequestPayload>>(emptyList())\n        val pendingJoinRequests: StateFlow<List<JoinRequestPayload>> = _pendingJoinRequests.asStateFlow()\n\n        private val _bufferingUsers = MutableStateFlow<List<String>>(emptyList())\n        val bufferingUsers: StateFlow<List<String>> = _bufferingUsers.asStateFlow()\n\n        // Suggestions: pending items visible to host\n        private val _pendingSuggestions = MutableStateFlow<List<SuggestionReceivedPayload>>(emptyList())\n        val pendingSuggestions: StateFlow<List<SuggestionReceivedPayload>> = _pendingSuggestions.asStateFlow()\n\n        // Blocked usernames (internal list for privacy)\n        private val _blockedUsernames = MutableStateFlow<Set<String>>(emptySet())\n        val blockedUsernames: StateFlow<Set<String>> = _blockedUsernames.asStateFlow()\n\n        private val _logs = MutableStateFlow<List<LogEntry>>(emptyList())\n        val logs: StateFlow<List<LogEntry>> = _logs.asStateFlow()\n\n        // Event flow\n        private val _events = MutableSharedFlow<ListenTogetherEvent>()\n        val events: SharedFlow<ListenTogetherEvent> = _events.asSharedFlow()\n\n        init {\n            setInstance(this)\n            ensureNotificationChannel()\n            // Load persisted session info asynchronously after construction to avoid calling log() before flows are initialized\n            CoroutineScope(Dispatchers.IO + SupervisorJob()).launch {\n                loadPersistedSession()\n                observeNetworkChanges()\n            }\n        }\n\n        /**\n         * Observe network changes to trigger reconnections\n         */\n        private fun observeNetworkChanges() {\n            scope.launch {\n                try {\n                    val observer = connectivityObserver ?: return@launch\n                    observer.networkStatus.collect { available: Boolean ->\n                        val previous = isNetworkAvailable\n                        isNetworkAvailable = available\n\n                        if (available && !previous) {\n                            log(LogLevel.INFO, \"Network restored, checking if reconnection needed\")\n                            // Reset attempts when network is restored to allow a fresh set of retries\n                            if (_connectionState.value == ConnectionState.ERROR ||\n                                _connectionState.value == ConnectionState.DISCONNECTED\n                            ) {\n                                if (sessionToken != null || _roomState.value != null || pendingAction != null) {\n                                    log(LogLevel.INFO, \"Network restored, triggering reconnection\")\n                                    reconnectAttempts = 0 // Reset attempts for a fresh start\n                                    connect()\n                                }\n                            }\n                        } else if (!available && previous) {\n                            log(LogLevel.WARNING, \"Network lost\")\n                        }\n                    }\n                } catch (e: Exception) {\n                    Timber.tag(TAG).e(e, \"Error observing network changes\")\n                }\n            }\n        }\n\n        /**\n         * Load persisted session information from storage\n         */\n        private fun loadPersistedSession() {\n            try {\n                val token = context.dataStore.get(ListenTogetherSessionTokenKey, \"\")\n                val roomCode = context.dataStore.get(ListenTogetherRoomCodeKey, \"\")\n                val userId = context.dataStore.get(ListenTogetherUserIdKey, \"\")\n                val isHost = context.dataStore.get(ListenTogetherIsHostKey, false)\n                val timestamp = context.dataStore.get(ListenTogetherSessionTimestampKey, 0L)\n\n                // Check if session is still valid (within grace period)\n                if (token.isNotEmpty() && roomCode.isNotEmpty() &&\n                    (System.currentTimeMillis() - timestamp < SESSION_GRACE_PERIOD_MS)\n                ) {\n                    sessionToken = token\n                    storedRoomCode = roomCode\n                    _userId.value = userId.ifEmpty { null }\n                    wasHost = isHost\n                    sessionStartTime = timestamp\n                    log(LogLevel.INFO, \"Loaded persisted session\", \"Room: $roomCode, Host: $isHost\")\n                } else if (token.isNotEmpty()) {\n                    log(LogLevel.WARNING, \"Session expired\", \"Age: ${System.currentTimeMillis() - timestamp}ms\")\n                    clearPersistedSession()\n                }\n            } catch (e: Exception) {\n                log(LogLevel.ERROR, \"Failed to load persisted session\", e.message)\n            }\n\n            // Also load blocked usernames\n            loadBlockedUsernames()\n\n            // Migrate old server URL to new one\n            migrateServerUrl()\n        }\n\n        /**\n         * Load blocked usernames from storage\n         */\n        private fun loadBlockedUsernames() {\n            try {\n                val blockedJson = context.dataStore.get(com.metrolist.music.constants.ListenTogetherBlockedUsersKey, \"\")\n                val blockedList =\n                    if (blockedJson.isNotEmpty()) {\n                        json.decodeFromString<List<String>>(blockedJson)\n                    } else {\n                        emptyList()\n                    }\n                _blockedUsernames.value = blockedList.toSet()\n            } catch (e: Exception) {\n                log(LogLevel.ERROR, \"Failed to load blocked usernames\", e.message)\n                _blockedUsernames.value = emptySet()\n            }\n        }\n\n        /**\n         * Save blocked usernames to storage\n         */\n        private suspend fun saveBlockedUsernames() {\n            try {\n                val blockedJson = json.encodeToString(_blockedUsernames.value.toList())\n                context.dataStore.edit { preferences ->\n                    preferences[com.metrolist.music.constants.ListenTogetherBlockedUsersKey] = blockedJson\n                }\n            } catch (e: Exception) {\n                log(LogLevel.ERROR, \"Failed to save blocked usernames\", e.message)\n            }\n        }\n\n        /**\n         * Migrate old server URL to new one if needed\n         */\n        private fun migrateServerUrl() {\n            try {\n                val oldServerUrl = \"wss://metroserver.meowery.eu/ws\"\n                val currentUrl = context.dataStore.get(ListenTogetherServerUrlKey, DEFAULT_SERVER_URL)\n\n                if (currentUrl == oldServerUrl) {\n                    log(LogLevel.INFO, \"Migrating server URL\", \"Old: $oldServerUrl -> New: $DEFAULT_SERVER_URL\")\n                    scope.launch {\n                        context.dataStore.edit { preferences ->\n                            preferences[ListenTogetherServerUrlKey] = DEFAULT_SERVER_URL\n                        }\n                    }\n                }\n            } catch (e: Exception) {\n                log(LogLevel.ERROR, \"Failed to migrate server URL\", e.message)\n            }\n        }\n\n        /**\n         * Save current session information to persistent storage\n         */\n        private fun savePersistedSession() {\n            try {\n                scope.launch {\n                    context.dataStore.edit { preferences ->\n                        if (sessionToken != null) {\n                            preferences[ListenTogetherSessionTokenKey] = sessionToken!!\n                            preferences[ListenTogetherRoomCodeKey] = storedRoomCode ?: \"\"\n                            preferences[ListenTogetherUserIdKey] = _userId.value ?: \"\"\n                            preferences[ListenTogetherIsHostKey] = wasHost\n                            preferences[ListenTogetherSessionTimestampKey] = System.currentTimeMillis()\n                        }\n                    }\n                }\n            } catch (e: Exception) {\n                log(LogLevel.ERROR, \"Failed to save persisted session\", e.message)\n            }\n        }\n\n        /**\n         * Clear persisted session information\n         */\n        private fun clearPersistedSession() {\n            try {\n                scope.launch {\n                    context.dataStore.edit { preferences ->\n                        preferences.remove(ListenTogetherSessionTokenKey)\n                        preferences.remove(ListenTogetherRoomCodeKey)\n                        preferences.remove(ListenTogetherUserIdKey)\n                        preferences.remove(ListenTogetherIsHostKey)\n                        preferences.remove(ListenTogetherSessionTimestampKey)\n                    }\n                }\n            } catch (e: Exception) {\n                log(LogLevel.ERROR, \"Failed to clear persisted session\", e.message)\n            }\n        }\n\n        private val json =\n            Json {\n                ignoreUnknownKeys = true\n                encodeDefaults = true\n            }\n\n        // Message codec - uses Protobuf with compression enabled\n        private val codec = MessageCodec(true)\n\n        private var webSocket: WebSocket? = null\n        private var pingJob: Job? = null\n        private var reconnectAttempts = 0\n\n        // Session info for reconnection\n        private var sessionToken: String? = null\n        private var storedUsername: String? = null\n        private var storedRoomCode: String? = null\n        private var wasHost: Boolean = false\n        private var sessionStartTime: Long = 0\n\n        // Pending actions to execute when connected\n        private var pendingAction: PendingAction? = null\n\n        // Wake lock to keep connection alive when in a room\n        private var wakeLock: PowerManager.WakeLock? = null\n\n        // Track notification IDs for join requests to dismiss them from both UI and notification actions\n        private val joinRequestNotifications = mutableMapOf<String, Int>()\n\n        // Track notification IDs for suggestions to dismiss them similarly\n        private val suggestionNotifications = mutableMapOf<String, Int>()\n\n        // Network connectivity monitoring - use lazy to avoid initialization order issues\n        private val connectivityObserver: NetworkConnectivityObserver? by lazy {\n            try {\n                NetworkConnectivityObserver(context)\n            } catch (e: Exception) {\n                Timber.tag(TAG).e(e, \"Failed to create NetworkConnectivityObserver\")\n                null\n            }\n        }\n        private var isNetworkAvailable =\n            try {\n                connectivityObserver?.isCurrentlyConnected() ?: true\n            } catch (e: Exception) {\n                true\n            }\n\n        private val client =\n            OkHttpClient\n                .Builder()\n                .connectTimeout(30, TimeUnit.SECONDS)\n                .readTimeout(60, TimeUnit.SECONDS)\n                .writeTimeout(30, TimeUnit.SECONDS)\n                .pingInterval(60, TimeUnit.SECONDS) // Match server ping interval\n                .build()\n\n        private fun getServerUrl(): String = context.dataStore.get(ListenTogetherServerUrlKey, DEFAULT_SERVER_URL)\n\n        /**\n         * Calculate exponential backoff delay with jitter\n         */\n        private fun calculateBackoffDelay(attempt: Int): Long {\n            val exponentialDelay = INITIAL_RECONNECT_DELAY_MS * (2 shl (minOf(attempt - 1, 4)))\n            val cappedDelay = minOf(exponentialDelay, MAX_RECONNECT_DELAY_MS)\n            // Add 0-20% jitter to prevent thundering herd\n            val jitter = (cappedDelay * 0.2 * Math.random()).toLong()\n            return cappedDelay + jitter\n        }\n\n        private fun log(\n            level: LogLevel,\n            message: String,\n            details: String? = null,\n        ) {\n            val timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern(\"HH:mm:ss.SSS\"))\n            val entry = LogEntry(timestamp, level, message, details)\n\n            _logs.value = (_logs.value + entry).takeLast(MAX_LOG_ENTRIES)\n\n            when (level) {\n                LogLevel.ERROR -> Timber.tag(TAG).e(\"$message ${details ?: \"\"}\")\n                LogLevel.WARNING -> Timber.tag(TAG).w(\"$message ${details ?: \"\"}\")\n                LogLevel.DEBUG -> Timber.tag(TAG).d(\"$message ${details ?: \"\"}\")\n                LogLevel.INFO -> Timber.tag(TAG).i(\"$message ${details ?: \"\"}\")\n            }\n        }\n\n        fun clearLogs() {\n            _logs.value = emptyList()\n        }\n\n        /**\n         * Connect to the Listen Together server\n         */\n        fun connect() {\n            if (_connectionState.value == ConnectionState.CONNECTED ||\n                _connectionState.value == ConnectionState.CONNECTING\n            ) {\n                log(LogLevel.WARNING, \"Already connected or connecting\")\n                return\n            }\n\n            _connectionState.value = ConnectionState.CONNECTING\n            log(LogLevel.INFO, \"Connecting to server\", getServerUrl())\n\n            val request =\n                Request\n                    .Builder()\n                    .url(getServerUrl())\n                    .build()\n\n            webSocket =\n                client.newWebSocket(\n                    request,\n                    object : WebSocketListener() {\n                        override fun onOpen(\n                            webSocket: WebSocket,\n                            response: Response,\n                        ) {\n                            log(LogLevel.INFO, \"Connected to server\")\n                            _connectionState.value = ConnectionState.CONNECTED\n                            reconnectAttempts = 0\n                            startPingJob()\n\n                            // Try to reconnect to previous session if we have a valid token\n                            if (sessionToken != null && storedRoomCode != null) {\n                                log(LogLevel.INFO, \"Attempting to reconnect to previous session\", \"Room: $storedRoomCode\")\n                                sendMessage(MessageTypes.RECONNECT, ReconnectPayload(sessionToken!!))\n                            } else {\n                                // Execute any pending action\n                                executePendingAction()\n                            }\n                        }\n\n                        override fun onMessage(\n                            webSocket: WebSocket,\n                            bytes: okio.ByteString,\n                        ) {\n                            // Handle binary protobuf messages\n                            handleMessage(bytes.toByteArray())\n                        }\n\n                        override fun onClosing(\n                            webSocket: WebSocket,\n                            code: Int,\n                            reason: String,\n                        ) {\n                            log(LogLevel.INFO, \"Server closing connection\", \"Code: $code, Reason: $reason\")\n                            webSocket.close(1000, null)\n                        }\n\n                        override fun onClosed(\n                            webSocket: WebSocket,\n                            code: Int,\n                            reason: String,\n                        ) {\n                            log(LogLevel.INFO, \"Connection closed\", \"Code: $code, Reason: $reason\")\n                            handleDisconnect()\n                        }\n\n                        override fun onFailure(\n                            webSocket: WebSocket,\n                            t: Throwable,\n                            response: Response?,\n                        ) {\n                            log(LogLevel.ERROR, \"Connection failure\", t.message)\n                            handleConnectionFailure(t)\n                        }\n                    },\n                )\n        }\n\n        private fun executePendingAction() {\n            val action = pendingAction ?: return\n            pendingAction = null\n\n            when (action) {\n                is PendingAction.CreateRoom -> {\n                    log(LogLevel.INFO, \"Executing pending create room\", action.username)\n                    sendMessage(MessageTypes.CREATE_ROOM, CreateRoomPayload(action.username))\n                }\n\n                is PendingAction.JoinRoom -> {\n                    log(LogLevel.INFO, \"Executing pending join room\", \"${action.roomCode} as ${action.username}\")\n                    sendMessage(MessageTypes.JOIN_ROOM, JoinRoomPayload(action.roomCode.uppercase(), action.username))\n                }\n            }\n        }\n\n        /**\n         * Disconnect from the server\n         */\n        fun disconnect() {\n            log(LogLevel.INFO, \"Disconnecting from server\")\n            releaseWakeLock() // Release wake lock when disconnecting\n            pingJob?.cancel()\n            pingJob = null\n            webSocket?.close(1000, \"User disconnected\")\n            webSocket = null\n            _connectionState.value = ConnectionState.DISCONNECTED\n\n            // Clear session and state on explicit disconnect\n            sessionToken = null\n            storedRoomCode = null\n            storedUsername = null\n            pendingAction = null\n            _roomState.value = null\n            _role.value = RoomRole.NONE\n            _userId.value = null\n            _pendingJoinRequests.value = emptyList()\n            _bufferingUsers.value = emptyList()\n\n            // Clear from persistent storage\n            clearPersistedSession()\n            reconnectAttempts = 0\n\n            scope.launch { _events.emit(ListenTogetherEvent.Disconnected) }\n        }\n\n        private fun startPingJob() {\n            pingJob?.cancel()\n            pingJob =\n                scope.launch {\n                    while (true) {\n                        delay(PING_INTERVAL_MS)\n                        // Refresh the WakeLock on every ping cycle so it never expires while the\n                        // connection is active. Without this, the 10-minute timeout can lapse during\n                        // long sessions with the screen off, allowing the CPU to throttle and\n                        // causing the WebSocket to degrade, resulting in choppy audio.\n                        acquireWakeLock()\n                        sendMessageNoPayload(MessageTypes.PING)\n                    }\n                }\n        }\n\n        @Suppress(\"DEPRECATION\")\n        private fun acquireWakeLock() {\n            if (wakeLock == null) {\n                val powerManager = context.getSystemService<PowerManager>()\n                wakeLock =\n                    powerManager?.newWakeLock(\n                        PowerManager.PARTIAL_WAKE_LOCK,\n                        \"Metrolist:ListenTogether\",\n                    )\n            }\n            // Always release before acquiring so that the timeout is reset on each call.\n            // This is safe because the ping job calls acquireWakeLock() every PING_INTERVAL_MS\n            // (25 s), ensuring the lock is refreshed well before the 10-minute window elapses.\n            // Without the release-and-reacquire pattern the first acquire() sets the countdown\n            // and subsequent calls while isHeld is true are no-ops, so the lock would still\n            // expire after 10 minutes of continuous screen-off sessions.\n            if (wakeLock?.isHeld == true) {\n                wakeLock?.release()\n            }\n            wakeLock?.acquire(10 * 60 * 1000L)\n            log(LogLevel.DEBUG, \"Wake lock acquired\")\n        }\n\n        private fun releaseWakeLock() {\n            if (wakeLock?.isHeld == true) {\n                wakeLock?.release()\n                log(LogLevel.DEBUG, \"Wake lock released\")\n            }\n        }\n\n        private fun ensureNotificationChannel() {\n            try {\n                val nm = context.getSystemService(NotificationManager::class.java)\n                val existing = nm?.getNotificationChannel(NOTIFICATION_CHANNEL_ID)\n                if (existing == null) {\n                    val channel =\n                        NotificationChannel(\n                            NOTIFICATION_CHANNEL_ID,\n                            context.getString(R.string.listen_together_notification_channel_name),\n                            NotificationManager.IMPORTANCE_HIGH,\n                        )\n                    channel.description = context.getString(R.string.listen_together_notification_channel_desc)\n                    nm?.createNotificationChannel(channel)\n                }\n            } catch (e: Exception) {\n                log(LogLevel.WARNING, \"Failed to create notification channel\", e.message)\n            }\n        }\n\n        @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)\n        private fun showJoinRequestNotification(payload: JoinRequestPayload) {\n            val notifId = (System.currentTimeMillis() % Int.MAX_VALUE).toInt()\n\n            // Store notification ID for this user so we can dismiss it from UI actions\n            joinRequestNotifications[payload.userId] = notifId\n\n            val approveIntent =\n                Intent(context, ListenTogetherActionReceiver::class.java).apply {\n                    action = ACTION_APPROVE_JOIN\n                    putExtra(EXTRA_USER_ID, payload.userId)\n                    putExtra(EXTRA_NOTIFICATION_ID, notifId)\n                }\n            val rejectIntent =\n                Intent(context, ListenTogetherActionReceiver::class.java).apply {\n                    action = ACTION_REJECT_JOIN\n                    putExtra(EXTRA_USER_ID, payload.userId)\n                    putExtra(EXTRA_NOTIFICATION_ID, notifId)\n                }\n\n            val approvePI =\n                PendingIntent.getBroadcast(\n                    context,\n                    payload.userId.hashCode(),\n                    approveIntent,\n                    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,\n                )\n            val rejectPI =\n                PendingIntent.getBroadcast(\n                    context,\n                    payload.userId.hashCode().inv(),\n                    rejectIntent,\n                    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,\n                )\n\n            val content = context.getString(R.string.listen_together_join_request_notification, payload.username)\n\n            val builder =\n                NotificationCompat\n                    .Builder(context, NOTIFICATION_CHANNEL_ID)\n                    .setSmallIcon(R.drawable.share)\n                    .setContentTitle(context.getString(R.string.listen_together))\n                    .setContentText(content)\n                    .setPriority(NotificationCompat.PRIORITY_HIGH)\n                    .setAutoCancel(true)\n                    .addAction(0, context.getString(R.string.approve), approvePI)\n                    .addAction(0, context.getString(R.string.reject), rejectPI)\n\n            if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {\n                NotificationManagerCompat.from(context).notify(notifId, builder.build())\n            }\n        }\n\n        @RequiresPermission(Manifest.permission.POST_NOTIFICATIONS)\n        private fun showSuggestionNotification(payload: SuggestionReceivedPayload) {\n            val notifId = (System.currentTimeMillis() % Int.MAX_VALUE).toInt()\n\n            // Store notification ID for this suggestion so we can dismiss it from UI actions\n            suggestionNotifications[payload.suggestionId] = notifId\n\n            val approveIntent =\n                Intent(context, ListenTogetherActionReceiver::class.java).apply {\n                    action = ACTION_APPROVE_SUGGESTION\n                    putExtra(EXTRA_SUGGESTION_ID, payload.suggestionId)\n                    putExtra(EXTRA_NOTIFICATION_ID, notifId)\n                }\n            val rejectIntent =\n                Intent(context, ListenTogetherActionReceiver::class.java).apply {\n                    action = ACTION_REJECT_SUGGESTION\n                    putExtra(EXTRA_SUGGESTION_ID, payload.suggestionId)\n                    putExtra(EXTRA_NOTIFICATION_ID, notifId)\n                }\n\n            val approvePI =\n                PendingIntent.getBroadcast(\n                    context,\n                    payload.suggestionId.hashCode(),\n                    approveIntent,\n                    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,\n                )\n            val rejectPI =\n                PendingIntent.getBroadcast(\n                    context,\n                    payload.suggestionId.hashCode().inv(),\n                    rejectIntent,\n                    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,\n                )\n\n            val content = context.getString(R.string.listen_together_suggestion_received, payload.fromUsername, payload.trackInfo.title)\n\n            val builder =\n                NotificationCompat\n                    .Builder(context, NOTIFICATION_CHANNEL_ID)\n                    .setSmallIcon(R.drawable.share)\n                    .setContentTitle(context.getString(R.string.listen_together))\n                    .setContentText(content)\n                    .setPriority(NotificationCompat.PRIORITY_HIGH)\n                    .setAutoCancel(true)\n                    .addAction(0, context.getString(R.string.approve), approvePI)\n                    .addAction(0, context.getString(R.string.reject), rejectPI)\n\n            if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED) {\n                NotificationManagerCompat.from(context).notify(notifId, builder.build())\n            }\n        }\n\n        private fun handleDisconnect() {\n            pingJob?.cancel()\n            pingJob = null\n\n            // Don't clear room state - we might reconnect\n            // Only update connection state\n            _connectionState.value = ConnectionState.DISCONNECTED\n            _pendingJoinRequests.value = emptyList()\n            _bufferingUsers.value = emptyList()\n\n            // If we have a session, try to reconnect\n            if (sessionToken != null && _roomState.value != null) {\n                log(LogLevel.INFO, \"Connection lost, will attempt to reconnect\")\n                handleConnectionFailure(Exception(\"Connection lost\"))\n            } else {\n                scope.launch { _events.emit(ListenTogetherEvent.Disconnected) }\n            }\n        }\n\n        private fun handleConnectionFailure(t: Throwable) {\n            pingJob?.cancel()\n            pingJob = null\n\n            // Always try to reconnect if we have a session token or pending action\n            val shouldReconnect = sessionToken != null || _roomState.value != null || pendingAction != null\n\n            if (!isNetworkAvailable) {\n                log(LogLevel.WARNING, \"Connection failure, waiting for network\", t.message)\n                _connectionState.value = ConnectionState.DISCONNECTED\n                return\n            }\n\n            if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS && shouldReconnect) {\n                reconnectAttempts++\n                _connectionState.value = ConnectionState.RECONNECTING\n\n                val delayMs = calculateBackoffDelay(reconnectAttempts)\n                val delaySeconds = delayMs / 1000\n\n                log(\n                    LogLevel.INFO,\n                    \"Attempting reconnect\",\n                    \"Attempt $reconnectAttempts/$MAX_RECONNECT_ATTEMPTS, waiting ${delaySeconds}s, reason: ${t.message}\",\n                )\n\n                scope.launch {\n                    _events.emit(ListenTogetherEvent.Reconnecting(reconnectAttempts, MAX_RECONNECT_ATTEMPTS))\n                    delay(delayMs)\n\n                    // Check if we're still supposed to be reconnecting\n                    if (_connectionState.value == ConnectionState.RECONNECTING || _connectionState.value == ConnectionState.DISCONNECTED) {\n                        log(LogLevel.INFO, \"Reconnecting after backoff\", \"Delay was ${delaySeconds}s\")\n                        connect()\n                    }\n                }\n            } else {\n                _connectionState.value = ConnectionState.ERROR\n\n                // If we had a session, notify user but keep session data for manual retry\n                if (sessionToken != null) {\n                    log(\n                        LogLevel.ERROR,\n                        \"Reconnection failed\",\n                        \"Max attempts reached, but session preserved for manual reconnect\",\n                    )\n                    scope.launch {\n                        _events.emit(\n                            ListenTogetherEvent.ConnectionError(\n                                \"Connection failed after $MAX_RECONNECT_ATTEMPTS attempts. ${t.message ?: \"Unknown error\"}\",\n                            ),\n                        )\n                    }\n                } else {\n                    // No session, so clear everything\n                    sessionToken = null\n                    storedRoomCode = null\n                    storedUsername = null\n                    _roomState.value = null\n                    _role.value = RoomRole.NONE\n                    clearPersistedSession()\n\n                    scope.launch {\n                        _events.emit(ListenTogetherEvent.ConnectionError(t.message ?: \"Unknown error\"))\n                    }\n                }\n            }\n        }\n\n        private fun handleMessage(data: ByteArray) {\n            log(LogLevel.DEBUG, \"Received message\", \"${data.size} bytes\")\n\n            try {\n                // Decode message using Protobuf\n                val (msgType, payloadBytes) = codec.decode(data)\n\n                when (msgType) {\n                    MessageTypes.ROOM_CREATED -> {\n                        val payload = codec.decodePayload(msgType, payloadBytes) as? RoomCreatedPayload ?: return\n                        _userId.value = payload.userId\n                        _role.value = RoomRole.HOST\n                        sessionToken = payload.sessionToken\n                        storedRoomCode = payload.roomCode\n                        wasHost = true\n                        sessionStartTime = System.currentTimeMillis()\n\n                        _roomState.value =\n                            RoomState(\n                                roomCode = payload.roomCode,\n                                hostId = payload.userId,\n                                users = listOf(UserInfo(payload.userId, storedUsername ?: \"\", true)),\n                                isPlaying = false,\n                                position = 0,\n                                lastUpdate = System.currentTimeMillis(),\n                                volume = 1f,\n                            )\n\n                        // Save session to persistent storage\n                        savePersistedSession()\n\n                        acquireWakeLock() // Keep connection alive while in room\n                        log(LogLevel.INFO, \"Room created\", \"Code: ${payload.roomCode}\")\n                        scope.launch { _events.emit(ListenTogetherEvent.RoomCreated(payload.roomCode, payload.userId)) }\n                        // Global toast for room creation so the host sees it regardless of UI\n                        scope.launch(Dispatchers.Main) {\n                            Toast\n                                .makeText(\n                                    context,\n                                    context.getString(R.string.listen_together_room_created, payload.roomCode),\n                                    Toast.LENGTH_LONG,\n                                ).show()\n                        }\n                    }\n\n                    MessageTypes.JOIN_REQUEST -> {\n                        val payload = codec.decodePayload(msgType, payloadBytes) as? JoinRequestPayload ?: return\n\n                        // Check if user is blocked\n                        if (isUserBlocked(payload.username)) {\n                            log(LogLevel.INFO, \"Join request from blocked user ignored\", \"User: ${payload.username}\")\n                            // Silently reject blocked users\n                            rejectJoin(payload.userId, \"You are blocked\")\n                            return\n                        }\n\n                        _pendingJoinRequests.value += payload\n                        log(LogLevel.INFO, \"Join request received\", \"User: ${payload.username}\")\n\n                        // Check if auto-approval is enabled\n                        val autoApprovalEnabled = context.dataStore.get(ListenTogetherAutoApprovalKey, false)\n\n                        if (_role.value == RoomRole.HOST) {\n                            if (autoApprovalEnabled) {\n                                // Automatically approve the join request\n                                log(LogLevel.INFO, \"Auto-approving join request\", \"User: ${payload.username}\")\n                                approveJoin(payload.userId)\n                            } else {\n                                // Notify host with Approve/Reject actions\n                                if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) ==\n                                    PackageManager.PERMISSION_GRANTED\n                                ) {\n                                    showJoinRequestNotification(payload)\n                                }\n                            }\n                        }\n                        scope.launch { _events.emit(ListenTogetherEvent.JoinRequestReceived(payload.userId, payload.username)) }\n                    }\n\n                    MessageTypes.JOIN_APPROVED -> {\n                        val payload = codec.decodePayload(msgType, payloadBytes) as? JoinApprovedPayload ?: return\n                        _userId.value = payload.userId\n                        _role.value = RoomRole.GUEST\n                        sessionToken = payload.sessionToken\n                        storedRoomCode = payload.roomCode\n                        wasHost = false\n                        sessionStartTime = System.currentTimeMillis()\n\n                        _roomState.value = payload.state\n\n                        // Save session to persistent storage\n                        savePersistedSession()\n\n                        acquireWakeLock() // Keep connection alive while in room\n                        log(LogLevel.INFO, \"Joined room\", \"Code: ${payload.roomCode}\")\n                        scope.launch { _events.emit(ListenTogetherEvent.JoinApproved(payload.roomCode, payload.userId, payload.state)) }\n                    }\n\n                    MessageTypes.JOIN_REJECTED -> {\n                        val payload = codec.decodePayload(msgType, payloadBytes) as? JoinRejectedPayload ?: return\n                        log(LogLevel.WARNING, \"Join rejected\", payload.reason)\n                        scope.launch { _events.emit(ListenTogetherEvent.JoinRejected(payload.reason)) }\n                    }\n\n                    MessageTypes.USER_JOINED -> {\n                        val payload = codec.decodePayload(msgType, payloadBytes) as? UserJoinedPayload ?: return\n                        _roomState.value =\n                            _roomState.value?.copy(\n                                users = _roomState.value!!.users + UserInfo(payload.userId, payload.username, false),\n                            )\n                        _pendingJoinRequests.value = _pendingJoinRequests.value.filter { it.userId != payload.userId }\n\n                        // Dismiss notification if it exists\n                        joinRequestNotifications.remove(payload.userId)?.let { notifId ->\n                            NotificationManagerCompat.from(context).cancel(notifId)\n                        }\n\n                        log(LogLevel.INFO, \"User joined\", payload.username)\n                        scope.launch { _events.emit(ListenTogetherEvent.UserJoined(payload.userId, payload.username)) }\n                    }\n\n                    MessageTypes.USER_LEFT -> {\n                        val payload = codec.decodePayload(msgType, payloadBytes) as? UserLeftPayload ?: return\n                        _roomState.value =\n                            _roomState.value?.copy(\n                                users = _roomState.value!!.users.filter { it.userId != payload.userId },\n                            )\n                        log(LogLevel.INFO, \"User left\", payload.username)\n                        scope.launch { _events.emit(ListenTogetherEvent.UserLeft(payload.userId, payload.username)) }\n                    }\n\n                    MessageTypes.HOST_CHANGED -> {\n                        val payload = codec.decodePayload(msgType, payloadBytes) as? HostChangedPayload ?: return\n                        _roomState.value =\n                            _roomState.value?.copy(\n                                hostId = payload.newHostId,\n                                users =\n                                    _roomState.value!!.users.map {\n                                        it.copy(isHost = it.userId == payload.newHostId)\n                                    },\n                            )\n                        if (payload.newHostId == _userId.value) {\n                            _role.value = RoomRole.HOST\n                        } else if (_role.value == RoomRole.HOST) {\n                            // Lost host role\n                            _role.value = RoomRole.GUEST\n                        }\n                        log(LogLevel.INFO, \"Host changed\", \"New host: ${payload.newHostName}\")\n                        scope.launch { _events.emit(ListenTogetherEvent.HostChanged(payload.newHostId, payload.newHostName)) }\n                    }\n\n                    MessageTypes.KICKED -> {\n                        val payload = codec.decodePayload(msgType, payloadBytes) as? KickedPayload ?: return\n                        log(LogLevel.WARNING, \"Kicked from room\", payload.reason)\n                        releaseWakeLock() // Release wake lock when kicked\n                        sessionToken = null\n                        _roomState.value = null\n                        _role.value = RoomRole.NONE\n                        scope.launch { _events.emit(ListenTogetherEvent.Kicked(payload.reason)) }\n                    }\n\n                    MessageTypes.SYNC_PLAYBACK -> {\n                        val payload = codec.decodePayload(msgType, payloadBytes) as? PlaybackActionPayload ?: return\n                        log(LogLevel.DEBUG, \"Playback sync\", \"Action: ${payload.action}\")\n\n                        // Update room state based on action\n                        when (payload.action) {\n                            PlaybackActions.PLAY -> {\n                                _roomState.value =\n                                    _roomState.value?.copy(\n                                        isPlaying = true,\n                                        position = payload.position ?: _roomState.value!!.position,\n                                    )\n                            }\n\n                            PlaybackActions.PAUSE -> {\n                                _roomState.value =\n                                    _roomState.value?.copy(\n                                        isPlaying = false,\n                                        position = payload.position ?: _roomState.value!!.position,\n                                    )\n                            }\n\n                            PlaybackActions.SEEK -> {\n                                _roomState.value =\n                                    _roomState.value?.copy(\n                                        position = payload.position ?: _roomState.value!!.position,\n                                    )\n                            }\n\n                            PlaybackActions.CHANGE_TRACK -> {\n                                _roomState.value =\n                                    _roomState.value?.copy(\n                                        currentTrack = payload.trackInfo,\n                                        isPlaying = false,\n                                        position = 0,\n                                    )\n                            }\n\n                            PlaybackActions.QUEUE_ADD -> {\n                                val ti = payload.trackInfo\n                                if (ti != null) {\n                                    val currentQueue = _roomState.value?.queue ?: emptyList()\n                                    _roomState.value =\n                                        _roomState.value?.copy(\n                                            queue = if (payload.insertNext == true) listOf(ti) + currentQueue else currentQueue + ti,\n                                        )\n                                }\n                            }\n\n                            PlaybackActions.QUEUE_REMOVE -> {\n                                val id = payload.trackId\n                                if (!id.isNullOrEmpty()) {\n                                    val currentQueue = _roomState.value?.queue ?: emptyList()\n                                    _roomState.value =\n                                        _roomState.value?.copy(\n                                            queue = currentQueue.filter { it.id != id },\n                                        )\n                                }\n                            }\n\n                            PlaybackActions.QUEUE_CLEAR -> {\n                                _roomState.value = _roomState.value?.copy(queue = emptyList())\n                            }\n\n                            PlaybackActions.SET_VOLUME -> {\n                                val vol = payload.volume\n                                if (vol != null) {\n                                    _roomState.value = _roomState.value?.copy(volume = vol.coerceIn(0f, 1f))\n                                }\n                            }\n                        }\n\n                        scope.launch { _events.emit(ListenTogetherEvent.PlaybackSync(payload)) }\n                    }\n\n                    MessageTypes.BUFFER_WAIT -> {\n                        val payload = codec.decodePayload(msgType, payloadBytes) as? BufferWaitPayload ?: return\n                        _bufferingUsers.value = payload.waitingFor\n                        log(LogLevel.DEBUG, \"Waiting for buffering\", \"Users: ${payload.waitingFor.size}\")\n                        scope.launch { _events.emit(ListenTogetherEvent.BufferWait(payload.trackId, payload.waitingFor)) }\n                    }\n\n                    MessageTypes.BUFFER_COMPLETE -> {\n                        val payload = codec.decodePayload(msgType, payloadBytes) as? BufferCompletePayload ?: return\n                        _bufferingUsers.value = emptyList()\n                        log(LogLevel.INFO, \"All users buffered\", \"Track: ${payload.trackId}\")\n                        scope.launch { _events.emit(ListenTogetherEvent.BufferComplete(payload.trackId)) }\n                    }\n\n                    MessageTypes.SYNC_STATE -> {\n                        val payload = codec.decodePayload(msgType, payloadBytes) as? SyncStatePayload ?: return\n                        log(LogLevel.INFO, \"Sync state received\", \"Playing: ${payload.isPlaying}, Position: ${payload.position}\")\n                        scope.launch { _events.emit(ListenTogetherEvent.SyncStateReceived(payload)) }\n                    }\n\n                    MessageTypes.SUGGESTION_RECEIVED -> {\n                        val payload = codec.decodePayload(msgType, payloadBytes) as? SuggestionReceivedPayload ?: return\n                        // Only host should receive suggestions\n                        if (_role.value == RoomRole.HOST) {\n                            // Check if user is blocked\n                            if (isUserBlocked(payload.fromUsername)) {\n                                log(LogLevel.INFO, \"Suggestion from blocked user ignored\", \"User: ${payload.fromUsername}\")\n                                return\n                            }\n\n                            log(LogLevel.INFO, \"Suggestion received\", \"${payload.fromUsername}: ${payload.trackInfo.title}\")\n\n                            // Check if auto-approval of suggestions is enabled\n                            val autoApproveSuggestionsEnabled = context.dataStore.get(ListenTogetherAutoApproveSuggestionsKey, false)\n\n                            if (autoApproveSuggestionsEnabled) {\n                                // Automatically approve the suggestion\n                                log(LogLevel.INFO, \"Auto-approving suggestion\", \"${payload.fromUsername}: ${payload.trackInfo.title}\")\n                                approveSuggestion(payload.suggestionId)\n                            } else {\n                                // Add to pending list and show notification\n                                _pendingSuggestions.value += payload\n                                // Notify the host with actionable notification\n                                if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) ==\n                                    PackageManager.PERMISSION_GRANTED\n                                ) {\n                                    showSuggestionNotification(payload)\n                                }\n                            }\n                        }\n                    }\n\n                    MessageTypes.SUGGESTION_APPROVED -> {\n                        val payload = codec.decodePayload(msgType, payloadBytes) as? SuggestionApprovedPayload ?: return\n                        log(LogLevel.INFO, \"Suggestion approved\", payload.trackInfo.title)\n\n                        // Dismiss notification if it exists (for host who approved via another device/modal)\n                        suggestionNotifications.remove(payload.suggestionId)?.let { notifId ->\n                            NotificationManagerCompat.from(context).cancel(notifId)\n                        }\n\n                        // For guests, optionally notify via events; UI can react if needed\n                    }\n\n                    MessageTypes.SUGGESTION_REJECTED -> {\n                        val payload = codec.decodePayload(msgType, payloadBytes) as? SuggestionRejectedPayload ?: return\n                        log(LogLevel.WARNING, \"Suggestion rejected\", payload.reason ?: \"\")\n\n                        // Dismiss notification if it exists\n                        suggestionNotifications.remove(payload.suggestionId)?.let { notifId ->\n                            NotificationManagerCompat.from(context).cancel(notifId)\n                        }\n\n                        // For guests, optionally notify via events\n                    }\n\n                    MessageTypes.ERROR -> {\n                        val payload = codec.decodePayload(msgType, payloadBytes) as? ErrorPayload ?: return\n                        log(LogLevel.ERROR, \"Server error\", \"${payload.code}: ${payload.message}\")\n\n                        // Handle specific error cases\n                        when (payload.code) {\n                            \"session_not_found\" -> {\n                                // Session expired on server, try to rejoin the room\n                                if (storedRoomCode != null && storedUsername != null && !wasHost) {\n                                    log(\n                                        LogLevel.WARNING,\n                                        \"Session expired on server\",\n                                        \"Attempting automatic rejoin to room: $storedRoomCode\",\n                                    )\n                                    // Try rejoining as a guest\n                                    scope.launch {\n                                        delay(500) // Small delay before rejoin attempt\n                                        joinRoom(storedRoomCode!!, storedUsername!!)\n                                    }\n                                } else if (storedRoomCode != null && storedUsername != null) {\n                                    // Host session expired - would need to create new room\n                                    log(\n                                        LogLevel.WARNING,\n                                        \"Host session expired\",\n                                        \"Room: $storedRoomCode - manual intervention may be needed\",\n                                    )\n                                    clearPersistedSession()\n                                    sessionToken = null\n                                } else {\n                                    clearPersistedSession()\n                                    sessionToken = null\n                                }\n                            }\n\n                            else -> {}\n                        }\n\n                        scope.launch { _events.emit(ListenTogetherEvent.ServerError(payload.code, payload.message)) }\n                    }\n\n                    MessageTypes.PONG -> {\n                        log(LogLevel.DEBUG, \"Pong received\")\n                    }\n\n                    MessageTypes.RECONNECTED -> {\n                        val payload = codec.decodePayload(msgType, payloadBytes) as? ReconnectedPayload ?: return\n                        _userId.value = payload.userId\n                        _role.value = if (payload.isHost) RoomRole.HOST else RoomRole.GUEST\n                        _roomState.value = payload.state\n\n                        // Update persisted session info\n                        wasHost = payload.isHost\n                        sessionStartTime = System.currentTimeMillis()\n                        savePersistedSession()\n\n                        // Reset reconnection attempts on successful reconnection\n                        reconnectAttempts = 0\n\n                        acquireWakeLock() // Re-acquire wake lock after reconnection\n                        log(\n                            LogLevel.INFO,\n                            \"Successfully reconnected to room\",\n                            \"Code: ${payload.roomCode}, isHost: ${payload.isHost}, attempt was $reconnectAttempts\",\n                        )\n                        scope.launch {\n                            _events.emit(\n                                ListenTogetherEvent.Reconnected(payload.roomCode, payload.userId, payload.state, payload.isHost),\n                            )\n                        }\n                    }\n\n                    MessageTypes.USER_RECONNECTED -> {\n                        val payload = codec.decodePayload(msgType, payloadBytes) as? UserReconnectedPayload ?: return\n                        // Mark user as connected in the room state\n                        _roomState.value =\n                            _roomState.value?.copy(\n                                users =\n                                    _roomState.value!!.users.map { user ->\n                                        if (user.userId == payload.userId) user.copy(isConnected = true) else user\n                                    },\n                            )\n                        log(LogLevel.INFO, \"User reconnected\", payload.username)\n                        scope.launch { _events.emit(ListenTogetherEvent.UserReconnected(payload.userId, payload.username)) }\n                    }\n\n                    MessageTypes.USER_DISCONNECTED -> {\n                        val payload = codec.decodePayload(msgType, payloadBytes) as? UserDisconnectedPayload ?: return\n                        // Mark user as disconnected in the room state\n                        _roomState.value =\n                            _roomState.value?.copy(\n                                users =\n                                    _roomState.value!!.users.map { user ->\n                                        if (user.userId == payload.userId) user.copy(isConnected = false) else user\n                                    },\n                            )\n                        log(LogLevel.INFO, \"User temporarily disconnected\", payload.username)\n                        scope.launch { _events.emit(ListenTogetherEvent.UserDisconnected(payload.userId, payload.username)) }\n                    }\n\n                    else -> {\n                        log(LogLevel.WARNING, \"Unknown message type\", msgType)\n                    }\n                }\n            } catch (e: Exception) {\n                log(LogLevel.ERROR, \"Error parsing message\", e.message)\n            }\n        }\n\n        private inline fun <reified T> sendMessage(\n            type: String,\n            payload: T?,\n        ) {\n            try {\n                val data = codec.encode(type, payload)\n                log(LogLevel.DEBUG, \"Sending message\", \"$type (protobuf)\")\n\n                val success = webSocket?.send(okio.ByteString.of(*data)) ?: false\n                if (!success) {\n                    log(LogLevel.ERROR, \"Failed to send message\", type)\n                }\n            } catch (e: Exception) {\n                log(LogLevel.ERROR, \"Error encoding message\", \"$type: ${e.message}\")\n            }\n        }\n\n        private fun sendMessageNoPayload(type: String) {\n            sendMessage<Unit>(type, null)\n        }\n\n        // Public API methods\n\n        /**\n         * Create a new listening room.\n         * If not connected, will queue the action and connect first.\n         */\n        fun createRoom(username: String) {\n            // Clear any existing session to ensure we create a new room instead of reconnecting\n            clearPersistedSession()\n            sessionToken = null\n            storedRoomCode = null\n            wasHost = false\n\n            storedUsername = username\n\n            if (_connectionState.value == ConnectionState.CONNECTED) {\n                sendMessage(MessageTypes.CREATE_ROOM, CreateRoomPayload(username))\n            } else {\n                log(LogLevel.INFO, \"Not connected, queueing create room action\")\n                pendingAction = PendingAction.CreateRoom(username)\n                if (_connectionState.value == ConnectionState.DISCONNECTED ||\n                    _connectionState.value == ConnectionState.ERROR\n                ) {\n                    connect()\n                }\n                // If CONNECTING or RECONNECTING, the action will be executed when connected\n            }\n        }\n\n        /**\n         * Join an existing room.\n         * If not connected, will queue the action and connect first.\n         */\n        fun joinRoom(\n            roomCode: String,\n            username: String,\n        ) {\n            // Clear any existing session to ensure we join the new room instead of reconnecting\n            clearPersistedSession()\n            sessionToken = null\n            storedRoomCode = null\n            wasHost = false\n\n            storedUsername = username\n\n            if (_connectionState.value == ConnectionState.CONNECTED) {\n                sendMessage(MessageTypes.JOIN_ROOM, JoinRoomPayload(roomCode.uppercase(), username))\n            } else {\n                log(LogLevel.INFO, \"Not connected, queueing join room action\")\n                pendingAction = PendingAction.JoinRoom(roomCode, username)\n                if (_connectionState.value == ConnectionState.DISCONNECTED ||\n                    _connectionState.value == ConnectionState.ERROR\n                ) {\n                    connect()\n                }\n                // If CONNECTING or RECONNECTING, the action will be executed when connected\n            }\n        }\n\n        /**\n         * Leave the current room\n         */\n        fun leaveRoom() {\n            sendMessageNoPayload(MessageTypes.LEAVE_ROOM)\n\n            // Clear session info on intentional leave\n            sessionToken = null\n            storedRoomCode = null\n            storedUsername = null\n            pendingAction = null\n            _roomState.value = null\n            _role.value = RoomRole.NONE\n            _userId.value = null\n            _pendingJoinRequests.value = emptyList()\n            _bufferingUsers.value = emptyList()\n\n            // Clear from persistent storage\n            clearPersistedSession()\n\n            releaseWakeLock()\n        }\n\n        /**\n         * Approve a join request (host only)\n         */\n        fun approveJoin(userId: String) {\n            if (_role.value != RoomRole.HOST) {\n                log(LogLevel.ERROR, \"Cannot approve join\", \"Not host\")\n                return\n            }\n            sendMessage(MessageTypes.APPROVE_JOIN, ApproveJoinPayload(userId))\n\n            // Dismiss notification immediately when approved from UI\n            joinRequestNotifications.remove(userId)?.let { notifId ->\n                NotificationManagerCompat.from(context).cancel(notifId)\n            }\n        }\n\n        /**\n         * Reject a join request (host only)\n         */\n        fun rejectJoin(\n            userId: String,\n            reason: String? = null,\n        ) {\n            if (_role.value != RoomRole.HOST) {\n                log(LogLevel.ERROR, \"Cannot reject join\", \"Not host\")\n                return\n            }\n            sendMessage(MessageTypes.REJECT_JOIN, RejectJoinPayload(userId, reason))\n            _pendingJoinRequests.value = _pendingJoinRequests.value.filter { it.userId != userId }\n\n            // Dismiss notification immediately when rejected from UI\n            joinRequestNotifications.remove(userId)?.let { notifId ->\n                NotificationManagerCompat.from(context).cancel(notifId)\n            }\n        }\n\n        /**\n         * Kick a user from the room (host only)\n         */\n        fun kickUser(\n            userId: String,\n            reason: String? = null,\n        ) {\n            if (_role.value != RoomRole.HOST) {\n                log(LogLevel.ERROR, \"Cannot kick user\", \"Not host\")\n                return\n            }\n            sendMessage(MessageTypes.KICK_USER, KickUserPayload(userId, reason))\n        }\n\n        /**\n         * Transfer host role to another user (host only)\n         */\n        fun transferHost(newHostId: String) {\n            if (_role.value != RoomRole.HOST) {\n                log(LogLevel.ERROR, \"Cannot transfer host\", \"Not host\")\n                return\n            }\n            sendMessage(MessageTypes.TRANSFER_HOST, TransferHostPayload(newHostId))\n        }\n\n        /**\n         * Send a playback action (host only)\n         */\n        fun sendPlaybackAction(\n            action: String,\n            trackId: String? = null,\n            position: Long? = null,\n            trackInfo: TrackInfo? = null,\n            insertNext: Boolean? = null,\n            queue: List<TrackInfo>? = null,\n            queueTitle: String? = null,\n            volume: Float? = null,\n        ) {\n            if (_role.value != RoomRole.HOST) {\n                log(LogLevel.ERROR, \"Cannot control playback\", \"Not host\")\n                return\n            }\n            sendMessage(\n                MessageTypes.PLAYBACK_ACTION,\n                PlaybackActionPayload(action, trackId, position, trackInfo, insertNext, queue, queueTitle, volume),\n            )\n        }\n\n        /**\n         * Signal that buffering is complete for the current track\n         */\n        fun sendBufferReady(trackId: String) {\n            sendMessage(MessageTypes.BUFFER_READY, BufferReadyPayload(trackId))\n        }\n\n        /**\n         * Suggest a track to the host (guest only)\n         */\n        fun suggestTrack(trackInfo: TrackInfo) {\n            if (!isInRoom) {\n                log(LogLevel.ERROR, \"Cannot suggest track\", \"Not in room\")\n                return\n            }\n            if (_role.value == RoomRole.HOST) {\n                log(LogLevel.WARNING, \"Host should not suggest tracks\")\n                return\n            }\n            sendMessage(MessageTypes.SUGGEST_TRACK, SuggestTrackPayload(trackInfo))\n            scope.launch(Dispatchers.Main) {\n                Toast.makeText(context, context.getString(R.string.listen_together_suggestion_sent), Toast.LENGTH_SHORT).show()\n            }\n        }\n\n        /**\n         * Approve a suggestion (host only)\n         */\n        fun approveSuggestion(suggestionId: String) {\n            if (_role.value != RoomRole.HOST) {\n                log(LogLevel.ERROR, \"Cannot approve suggestion\", \"Not host\")\n                return\n            }\n            sendMessage(MessageTypes.APPROVE_SUGGESTION, ApproveSuggestionPayload(suggestionId))\n            // Remove locally from pending list\n            _pendingSuggestions.value = _pendingSuggestions.value.filter { it.suggestionId != suggestionId }\n\n            // Dismiss notification immediately when approved from UI\n            suggestionNotifications.remove(suggestionId)?.let { notifId ->\n                NotificationManagerCompat.from(context).cancel(notifId)\n            }\n        }\n\n        /**\n         * Reject a suggestion (host only)\n         */\n        fun rejectSuggestion(\n            suggestionId: String,\n            reason: String? = null,\n        ) {\n            if (_role.value != RoomRole.HOST) {\n                log(LogLevel.ERROR, \"Cannot reject suggestion\", \"Not host\")\n                return\n            }\n            sendMessage(MessageTypes.REJECT_SUGGESTION, RejectSuggestionPayload(suggestionId, reason))\n            _pendingSuggestions.value = _pendingSuggestions.value.filter { it.suggestionId != suggestionId }\n\n            // Dismiss notification immediately when rejected from UI\n            suggestionNotifications.remove(suggestionId)?.let { notifId ->\n                NotificationManagerCompat.from(context).cancel(notifId)\n            }\n        }\n\n        /**\n         * Request current playback state from server (for guest re-sync)\n         */\n        fun requestSync() {\n            if (_roomState.value == null) {\n                log(LogLevel.ERROR, \"Cannot request sync\", \"Not in room\")\n                return\n            }\n            log(LogLevel.INFO, \"Requesting sync state from server\")\n            sendMessageNoPayload(MessageTypes.REQUEST_SYNC)\n        }\n\n        /**\n         * Block a user permanently (internal list). Prevents their join requests and suggestions from appearing.\n         */\n        fun blockUser(username: String) {\n            val updated = _blockedUsernames.value.toMutableSet()\n            updated.add(username)\n            _blockedUsernames.value = updated\n\n            // Filter out blocked users from pending requests and suggestions\n            _pendingJoinRequests.value =\n                _pendingJoinRequests.value\n                    .filter { it.username !in _blockedUsernames.value }\n            _pendingSuggestions.value =\n                _pendingSuggestions.value\n                    .filter { it.fromUsername !in _blockedUsernames.value }\n\n            // Save to storage\n            scope.launch {\n                saveBlockedUsernames()\n            }\n\n            log(LogLevel.INFO, \"User blocked\", username)\n        }\n\n        /**\n         * Unblock a previously blocked user\n         */\n        fun unblockUser(username: String) {\n            val updated = _blockedUsernames.value.toMutableSet()\n            updated.remove(username)\n            _blockedUsernames.value = updated\n\n            // Save to storage\n            scope.launch {\n                saveBlockedUsernames()\n            }\n\n            log(LogLevel.INFO, \"User unblocked\", username)\n        }\n\n        /**\n         * Check if a user is blocked\n         */\n        fun isUserBlocked(username: String): Boolean = username in _blockedUsernames.value\n\n        /**\n         * Check if currently in a room\n         */\n        val isInRoom: Boolean\n            get() = _roomState.value != null\n\n        /**\n         * Check if current user is host\n         */\n        val isHost: Boolean\n            get() = _role.value == RoomRole.HOST\n\n        /**\n         * Force reconnection to server (useful for manual recovery)\n         */\n        fun forceReconnect() {\n            log(LogLevel.INFO, \"Forcing reconnection to server\")\n            reconnectAttempts = 0 // Reset attempts to retry from start\n\n            if (webSocket != null) {\n                try {\n                    webSocket?.close(1000, \"Forcing reconnection\")\n                } catch (e: Exception) {\n                    log(LogLevel.DEBUG, \"Error closing WebSocket\", e.message)\n                }\n                webSocket = null\n            }\n\n            _connectionState.value = ConnectionState.DISCONNECTED\n\n            // Attempt connection with reset backoff\n            scope.launch {\n                delay(500)\n                connect()\n            }\n        }\n\n        /**\n         * Check if there's a persisted session available for recovery\n         */\n        val hasPersistedSession: Boolean\n            get() = sessionToken != null && storedRoomCode != null\n\n        /**\n         * Get the persisted room code if available\n         */\n        fun getPersistedRoomCode(): String? = storedRoomCode\n\n        /**\n         * Get current session age in milliseconds\n         */\n        fun getSessionAge(): Long =\n            if (sessionStartTime > 0) {\n                System.currentTimeMillis() - sessionStartTime\n            } else {\n                0L\n            }\n    }\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/listentogether/ListenTogetherManager.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.listentogether\n\nimport android.content.Context\nimport androidx.media3.common.MediaItem\nimport androidx.media3.common.Player\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.WatchEndpoint\nimport com.metrolist.music.constants.ListenTogetherSyncVolumeKey\nimport com.metrolist.music.extensions.currentMetadata\nimport com.metrolist.music.extensions.metadata\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.models.MediaMetadata\nimport com.metrolist.music.models.MediaMetadata.Album\nimport com.metrolist.music.models.MediaMetadata.Artist\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.playback.PlayerConnection\nimport com.metrolist.music.playback.queues.YouTubeQueue\nimport com.metrolist.music.utils.dataStore\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.launch\nimport timber.log.Timber\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n/**\n * Manager that bridges the Listen Together WebSocket client with the music player.\n * Handles syncing playback actions between connected users.\n */\n@Singleton\nclass ListenTogetherManager @Inject constructor(\n    private val client: ListenTogetherClient,\n    @ApplicationContext private val context: Context\n) {\n    companion object {\n        private const val TAG = \"ListenTogetherManager\"\n        // Debounce threshold for playback syncs - prevents excessive seeking/pausing\n        // Increased from 200ms to 1000ms to reduce choppy audio for guests\n        private const val SYNC_DEBOUNCE_THRESHOLD_MS = 1000L\n        // Position tolerance - only seek if difference exceeds this (prevents micro-adjustments)\n        // Increased from 500ms to 2000ms to reduce unnecessary seeks that interrupt playback\n        private const val POSITION_TOLERANCE_MS = 2000L\n        // Large position tolerance - only seek during playback if difference exceeds this\n        // This prevents interrupting active playback for small drifts\n        private const val PLAYBACK_POSITION_TOLERANCE_MS = 3000L\n    }\n\n    private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())\n\n    init {\n        observePreferences()\n    }\n    \n    private var playerConnection: PlayerConnection? = null\n    private var eventCollectorJob: Job? = null\n    private var queueObserverJob: Job? = null\n    private var volumeObserverJob: Job? = null\n    private var playerListenerRegistered = false\n\n    private val syncHostVolumeEnabled = MutableStateFlow(true)\n    private var lastSyncedVolume: Float? = null\n    private var previousMuteState: Boolean? = null\n    private var muteForcedByPreference = false\n\n    private var lastRole: RoomRole = RoomRole.NONE\n    \n    // Whether we're currently syncing (to prevent feedback loops)\n    @Volatile\n    private var isSyncing = false\n    \n    // Track the last state we synced to avoid duplicate events\n    private var lastSyncedIsPlaying: Boolean? = null\n    private var lastSyncedTrackId: String? = null\n    \n    // Track last sync action time for debouncing (prevents excessive seeking/pausing)\n    private var lastSyncActionTime: Long = 0L\n    \n    // Track ID being buffered\n    private var bufferingTrackId: String? = null\n    \n    // Track active sync job to cancel it if a better update arrives\n    private var activeSyncJob: Job? = null\n    \n    // Generation ID for track changes - incremented on each new track change\n    // Used to prevent old coroutines from overwriting newer track loads\n    private var currentTrackGeneration: Int = 0\n\n    // Pending sync to apply after buffering completes for guest\n    private var pendingSyncState: SyncStatePayload? = null\n\n    // Track if a buffer-complete arrived before the pending sync was ready\n    private var bufferCompleteReceivedForTrack: String? = null\n\n    // Expose client state\n    val connectionState = client.connectionState\n    val roomState = client.roomState\n    val role = client.role\n    val userId = client.userId\n    val pendingJoinRequests = client.pendingJoinRequests\n    val bufferingUsers = client.bufferingUsers\n    val logs = client.logs\n    val events = client.events\n    val blockedUsernames = client.blockedUsernames\n    val pendingSuggestions = client.pendingSuggestions\n\n    val isInRoom: Boolean get() = client.isInRoom\n    val isHost: Boolean get() = client.isHost\n    val hasPersistedSession: Boolean get() = client.hasPersistedSession\n    \n    private val playerListener = object : Player.Listener {\n        override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {\n            try {\n                if (isSyncing || !isHost || !isInRoom) return\n                \n                val connection = playerConnection ?: return\n                val player = connection.player\n\n                Timber.tag(TAG).d(\"Play state changed: $playWhenReady (reason: $reason)\")\n                \n                // ALWAYS ensure track is synced before play/pause\n                val currentTrackId = player.currentMediaItem?.mediaId\n                if (currentTrackId != null && currentTrackId != lastSyncedTrackId) {\n                    Timber.tag(TAG)\n                        .d(\"[SYNC] Sending track change before play state: track = $currentTrackId\")\n                    player.currentMetadata?.let { metadata ->\n                        sendTrackChangeInternal(metadata)\n                        lastSyncedTrackId = currentTrackId\n                        // Reset play state since server resets IsPlaying on track change\n                        lastSyncedIsPlaying = false\n                    }\n                    // ALWAYS send play state after track change if host is playing\n                    // Server sets IsPlaying=false on track change, so we must send it\n                    if (playWhenReady) {\n                        Timber.tag(TAG).d(\"[SYNC] Host is playing, sending PLAY after track change\")\n                        lastSyncedIsPlaying = true\n                        val position = player.currentPosition\n                        client.sendPlaybackAction(PlaybackActions.PLAY, position = position)\n                    }\n                    return\n                }\n                \n                // Only send play/pause if track is already synced\n                sendPlayState(playWhenReady, player)\n            } catch (e: Exception) {\n                Timber.tag(TAG).e(e, \"Error in onPlayWhenReadyChanged\")\n            }\n        }\n        \n        private fun sendPlayState(playWhenReady: Boolean, player: Player) {\n            try {\n                val position = player.currentPosition\n                \n                if (playWhenReady) {\n                    Timber.tag(TAG).d(\"Host sending PLAY at position $position\")\n                    client.sendPlaybackAction(PlaybackActions.PLAY, position = position)\n                    lastSyncedIsPlaying = true\n                } else if (!playWhenReady && (lastSyncedIsPlaying == true)) {\n                    Timber.tag(TAG).d(\"Host sending PAUSE at position $position\")\n                    client.sendPlaybackAction(PlaybackActions.PAUSE, position = position)\n                    lastSyncedIsPlaying = false\n                }\n            } catch (e: Exception) {\n                Timber.tag(TAG).e(e, \"Error in sendPlayState\")\n            }\n        }\n        \n        override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {\n            try {\n                if (isSyncing || !isHost || !isInRoom) return\n                if (mediaItem == null) return\n                \n                val connection = playerConnection ?: return\n                val player = connection.player\n                \n                val trackId = mediaItem.mediaId\n                if (trackId == lastSyncedTrackId) return\n                \n                lastSyncedTrackId = trackId\n                // Reset play state tracking since server resets IsPlaying on track change\n                lastSyncedIsPlaying = false\n                \n                // Get metadata and send track change\n                player.currentMetadata?.let { metadata ->\n                    Timber.tag(TAG).d(\"Host sending track change: ${metadata.title}\")\n                    sendTrackChange(metadata)\n                    \n                    // ALWAYS send PLAY after track change if host is currently playing\n                    // Server sets IsPlaying=false on track change, so we must re-send it\n                    val isPlaying = player.playWhenReady\n                    if (isPlaying) {\n                        Timber.tag(TAG).d(\"Host is playing during track change, sending PLAY\")\n                        lastSyncedIsPlaying = true\n                        val position = player.currentPosition\n                        client.sendPlaybackAction(PlaybackActions.PLAY, position = position)\n                    }\n                }\n            } catch (e: Exception) {\n                Timber.tag(TAG).e(e, \"Error in onMediaItemTransition\")\n            }\n        }\n        \n        override fun onPositionDiscontinuity(\n            oldPosition: Player.PositionInfo,\n            newPosition: Player.PositionInfo,\n            reason: Int\n        ) {\n            try {\n                if (isSyncing || !isHost || !isInRoom) return\n                \n                // Only send seek if it was a user-initiated seek\n                if (reason == Player.DISCONTINUITY_REASON_SEEK) {\n                    Timber.tag(TAG).d(\"Host sending SEEK to ${newPosition.positionMs}\")\n                    client.sendPlaybackAction(PlaybackActions.SEEK, position = newPosition.positionMs)\n                }\n            } catch (e: Exception) {\n                Timber.tag(TAG).e(e, \"Error in onPositionDiscontinuity\")\n            }\n        }\n    }\n\n    /**\n     * Set the player connection for playback sync.\n     * Should be called when PlayerConnection is available.\n     */\n    fun setPlayerConnection(connection: PlayerConnection?) {\n        Timber.tag(TAG).d(\"setPlayerConnection: ${connection != null}, isInRoom: $isInRoom\")\n        \n        try {\n            // Remove old listener and callback safely\n            val oldConnection = playerConnection\n            if (playerListenerRegistered && oldConnection != null) {\n                try {\n                    oldConnection.player.removeListener(playerListener)\n                } catch (e: Exception) {\n                    Timber.tag(TAG).e(e, \"Error removing old player listener\")\n                }\n                playerListenerRegistered = false\n            }\n            oldConnection?.shouldBlockPlaybackChanges = null\n            oldConnection?.onSkipPrevious = null\n            oldConnection?.onSkipNext = null\n            oldConnection?.onRestartSong = null\n            \n            playerConnection = connection\n            \n            // Set up playback blocking for guests\n            connection?.shouldBlockPlaybackChanges = {\n                // Block if we're in a room as a guest (not host)\n                isInRoom && !isHost\n            }\n            \n            // Add listener if in room\n            if (connection != null && isInRoom) {\n                try {\n                    connection.player.addListener(playerListener)\n                    playerListenerRegistered = true\n                    Timber.tag(TAG).d(\"Added player listener for room sync\")\n                } catch (e: Exception) {\n                    Timber.tag(TAG).e(e, \"Failed to add player listener\")\n                    playerListenerRegistered = false\n                }\n                \n                // Hook up skip actions\n                connection.onSkipPrevious = {\n                    try {\n                        if (isHost && !isSyncing) {\n                            Timber.tag(TAG).d(\"Host Skip Previous triggered\")\n                            client.sendPlaybackAction(PlaybackActions.SKIP_PREV)\n                        }\n                    } catch (e: Exception) {\n                        Timber.tag(TAG).e(e, \"Error in onSkipPrevious\")\n                    }\n                }\n                connection.onSkipNext = {\n                try {\n                        if (isHost && !isSyncing) {\n                            Timber.tag(TAG).d(\"Host Skip Next triggered\")\n                            client.sendPlaybackAction(PlaybackActions.SKIP_NEXT)\n                        }\n                    } catch (e: Exception) {\n                        Timber.tag(TAG).e(e, \"Error in onSkipNext\")\n                    }\n                }\n                \n                // Hook up restart action\n                connection.onRestartSong = {\n                    try {\n                        if (isHost && !isSyncing) {\n                            Timber.tag(TAG).d(\"Host Restart Song triggered (sending 1ms as 0ms workaround)\")\n                            client.sendPlaybackAction(PlaybackActions.SEEK, position = 1L)\n                        }\n                    } catch (e: Exception) {\n                        Timber.tag(TAG).e(e, \"Error in onRestartSong\")\n                    }\n                }\n            }\n\n            // Start/stop queue observation based on role\n            if (connection != null && isInRoom && isHost) {\n                startQueueSyncObservation()\n                startHeartbeat()\n                startVolumeSyncObservation()\n            } else {\n                stopQueueSyncObservation()\n                stopHeartbeat()\n                stopVolumeSyncObservation()\n            }\n            updateGuestMuteState()\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Error in setPlayerConnection\")\n        }\n    }\n\n    private fun observePreferences() {\n        scope.launch {\n            context.dataStore.data\n                .map { it[ListenTogetherSyncVolumeKey] ?: true }\n                .distinctUntilChanged()\n                .collect { enabled ->\n                    syncHostVolumeEnabled.value = enabled\n                }\n        }\n    }\n\n    /**\n    * Initialize event collection. Should be called once at app start.\n     */\n    fun initialize() {\n        Timber.tag(TAG).d(\"Initializing ListenTogetherManager\")\n        eventCollectorJob?.cancel()\n        eventCollectorJob = scope.launch {\n            client.events.collect { event ->\n                try {\n                    Timber.tag(TAG).d(\"Received event: $event\")\n                    handleEvent(event)\n                } catch (e: Exception) {\n                    Timber.tag(TAG).e(e, \"Error handling event: $event\")\n                }\n            }\n        }\n        \n        // Role change listener\n        scope.launch {\n            role.collect { newRole ->\n                try {\n                    val previousRole = lastRole\n                    lastRole = newRole\n\n                    val wasHost = previousRole == RoomRole.HOST\n                    if (newRole == RoomRole.HOST && !wasHost) {\n                        val connection = playerConnection\n                        if (connection != null) {\n                            Timber.tag(TAG).d(\"Role changed to HOST, starting sync services\")\n                            startQueueSyncObservation()\n                            startHeartbeat()\n                            startVolumeSyncObservation()\n                            // Re-register listener if needed\n                            if (!playerListenerRegistered) {\n                                try {\n                                    connection.player.addListener(playerListener)\n                                    playerListenerRegistered = true\n                                } catch (e: Exception) {\n                                    Timber.tag(TAG).e(e, \"Failed to add player listener on role change\")\n                                }\n                            }\n                        }\n                    } else if (newRole != RoomRole.HOST && wasHost) {\n                        Timber.tag(TAG).d(\"Role changed from HOST, stopping sync services\")\n                        stopQueueSyncObservation()\n                        stopHeartbeat()\n                        stopVolumeSyncObservation()\n                    }\n                    updateGuestMuteState()\n                } catch (e: Exception) {\n                    Timber.tag(TAG).e(e, \"Error in role change handler\")\n                }\n            }\n        }\n    }\n\n    private fun handleEvent(event: ListenTogetherEvent) {\n        when (event) {\n            is ListenTogetherEvent.Connected -> {\n                Timber.tag(TAG).d(\"Connected to server with userId: ${event.userId}\")\n            }\n            \n            is ListenTogetherEvent.RoomCreated -> {\n                Timber.tag(TAG).d(\"Room created: ${event.roomCode}\")\n                try {\n                    // Register player listener for host\n                    val connection = playerConnection\n                    val player = connection?.player\n                    if (player != null && !playerListenerRegistered) {\n                        try {\n                            player.addListener(playerListener)\n                            playerListenerRegistered = true\n                            Timber.tag(TAG).d(\"Added player listener as host\")\n                        } catch (e: Exception) {\n                            Timber.tag(TAG).e(e, \"Failed to add player listener on room create\")\n                        }\n                    }\n                    // Initialize sync state\n                    lastSyncedIsPlaying = player?.playWhenReady\n                    lastSyncedTrackId = player?.currentMediaItem?.mediaId\n\n                    // If there's already a track loaded, send it to the server\n                    player?.currentMetadata?.let { metadata ->\n                        Timber.tag(TAG).d(\"Room created with existing track: ${metadata.title}\")\n                        // Send track change so server has the current track info\n                        sendTrackChangeInternal(metadata)\n                        // If host is already playing, immediately send PLAY with current position\n                        val isPlaying = player.playWhenReady\n                        if (isPlaying) {\n                            lastSyncedIsPlaying = true\n                            val position = player.currentPosition\n                            Timber.tag(TAG).d(\"Host already playing on room create, sending PLAY at $position\")\n                            client.sendPlaybackAction(PlaybackActions.PLAY, position = position)\n                        }\n                    }\n                    startQueueSyncObservation()\n                    startHeartbeat()\n                    startVolumeSyncObservation()\n                } catch (e: Exception) {\n                    Timber.tag(TAG).e(e, \"Error handling RoomCreated event\")\n                }\n            }\n            \n            is ListenTogetherEvent.JoinApproved -> {\n                Timber.tag(TAG).d(\"Join approved for room: ${event.roomCode}\")\n                // Save current mute state before joining as guest so we can restore it on leave\n                saveMuteStateOnJoin()\n                // Apply the full initial state including queue\n                applyPlaybackState(\n                    currentTrack = event.state.currentTrack,\n                    isPlaying = event.state.isPlaying,\n                    position = event.state.position,\n                    queue = event.state.queue\n                    // bypassBuffer=false (default) for initial join buffer sync\n                )\n                applyHostVolumeIfNeeded(event.state.volume)\n                updateGuestMuteState()\n            }\n            \n            is ListenTogetherEvent.PlaybackSync -> {\n                Timber.tag(TAG).d(\"PlaybackSync received: ${event.action.action}\")\n                // Guests handle all sync actions. Host should also apply queue ops.\n                val actionType = event.action.action\n                val isQueueOp = actionType == PlaybackActions.QUEUE_ADD ||\n                        actionType == PlaybackActions.QUEUE_REMOVE ||\n                        actionType == PlaybackActions.QUEUE_CLEAR\n                if (!isHost || isQueueOp) {\n                    handlePlaybackSync(event.action)\n                }\n            }\n            \n            is ListenTogetherEvent.UserJoined -> {\n                Timber.tag(TAG).d(\"[SYNC] User joined: ${event.username}\")\n                // When a new user joins, host should send current track immediately\n                if (isHost) {\n                    try {\n                        val connection = playerConnection\n                        val player = connection?.player\n                        player?.currentMetadata?.let { metadata ->\n                            Timber.tag(TAG).d(\"[SYNC] Sending current track to newly joined user: ${metadata.title}\")\n                            sendTrackChangeInternal(metadata)\n                            // If host is currently playing, also send PLAY with current position so the guest jumps to the live position\n                            if (player.playWhenReady) {\n                                val pos = player.currentPosition\n                                Timber.tag(TAG).d(\"[SYNC] Host playing, sending PLAY at $pos for new joiner\")\n                                client.sendPlaybackAction(PlaybackActions.PLAY, position = pos)\n                            }\n                            // Don't send play state - let buffering complete first\n                        }\n                    } catch (e: Exception) {\n                        Timber.tag(TAG).e(e, \"Error handling UserJoined event\")\n                    }\n                }\n            }\n\n            is ListenTogetherEvent.BufferWait -> {\n                Timber.tag(TAG).d(\"BufferWait: waiting for ${event.waitingFor.size} users\")\n            }\n            \n            is ListenTogetherEvent.BufferComplete -> {\n                Timber.tag(TAG).d(\"BufferComplete for track: ${event.trackId}\")\n                if (!isHost && bufferingTrackId == event.trackId) {\n                    bufferCompleteReceivedForTrack = event.trackId\n                    applyPendingSyncIfReady()\n                }\n            }\n            \n            is ListenTogetherEvent.SyncStateReceived -> {\n                Timber.tag(TAG).d(\"SyncStateReceived: playing=${event.state.isPlaying}, pos=${event.state.position}, track=${event.state.currentTrack?.id}\")\n                if (!isHost) {\n                    handleSyncState(event.state)\n                }\n            }\n            \n            is ListenTogetherEvent.Kicked -> {\n                Timber.tag(TAG).d(\"Kicked from room: ${event.reason}\")\n                cleanup()\n            }\n            \n            is ListenTogetherEvent.Disconnected -> {\n                Timber.tag(TAG).d(\"Disconnected from server\")\n                // Don't cleanup on disconnect - we might reconnect\n                // cleanup() is called when leaving room intentionally or when kicked\n            }\n\n            is ListenTogetherEvent.Reconnecting -> {\n                Timber.tag(TAG).d(\"Reconnecting: attempt ${event.attempt}/${event.maxAttempts}\")\n            }\n            \n            is ListenTogetherEvent.Reconnected -> {\n                Timber.tag(TAG).d(\"Reconnected to room: ${event.roomCode}, isHost: ${event.isHost}\")\n                try {\n                    // Re-register player listener\n                    val connection = playerConnection\n                    val player = connection?.player\n                    if (player != null && !playerListenerRegistered) {\n                        try {\n                            player.addListener(playerListener)\n                            playerListenerRegistered = true\n                            Timber.tag(TAG).d(\"Re-added player listener after reconnect\")\n                        } catch (e: Exception) {\n                            Timber.tag(TAG).e(e, \"Failed to re-add player listener after reconnect\")\n                        }\n                    }\n                    \n                    // Sync state based on role\n                    if (event.isHost) {\n                        // Host: only send sync if necessary\n                        lastSyncedIsPlaying = player?.playWhenReady\n                        lastSyncedTrackId = player?.currentMediaItem?.mediaId\n                        \n                        val currentMetadata = player?.currentMetadata\n                        if (currentMetadata != null) {\n                            // Check if server already has the right track (from event.state)\n                            val serverTrackId = event.state.currentTrack?.id\n                            if (serverTrackId != currentMetadata.id) {\n                                Timber.tag(TAG).d(\"Reconnected as host, server track ($serverTrackId) differs from local (${currentMetadata.id}), syncing\")\n                                sendTrackChangeInternal(currentMetadata)\n                            } else {\n                                Timber.tag(TAG).d(\"Reconnected as host, server already has current track $serverTrackId\")\n                            }\n                            \n                            // Small delay before sending play state to let connection stabilize\n                            scope.launch {\n                                delay(500)\n                                try {\n                                    val currentPlayer = playerConnection?.player\n                                    if (currentPlayer?.playWhenReady == true) {\n                                        val pos = currentPlayer.currentPosition\n                                        Timber.tag(TAG)\n                                            .d(\"Reconnected host is playing, sending PLAY at $pos\")\n                                        client.sendPlaybackAction(PlaybackActions.PLAY, position = pos)\n                                    }\n                                } catch (e: Exception) {\n                                    Timber.tag(TAG).e(e, \"Error sending play state after reconnect\")\n                                }\n                            }\n                        }\n                    } else {\n                        // Guest: ALWAYS sync to host's state after reconnection\n                        Timber.tag(TAG).d(\"Reconnected as guest, syncing to host's current state\")\n                        applyPlaybackState(\n                            currentTrack = event.state.currentTrack,\n                            isPlaying = event.state.isPlaying,\n                            position = event.state.position,\n                            queue = event.state.queue,\n                            bypassBuffer = true  // Reconnect: bypass buffer protocol\n                        )\n                        applyHostVolumeIfNeeded(event.state.volume)\n                        \n                        // Immediately request fresh sync after a short delay to catch live position\n                        scope.launch {\n                        delay(1000)\n                            if (isInRoom && !isHost) {\n                                Timber.tag(TAG).d(\"Requesting fresh sync after reconnect\")\n                                requestSync()\n                            }\n                        }\n                    }\n                } catch (e: Exception) {\n                    Timber.tag(TAG).e(e, \"Error handling Reconnected event\")\n                }\n            }\n            \n            is ListenTogetherEvent.UserReconnected -> {\n                Timber.tag(TAG).d(\"User reconnected: ${event.username}\")\n                // No action needed - reconnected user already synced via reconnect state\n            }\n            \n            is ListenTogetherEvent.UserDisconnected -> {\n                Timber.tag(TAG).d(\"User temporarily disconnected: ${event.username}\")\n                // User might reconnect, no action needed\n            }\n\n            is ListenTogetherEvent.HostChanged -> {\n                Timber.tag(TAG).d(\"Host changed: new host is ${event.newHostName} (${event.newHostId})\")\n                val wasHost = isHost\n                val nowIsHost = event.newHostId == userId.value\n                \n                if (wasHost && !nowIsHost) {\n                    // Lost host role\n                    Timber.tag(TAG).d(\"Local user lost host role\")\n                    stopQueueSyncObservation()\n                    stopVolumeSyncObservation()\n                    if (playerListenerRegistered) {\n                        playerConnection?.player?.removeListener(playerListener)\n                        playerListenerRegistered = false\n                    }\n                    // Restore guest mute state since we're now a guest\n                    updateGuestMuteState()\n                } else if (!wasHost && nowIsHost) {\n                    // Gained host role\n                    Timber.tag(TAG).d(\"Local user gained host role\")\n                    updateGuestMuteState() // This will restore mute state since we're now host\n\n                    // Register player listener\n                    val connection = playerConnection\n                    val player = connection?.player\n                    if (player != null && !playerListenerRegistered) {\n                        try {\n                            player.addListener(playerListener)\n                            playerListenerRegistered = true\n                            Timber.tag(TAG).d(\"Added player listener as new host\")\n                        } catch (e: Exception) {\n                            Timber.tag(TAG).e(e, \"Failed to add player listener on host transfer\")\n                        }\n                    }\n\n                    // Start the queue and volume sync observations now that we're host\n                    startQueueSyncObservation()\n                    startVolumeSyncObservation()\n                    \n                    // Send current player state to guests\n                    val metadata = player?.currentMetadata\n                    if (metadata != null) {\n                        Timber.tag(TAG).d(\"New host sending current track: ${metadata.title}\")\n                        sendTrackChangeInternal(metadata)\n                        \n                        // If currently playing, send play state\n                        if (player.playWhenReady) {\n                            val position = player.currentPosition\n                            Timber.tag(TAG).d(\"New host is playing, sending PLAY at $position\")\n                            client.sendPlaybackAction(PlaybackActions.PLAY, position = position)\n                        }\n                    }\n                }\n            }\n            \n            is ListenTogetherEvent.ConnectionError -> {\n                Timber.tag(TAG).e(\"Connection error: ${event.error}\")\n                cleanup()\n            }\n            \n            else -> { /* Other events handled by UI */ }\n        }\n    }\n    \n    private fun cleanup() {\n        if (lastRole == RoomRole.GUEST) {\n            restoreGuestMuteState()\n        }\n        if (playerListenerRegistered) {\n            playerConnection?.player?.removeListener(playerListener)\n            playerListenerRegistered = false\n        }\n        stopQueueSyncObservation()\n        stopHeartbeat()\n        stopVolumeSyncObservation()\n        // Note: Don't clear shouldBlockPlaybackChanges callback - it checks isInRoom dynamically\n        lastSyncedIsPlaying = null\n        lastSyncedTrackId = null\n        bufferingTrackId = null\n        isSyncing = false\n        bufferCompleteReceivedForTrack = null\n        lastRole = RoomRole.NONE\n        lastSyncActionTime = 0L  // Reset sync debouncing\n        ++currentTrackGeneration  // Increment to invalidate any pending track-change coroutines\n    }\n\n    private fun updateGuestMuteState() {\n        // Guests are no longer forced to mute - they can hear the music too\n        val connection = playerConnection ?: return\n        // Just restore any previously forced mute state (should typically be none)\n        restoreGuestMuteState()\n    }\n    \n    /**\n     * Save the current mute state when joining a room as guest.\n     * This allows us to restore it when leaving the room.\n     */\n    private fun saveMuteStateOnJoin() {\n        val connection = playerConnection ?: return\n        // Only save if we haven't already saved (avoid overwriting on role changes)\n        if (previousMuteState == null) {\n            previousMuteState = connection.isMuted.value\n            Timber.tag(TAG).d(\"Saved mute state on join: ${previousMuteState}\")\n        }\n    }\n\n    /**\n     * Restore the mute state that was saved when joining the room.\n     * This is called when leaving the room to ensure the user's\n     * mute preference is restored to what it was before joining Listen Together.\n     */\n    private fun restoreGuestMuteState() {\n        val connection = playerConnection ?: return\n        val savedState = previousMuteState\n        \n        if (savedState != null) {\n            Timber.tag(TAG).d(\"Restoring mute state on leave: was muted=$savedState, currently muted=${connection.isMuted.value}\")\n            connection.setMuted(savedState)\n        } else {\n            // No saved state means we never properly saved (e.g., player wasn't ready on join)\n            // In this case, if currently muted, unmute as a fallback\n            if (connection.isMuted.value) {\n                Timber.tag(TAG).d(\"No saved mute state on leave, unmuting player as fallback\")\n                connection.setMuted(false)\n            }\n        }\n        \n        previousMuteState = null\n        muteForcedByPreference = false\n    }\n\n    private fun applyHostVolumeIfNeeded(volume: Float?) {\n        if (!syncHostVolumeEnabled.value || isHost || !isInRoom) return\n        val connection = playerConnection ?: return\n        val target = volume?.coerceIn(0f, 1f) ?: return\n        connection.service.playerVolume.value = target\n    }\n\n    private fun applyPendingSyncIfReady() {\n        val pending = pendingSyncState ?: return\n        val pendingTrackId = pending.currentTrack?.id ?: bufferingTrackId ?: return\n        val completeForTrack = bufferCompleteReceivedForTrack\n\n        if (completeForTrack != pendingTrackId) return\n\n        val connection = playerConnection ?: return\n        val player = connection.player\n\n        Timber.tag(TAG).d(\"Applying pending sync: track=$pendingTrackId, pos=${pending.position}, play=${pending.isPlaying}\")\n        isSyncing = true\n\n        val targetPos = pending.position\n        val posDiff = kotlin.math.abs(player.currentPosition - targetPos)\n        val willPlay = pending.isPlaying\n        \n        // Use appropriate tolerance based on whether we're about to play\n        val tolerance = if (willPlay && player.playWhenReady) PLAYBACK_POSITION_TOLERANCE_MS else POSITION_TOLERANCE_MS\n        \n        if (posDiff > tolerance) {\n            Timber.tag(TAG).d(\"Applying pending sync: seeking ${player.currentPosition} -> $targetPos (diff ${posDiff}ms > ${tolerance}ms)\")\n            connection.seekTo(targetPos)\n        } else {\n            Timber.tag(TAG).d(\"Applying pending sync: skipping seek (diff ${posDiff}ms < ${tolerance}ms)\")\n        }\n\n        // Apply play/pause state only if it needs to change\n        if (willPlay && !player.playWhenReady) {\n            Timber.tag(TAG).d(\"Applying pending sync: starting playback\")\n            connection.play()\n        } else if (!willPlay && player.playWhenReady) {\n            Timber.tag(TAG).d(\"Applying pending sync: pausing playback\")\n            connection.pause()\n        }\n\n        scope.launch {\n            delay(200)\n            isSyncing = false\n        }\n\n        bufferingTrackId = null\n        pendingSyncState = null\n        bufferCompleteReceivedForTrack = null\n    }\n\n    private fun handlePlaybackSync(action: PlaybackActionPayload) {\n        val connection = playerConnection\n        if (connection == null) {\n            Timber.tag(TAG).w(\"Cannot sync playback - no player connection\")\n            return\n        }\n        val player = connection.player\n        \n        Timber.tag(TAG).d(\"Handling playback sync: ${action.action}, position: ${action.position}\")\n\n        isSyncing = true\n\n        try {\n            when (action.action) {\n                PlaybackActions.PLAY -> {\n                    val basePos = action.position ?: 0L\n                    val now = System.currentTimeMillis()\n                    val adjustedPos = action.serverTime?.let { serverTime ->\n                        basePos + kotlin.math.max(0L, now - serverTime)\n                    } ?: basePos\n\n                    Timber.tag(TAG).d(\"Guest: PLAY at position $adjustedPos, currently playing=${player.playWhenReady}\")\n\n                    if (bufferingTrackId != null) {\n                        pendingSyncState = (pendingSyncState ?: SyncStatePayload(\n                            currentTrack = roomState.value?.currentTrack,\n                            isPlaying = true,\n                            position = adjustedPos,\n                            lastUpdate = now\n                        )).copy(\n                            isPlaying = true,\n                            position = adjustedPos,\n                            lastUpdate = now\n                        )\n                        applyPendingSyncIfReady()\n                        return\n                    }\n\n                    // Debounce PLAY actions when already playing and in sync\n                    val posDiff = kotlin.math.abs(player.currentPosition - adjustedPos)\n                    val alreadyPlaying = player.playWhenReady\n                    \n                    if (alreadyPlaying && posDiff < POSITION_TOLERANCE_MS && (now - lastSyncActionTime) < SYNC_DEBOUNCE_THRESHOLD_MS) {\n                        Timber.tag(TAG).d(\"Guest: PLAY debounced - already playing and in sync (diff ${posDiff}ms)\")\n                        return\n                    }\n\n                    // CRITICAL: Only seek during active playback if position is VERY far off\n                    // This prevents interrupting the audio for small drifts\n                    if (alreadyPlaying) {\n                        if (posDiff > PLAYBACK_POSITION_TOLERANCE_MS) {\n                            Timber.tag(TAG).d(\"Guest: PLAY seeking during playback ${player.currentPosition} -> $adjustedPos (diff ${posDiff}ms)\")\n                            connection.seekTo(adjustedPos)\n                        } else {\n                            Timber.tag(TAG).d(\"Guest: PLAY skipping seek - already playing, drift acceptable (${posDiff}ms < ${PLAYBACK_POSITION_TOLERANCE_MS}ms)\")\n                        }\n                    } else {\n                        // When paused/stopped, we can seek more aggressively\n                        if (posDiff > POSITION_TOLERANCE_MS) {\n                            Timber.tag(TAG).d(\"Guest: PLAY seeking while paused ${player.currentPosition} -> $adjustedPos (diff ${posDiff}ms)\")\n                            connection.seekTo(adjustedPos)\n                        }\n                        // Start playback\n                        Timber.tag(TAG).d(\"Guest: Starting playback\")\n                        connection.play()\n                    }\n                    lastSyncActionTime = now\n                }\n                \n                PlaybackActions.PAUSE -> {\n                    val pos = action.position ?: 0L\n                    val now = System.currentTimeMillis()\n                    \n                    Timber.tag(TAG).d(\"Guest: PAUSE at position $pos, currently playing=${player.playWhenReady}\")\n\n                    if (bufferingTrackId != null) {\n                        pendingSyncState = (pendingSyncState ?: SyncStatePayload(\n                            currentTrack = roomState.value?.currentTrack,\n                            isPlaying = false,\n                            position = pos,\n                            lastUpdate = now\n                        )).copy(\n                            isPlaying = false,\n                            position = pos,\n                            lastUpdate = now\n                        )\n                        applyPendingSyncIfReady()\n                        return\n                    }\n\n                    // Debounce PAUSE actions when already paused and in sync\n                    val posDiff = kotlin.math.abs(player.currentPosition - pos)\n                    val alreadyPaused = !player.playWhenReady\n                    \n                    if (alreadyPaused && posDiff < POSITION_TOLERANCE_MS && (now - lastSyncActionTime) < SYNC_DEBOUNCE_THRESHOLD_MS) {\n                        Timber.tag(TAG).d(\"Guest: PAUSE debounced - already paused and in sync (diff ${posDiff}ms)\")\n                        return\n                    }\n\n                    // Pause playback first\n                    if (player.playWhenReady) {\n                        Timber.tag(TAG).d(\"Guest: Pausing playback\")\n                        connection.pause()\n                    }\n                    \n                    // Only seek if position difference is significant\n                    if (posDiff > POSITION_TOLERANCE_MS) {\n                        Timber.tag(TAG).d(\"Guest: PAUSE seeking ${player.currentPosition} -> $pos (diff ${posDiff}ms)\")\n                        connection.seekTo(pos)\n                    } else {\n                        Timber.tag(TAG).d(\"Guest: PAUSE skipping seek (diff ${posDiff}ms < ${POSITION_TOLERANCE_MS}ms)\")\n                    }\n                    lastSyncActionTime = now\n                }\n\n                PlaybackActions.SEEK -> {\n                    val pos = action.position ?: 0L\n                    val now = System.currentTimeMillis()\n                    \n                    // Debounce SEEK actions - don't seek if one just happened\n                    if (now - lastSyncActionTime < SYNC_DEBOUNCE_THRESHOLD_MS) {\n                        Timber.tag(TAG).d(\"Guest: SEEK debounced (only ${now - lastSyncActionTime}ms since last sync)\")\n                        return\n                    }\n                    \n                    // Use larger position tolerance\n                    if (kotlin.math.abs(player.currentPosition - pos) > POSITION_TOLERANCE_MS) {\n                        Timber.tag(TAG).d(\"Guest: SEEK to $pos from ${player.currentPosition} (diff > ${POSITION_TOLERANCE_MS}ms)\")\n                        connection.seekTo(pos)\n                        lastSyncActionTime = now\n                    } else {\n                        Timber.tag(TAG).d(\"Guest: SEEK ignored (position diff < ${POSITION_TOLERANCE_MS}ms)\")\n                    }\n                }\n                \n                PlaybackActions.CHANGE_TRACK -> {\n                    action.trackInfo?.let { track ->\n                        Timber.tag(TAG).d(\"Guest: CHANGE_TRACK to ${track.title}, queue size=${action.queue?.size}\")\n                        \n                        // Reset sync debounce timer on track change - this is a fresh sync cycle\n                        lastSyncActionTime = 0L\n                        \n                        // If we have a queue, use it! This is the \"smart\" sync path.\n                        if (action.queue != null && action.queue.isNotEmpty()) {\n                            val queueTitle = action.queueTitle\n                            applyPlaybackState(\n                                currentTrack = track,\n                                isPlaying = false, // Will be updated by subsequent PLAY or pending sync\n                                position = 0,\n                                queue = action.queue,\n                                queueTitle = queueTitle\n                            )\n                        } else {\n                            // Fallback to old behavior (network fetch) if no queue provided\n                            bufferingTrackId = track.id\n                            syncToTrack(track, false, 0)\n                        }\n                    }\n                }\n                \n                PlaybackActions.SKIP_NEXT -> {\n                    Timber.tag(TAG).d(\"Guest: SKIP_NEXT\")\n                    connection.seekToNext()\n                }\n\n                PlaybackActions.SKIP_PREV -> {\n                    Timber.tag(TAG).d(\"Guest: SKIP_PREV\")\n                    connection.seekToPrevious()\n                }\n\n                PlaybackActions.QUEUE_ADD -> {\n                    val track = action.trackInfo\n                    if (track == null) {\n                        Timber.tag(TAG).w(\"QUEUE_ADD missing trackInfo\")\n                    } else {\n                        Timber.tag(TAG).d(\"Guest: QUEUE_ADD ${track.title}, insertNext=${action.insertNext == true}\")\n                        scope.launch(Dispatchers.IO) {\n                            // Fetch MediaItem via YouTube metadata\n                            YouTube.queue(listOf(track.id)).onSuccess { list ->\n                                val mediaItem = list.firstOrNull()?.toMediaMetadata()?.copy(\n                                    suggestedBy = track.suggestedBy\n                                )?.toMediaItem()\n                                if (mediaItem != null) {\n                                    launch(Dispatchers.Main) {\n                                        // Allow internal sync to bypass guest restrictions\n                                        connection.allowInternalSync = true\n                                        if (action.insertNext == true) {\n                                            connection.playNext(mediaItem)\n                                        } else {\n                                            connection.addToQueue(mediaItem)\n                                        }\n                                        connection.allowInternalSync = false\n                                    }\n                                } else {\n                                    Timber.tag(TAG).w(\"QUEUE_ADD failed to resolve media item for ${track.id}\")\n                                }\n                            }.onFailure {\n                                Timber.tag(TAG).e(it, \"QUEUE_ADD metadata fetch failed\")\n                            }\n                        }\n                    }\n                }\n\n                PlaybackActions.QUEUE_REMOVE -> {\n                    val removeId = action.trackId\n                    if (removeId.isNullOrEmpty()) {\n                        Timber.tag(TAG).w(\"QUEUE_REMOVE missing trackId\")\n                    } else {\n                        // Find first queue item with matching mediaId after current index\n                        val startIndex = player.currentMediaItemIndex + 1\n                        var removeIndex = -1\n                        val total = player.mediaItemCount\n                        for (i in startIndex until total) {\n                            val id = player.getMediaItemAt(i).mediaId\n                            if (id == removeId) { removeIndex = i; break }\n                        }\n                        if (removeIndex >= 0) {\n                            Timber.tag(TAG).d(\"Guest: QUEUE_REMOVE index=$removeIndex id=$removeId\")\n                            player.removeMediaItem(removeIndex)\n                        } else {\n                            Timber.tag(TAG).w(\"QUEUE_REMOVE id not found in queue: $removeId\")\n                        }\n                    }\n                }\n\n                PlaybackActions.QUEUE_CLEAR -> {\n                    val currentIndex = player.currentMediaItemIndex\n                    val count = player.mediaItemCount\n                    val itemsAfter = count - (currentIndex + 1)\n                    if (itemsAfter > 0) {\n                        Timber.tag(TAG).d(\"Guest: QUEUE_CLEAR removing $itemsAfter items after current\")\n                        player.removeMediaItems(currentIndex + 1, count - (currentIndex + 1))\n                    }\n                }\n\n                PlaybackActions.SET_VOLUME -> {\n                    applyHostVolumeIfNeeded(action.volume)\n                }\n\n                PlaybackActions.SYNC_QUEUE -> {\n                    val queue = action.queue\n                    val queueTitle = action.queueTitle\n                    if (queue != null) {\n                        Timber.tag(TAG).d(\"Guest: SYNC_QUEUE size=${queue.size}\")\n                        // Cancel any pending \"smart\" sync (e.g. YouTube radio fetch) in favor of this authoritative queue\n                        activeSyncJob?.cancel()\n                        \n                        scope.launch(Dispatchers.Main) {\n                            if (playerConnection !== connection) return@launch\n                            val player = connection.player\n                            \n                            // Map TrackInfo to MediaItems\n                            val mediaItems = queue.map { track ->\n                                track.toMediaMetadata().toMediaItem()\n                            }\n                            \n                            // Try to find current track in new queue to preserve playback state\n                            val currentId = player.currentMediaItem?.mediaId\n                            var newIndex = -1\n                            if (currentId != null) {\n                                newIndex = mediaItems.indexOfFirst { it.mediaId == currentId }\n                            }\n                            \n                            val currentPos = player.currentPosition\n                            val wasPlaying = player.isPlaying\n                            \n                            connection.allowInternalSync = true\n                            if (newIndex != -1) {\n                                player.setMediaItems(mediaItems, newIndex, currentPos)\n                            } else {\n                                player.setMediaItems(mediaItems)\n                            }\n                            connection.allowInternalSync = false\n\n                            // Restore playing state if needed\n                            if (wasPlaying && !player.isPlaying) {\n                                connection.play()\n                            }\n                            \n                            // Sync queue title\n                            try {\n                                connection.service.queueTitle = queueTitle\n                            } catch (e: Exception) {\n                                Timber.tag(TAG).e(e, \"Failed to set queue title during SYNC_QUEUE\")\n                            }\n                        }\n                    }\n                }\n            }\n        } finally {\n            // Minimal delay to prevent feedback loops\n            scope.launch {\n                delay(200)\n                isSyncing = false\n            }\n        }\n    }\n    \n    private fun handleSyncState(state: SyncStatePayload) {\n        Timber.tag(TAG).d(\"handleSyncState: playing=${state.isPlaying}, pos=${state.position}, track=${state.currentTrack?.id}\")\n        applyPlaybackState(\n            currentTrack = state.currentTrack,\n            isPlaying = state.isPlaying,\n            position = state.position,\n            queue = state.queue,\n            bypassBuffer = true  // Manual sync: bypass buffer\n        )\n        applyHostVolumeIfNeeded(state.volume)\n    }\n\n    private fun applyPlaybackState(\n        currentTrack: TrackInfo?,\n        isPlaying: Boolean,\n        position: Long,\n        queue: List<TrackInfo>?,\n        queueTitle: String? = null,  // New param\n        bypassBuffer: Boolean = false\n    ) {\n        val connection = playerConnection\n        if (connection == null) {\n            Timber.tag(TAG).w(\"Cannot apply playback state - no player\")\n            return\n        }\n        val player = connection.player\n\n        Timber.tag(TAG).d(\"Applying playback state: track=${currentTrack?.id}, pos=$position, queue=${queue?.size}, bypassBuffer=$bypassBuffer\")\n\n        // Cancel any pending sync job\n        activeSyncJob?.cancel()\n\n        // If no track, just pause and clear/set queue\n        if (currentTrack == null) {\n            Timber.tag(TAG).d(\"No track in state, pausing\")\n            val generation = ++currentTrackGeneration\n            scope.launch(Dispatchers.Main) {\n                // Verify we're still on the same track generation (no newer track change arrived)\n                if (currentTrackGeneration != generation) {\n                    Timber.tag(TAG).d(\"Skipping stale track generation: $generation vs current $currentTrackGeneration\")\n                    return@launch\n                }\n                \n                if (playerConnection !== connection) return@launch\n                isSyncing = true\n                connection.allowInternalSync = true\n                if (queue != null && queue.isNotEmpty()) {\n                    val mediaItems = queue.map { it.toMediaMetadata().toMediaItem() }\n                    player.setMediaItems(mediaItems)\n                } else if (queue != null) {\n                    player.clearMediaItems()\n                }\n                connection.pause()\n                try {\n                    connection.service.queueTitle = queueTitle\n                } catch (e: Exception) {\n                    Timber.tag(TAG).e(e, \"Failed to set queue title for empty state\")\n                }\n                connection.allowInternalSync = false\n                isSyncing = false\n            }\n            return\n        }\n\n        bufferingTrackId = currentTrack.id\n        val generation = ++currentTrackGeneration\n        \n        scope.launch(Dispatchers.Main) {\n            // Verify we're still on the same track generation (no newer track change arrived)\n            if (currentTrackGeneration != generation) {\n                Timber.tag(TAG).d(\"Skipping stale track generation: $generation vs current $currentTrackGeneration (track ${currentTrack.id})\")\n                return@launch\n            }\n            \n            if (playerConnection !== connection) return@launch\n            isSyncing = true\n            connection.allowInternalSync = true\n\n            try {\n                // Re-verify generation before applying media items (critical section)\n                if (currentTrackGeneration != generation) {\n                    Timber.tag(TAG).d(\"Stale generation detected before setMediaItems: $generation vs $currentTrackGeneration\")\n                    return@launch\n                }\n                \n                // Apply queue/media (same)\n                if (queue != null && queue.isNotEmpty()) {\n                    val mediaItems = queue.map { it.toMediaMetadata().toMediaItem() }\n                    \n                    // Find index of current track\n                    var startIndex = mediaItems.indexOfFirst { it.mediaId == currentTrack.id }\n                    if (startIndex == -1) {\n                        Timber.tag(TAG).w(\"Current track ${currentTrack.id} not found in queue, defaulting to 0\")\n                        val singleItem = currentTrack.toMediaMetadata().toMediaItem()\n                        // Prepend or fallback? Let's just play the track alone if not in queue\n                        player.setMediaItems(listOf(singleItem), 0, position)\n                    } else {\n                        player.setMediaItems(mediaItems, startIndex, position)\n                    }\n                } else {\n                    // No queue provided, fallback to loading just the track (or radio) via syncToTrack logic\n                    // But we want to avoid double loading.\n                    // If queue is null, we might be in a state where we should fetch radio?\n                    // But here we assume authoritative state.\n                    Timber.tag(TAG).d(\"No queue in state, loading single track\")\n                    // Construct single item\n                    val item = currentTrack.toMediaMetadata().toMediaItem()\n                    player.setMediaItems(listOf(item), 0, position)\n                }\n                \n                connection.seekTo(position)  // Always seek immediately to target pos\n\n                // Sync queue title\n                try {\n                    connection.service.queueTitle = queueTitle ?: \"Listen Together\"\n                } catch (e: Exception) {\n                    Timber.tag(TAG).e(e, \"Failed to set queue title during applyPlaybackState\")\n                }\n                \n                if (bypassBuffer) {\n                    // Manual sync/reconnect: apply play/pause immediately, no buffer protocol\n                    Timber.tag(TAG).d(\"Bypass buffer: immediately applying play=$isPlaying at pos=$position\")\n                    \n                    // Wait for player to be ready before seek/play\n                    var attempts = 0\n                    while (player.playbackState != Player.STATE_READY && attempts < 100) {\n                        delay(50)\n                        attempts++\n                    }\n                    if (player.playbackState == Player.STATE_READY) {\n                        Timber.tag(TAG).d(\"Player ready after ${attempts * 50}ms, seeking to $position\")\n                        player.seekTo(position)\n                        if (isPlaying) {\n                            connection.play()\n                            Timber.tag(TAG).d(\"Bypass: PLAY issued\")\n                        } else {\n                            connection.pause()\n                            Timber.tag(TAG).d(\"Bypass: PAUSE issued\")\n                        }\n                    } else {\n                        Timber.tag(TAG).w(\"Player not ready after 5s timeout during bypass sync\")\n                    }\n                    \n                    // Clear sync state\n                    pendingSyncState = null\n                    bufferingTrackId = null\n                    bufferCompleteReceivedForTrack = null\n                } else {\n                    // Normal sync: pause, store pending, send buffer_ready\n                    connection.pause()\n                    pendingSyncState = SyncStatePayload(\n                        currentTrack = currentTrack,\n                        isPlaying = isPlaying,\n                        position = position,\n                        lastUpdate = System.currentTimeMillis()\n                    )\n                    applyPendingSyncIfReady()\n                    client.sendBufferReady(currentTrack.id)\n                }\n                \n            } catch (e: Exception) {\n                Timber.tag(TAG).e(e, \"Error applying playback state\")\n            } finally {\n                connection.allowInternalSync = false\n                delay(200)\n                isSyncing = false\n            }\n        }\n    }\n\n    private fun syncToTrack(track: TrackInfo, shouldPlay: Boolean, position: Long) {\n        Timber.tag(TAG).d(\"syncToTrack: ${track.title}, play: $shouldPlay, pos: $position\")\n\n        // Track which buffer-complete we expect for this load\n        bufferingTrackId = track.id\n        val generation = currentTrackGeneration\n        \n        activeSyncJob?.cancel()\n        activeSyncJob = scope.launch(Dispatchers.IO) {\n            try {\n                // Check if a newer track change arrived - skip this load if stale\n                if (currentTrackGeneration != generation) {\n                    Timber.tag(TAG).d(\"Skipping stale syncToTrack for ${track.id} (generation $generation vs $currentTrackGeneration)\")\n                    isSyncing = false\n                    return@launch\n                }\n                \n                // Use YouTube API to play the track by ID\n                YouTube.queue(listOf(track.id)).onSuccess { queue ->\n                    Timber.tag(TAG).d(\"Got queue for track ${track.id}\")\n                    launch(Dispatchers.Main) {\n                        // Final generation check before applying changes\n                        if (currentTrackGeneration != generation) {\n                            Timber.tag(TAG).d(\"Skipping stale track application for ${track.id} (generation $generation vs $currentTrackGeneration)\")\n                            isSyncing = false\n                            return@launch\n                        }\n                        \n                        val connection = playerConnection ?: run {\n                            isSyncing = false\n                            return@launch\n                        }\n                        if (playerConnection !== connection) {\n                            isSyncing = false\n                            return@launch\n                        }\n                        isSyncing = true\n                        // Allow internal sync to bypass playback blocking for guests\n                        connection.allowInternalSync = true\n                        connection.playQueue(\n                            YouTubeQueue(\n                                endpoint = WatchEndpoint(videoId = track.id),\n                                preloadItem = queue.firstOrNull()?.toMediaMetadata()\n                            )\n                        )\n                        try {\n                            connection.service.queueTitle = \"Listen Together\" // Set default title\n                        } catch (e: Exception) {\n                            Timber.tag(TAG).e(e, \"Failed to set queue title\")\n                        }\n                        connection.allowInternalSync = false\n                        \n                        // Wait for player to be ready - monitor actual player state\n                        var waitCount = 0\n                        while (waitCount < 40) { // Max 2 seconds (40 * 50ms)\n                            // Check generation again while waiting\n                            if (currentTrackGeneration != generation) {\n                                Timber.tag(TAG).d(\"Generation changed while waiting for player ready - aborting sync for ${track.id}\")\n                                isSyncing = false\n                                return@launch\n                            }\n                            try {\n                                val player = connection.player\n                                if (player.playbackState == Player.STATE_READY) {\n                                    Timber.tag(TAG).d(\"Player ready after ${waitCount * 50}ms\")\n                                    break\n                                }\n                            } catch (e: Exception) {\n                                Timber.tag(TAG).e(e, \"Error checking player state\")\n                                break\n                            }\n                            delay(50)\n                            waitCount++\n                        }\n\n                        // Do NOT seek here; defer the exact seek until after the server signals buffer-complete\n                        // Ensure paused state before signaling ready\n                        connection.pause()\n\n                        // Store pending sync (guest will apply seek + play/pause after BufferComplete)\n                        pendingSyncState = SyncStatePayload(\n                            currentTrack = track,\n                            isPlaying = shouldPlay,\n                            position = position,\n                            lastUpdate = System.currentTimeMillis()\n                        )\n\n                        // Apply immediately if buffer-complete already arrived\n                        applyPendingSyncIfReady()\n\n                        // Signal we're ready to play\n                        client.sendBufferReady(track.id)\n                        Timber.tag(TAG).d(\"Sent buffer ready for ${track.id}, pending sync stored: pos=$position, play=$shouldPlay\")\n\n                        // Minimal delay before accepting sync commands\n                        delay(100)\n                        isSyncing = false\n                    }\n                }.onFailure { e ->\n                    Timber.tag(TAG).e(e, \"Failed to load track ${track.id}\")\n                    playerConnection?.allowInternalSync = false\n                    isSyncing = false\n                }\n            } catch (e: Exception) {\n                Timber.tag(TAG).e(e, \"Error syncing to track\")\n                playerConnection?.allowInternalSync = false\n                isSyncing = false\n            }\n        }\n    }\n\n    // Public API for host actions\n\n    /**\n     * Connect to the Listen Together server\n     */\n    fun connect() {\n        Timber.tag(TAG).d(\"Connecting to server\")\n        client.connect()\n    }\n\n    /**\n     * Disconnect from the server\n     */\n    fun disconnect() {\n        Timber.tag(TAG).d(\"Disconnecting from server\")\n        cleanup()\n        client.disconnect()\n    }\n\n    /**\n     * Create a new room\n     */\n    fun createRoom(username: String) {\n        Timber.tag(TAG).d(\"Creating room with username: $username\")\n        client.createRoom(username)\n    }\n\n    /**\n     * Join an existing room\n     */\n    fun joinRoom(roomCode: String, username: String) {\n        Timber.tag(TAG).d(\"Joining room $roomCode as $username\")\n        client.joinRoom(roomCode, username)\n    }\n\n    /**\n     * Leave the current room\n     */\n    fun leaveRoom() {\n        Timber.tag(TAG).d(\"Leaving room\")\n        cleanup()\n        client.leaveRoom()\n    }\n\n    /**\n     * Approve a join request\n     */\n    fun approveJoin(userId: String) = client.approveJoin(userId)\n\n    /**\n     * Reject a join request\n     */\n    fun rejectJoin(userId: String, reason: String? = null) = client.rejectJoin(userId, reason)\n\n    /**\n     * Kick a user\n     */\n    fun kickUser(userId: String, reason: String? = null) = client.kickUser(userId, reason)\n\n    /**\n     * Block a user permanently (internal list)\n     */\n    fun blockUser(username: String) = client.blockUser(username)\n\n    /**\n     * Unblock a previously blocked user\n     */\n    fun unblockUser(username: String) = client.unblockUser(username)\n\n    /**\n     * Get all currently blocked usernames\n     */\n    fun getBlockedUsernames(): Set<String> = blockedUsernames.value\n\n    /**\n     * Transfer host role to another user\n     */\n    fun transferHost(newHostId: String) = client.transferHost(newHostId)\n\n    /**\n     * Send track change (host only) - called when host changes track\n     */\n    fun sendTrackChange(metadata: MediaMetadata) {\n        if (!isHost || isSyncing) return\n        sendTrackChangeInternal(metadata)\n    }\n    \n    /**\n     * Internal track change - bypasses isSyncing check for initial state sync\n     */\n    private fun sendTrackChangeInternal(metadata: MediaMetadata) {\n        if (!isHost) return\n        \n        // Use a default duration of 3 minutes if duration is 0 or negative\n        val durationMs = if (metadata.duration > 0) metadata.duration.toLong() * 1000 else 180000L\n        \n        val trackInfo = TrackInfo(\n            id = metadata.id,\n            title = metadata.title,\n            artist = metadata.artists.joinToString(\", \") { it.name },\n            album = metadata.album?.title,\n            duration = durationMs,\n            thumbnail = metadata.thumbnailUrl,\n            suggestedBy = metadata.suggestedBy\n        )\n        \n        Timber.tag(TAG).d(\"Sending track change: ${trackInfo.title}, duration: $durationMs\")\n        \n        // Also grab current queue to send along with track change\n        val currentQueue = try {\n            playerConnection?.queueWindows?.value?.map { it.toTrackInfo() }\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Failed to get current queue\")\n            null\n        }\n        val currentTitle = try {\n            playerConnection?.queueTitle?.value\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Failed to get current title\")\n            null\n        }\n        \n        client.sendPlaybackAction(\n            PlaybackActions.CHANGE_TRACK,\n            queueTitle = currentTitle,\n            trackInfo = trackInfo,\n            queue = currentQueue\n        )\n    }\n\n    private fun startQueueSyncObservation() {\n        if (queueObserverJob?.isActive == true) return\n    \n        Timber.tag(TAG).d(\"Starting queue sync observation\")\n        queueObserverJob = scope.launch {\n            playerConnection?.queueWindows\n                ?.map { windows ->\n                    windows.map { it.toTrackInfo() }\n                }\n                ?.distinctUntilChanged()\n                ?.collectLatest { tracks ->\n                    if (!isHost || !isInRoom || isSyncing) return@collectLatest\n                \n                    delay(500) // Debounce rapid playlist manipulations\n                \n                    Timber.tag(TAG).d(\"Sending SYNC_QUEUE with ${tracks.size} items\")\n                    val queueTitle = try {\n                        playerConnection?.queueTitle?.value\n                    } catch (e: Exception) {\n                        Timber.tag(TAG).e(e, \"Failed to get queue title\")\n                        null\n                    }\n                    client.sendPlaybackAction(\n                        PlaybackActions.SYNC_QUEUE,\n                        queueTitle = queueTitle,\n                        queue = tracks\n                    )\n                }\n        }\n    }\n\n    private fun startVolumeSyncObservation() {\n        if (volumeObserverJob?.isActive == true) return\n\n        Timber.tag(TAG).d(\"Starting volume sync observation\")\n        volumeObserverJob = scope.launch {\n            playerConnection?.service?.playerVolume\n                ?.collectLatest { volume ->\n                    if (!isHost || !isInRoom || !syncHostVolumeEnabled.value) return@collectLatest\n\n                    val normalized = volume.coerceIn(0f, 1f)\n                    val last = lastSyncedVolume\n                    if (last != null && kotlin.math.abs(last - normalized) < 0.01f) return@collectLatest\n\n                    lastSyncedVolume = normalized\n                    client.sendPlaybackAction(PlaybackActions.SET_VOLUME, volume = normalized)\n                }\n        }\n    }\n\n    private fun stopVolumeSyncObservation() {\n        volumeObserverJob?.cancel()\n        volumeObserverJob = null\n        lastSyncedVolume = null\n    }\n\n    private fun androidx.media3.common.Timeline.Window.toTrackInfo(): TrackInfo {\n        val metadata = mediaItem.metadata ?: return TrackInfo(\"unknown\", \"Unknown\", \"Unknown\", \"\", 0, \"\")\n        val durationMs = if (metadata.duration > 0) metadata.duration.toLong() * 1000 else 180000L\n        return TrackInfo(\n            id = metadata.id,\n            title = metadata.title,\n            artist = metadata.artists.joinToString(\", \") { it.name },\n            album = metadata.album?.title,\n            duration = durationMs,\n            thumbnail = metadata.thumbnailUrl,\n            suggestedBy = metadata.suggestedBy\n        )\n    }\n\n    private fun stopQueueSyncObservation() {\n        queueObserverJob?.cancel()\n        queueObserverJob = null\n    }\n\n    private fun TrackInfo.toMediaMetadata(): MediaMetadata {\n        return MediaMetadata(\n            id = id,\n            title = title,\n            artists = listOf(Artist(id = \"\", name = artist)),\n            album = if (album != null) Album(id = \"\", title = album) else null,\n            duration = (duration / 1000).toInt(),\n            thumbnailUrl = thumbnail,\n            suggestedBy = suggestedBy\n        )\n    }\n\n    /**\n     * Request sync state from server (for guests to re-sync)\n     * Call this when a guest presses play/pause to sync with host\n     */\n    fun requestSync() {\n        if (!isInRoom || isHost) {\n            Timber.tag(TAG).d(\"requestSync: not applicable (isInRoom=$isInRoom, isHost=$isHost)\")\n            return\n        }\n        Timber.tag(TAG).d(\"Requesting sync from server\")\n        client.requestSync()\n    }\n\n    /**\n     * Clear logs\n     */\n    fun clearLogs() = client.clearLogs()\n\n    // Suggestions API\n\n    /**\n     * Suggest the given track to the host (guest only)\n     */\n    fun suggestTrack(track: TrackInfo) = client.suggestTrack(track)\n\n    /**\n     * Approve a suggestion (host only)\n     */\n    fun approveSuggestion(suggestionId: String) {\n        if (!isHost) return\n        // Send approval; server will insert-next and broadcast once\n        client.approveSuggestion(suggestionId)\n    }\n\n    /**\n     * Reject a suggestion (host only)\n     */\n    fun rejectSuggestion(suggestionId: String, reason: String? = null) = client.rejectSuggestion(suggestionId, reason)\n    \n    /**\n     * Force reconnection to server (for manual recovery)\n     */\n    fun forceReconnect() {\n        Timber.tag(TAG).d(\"Forcing reconnection\")\n        client.forceReconnect()\n    }\n    \n    /**\n     * Get persisted room code if available\n     */\n    fun getPersistedRoomCode(): String? = client.getPersistedRoomCode()\n    \n    /**\n     * Get current session age\n     */\n    fun getSessionAge(): Long = client.getSessionAge()\n\n    // Heartbeat timer\n    private var heartbeatJob: Job? = null\n\n    private fun startHeartbeat() {\n        if (heartbeatJob?.isActive == true) return\n        heartbeatJob = scope.launch {\n            while (heartbeatJob?.isActive == true && isInRoom && isHost) {\n                delay(15000L) // 15 seconds\n                playerConnection?.player?.let { player ->\n                    if (player.playWhenReady && player.playbackState == Player.STATE_READY) {\n                        val pos = player.currentPosition\n                        Timber.tag(TAG).d(\"Host heartbeat: sending PLAY at pos $pos\")\n                        client.sendPlaybackAction(PlaybackActions.PLAY, position = pos)\n                    }\n                }\n            }\n        }\n        Timber.tag(TAG).d(\"Host heartbeat started (15s interval)\")\n    }\n\n    private fun stopHeartbeat() {\n        heartbeatJob?.cancel()\n        heartbeatJob = null\n        Timber.tag(TAG).d(\"Host heartbeat stopped\")\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/listentogether/ListenTogetherServers.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.listentogether\n\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.Json\n\n@Serializable\ndata class ListenTogetherServer(\n    val name: String,\n    val url: String,\n    val location: String,\n    val operator: String\n)\n\nobject ListenTogetherServers {\n    private const val ServersJson = \"\"\"\n        [\n          {\n            \"name\": \"The Meowery\",\n            \"url\": \"wss://metroserverx.meowery.eu/ws\",\n            \"location\": \"Poland\",\n            \"operator\": \"Nyx\"\n          }\n        ]\n    \"\"\"\n\n    private val json = Json { ignoreUnknownKeys = true }\n\n    val servers: List<ListenTogetherServer> by lazy {\n        json.decodeFromString(ServersJson)\n    }\n\n    val defaultServerUrl: String\n        get() = servers.first().url\n\n    fun findByUrl(url: String): ListenTogetherServer? = servers.firstOrNull { it.url == url }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/listentogether/MessageCodec.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.listentogether\n\nimport com.google.protobuf.MessageLite\nimport com.metrolist.music.listentogether.proto.Listentogether\nimport timber.log.Timber\nimport java.io.ByteArrayInputStream\nimport java.io.ByteArrayOutputStream\nimport java.util.zip.GZIPInputStream\nimport java.util.zip.GZIPOutputStream\n\n/**\n * Codec for encoding and decoding messages using Protocol Buffers\n */\nclass MessageCodec(\n    var compressionEnabled: Boolean = false\n) {\n    companion object {\n        private const val TAG = \"MessageCodec\"\n        private const val COMPRESSION_THRESHOLD = 100 // Only compress if > 100 bytes\n    }\n    \n    /**\n     * Encode a message using Protocol Buffers\n     */\n    fun encode(msgType: String, payload: Any?): ByteArray {\n        return encodeProtobuf(msgType, payload)\n    }\n    \n    /**\n     * Decode a protobuf message\n     */\n    fun decode(data: ByteArray): Pair<String, ByteArray> {\n        return decodeProtobuf(data)\n    }\n    \n    /**\n     * Encode message using Protocol Buffers\n     */\n    private fun encodeProtobuf(msgType: String, payload: Any?): ByteArray {\n        var payloadBytes = byteArrayOf()\n        var compressed = false\n        \n        if (payload != null) {\n            val protoMsg = toProtoMessage(payload)\n            payloadBytes = protoMsg.toByteArray()\n            \n            // Compress if enabled and payload is large enough\n            if (compressionEnabled && payloadBytes.size > COMPRESSION_THRESHOLD) {\n                val compressedBytes = compressData(payloadBytes)\n                if (compressedBytes.size < payloadBytes.size) {\n                    payloadBytes = compressedBytes\n                    compressed = true\n                }\n            }\n        }\n        \n        val envelope = Listentogether.Envelope.newBuilder()\n            .setType(msgType)\n            .setPayload(com.google.protobuf.ByteString.copyFrom(payloadBytes))\n            .setCompressed(compressed)\n            .build()\n        \n        return envelope.toByteArray()\n    }\n    \n    /**\n     * Decode protobuf message\n     */\n    private fun decodeProtobuf(data: ByteArray): Pair<String, ByteArray> {\n        val envelope = Listentogether.Envelope.parseFrom(data)\n        \n        var payloadBytes = envelope.payload.toByteArray()\n        \n        if (envelope.compressed) {\n            payloadBytes = decompressData(payloadBytes) ?: payloadBytes\n        }\n        \n        return Pair(envelope.type, payloadBytes)\n    }\n    \n    /**\n     * Compress data using GZIP\n     */\n    private fun compressData(data: ByteArray): ByteArray {\n        val outputStream = ByteArrayOutputStream()\n        GZIPOutputStream(outputStream).use { gzip ->\n            gzip.write(data)\n        }\n        return outputStream.toByteArray()\n    }\n    \n    /**\n     * Decompress GZIP data\n     */\n    private fun decompressData(data: ByteArray): ByteArray? {\n        return try {\n            val inputStream = ByteArrayInputStream(data)\n            GZIPInputStream(inputStream).use { gzip ->\n                gzip.readBytes()\n            }\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Failed to decompress data\")\n            null\n        }\n    }\n    \n    /**\n     * Convert Kotlin objects to protobuf messages\n     */\n    private fun toProtoMessage(payload: Any): MessageLite {\n        return when (payload) {\n            is CreateRoomPayload -> Listentogether.CreateRoomPayload.newBuilder()\n                .setUsername(payload.username)\n                .build()\n            is JoinRoomPayload -> Listentogether.JoinRoomPayload.newBuilder()\n                .setRoomCode(payload.roomCode)\n                .setUsername(payload.username)\n                .build()\n            is ApproveJoinPayload -> Listentogether.ApproveJoinPayload.newBuilder()\n                .setUserId(payload.userId)\n                .build()\n            is RejectJoinPayload -> Listentogether.RejectJoinPayload.newBuilder()\n                .setUserId(payload.userId)\n                .setReason(payload.reason ?: \"\")\n                .build()\n            is PlaybackActionPayload -> {\n                val builder = Listentogether.PlaybackActionPayload.newBuilder()\n                    .setAction(payload.action)\n                    .setPosition(payload.position ?: 0)\n                    .setInsertNext(payload.insertNext ?: false)\n                    .setVolume(payload.volume ?: 1f)\n                    .setServerTime(payload.serverTime ?: 0)\n                \n                payload.trackId?.let { builder.setTrackId(it) }\n                payload.trackInfo?.let { builder.setTrackInfo(trackInfoToProto(it)) }\n                payload.queueTitle?.let { builder.setQueueTitle(it) }\n                payload.queue?.forEach { track ->\n                    builder.addQueue(trackInfoToProto(track))\n                }\n                \n                builder.build()\n            }\n            is BufferReadyPayload -> Listentogether.BufferReadyPayload.newBuilder()\n                .setTrackId(payload.trackId)\n                .build()\n            is KickUserPayload -> Listentogether.KickUserPayload.newBuilder()\n                .setUserId(payload.userId)\n                .setReason(payload.reason ?: \"\")\n                .build()\n            is SuggestTrackPayload -> {\n                val builder = Listentogether.SuggestTrackPayload.newBuilder()\n                payload.trackInfo.let { builder.setTrackInfo(trackInfoToProto(it)) }\n                builder.build()\n            }\n            is ApproveSuggestionPayload -> Listentogether.ApproveSuggestionPayload.newBuilder()\n                .setSuggestionId(payload.suggestionId)\n                .build()\n            is RejectSuggestionPayload -> Listentogether.RejectSuggestionPayload.newBuilder()\n                .setSuggestionId(payload.suggestionId)\n                .setReason(payload.reason ?: \"\")\n                .build()\n            is ReconnectPayload -> Listentogether.ReconnectPayload.newBuilder()\n                .setSessionToken(payload.sessionToken)\n                .build()\n            is TransferHostPayload -> Listentogether.TransferHostPayload.newBuilder()\n                .setNewHostId(payload.newHostId)\n                .build()\n            else -> throw IllegalArgumentException(\"Unsupported payload type: ${payload::class.simpleName}\")\n        }\n    }\n    \n    /**\n     * Decode protobuf payload to Kotlin objects\n     */\n    fun decodePayload(msgType: String, payloadBytes: ByteArray): Any? {\n        if (payloadBytes.isEmpty()) return null\n        \n        return decodeProtobufPayload(msgType, payloadBytes)\n    }\n    \n    /**\n     * Decode protobuf payload\n     */\n    private fun decodeProtobufPayload(msgType: String, payloadBytes: ByteArray): Any? {\n        return when (msgType) {\n            MessageTypes.ROOM_CREATED -> {\n                val pb = Listentogether.RoomCreatedPayload.parseFrom(payloadBytes)\n                RoomCreatedPayload(pb.roomCode, pb.userId, pb.sessionToken)\n            }\n            MessageTypes.JOIN_REQUEST -> {\n                val pb = Listentogether.JoinRequestPayload.parseFrom(payloadBytes)\n                JoinRequestPayload(pb.userId, pb.username)\n            }\n            MessageTypes.JOIN_APPROVED -> {\n                val pb = Listentogether.JoinApprovedPayload.parseFrom(payloadBytes)\n                JoinApprovedPayload(\n                    pb.roomCode,\n                    pb.userId,\n                    pb.sessionToken,\n                    protoToRoomState(pb.state)\n                )\n            }\n            MessageTypes.JOIN_REJECTED -> {\n                val pb = Listentogether.JoinRejectedPayload.parseFrom(payloadBytes)\n                JoinRejectedPayload(pb.reason)\n            }\n            MessageTypes.USER_JOINED -> {\n                val pb = Listentogether.UserJoinedPayload.parseFrom(payloadBytes)\n                UserJoinedPayload(pb.userId, pb.username)\n            }\n            MessageTypes.USER_LEFT -> {\n                val pb = Listentogether.UserLeftPayload.parseFrom(payloadBytes)\n                UserLeftPayload(pb.userId, pb.username)\n            }\n            MessageTypes.SYNC_PLAYBACK -> {\n                val pb = Listentogether.PlaybackActionPayload.parseFrom(payloadBytes)\n                PlaybackActionPayload(\n                    action = pb.action,\n                    trackId = pb.trackId.takeIf { it.isNotEmpty() },\n                    position = pb.position.takeIf { it > 0 },\n                    trackInfo = pb.trackInfo?.let { protoToTrackInfo(it) },\n                    insertNext = pb.insertNext.takeIf { it },\n                    queue = pb.queueList?.map { protoToTrackInfo(it) },\n                    queueTitle = pb.queueTitle.takeIf { it.isNotEmpty() },\n                    volume = pb.volume.takeIf { it > 0 },\n                    serverTime = pb.serverTime.takeIf { it > 0 }\n                )\n            }\n            MessageTypes.BUFFER_WAIT -> {\n                val pb = Listentogether.BufferWaitPayload.parseFrom(payloadBytes)\n                BufferWaitPayload(pb.trackId, pb.waitingForList)\n            }\n            MessageTypes.BUFFER_COMPLETE -> {\n                val pb = Listentogether.BufferCompletePayload.parseFrom(payloadBytes)\n                BufferCompletePayload(pb.trackId)\n            }\n            MessageTypes.ERROR -> {\n                val pb = Listentogether.ErrorPayload.parseFrom(payloadBytes)\n                ErrorPayload(pb.code, pb.message)\n            }\n            MessageTypes.HOST_CHANGED -> {\n                val pb = Listentogether.HostChangedPayload.parseFrom(payloadBytes)\n                HostChangedPayload(pb.newHostId, pb.newHostName)\n            }\n            MessageTypes.KICKED -> {\n                val pb = Listentogether.KickedPayload.parseFrom(payloadBytes)\n                KickedPayload(pb.reason)\n            }\n            MessageTypes.SYNC_STATE -> {\n                val pb = Listentogether.SyncStatePayload.parseFrom(payloadBytes)\n                SyncStatePayload(\n                    currentTrack = pb.currentTrack?.let { protoToTrackInfo(it) },\n                    isPlaying = pb.isPlaying,\n                    position = pb.position,\n                    lastUpdate = pb.lastUpdate,\n                    queue = pb.queueList?.map { protoToTrackInfo(it) },\n                    volume = pb.volume.takeIf { it > 0 }\n                )\n            }\n            MessageTypes.RECONNECTED -> {\n                val pb = Listentogether.ReconnectedPayload.parseFrom(payloadBytes)\n                ReconnectedPayload(\n                    pb.roomCode,\n                    pb.userId,\n                    protoToRoomState(pb.state),\n                    pb.isHost\n                )\n            }\n            MessageTypes.USER_RECONNECTED -> {\n                val pb = Listentogether.UserReconnectedPayload.parseFrom(payloadBytes)\n                UserReconnectedPayload(pb.userId, pb.username)\n            }\n            MessageTypes.USER_DISCONNECTED -> {\n                val pb = Listentogether.UserDisconnectedPayload.parseFrom(payloadBytes)\n                UserDisconnectedPayload(pb.userId, pb.username)\n            }\n            MessageTypes.SUGGESTION_RECEIVED -> {\n                val pb = Listentogether.SuggestionReceivedPayload.parseFrom(payloadBytes)\n                SuggestionReceivedPayload(\n                    pb.suggestionId,\n                    pb.fromUserId,\n                    pb.fromUsername,\n                    protoToTrackInfo(pb.trackInfo)\n                )\n            }\n            MessageTypes.SUGGESTION_APPROVED -> {\n                val pb = Listentogether.SuggestionApprovedPayload.parseFrom(payloadBytes)\n                SuggestionApprovedPayload(\n                    pb.suggestionId,\n                    protoToTrackInfo(pb.trackInfo)\n                )\n            }\n            MessageTypes.SUGGESTION_REJECTED -> {\n                val pb = Listentogether.SuggestionRejectedPayload.parseFrom(payloadBytes)\n                SuggestionRejectedPayload(pb.suggestionId, pb.reason.takeIf { it.isNotEmpty() })\n            }\n            else -> null\n        }\n    }\n    \n    // Helper conversion functions\n    \n    private fun trackInfoToProto(track: TrackInfo): Listentogether.TrackInfo {\n        return Listentogether.TrackInfo.newBuilder()\n            .setId(track.id)\n            .setTitle(track.title)\n            .setArtist(track.artist)\n            .setAlbum(track.album ?: \"\")\n            .setDuration(track.duration)\n            .setThumbnail(track.thumbnail ?: \"\")\n            .setSuggestedBy(track.suggestedBy ?: \"\")\n            .build()\n    }\n    \n    private fun protoToTrackInfo(proto: Listentogether.TrackInfo): TrackInfo {\n        return TrackInfo(\n            id = proto.id,\n            title = proto.title,\n            artist = proto.artist,\n            album = proto.album.takeIf { it.isNotEmpty() },\n            duration = proto.duration,\n            thumbnail = proto.thumbnail.takeIf { it.isNotEmpty() },\n            suggestedBy = proto.suggestedBy.takeIf { it.isNotEmpty() }\n        )\n    }\n    \n    private fun protoToUserInfo(proto: Listentogether.UserInfo): UserInfo {\n        return UserInfo(\n            userId = proto.userId,\n            username = proto.username,\n            isHost = proto.isHost,\n            isConnected = proto.isConnected\n        )\n    }\n    \n    private fun protoToRoomState(proto: Listentogether.RoomState): RoomState {\n        return RoomState(\n            roomCode = proto.roomCode,\n            hostId = proto.hostId,\n            users = proto.usersList.map { protoToUserInfo(it) },\n            currentTrack = proto.currentTrack?.let { protoToTrackInfo(it) },\n            isPlaying = proto.isPlaying,\n            position = proto.position,\n            lastUpdate = proto.lastUpdate,\n            volume = proto.volume,\n            queue = proto.queueList.map { protoToTrackInfo(it) }\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/listentogether/Protocol.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.listentogether\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * Message types for Listen Together protocol\n */\nobject MessageTypes {\n    // Client -> Server\n    const val CREATE_ROOM = \"create_room\"\n    const val JOIN_ROOM = \"join_room\"\n    const val LEAVE_ROOM = \"leave_room\"\n    const val APPROVE_JOIN = \"approve_join\"\n    const val REJECT_JOIN = \"reject_join\"\n    const val PLAYBACK_ACTION = \"playback_action\"\n    const val BUFFER_READY = \"buffer_ready\"\n    const val KICK_USER = \"kick_user\"\n    const val TRANSFER_HOST = \"transfer_host\"\n    const val PING = \"ping\"\n    const val CHAT = \"chat\"\n    const val REQUEST_SYNC = \"request_sync\"\n    const val RECONNECT = \"reconnect\"\n    const val SUGGEST_TRACK = \"suggest_track\"\n    const val APPROVE_SUGGESTION = \"approve_suggestion\"\n    const val REJECT_SUGGESTION = \"reject_suggestion\"\n\n    // Server -> Client\n    const val ROOM_CREATED = \"room_created\"\n    const val JOIN_REQUEST = \"join_request\"\n    const val JOIN_APPROVED = \"join_approved\"\n    const val JOIN_REJECTED = \"join_rejected\"\n    const val USER_JOINED = \"user_joined\"\n    const val USER_LEFT = \"user_left\"\n    const val SYNC_PLAYBACK = \"sync_playback\"\n    const val BUFFER_WAIT = \"buffer_wait\"\n    const val BUFFER_COMPLETE = \"buffer_complete\"\n    const val ERROR = \"error\"\n    const val PONG = \"pong\"\n    const val HOST_CHANGED = \"host_changed\"\n    const val KICKED = \"kicked\"\n    const val SYNC_STATE = \"sync_state\"\n    const val RECONNECTED = \"reconnected\"\n    const val USER_RECONNECTED = \"user_reconnected\"\n    const val USER_DISCONNECTED = \"user_disconnected\"\n    const val SUGGESTION_RECEIVED = \"suggestion_received\"\n    const val SUGGESTION_APPROVED = \"suggestion_approved\"\n    const val SUGGESTION_REJECTED = \"suggestion_rejected\"\n}\n\n/**\n * Playback action types\n */\nobject PlaybackActions {\n    const val PLAY = \"play\"\n    const val PAUSE = \"pause\"\n    const val SEEK = \"seek\"\n    const val SKIP_NEXT = \"skip_next\"\n    const val SKIP_PREV = \"skip_prev\"\n    const val CHANGE_TRACK = \"change_track\"\n    const val QUEUE_ADD = \"queue_add\"\n    const val QUEUE_REMOVE = \"queue_remove\"\n    const val QUEUE_CLEAR = \"queue_clear\"\n    const val SYNC_QUEUE = \"sync_queue\"\n    const val SET_VOLUME = \"set_volume\"\n}\n\n/**\n * Track information\n */\n@Serializable\ndata class TrackInfo(\n    val id: String,\n    val title: String,\n    val artist: String,\n    val album: String? = null,\n    val duration: Long, // milliseconds\n    val thumbnail: String? = null,\n    @SerialName(\"suggested_by\") val suggestedBy: String? = null\n)\n\n/**\n * User information\n */\n@Serializable\ndata class UserInfo(\n    @SerialName(\"user_id\") val userId: String,\n    val username: String,\n    @SerialName(\"is_host\") val isHost: Boolean,\n    @SerialName(\"is_connected\") val isConnected: Boolean = true\n)\n\n/**\n * Room state\n */\n@Serializable\ndata class RoomState(\n    @SerialName(\"room_code\") val roomCode: String,\n    @SerialName(\"host_id\") val hostId: String,\n    val users: List<UserInfo>,\n    @SerialName(\"current_track\") val currentTrack: TrackInfo? = null,\n    @SerialName(\"is_playing\") val isPlaying: Boolean,\n    val position: Long, // milliseconds\n    @SerialName(\"last_update\") val lastUpdate: Long, // unix timestamp ms\n    val volume: Float = 1f,\n    val queue: List<TrackInfo> = emptyList()\n)\n\n// Request payloads\n\n@Serializable\ndata class CreateRoomPayload(\n    val username: String\n)\n\n@Serializable\ndata class JoinRoomPayload(\n    @SerialName(\"room_code\") val roomCode: String,\n    val username: String\n)\n\n@Serializable\ndata class ApproveJoinPayload(\n    @SerialName(\"user_id\") val userId: String\n)\n\n@Serializable\ndata class RejectJoinPayload(\n    @SerialName(\"user_id\") val userId: String,\n    val reason: String? = null\n)\n\n@Serializable\ndata class PlaybackActionPayload(\n    val action: String,\n    @SerialName(\"track_id\") val trackId: String? = null,\n    val position: Long? = null, // milliseconds\n    @SerialName(\"track_info\") val trackInfo: TrackInfo? = null,\n    @SerialName(\"insert_next\") val insertNext: Boolean? = null,\n    val queue: List<TrackInfo>? = null,\n    @SerialName(\"queue_title\") val queueTitle: String? = null,\n    val volume: Float? = null,\n    @SerialName(\"server_time\") val serverTime: Long? = null\n)\n\n@Serializable\ndata class BufferReadyPayload(\n    @SerialName(\"track_id\") val trackId: String\n)\n\n@Serializable\ndata class KickUserPayload(\n    @SerialName(\"user_id\") val userId: String,\n    val reason: String? = null\n)\n\n@Serializable\ndata class TransferHostPayload(\n    @SerialName(\"new_host_id\") val newHostId: String\n)\n\n@Serializable\ndata class ChatPayload(\n    val message: String\n)\n\n// Suggestions payloads\n\n@Serializable\ndata class SuggestTrackPayload(\n    @SerialName(\"track_info\") val trackInfo: TrackInfo\n)\n\n@Serializable\ndata class SuggestionReceivedPayload(\n    @SerialName(\"suggestion_id\") val suggestionId: String,\n    @SerialName(\"from_user_id\") val fromUserId: String,\n    @SerialName(\"from_username\") val fromUsername: String,\n    @SerialName(\"track_info\") val trackInfo: TrackInfo\n)\n\n@Serializable\ndata class ApproveSuggestionPayload(\n    @SerialName(\"suggestion_id\") val suggestionId: String\n)\n\n@Serializable\ndata class RejectSuggestionPayload(\n    @SerialName(\"suggestion_id\") val suggestionId: String,\n    val reason: String? = null\n)\n\n@Serializable\ndata class SuggestionApprovedPayload(\n    @SerialName(\"suggestion_id\") val suggestionId: String,\n    @SerialName(\"track_info\") val trackInfo: TrackInfo\n)\n\n@Serializable\ndata class SuggestionRejectedPayload(\n    @SerialName(\"suggestion_id\") val suggestionId: String,\n    val reason: String? = null\n)\n\n// Response payloads\n\n@Serializable\ndata class RoomCreatedPayload(\n    @SerialName(\"room_code\") val roomCode: String,\n    @SerialName(\"user_id\") val userId: String,\n    @SerialName(\"session_token\") val sessionToken: String\n)\n\n@Serializable\ndata class JoinRequestPayload(\n    @SerialName(\"user_id\") val userId: String,\n    val username: String\n)\n\n@Serializable\ndata class JoinApprovedPayload(\n    @SerialName(\"room_code\") val roomCode: String,\n    @SerialName(\"user_id\") val userId: String,\n    @SerialName(\"session_token\") val sessionToken: String,\n    val state: RoomState\n)\n\n@Serializable\ndata class JoinRejectedPayload(\n    val reason: String\n)\n\n@Serializable\ndata class UserJoinedPayload(\n    @SerialName(\"user_id\") val userId: String,\n    val username: String\n)\n\n@Serializable\ndata class UserLeftPayload(\n    @SerialName(\"user_id\") val userId: String,\n    val username: String\n)\n\n@Serializable\ndata class BufferWaitPayload(\n    @SerialName(\"track_id\") val trackId: String,\n    @SerialName(\"waiting_for\") val waitingFor: List<String>\n)\n\n@Serializable\ndata class BufferCompletePayload(\n    @SerialName(\"track_id\") val trackId: String\n)\n\n@Serializable\ndata class ErrorPayload(\n    val code: String,\n    val message: String\n)\n\n@Serializable\ndata class ChatMessagePayload(\n    @SerialName(\"user_id\") val userId: String,\n    val username: String,\n    val message: String,\n    val timestamp: Long\n)\n\n@Serializable\ndata class HostChangedPayload(\n    @SerialName(\"new_host_id\") val newHostId: String,\n    @SerialName(\"new_host_name\") val newHostName: String\n)\n\n@Serializable\ndata class KickedPayload(\n    val reason: String\n)\n\n/**\n * Sync state payload - sent to guest when they request current state\n */\n@Serializable\ndata class SyncStatePayload(\n    @SerialName(\"current_track\") val currentTrack: TrackInfo?,\n    @SerialName(\"is_playing\") val isPlaying: Boolean,\n    val position: Long,\n    @SerialName(\"last_update\") val lastUpdate: Long,\n    val queue: List<TrackInfo>? = null,\n    val volume: Float? = null\n)\n\n// Reconnection payloads\n\n@Serializable\ndata class ReconnectPayload(\n    @SerialName(\"session_token\") val sessionToken: String\n)\n\n@Serializable\ndata class ReconnectedPayload(\n    @SerialName(\"room_code\") val roomCode: String,\n    @SerialName(\"user_id\") val userId: String,\n    val state: RoomState,\n    @SerialName(\"is_host\") val isHost: Boolean\n)\n\n@Serializable\ndata class UserReconnectedPayload(\n    @SerialName(\"user_id\") val userId: String,\n    val username: String\n)\n\n@Serializable\ndata class UserDisconnectedPayload(\n    @SerialName(\"user_id\") val userId: String,\n    val username: String\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/lyrics/BetterLyricsProvider.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.lyrics\n\nimport android.content.Context\nimport com.metrolist.music.betterlyrics.BetterLyrics\nimport com.metrolist.music.constants.EnableBetterLyricsKey\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\n\nobject BetterLyricsProvider : LyricsProvider {\n    override val name = \"BetterLyrics\"\n\n    override fun isEnabled(context: Context): Boolean = context.dataStore[EnableBetterLyricsKey] ?: true\n\n    override suspend fun getLyrics(\n        id: String,\n        title: String,\n        artist: String,\n        duration: Int,\n        album: String?,\n    ): Result<String> = BetterLyrics.getLyrics(title, artist, duration, album)\n\n    override suspend fun getAllLyrics(\n        id: String,\n        title: String,\n        artist: String,\n        duration: Int,\n        album: String?,\n        callback: (String) -> Unit,\n    ) {\n        BetterLyrics.getAllLyrics(title, artist, duration, album, callback)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/lyrics/KuGouLyricsProvider.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.lyrics\n\nimport android.content.Context\nimport com.metrolist.kugou.KuGou\nimport com.metrolist.music.constants.EnableKugouKey\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\n\nobject KuGouLyricsProvider : LyricsProvider {\n    override val name = \"Kugou\"\n    override fun isEnabled(context: Context): Boolean =\n        context.dataStore[EnableKugouKey] ?: true\n\n    override suspend fun getLyrics(\n        id: String,\n        title: String,\n        artist: String,\n        duration: Int,\n        album: String?,\n    ): Result<String> =\n        KuGou.getLyrics(title, artist, duration, album)\n\n    override suspend fun getAllLyrics(\n        id: String,\n        title: String,\n        artist: String,\n        duration: Int,\n        album: String?,\n        callback: (String) -> Unit,\n    ) {\n        KuGou.getAllPossibleLyricsOptions(title, artist, duration, album, callback)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/lyrics/LrcLibLyricsProvider.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.lyrics\n\nimport android.content.Context\nimport com.metrolist.lrclib.LrcLib\nimport com.metrolist.music.constants.EnableLrcLibKey\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\n\nobject LrcLibLyricsProvider : LyricsProvider {\n    override val name = \"LrcLib\"\n\n    override fun isEnabled(context: Context): Boolean = context.dataStore[EnableLrcLibKey] ?: true\n\n    override suspend fun getLyrics(\n        id: String,\n        title: String,\n        artist: String,\n        duration: Int,\n        album: String?,\n    ): Result<String> = LrcLib.getLyrics(title, artist, duration, album)\n\n    override suspend fun getAllLyrics(\n        id: String,\n        title: String,\n        artist: String,\n        duration: Int,\n        album: String?,\n        callback: (String) -> Unit,\n    ) {\n        LrcLib.getAllLyrics(title, artist, duration, album, callback)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/lyrics/LyricsEntry.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.lyrics\n\nimport kotlinx.coroutines.flow.MutableStateFlow\n\ndata class WordTimestamp(\n    val text: String,\n    val startTime: Double,\n    val endTime: Double\n)\n\ndata class LyricsEntry(\n    val time: Long,\n    val text: String,\n    val words: List<WordTimestamp>? = null,\n    val romanizedTextFlow: MutableStateFlow<String?> = MutableStateFlow(null),\n    val translatedTextFlow: MutableStateFlow<String?> = MutableStateFlow(null),\n    val agent: String? = null,\n    val isBackground: Boolean = false\n) : Comparable<LyricsEntry> {\n    override fun compareTo(other: LyricsEntry): Int = (time - other.time).toInt()\n\n    companion object {\n        val HEAD_LYRICS_ENTRY = LyricsEntry(0L, \"\")\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/lyrics/LyricsHelper.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.lyrics\n\nimport android.content.Context\nimport android.util.LruCache\nimport com.metrolist.music.constants.LyricsProviderOrderKey\nimport com.metrolist.music.constants.PreferredLyricsProvider\nimport com.metrolist.music.constants.PreferredLyricsProviderKey\nimport com.metrolist.music.db.entities.LyricsEntity.Companion.LYRICS_NOT_FOUND\nimport com.metrolist.music.extensions.toEnum\nimport com.metrolist.music.models.MediaMetadata\nimport com.metrolist.music.utils.NetworkConnectivityObserver\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.reportException\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.cancel\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.launch\nimport timber.log.Timber\nimport javax.inject.Inject\n\nclass LyricsHelper\n@Inject\nconstructor(\n    @ApplicationContext private val context: Context,\n    private val networkConnectivity: NetworkConnectivityObserver,\n) {\n    private var lyricsProviders =\n        listOf(\n            BetterLyricsProvider,\n            SimpMusicLyricsProvider,\n            LrcLibLyricsProvider,\n            KuGouLyricsProvider,\n            LyricsPlusProvider,\n            YouTubeSubtitleLyricsProvider,\n            YouTubeLyricsProvider\n        )\n\n    val preferred =\n        context.dataStore.data\n            .map { preferences ->\n                val providerOrder = preferences[LyricsProviderOrderKey] ?: \"\"\n                if (providerOrder.isNotBlank()) {\n                    // Use the new provider order if available\n                    LyricsProviderRegistry.getOrderedProviders(providerOrder)\n                } else {\n                    // Fall back to preferred provider logic for backward compatibility\n                    val preferredProvider = preferences[PreferredLyricsProviderKey]\n                        .toEnum(PreferredLyricsProvider.LRCLIB)\n                    when (preferredProvider) {\n                        PreferredLyricsProvider.LRCLIB -> listOf(\n                            LrcLibLyricsProvider,\n                            BetterLyricsProvider,\n                            SimpMusicLyricsProvider,\n                            KuGouLyricsProvider,\n                            LyricsPlusProvider,\n                            YouTubeSubtitleLyricsProvider,\n                            YouTubeLyricsProvider\n                        )\n                        PreferredLyricsProvider.KUGOU -> listOf(\n                            KuGouLyricsProvider,\n                            BetterLyricsProvider,\n                            SimpMusicLyricsProvider,\n                            LrcLibLyricsProvider,\n                            LyricsPlusProvider,\n                            YouTubeSubtitleLyricsProvider,\n                            YouTubeLyricsProvider\n                        )\n                        PreferredLyricsProvider.BETTER_LYRICS -> listOf(\n                            BetterLyricsProvider,\n                            SimpMusicLyricsProvider,\n                            LrcLibLyricsProvider,\n                            KuGouLyricsProvider,\n                            LyricsPlusProvider,\n                            YouTubeSubtitleLyricsProvider,\n                            YouTubeLyricsProvider\n                        )\n                        PreferredLyricsProvider.SIMPMUSIC -> listOf(\n                            SimpMusicLyricsProvider,\n                            BetterLyricsProvider,\n                            LrcLibLyricsProvider,\n                            KuGouLyricsProvider,\n                            LyricsPlusProvider,\n                            YouTubeSubtitleLyricsProvider,\n                            YouTubeLyricsProvider\n                        )\n                    }\n                }\n            }.distinctUntilChanged()\n            .map { providers ->\n                lyricsProviders = providers\n            }\n\n    private val cache = LruCache<String, List<LyricsResult>>(MAX_CACHE_SIZE)\n    private var currentLyricsJob: Job? = null\n\n    suspend fun getLyrics(mediaMetadata: MediaMetadata): LyricsWithProvider {\n        currentLyricsJob?.cancel()\n\n        val cached = cache.get(mediaMetadata.id)?.firstOrNull()\n        if (cached != null) {\n            return LyricsWithProvider(cached.lyrics, cached.providerName)\n        }\n\n        // Check network connectivity before making network requests\n        // Use synchronous check as fallback if flow doesn't emit\n        val isNetworkAvailable = try {\n            networkConnectivity.isCurrentlyConnected()\n        } catch (e: Exception) {\n            // If network check fails, try to proceed anyway\n            true\n        }\n\n        if (!isNetworkAvailable) {\n            // Still proceed but return not found to avoid hanging\n            return LyricsWithProvider(LYRICS_NOT_FOUND, \"Unknown\")\n        }\n\n        val scope = CoroutineScope(SupervisorJob())\n        val deferred = scope.async {\n            val cleanedTitle = LyricsUtils.cleanTitleForSearch(mediaMetadata.title)\n            for (provider in lyricsProviders) {\n                if (provider.isEnabled(context)) {\n                    try {\n                        Timber.tag(\"LyricsHelper\")\n                            .d(\"Trying provider: ${provider.name} for $cleanedTitle\")\n                        val result = provider.getLyrics(\n                            mediaMetadata.id,\n                            cleanedTitle,\n                            mediaMetadata.artists.joinToString { it.name },\n                            mediaMetadata.duration,\n                            mediaMetadata.album?.title,\n                        )\n                        result.onSuccess { lyrics ->\n                            Timber.tag(\"LyricsHelper\").i(\"Successfully got lyrics from ${provider.name}\")\n                            return@async LyricsWithProvider(lyrics, provider.name)\n                        }.onFailure { e ->\n                            Timber.tag(\"LyricsHelper\").w(\"${provider.name} failed: ${e.message}\")\n                            reportException(e)\n                        }\n                    } catch (e: Exception) {\n                        // Catch network-related exceptions like UnresolvedAddressException\n                        Timber.tag(\"LyricsHelper\").w(\"${provider.name} threw exception: ${e.message}\")\n                        reportException(e)\n                    }\n                } else {\n                    Timber.tag(\"LyricsHelper\").d(\"Provider ${provider.name} is disabled\")\n                }\n            }\n            Timber.tag(\"LyricsHelper\").w(\"All providers failed for ${mediaMetadata.title}\")\n            return@async LyricsWithProvider(LYRICS_NOT_FOUND, \"Unknown\")\n        }\n\n        val result = deferred.await()\n        scope.cancel()\n        return result\n    }\n\n    suspend fun getAllLyrics(\n        mediaId: String,\n        songTitle: String,\n        songArtists: String,\n        duration: Int,\n        album: String? = null,\n        callback: (LyricsResult) -> Unit,\n    ) {\n        currentLyricsJob?.cancel()\n\n        val cacheKey = \"$songArtists-$songTitle\".replace(\" \", \"\")\n        cache.get(cacheKey)?.let { results ->\n            results.forEach {\n                callback(it)\n            }\n            return\n        }\n\n        // Check network connectivity before making network requests\n        // Use synchronous check as fallback if flow doesn't emit\n        val isNetworkAvailable = try {\n            networkConnectivity.isCurrentlyConnected()\n        } catch (e: Exception) {\n            // If network check fails, try to proceed anyway\n            true\n        }\n\n        if (!isNetworkAvailable) {\n            // Still try to proceed in case of false negative\n            return\n        }\n\n        val allResult = mutableListOf<LyricsResult>()\n        currentLyricsJob = CoroutineScope(SupervisorJob()).launch {\n            val cleanedTitle = LyricsUtils.cleanTitleForSearch(songTitle)\n            lyricsProviders.forEach { provider ->\n                if (provider.isEnabled(context)) {\n                    try {\n                        provider.getAllLyrics(mediaId, cleanedTitle, songArtists, duration, album) { lyrics ->\n                            val result = LyricsResult(provider.name, lyrics)\n                            allResult += result\n                            callback(result)\n                        }\n                    } catch (e: Exception) {\n                        // Catch network-related exceptions like UnresolvedAddressException\n                        reportException(e)\n                    }\n                }\n            }\n            cache.put(cacheKey, allResult)\n        }\n\n        currentLyricsJob?.join()\n    }\n\n    fun cancelCurrentLyricsJob() {\n        currentLyricsJob?.cancel()\n        currentLyricsJob = null\n    }\n\n    companion object {\n        private const val MAX_CACHE_SIZE = 3\n    }\n}\n\ndata class LyricsResult(\n    val providerName: String,\n    val lyrics: String,\n)\n\ndata class LyricsWithProvider(\n    val lyrics: String,\n    val provider: String,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/lyrics/LyricsPlusProvider.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.lyrics\n\nimport android.content.Context\nimport com.metrolist.music.constants.EnableLyricsPlus\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\nimport io.ktor.client.HttpClient\nimport io.ktor.client.call.body\nimport io.ktor.client.engine.cio.CIO\nimport io.ktor.client.plugins.HttpTimeout\nimport io.ktor.client.plugins.contentnegotiation.ContentNegotiation\nimport io.ktor.client.request.get\nimport io.ktor.client.request.parameter\nimport io.ktor.http.HttpStatusCode\nimport io.ktor.serialization.kotlinx.json.json\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.Json\nimport timber.log.Timber\n\n@Serializable\nprivate data class LyricLineResponse(\n    val time: Long,\n    val duration: Long,\n    val text: String,\n)\n\n@Serializable\nprivate data class LyricsPlusResponse(\n    val type: String? = null,\n    val lyrics: List<LyricLineResponse>? = null,\n    val cached: String? = null,\n)\n\nobject LyricsPlusProvider : LyricsProvider {\n    override val name = \"LyricsPlus\"\n\n    private val baseUrls = listOf(\n        \"https://lyricsplus.binimum.org\",\n        \"https://lyricsplus.atomix.one\",\n        \"https://lyricsplus-seven.vercel.app\", // might fail since its on vercel...\n        //\"https://lyricsplus.prjktla.workers.dev\", seems to be easily rate-limited\n        //\"https://lyrics-plus-backend.vercel.app\", deployment paused\n    )\n\n    private val client by lazy {\n        HttpClient(CIO) {\n            install(ContentNegotiation) {\n                json(\n                    Json {\n                        isLenient = true\n                        ignoreUnknownKeys = true\n                    },\n                )\n            }\n\n            install(HttpTimeout) {\n                requestTimeoutMillis = 15000\n                connectTimeoutMillis = 10000\n                socketTimeoutMillis = 15000\n            }\n\n            expectSuccess = false\n        }\n    }\n\n    override fun isEnabled(context: Context): Boolean = context.dataStore[EnableLyricsPlus] ?: false\n\n    private suspend fun fetchFromUrl(\n        url: String,\n        title: String,\n        artist: String,\n        duration: Int,\n        album: String?,\n    ): LyricsPlusResponse? = runCatching {\n        val response = client.get(\"$url/v2/lyrics/get\") {\n            parameter(\"title\", title)\n            parameter(\"artist\", artist)\n            parameter(\"duration\", if (duration > 0) duration / 1000 else -1)\n            if (!album.isNullOrBlank()) {\n                parameter(\"album\", album)\n            }\n            parameter(\"source\", \"apple,lyricsplus,musixmatch,spotify,musixmatch-word\")\n        }\n\n        if (response.status == HttpStatusCode.OK) {\n            response.body<LyricsPlusResponse>()\n        } else {\n            null\n        }\n    }.getOrNull()\n\n    private suspend fun fetchLyrics(\n        title: String,\n        artist: String,\n        duration: Int,\n        album: String?,\n    ): LyricsPlusResponse? {\n        for (baseUrl in baseUrls) {\n            try {\n                val result = fetchFromUrl(baseUrl, title, artist, duration, album)\n                if (result != null && !result.lyrics.isNullOrEmpty()) {\n                    return result\n                }\n            } catch (e: Exception) {\n                Timber.tag(\"LyricsPlus\").d(e, \"Failed to fetch from $baseUrl\")\n                continue\n            }\n        }\n        return null\n    }\n\n    private fun convertToLrc(response: LyricsPlusResponse?): String? {\n        if (response?.lyrics == null || response.lyrics.isEmpty()) {\n            return null\n        }\n\n        return response.lyrics.mapNotNull { line ->\n            val minutes = line.time / 1000 / 60\n            val seconds = (line.time / 1000) % 60\n            val millis = line.time % 1000 / 10\n            \n            if (line.text.isNotBlank()) {\n                String.format(\"[%02d:%02d.%02d]%s\", minutes, seconds, millis, line.text)\n            } else {\n                null\n            }\n        }.joinToString(\"\\n\")\n    }\n\n    override suspend fun getLyrics(\n        id: String,\n        title: String,\n        artist: String,\n        duration: Int,\n        album: String?,\n    ): Result<String> = runCatching {\n        val response = fetchLyrics(title, artist, duration, album)\n        val lrc = convertToLrc(response)\n        \n        if (lrc.isNullOrBlank()) {\n            throw IllegalStateException(\"Lyrics unavailable\")\n        }\n        \n        lrc\n    }\n\n    override suspend fun getAllLyrics(\n        id: String,\n        title: String,\n        artist: String,\n        duration: Int,\n        album: String?,\n        callback: (String) -> Unit,\n    ) {\n        getLyrics(id, title, artist, duration, album)\n            .onSuccess { lrcString ->\n                callback(lrcString)\n            }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/lyrics/LyricsProvider.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.lyrics\n\nimport android.content.Context\n\ninterface LyricsProvider {\n    val name: String\n\n    fun isEnabled(context: Context): Boolean\n\n    suspend fun getLyrics(\n        id: String,\n        title: String,\n        artist: String,\n        duration: Int,\n        album: String? = null,\n    ): Result<String>\n\n    suspend fun getAllLyrics(\n        id: String,\n        title: String,\n        artist: String,\n        duration: Int,\n        album: String? = null,\n        callback: (String) -> Unit,\n    ) {\n        getLyrics(id, title, artist, duration, album).onSuccess(callback)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/lyrics/LyricsProviderRegistry.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.lyrics\n\nobject LyricsProviderRegistry {\n    private val providerMap = mapOf(\n        \"BetterLyrics\" to BetterLyricsProvider,\n        \"SimpMusic\" to SimpMusicLyricsProvider,\n        \"LrcLib\" to LrcLibLyricsProvider,\n        \"KuGou\" to KuGouLyricsProvider,\n        \"LyricsPlus\" to LyricsPlusProvider,\n        \"YouTubeSubtitle\" to YouTubeSubtitleLyricsProvider,\n        \"YouTube\" to YouTubeLyricsProvider,\n    )\n\n    val providerNames = providerMap.keys.toList()\n\n    fun getProviderByName(name: String): LyricsProvider? = providerMap[name]\n\n    fun getProviderName(provider: LyricsProvider): String? =\n        providerMap.entries.find { it.value == provider }?.key\n\n    fun deserializeProviderOrder(orderString: String): List<String> {\n        if (orderString.isBlank()) {\n            return getDefaultProviderOrder()\n        }\n        return orderString.split(\",\").map { it.trim() }.filter { it in providerNames }\n    }\n\n    fun serializeProviderOrder(providers: List<String>): String {\n        return providers.filter { it in providerNames }.joinToString(\",\")\n    }\n\n    fun getDefaultProviderOrder(): List<String> = listOf(\n        \"BetterLyrics\",\n        \"SimpMusic\",\n        \"LrcLib\",\n        \"KuGou\",\n        \"LyricsPlus\",\n        \"YouTubeSubtitle\",\n        \"YouTube\",\n    )\n\n    fun getOrderedProviders(orderString: String): List<LyricsProvider> {\n        val order = deserializeProviderOrder(orderString)\n        return order.mapNotNull { getProviderByName(it) }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/lyrics/LyricsTranslationHelper.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.lyrics\n\nimport android.content.Context\nimport com.metrolist.music.api.DeepLService\nimport com.metrolist.music.api.MistralService\nimport com.metrolist.music.api.OpenRouterService\nimport com.metrolist.music.api.OpenRouterStreamingService\nimport com.metrolist.music.constants.LanguageCodeToName\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.db.entities.LyricsEntity\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharedFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asSharedFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.launch\nimport timber.log.Timber\nimport java.util.Locale\n\nobject LyricsTranslationHelper {\n    private val _status = MutableStateFlow<TranslationStatus>(TranslationStatus.Idle)\n    val status: StateFlow<TranslationStatus> = _status.asStateFlow()\n\n    // Single source of truth for whether translations are currently active in the UI\n    private val _hasActiveTranslations = MutableStateFlow(false)\n    val hasActiveTranslations: StateFlow<Boolean> = _hasActiveTranslations.asStateFlow()\n\n    private val _manualTrigger =\n        MutableSharedFlow<Unit>(\n            extraBufferCapacity = 1,\n            onBufferOverflow = kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST,\n        )\n    val manualTrigger: SharedFlow<Unit> = _manualTrigger.asSharedFlow()\n\n    private val _clearTranslationsTrigger =\n        MutableSharedFlow<Unit>(\n            extraBufferCapacity = 1,\n            onBufferOverflow = kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST,\n        )\n    val clearTranslationsTrigger: SharedFlow<Unit> = _clearTranslationsTrigger.asSharedFlow()\n\n    private val _translationSaved =\n        MutableSharedFlow<Unit>(\n            extraBufferCapacity = 1,\n            onBufferOverflow = kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST,\n        )\n    val translationSaved: SharedFlow<Unit> = _translationSaved.asSharedFlow()\n\n    private var translationJob: Job? = null\n    private var isCompositionActive = true\n\n    // Cache for translations: key = hash of (lyrics content + mode + language), value = list of translations\n    private val translationCache = mutableMapOf<String, List<String>>()\n\n    private fun getCacheKey(\n        lyricsText: String,\n        mode: String,\n        language: String,\n    ): String = \"${lyricsText.hashCode()}_${mode}_$language\"\n\n    /**\n     * Try to parse partial JSON array from streaming content\n     * Returns whatever complete lines we can extract so far\n     */\n    private fun tryParsePartialTranslation(\n        content: String,\n        expectedCount: Int,\n    ): List<String> {\n        // Look for opening bracket\n        val startIdx = content.indexOf('[')\n        if (startIdx == -1) return emptyList()\n\n        // Try to find complete string entries in the array\n        val result = mutableListOf<String>()\n        var pos = startIdx + 1\n        var inString = false\n        var escaping = false\n        val currentString = StringBuilder()\n\n        while (pos < content.length && result.size < expectedCount) {\n            val char = content[pos]\n\n            when {\n                escaping -> {\n                    currentString.append(char)\n                    escaping = false\n                }\n\n                char == '\\\\' && inString -> {\n                    currentString.append(char)\n                    escaping = true\n                }\n\n                char == '\"' -> {\n                    if (inString) {\n                        // End of string - we have a complete entry\n                        result.add(currentString.toString())\n                        currentString.clear()\n                        inString = false\n                    } else {\n                        // Start of string\n                        inString = true\n                    }\n                }\n\n                inString -> {\n                    currentString.append(char)\n                }\n\n                char == ']' -> {\n                    // End of array\n                    break\n                }\n            }\n            pos++\n        }\n\n        return result\n    }\n\n    fun getCachedTranslations(\n        lyrics: List<LyricsEntry>,\n        mode: String,\n        language: String,\n    ): List<String>? {\n        val lyricsText = lyrics.filter { it.text.isNotBlank() }.joinToString(\"\\n\") { it.text }\n        val key = getCacheKey(lyricsText, mode, language)\n        return translationCache[key]\n    }\n\n    fun applyCachedTranslations(\n        lyrics: List<LyricsEntry>,\n        mode: String,\n        language: String,\n    ): Boolean {\n        val cached = getCachedTranslations(lyrics, mode, language) ?: return false\n        val nonEmptyEntries =\n            lyrics.mapIndexedNotNull { index, entry ->\n                if (entry.text.isNotBlank()) index to entry else null\n            }\n\n        if (cached.size >= nonEmptyEntries.size) {\n            nonEmptyEntries.forEachIndexed { idx, (originalIndex, _) ->\n                lyrics[originalIndex].translatedTextFlow.value = cached[idx]\n            }\n            return true\n        }\n        return false\n    }\n\n    fun triggerManualTranslation() {\n        _manualTrigger.tryEmit(Unit)\n    }\n\n    fun triggerClearTranslations() {\n        _hasActiveTranslations.value = false\n        _clearTranslationsTrigger.tryEmit(Unit)\n    }\n\n    fun hasTranslations(lyricsEntity: LyricsEntity?): Boolean = !lyricsEntity?.translatedLyrics.isNullOrBlank()\n\n    fun clearTranslations(lyricsEntity: LyricsEntity): LyricsEntity =\n        lyricsEntity.copy(\n            translatedLyrics = \"\",\n            translationLanguage = \"\",\n            translationMode = \"\",\n        )\n\n    fun resetStatus() {\n        _status.value = TranslationStatus.Idle\n    }\n\n    fun clearCache() {\n        translationCache.clear()\n    }\n\n    fun setCompositionActive(active: Boolean) {\n        isCompositionActive = active\n    }\n\n    fun cancelTranslation() {\n        isCompositionActive = false\n        translationJob?.cancel()\n        translationJob = null\n    }\n\n    /**\n     * Load translations from database into lyrics entries\n     */\n    fun loadTranslationsFromDatabase(\n        lyrics: List<LyricsEntry>,\n        lyricsEntity: LyricsEntity?,\n        targetLanguage: String,\n        mode: String,\n    ) {\n        // Always clear translations first\n        lyrics.forEach { it.translatedTextFlow.value = null }\n\n        // Only load if all conditions are met\n        if (lyricsEntity?.translatedLyrics.isNullOrBlank()) {\n            _hasActiveTranslations.value = false\n            return\n        }\n        if (lyricsEntity.translationLanguage != targetLanguage) {\n            _hasActiveTranslations.value = false\n            return\n        }\n        if (lyricsEntity.translationMode != mode) {\n            _hasActiveTranslations.value = false\n            return\n        }\n\n        val translatedLines = lyricsEntity.translatedLyrics.lines()\n        val nonEmptyEntries =\n            lyrics.mapIndexedNotNull { index, entry ->\n                if (entry.text.isNotBlank()) index to entry else null\n            }\n\n        nonEmptyEntries.forEachIndexed { idx, (originalIndex, _) ->\n            if (idx < translatedLines.size) {\n                lyrics[originalIndex].translatedTextFlow.value = translatedLines[idx]\n            }\n        }\n\n        // Also populate the cache with these translations so future re-translations don't need API calls\n        // This ensures translations persist through app restarts (loaded from DB) without wasting API calls\n        val lyricsText = lyrics.filter { it.text.isNotBlank() }.joinToString(\"\\n\") { it.text }\n        val cacheKey = getCacheKey(lyricsText, mode, targetLanguage)\n        translationCache[cacheKey] = translatedLines\n        _hasActiveTranslations.value = true\n    }\n\n    fun translateLyrics(\n        lyrics: List<LyricsEntry>,\n        targetLanguage: String,\n        apiKey: String,\n        baseUrl: String,\n        model: String,\n        mode: String,\n        scope: CoroutineScope,\n        context: Context,\n        provider: String = \"OpenRouter\",\n        deeplApiKey: String = \"\",\n        deeplFormality: String = \"default\",\n        useStreaming: Boolean = true,\n        songId: String = \"\",\n        database: MusicDatabase? = null,\n        systemPrompt: String = \"\",\n    ) {\n        translationJob?.cancel()\n        _status.value = TranslationStatus.Translating\n\n        // Clear existing translations to indicate re-translation\n        lyrics.forEach { it.translatedTextFlow.value = null }\n\n        translationJob =\n            scope.launch(Dispatchers.IO) {\n                try {\n                    // Validate inputs\n                    val effectiveApiKey = if (provider == \"DeepL\") deeplApiKey else apiKey\n                    if (effectiveApiKey.isBlank()) {\n                        _status.value = TranslationStatus.Error(context.getString(com.metrolist.music.R.string.ai_error_api_key_required))\n                        return@launch\n                    }\n\n                    if (lyrics.isEmpty()) {\n                        _status.value = TranslationStatus.Error(context.getString(com.metrolist.music.R.string.ai_error_no_lyrics))\n                        return@launch\n                    }\n\n                    // Filter out empty lines and keep track of their indices\n                    val nonEmptyEntries =\n                        lyrics.mapIndexedNotNull { index, entry ->\n                            if (entry.text.isNotBlank()) index to entry else null\n                        }\n\n                    if (nonEmptyEntries.isEmpty()) {\n                        _status.value = TranslationStatus.Error(context.getString(com.metrolist.music.R.string.ai_error_lyrics_empty))\n                        return@launch\n                    }\n\n                    // Create text from non-empty lines only\n                    val fullText = nonEmptyEntries.joinToString(\"\\n\") { it.second.text }\n\n                    // Check cache first\n                    val cacheKey = getCacheKey(fullText, mode, targetLanguage)\n                    val cachedTranslations = translationCache[cacheKey]\n                    if (cachedTranslations != null && cachedTranslations.size >= nonEmptyEntries.size) {\n                        // Use cached translations\n                        nonEmptyEntries.forEachIndexed { idx, (originalIndex, _) ->\n                            if (idx < cachedTranslations.size) {\n                                lyrics[originalIndex].translatedTextFlow.value = cachedTranslations[idx]\n                            }\n                        }\n                        _hasActiveTranslations.value = true\n                        _status.value = TranslationStatus.Success\n\n                        // Persist cached translations to DB so loadTranslationsFromDatabase can't\n                        // overwrite them with a stale empty entity (e.g. after an untranslate race).\n                        if (songId.isNotBlank() && database != null) {\n                            try {\n                                val currentLyrics = database.lyrics(songId).first()\n                                if (currentLyrics != null && currentLyrics.translatedLyrics.isNullOrBlank()) {\n                                    database.query {\n                                        upsert(\n                                            currentLyrics.copy(\n                                                translatedLyrics = cachedTranslations.joinToString(\"\\n\"),\n                                                translationLanguage = targetLanguage,\n                                                translationMode = mode,\n                                            ),\n                                        )\n                                    }\n                                    _translationSaved.tryEmit(Unit)\n                                }\n                            } catch (e: Exception) {\n                                Timber.e(e, \"Failed to persist cached translations to database\")\n                            }\n                        }\n\n                        delay(3000)\n                        if (_status.value is TranslationStatus.Success && isCompositionActive) {\n                            _status.value = TranslationStatus.Idle\n                        }\n                        return@launch\n                    }\n\n                    // Validate language for all modes\n                    if (targetLanguage.isBlank()) {\n                        _status.value = TranslationStatus.Error(context.getString(com.metrolist.music.R.string.ai_error_language_required))\n                        return@launch\n                    }\n\n                    // Convert language code to full language name for better AI understanding\n                    val fullLanguageName =\n                        LanguageCodeToName[targetLanguage]\n                            ?: try {\n                                Locale.forLanguageTag(targetLanguage).displayLanguage.takeIf { it.isNotBlank() && it != targetLanguage }\n                            } catch (e: Exception) {\n                                null\n                            }\n                            ?: targetLanguage\n\n                    val result =\n                        if (provider == \"DeepL\") {\n                            Timber.d(\"Using DeepL for translation\")\n                            // DeepL only supports translation mode\n                            DeepLService.translate(\n                                text = fullText,\n                                targetLanguage = targetLanguage,\n                                apiKey = deeplApiKey,\n                                formality = deeplFormality,\n                            )\n                        } else if (provider == \"Mistral\") {\n                            Timber.d(\"Using Mistral for translation\")\n                            // Use Mistral API directly\n                            MistralService.translate(\n                                text = fullText,\n                                targetLanguage = fullLanguageName,\n                                apiKey = apiKey,\n                                model = model,\n                                mode = mode,\n                                customSystemPrompt = systemPrompt,\n                            )\n                        } else if (useStreaming && provider != \"Custom\") {\n                            Timber.d(\"Using streaming for translation with provider: $provider\")\n                            // Use streaming for supported providers\n                            var translatedLines: List<String>? = null\n                            var hasError = false\n                            var errorMessage = \"\"\n                            val contentAccumulator = StringBuilder()\n\n                            OpenRouterStreamingService\n                                .streamTranslation(\n                                    text = fullText,\n                                    targetLanguage = fullLanguageName,\n                                    apiKey = apiKey,\n                                    baseUrl = baseUrl,\n                                    model = model,\n                                    mode = mode,\n                                    customSystemPrompt = systemPrompt,\n                                ).collect { chunk ->\n                                    Timber.v(\"Received streaming chunk: $chunk\")\n                                    when (chunk) {\n                                        is OpenRouterStreamingService.StreamChunk.Content -> {\n                                            // Accumulate content for progressive parsing\n                                            contentAccumulator.append(chunk.text)\n\n                                            // Try to parse partial content and update UI progressively\n                                            val partialContent = contentAccumulator.toString()\n                                            val partialResult = tryParsePartialTranslation(partialContent, nonEmptyEntries.size)\n                                            if (partialResult.isNotEmpty()) {\n                                                // Update lyrics with partial translations as they become available\n                                                partialResult.forEachIndexed { idx, translation ->\n                                                    if (idx < nonEmptyEntries.size && translation.isNotBlank()) {\n                                                        val originalIndex = nonEmptyEntries[idx].first\n                                                        lyrics[originalIndex].translatedTextFlow.value = translation\n                                                    }\n                                                }\n                                                _status.value = TranslationStatus.Translating\n                                            }\n                                        }\n\n                                        is OpenRouterStreamingService.StreamChunk.Complete -> {\n                                            Timber.d(\"Streaming complete with ${chunk.translatedLines.size} lines\")\n                                            translatedLines = chunk.translatedLines\n                                        }\n\n                                        is OpenRouterStreamingService.StreamChunk.Error -> {\n                                            Timber.e(\"Streaming error: ${chunk.message}\")\n                                            hasError = true\n                                            errorMessage = chunk.message\n                                        }\n                                    }\n                                }\n\n                            Timber.d(\"Streaming collection complete. hasError=$hasError, translatedLines=${translatedLines?.size}\")\n                            if (hasError) {\n                                Result.failure(Exception(errorMessage))\n                            } else if (translatedLines != null) {\n                                Result.success(translatedLines)\n                            } else {\n                                Result.failure(Exception(\"No translation received\"))\n                            }\n                        } else {\n                            Timber.d(\"Using non-streaming for translation\")\n                            // Use non-streaming for Custom provider or when streaming is disabled\n                            OpenRouterService.translate(\n                                text = fullText,\n                                targetLanguage = fullLanguageName,\n                                apiKey = apiKey,\n                                baseUrl = baseUrl,\n                                model = model,\n                                mode = mode,\n                                customSystemPrompt = systemPrompt,\n                            )\n                        }\n\n                    result\n                        .onSuccess { translatedLines ->\n                            // Check if composition is still active before updating state\n                            if (!isCompositionActive) {\n                                return@onSuccess\n                            }\n\n                            // Cache the translations\n                            val cacheKey = getCacheKey(fullText, mode, targetLanguage)\n                            translationCache[cacheKey] = translatedLines\n\n                            // Save to database if songId is provided\n                            if (songId.isNotBlank() && database != null) {\n                                scope.launch(Dispatchers.IO) {\n                                    try {\n                                        val currentLyrics = database.lyrics(songId).first()\n                                        if (currentLyrics != null) {\n                                            database.query {\n                                                upsert(\n                                                    currentLyrics.copy(\n                                                        translatedLyrics = translatedLines.joinToString(\"\\n\"),\n                                                        translationLanguage = targetLanguage,\n                                                        translationMode = mode,\n                                                    ),\n                                                )\n                                            }\n                                            // Signal that translations have been saved\n                                            _translationSaved.tryEmit(Unit)\n                                        }\n                                    } catch (e: Exception) {\n                                        timber.log.Timber.e(e, \"Failed to save translated lyrics to database\")\n                                    }\n                                }\n                            }\n\n                            // Map translations back to original non-empty entries only\n                            val expectedCount = nonEmptyEntries.size\n\n                            when {\n                                translatedLines.size >= expectedCount -> {\n                                    // Perfect match or more - map to non-empty entries\n                                    nonEmptyEntries.forEachIndexed { idx, (originalIndex, _) ->\n                                        lyrics[originalIndex].translatedTextFlow.value = translatedLines[idx]\n                                    }\n                                    _hasActiveTranslations.value = true\n                                    _status.value = TranslationStatus.Success\n                                }\n\n                                translatedLines.size < expectedCount -> {\n                                    // Fewer translations than expected - map what we have\n                                    translatedLines.forEachIndexed { idx, translation ->\n                                        if (idx < nonEmptyEntries.size) {\n                                            val originalIndex = nonEmptyEntries[idx].first\n                                            lyrics[originalIndex].translatedTextFlow.value = translation\n                                        }\n                                    }\n                                    _hasActiveTranslations.value = true\n                                    _status.value = TranslationStatus.Success\n                                }\n\n                                else -> {\n                                    _status.value =\n                                        TranslationStatus.Error(context.getString(com.metrolist.music.R.string.ai_error_unexpected))\n                                }\n                            }\n\n                            // Auto-hide success message after 3 seconds\n                            delay(3000)\n                            if (_status.value is TranslationStatus.Success && isCompositionActive) {\n                                _status.value = TranslationStatus.Idle\n                            }\n                        }.onFailure { error ->\n                            if (!isCompositionActive) {\n                                return@onFailure\n                            }\n\n                            val errorMessage = error.message ?: context.getString(com.metrolist.music.R.string.ai_error_unknown)\n\n                            // Show error in UI\n                            _status.value = TranslationStatus.Error(errorMessage)\n                        }\n                } catch (e: Exception) {\n                    // Ignore cancellation exceptions or if composition is no longer active\n                    if (e !is kotlinx.coroutines.CancellationException && isCompositionActive) {\n                        val errorMessage = e.message ?: context.getString(com.metrolist.music.R.string.ai_error_translation_failed)\n                        _status.value = TranslationStatus.Error(errorMessage)\n                    }\n                }\n            }\n    }\n\n    sealed class TranslationStatus {\n        data object Idle : TranslationStatus()\n\n        data object Translating : TranslationStatus()\n\n        data object Success : TranslationStatus()\n\n        data class Error(\n            val message: String,\n        ) : TranslationStatus()\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/lyrics/LyricsUtils.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.lyrics\n\nimport android.text.format.DateUtils\nimport com.atilika.kuromoji.ipadic.Tokenizer\nimport com.github.promeg.pinyinhelper.Pinyin\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport java.util.Locale\n\n@Suppress(\"RegExpRedundantEscape\")\nobject LyricsUtils {\n    val LINE_REGEX = \"((\\\\[\\\\d\\\\d:\\\\d\\\\d\\\\.\\\\d{2,3}\\\\] ?)+)(.+)\".toRegex()\n    val TIME_REGEX = \"\\\\[(\\\\d\\\\d):(\\\\d\\\\d)\\\\.(\\\\d{2,3})\\\\]\".toRegex()\n\n    fun cleanTitleForSearch(title: String): String {\n        return title.replace(Regex(\"\\\\s*[(\\\\[].*?[)\\\\]]\"), \"\").trim()\n    }\n\n    // Regex for rich sync format: [MM:SS.mm]<MM:SS.mm> word <MM:SS.mm> word ...\n    private val RICH_SYNC_LINE_REGEX = \"\\\\[(\\\\d{1,2}):(\\\\d{2})\\\\.(\\\\d{2,3})\\\\](.+)\".toRegex()\n    private val RICH_SYNC_WORD_REGEX = \"<(\\\\d{1,2}):(\\\\d{2})\\\\.(\\\\d{2,3})>\\\\s*([^<]+)\".toRegex()\n\n    // Regex for agent and background markers\n    private val AGENT_REGEX = \"\\\\{agent:([^}]+)\\\\}\".toRegex()\n    private val BACKGROUND_REGEX = \"^\\\\{bg\\\\}\".toRegex()\n\n    private val KANA_ROMAJI_MAP: Map<String, String> = mapOf(\n        // Digraphs (Yōon - combinations like kya, sho)\n        \"キャ\" to \"kya\", \"キュ\" to \"kyu\", \"キョ\" to \"kyo\",\n        \"シャ\" to \"sha\", \"シュ\" to \"shu\", \"ショ\" to \"sho\",\n        \"チャ\" to \"cha\", \"チュ\" to \"chu\", \"チョ\" to \"cho\",\n        \"ニャ\" to \"nya\", \"ニュ\" to \"nyu\", \"ニョ\" to \"nyo\",\n        \"ヒャ\" to \"hya\", \"ヒュ\" to \"hyu\", \"ヒョ\" to \"hyo\",\n        \"ミャ\" to \"mya\", \"ミュ\" to \"myu\", \"ミョ\" to \"myo\",\n        \"リャ\" to \"rya\", \"リュ\" to \"ryu\", \"リョ\" to \"ryo\",\n        \"ギャ\" to \"gya\", \"ギュ\" to \"gyu\", \"ギョ\" to \"gyo\",\n        \"ジャ\" to \"ja\", \"ジュ\" to \"ju\", \"ジョ\" to \"jo\",\n        \"ヂャ\" to \"ja\", \"ヂュ\" to \"ju\", \"ヂョ\" to \"jo\",\n        \"ビャ\" to \"bya\", \"ビュ\" to \"byu\", \"ビョ\" to \"byo\",\n        \"ピャ\" to \"pya\", \"ピュ\" to \"pyu\", \"ピョ\" to \"pyo\",\n        // Basic Katakana Characters\n        \"ア\" to \"a\", \"イ\" to \"i\", \"ウ\" to \"u\", \"エ\" to \"e\", \"オ\" to \"o\",\n        \"カ\" to \"ka\", \"キ\" to \"ki\", \"ク\" to \"ku\", \"ケ\" to \"ke\", \"コ\" to \"ko\",\n        \"サ\" to \"sa\", \"シ\" to \"shi\", \"ス\" to \"su\", \"セ\" to \"se\", \"ソ\" to \"so\",\n        \"タ\" to \"ta\", \"チ\" to \"chi\", \"ツ\" to \"tsu\", \"テ\" to \"te\", \"ト\" to \"to\",\n        \"ナ\" to \"na\", \"ニ\" to \"ni\", \"ヌ\" to \"nu\", \"ネ\" to \"ne\", \"ノ\" to \"no\",\n        \"ハ\" to \"ha\", \"ヒ\" to \"hi\", \"フ\" to \"fu\", \"ヘ\" to \"he\", \"ホ\" to \"ho\",\n        \"マ\" to \"ma\", \"ミ\" to \"mi\", \"ム\" to \"mu\", \"メ\" to \"me\", \"モ\" to \"mo\",\n        \"ヤ\" to \"ya\", \"ユ\" to \"yu\", \"ヨ\" to \"yo\",\n        \"ラ\" to \"ra\", \"リ\" to \"ri\", \"ル\" to \"ru\", \"レ\" to \"re\", \"ロ\" to \"ro\",\n        \"ワ\" to \"wa\", \"ヲ\" to \"o\", \"ン\" to \"n\",\n        // Dakuten (voiced consonants)\n        \"ガ\" to \"ga\", \"ギ\" to \"gi\", \"グ\" to \"gu\", \"ゲ\" to \"ge\", \"ゴ\" to \"go\",\n        \"ザ\" to \"za\", \"ジ\" to \"ji\", \"ズ\" to \"zu\", \"ゼ\" to \"ze\", \"ゾ\" to \"zo\",\n        \"ダ\" to \"da\", \"ヂ\" to \"ji\", \"ヅ\" to \"zu\", \"デ\" to \"de\", \"ド\" to \"do\",\n        // Handakuten (p-sounds for 'h' group)\n        \"バ\" to \"ba\", \"ビ\" to \"bi\", \"ブ\" to \"bu\", \"ベ\" to \"be\", \"ボ\" to \"bo\",\n        \"パ\" to \"pa\", \"ピ\" to \"pi\", \"プ\" to \"pu\", \"ペ\" to \"pe\", \"ポ\" to \"po\",\n        // Chōonpu (long vowel mark)\n        \"ー\" to \"\"\n    )\n\n    private val HANGUL_ROMAJA_MAP: Map<String, Map<String, String>> = mapOf(\n        \"cho\" to mapOf(\n            \"ᄀ\" to \"g\", \"ᄁ\" to \"kk\", \"ᄂ\" to \"n\", \"ᄃ\" to \"d\",\n            \"ᄄ\" to \"tt\", \"ᄅ\" to \"r\", \"ᄆ\" to \"m\", \"ᄇ\" to \"b\",\n            \"ᄈ\" to \"pp\", \"ᄉ\" to \"s\", \"ᄊ\" to \"ss\", \"ᄋ\" to \"\",\n            \"ᄌ\" to \"j\", \"ᄍ\" to \"jj\", \"ᄎ\" to \"ch\", \"ᄏ\" to \"k\",\n            \"ᄐ\" to \"t\", \"ᄑ\" to \"p\", \"ᄒ\" to \"h\"\n        ),\n        \"jung\" to mapOf(\n            \"ᅡ\" to \"a\", \"ᅢ\" to \"ae\", \"ᅣ\" to \"ya\", \"ᅤ\" to \"yae\",\n            \"ᅥ\" to \"eo\", \"ᅦ\" to \"e\", \"ᅧ\" to \"yeo\", \"ᅨ\" to \"ye\",\n            \"ᅩ\" to \"o\", \"ᅪ\" to \"wa\", \"ᅫ\" to \"wae\", \"ᅬ\" to \"oe\",\n            \"ᅭ\" to \"yo\", \"ᅮ\" to \"u\", \"ᅯ\" to \"wo\", \"ᅰ\" to \"we\",\n            \"ᅱ\" to \"wi\", \"ᅲ\" to \"yu\", \"ᅳ\" to \"eu\", \"ᅴ\" to \"eui\",\n            \"ᅵ\" to \"i\"\n        ),\n        \"jong\" to mapOf(\n            \"ᆨ\" to \"k\", \"ᆨᄋ\" to \"g\", \"ᆨᄂ\" to \"ngn\", \"ᆨᄅ\" to \"ngn\", \"ᆨᄆ\" to \"ngm\", \"ᆨᄒ\" to \"kh\",\n            \"ᆩ\" to \"kk\", \"ᆩᄋ\" to \"kg\", \"ᆩᄂ\" to \"ngn\", \"ᆩᄅ\" to \"ngn\", \"ᆩᄆ\" to \"ngm\", \"ᆩᄒ\" to \"kh\",\n            \"ᆪ\" to \"k\", \"ᆪᄋ\" to \"ks\", \"ᆪᄂ\" to \"ngn\", \"ᆪᄅ\" to \"ngn\", \"ᆪᄆ\" to \"ngm\", \"ᆪᄒ\" to \"kch\",\n            \"ᆫ\" to \"n\", \"ᆫᄅ\" to \"ll\", \"ᆬ\" to \"n\", \"ᆬᄋ\" to \"nj\", \"ᆬᄂ\" to \"nn\", \"ᆬᄅ\" to \"nn\",\n            \"ᆬᄆ\" to \"nm\", \"ᆬㅎ\" to \"nch\", \"ᆭ\" to \"n\", \"ᆭᄋ\" to \"nh\", \"ᆭᄅ\" to \"nn\", \"ᆮ\" to \"t\",\n            \"ᆮᄋ\" to \"d\", \"ᆮᄂ\" to \"nn\", \"ᆮᄅ\" to \"nn\", \"ᆮᄆ\" to \"nm\", \"ᆮᄒ\" to \"th\", \"ᆯ\" to \"l\",\n            \"ᆯᄋ\" to \"r\", \"ᆯᄂ\" to \"ll\", \"ᆯᄅ\" to \"ll\", \"ᆰ\" to \"k\", \"ᆰᄋ\" to \"lg\", \"ᆰᄂ\" to \"ngn\",\n            \"ᆰᄅ\" to \"ngn\", \"ᆰᄆ\" to \"ngm\", \"ᆰᄒ\" to \"lkh\", \"ᆱ\" to \"m\", \"ᆱᄋ\" to \"lm\", \"ᆱᄂ\" to \"mn\",\n            \"ᆱᄅ\" to \"mn\", \"ᆱᄆ\" to \"mm\", \"ᆱᄒ\" to \"lmh\", \"ᆲ\" to \"p\", \"ᆲᄋ\" to \"lb\", \"ᆲᄂ\" to \"mn\",\n            \"ᆲᄅ\" to \"mn\", \"ᆲᄆ\" to \"mm\", \"ᆲᄒ\" to \"lph\", \"ᆳ\" to \"t\", \"ᆳᄋ\" to \"ls\", \"ᆳᄂ\" to \"nn\",\n            \"ᆳᄅ\" to \"nn\", \"ᆳᄆ\" to \"nm\", \"ᆳᄒ\" to \"lsh\", \"ᆴ\" to \"t\", \"ᆴᄋ\" to \"lt\", \"ᆴᄂ\" to \"nn\",\n            \"ᆴᄅ\" to \"nn\", \"ᆴᄆ\" to \"nm\", \"ᆴᄒ\" to \"lth\", \"ᆵ\" to \"p\", \"ᆵᄋ\" to \"lp\", \"ᆵᄂ\" to \"mn\",\n            \"ᆵᄅ\" to \"mn\", \"ᆵᄆ\" to \"mm\", \"ᆵᄒ\" to \"lph\", \"ᆶ\" to \"l\", \"ᆶᄋ\" to \"lh\", \"ᆶᄂ\" to \"ll\",\n            \"ᆶᄅ\" to \"ll\", \"ᆶᄆ\" to \"lm\", \"ᆶᄒ\" to \"lh\", \"ᆷ\" to \"m\", \"ᆷᄅ\" to \"mn\", \"ᆸ\" to \"p\",\n            \"ᆸᄋ\" to \"b\", \"ᆸᄂ\" to \"mn\", \"ᆸᄅ\" to \"mn\", \"ᆸᄆ\" to \"mm\", \"ᆸᄒ\" to \"ph\", \"ᆹ\" to \"p\",\n            \"ᆹᄋ\" to \"ps\", \"ᆹᄂ\" to \"mn\", \"ᆹᄅ\" to \"mn\", \"ᆹᄆ\" to \"mm\", \"ᆹᄒ\" to \"psh\", \"ᆺ\" to \"t\",\n            \"ᆺᄋ\" to \"s\", \"ᆺᄂ\" to \"nn\", \"ᆺᄅ\" to \"nn\", \"ᆺᄆ\" to \"nm\", \"ᆺᄒ\" to \"sh\", \"ᆻ\" to \"t\",\n            \"ᆻᄋ\" to \"ss\", \"ᆻᄂ\" to \"tn\", \"ᆻᄅ\" to \"tn\", \"ᆻᄆ\" to \"nm\", \"ᆻᄒ\" to \"th\", \"ᆼ\" to \"ng\",\n            \"ᆽ\" to \"t\", \"ᆽᄋ\" to \"j\", \"ᆽᄂ\" to \"nn\", \"ᆽᄅ\" to \"nn\", \"ᆽᄆ\" to \"nm\", \"ᆽᄒ\" to \"ch\",\n            \"ᆾ\" to \"t\", \"ᆾᄋ\" to \"ch\", \"ᆾᄂ\" to \"nn\", \"ᆾᄅ\" to \"nn\", \"ᆾᄆ\" to \"nm\", \"ᆾᄒ\" to \"ch\",\n            \"ᆿ\" to \"k\", \"ᆿᄋ\" to \"k\", \"ᆿᄂ\" to \"ngn\", \"ᆿᄅ\" to \"ngn\", \"ᆿᄆ\" to \"ngm\", \"ᆿᄒ\" to \"kh\",\n            \"ᇀ\" to \"t\", \"ᇀᄋ\" to \"t\", \"ᇀᄂ\" to \"nn\", \"ᇀᄅ\" to \"nn\", \"ᇀᄆ\" to \"nm\", \"ᇀᄒ\" to \"th\",\n            \"ᇁ\" to \"p\", \"ᇁᄋ\" to \"p\", \"ᇁᄂ\" to \"mn\", \"ᇁᄅ\" to \"mn\", \"ᇁᄆ\" to \"mm\", \"ᇁᄒ\" to \"ph\",\n            \"ᇂ\" to \"t\", \"ᇂᄋ\" to \"h\", \"ᇂᄂ\" to \"nn\", \"ᇂᄅ\" to \"nn\", \"ᇂᄆ\" to \"mm\", \"ᇂᄒ\" to \"t\",\n            \"ᇂᄀ\" to \"k\"\n        )\n    )\n\n    private val DEVANAGARI_ROMAJI_MAP: Map<String, String> = mapOf(\n        \"अ\" to \"a\", \"आ\" to \"aa\", \"इ\" to \"i\", \"ई\" to \"ee\", \"उ\" to \"u\", \"ऊ\" to \"oo\",\n        \"ऋ\" to \"ri\", \"ए\" to \"e\", \"ऐ\" to \"ai\", \"ओ\" to \"o\", \"औ\" to \"au\",\n        \"क\" to \"k\", \"ख\" to \"kh\", \"ग\" to \"g\", \"घ\" to \"gh\", \"ङ\" to \"ng\",\n        \"च\" to \"ch\", \"छ\" to \"chh\", \"ज\" to \"j\", \"झ\" to \"jh\", \"ञ\" to \"ny\",\n        \"ट\" to \"t\", \"ठ\" to \"th\", \"ड\" to \"d\", \"ढ\" to \"dh\", \"ण\" to \"n\",\n        \"त\" to \"t\", \"थ\" to \"th\", \"द\" to \"d\", \"ध\" to \"dh\", \"न\" to \"n\",\n        \"प\" to \"p\", \"फ\" to \"ph\", \"ब\" to \"b\", \"भ\" to \"bh\", \"म\" to \"m\",\n        \"य\" to \"y\", \"र\" to \"r\", \"ल\" to \"l\", \"व\" to \"v\",\n        \"श\" to \"sh\", \"ष\" to \"sh\", \"स\" to \"s\", \"ह\" to \"h\",\n        \"क्ष\" to \"ksh\", \"त्र\" to \"tr\", \"ज्ञ\" to \"gy\", \"श्र\" to \"shr\",\n        \"ा\" to \"aa\", \"ि\" to \"i\", \"ी\" to \"ee\", \"ु\" to \"u\", \"ू\" to \"oo\",\n        \"ृ\" to \"ri\", \"े\" to \"e\", \"ै\" to \"ai\", \"ो\" to \"o\", \"ौ\" to \"au\",\n        \"ं\" to \"n\", \"ः\" to \"h\", \"ँ\" to \"n\", \"़\" to \"\", \"्\" to \"\",\n        \"०\" to \"0\", \"१\" to \"1\", \"२\" to \"2\", \"३\" to \"3\", \"४\" to \"4\",\n        \"५\" to \"5\", \"६\" to \"6\", \"७\" to \"7\", \"८\" to \"8\", \"९\" to \"9\",\n        \"ॐ\" to \"Om\", \"ऽ\" to \"\",\n        \"क़\" to \"q\", \"ख़\" to \"kh\", \"ग़\" to \"g\", \"ज़\" to \"z\", \"ड़\" to \"r\", \"ढ़\" to \"rh\", \"फ़\" to \"f\", \"य़\" to \"y\",\n        // Decomposed characters with Nukta\n        \"क\\u093C\" to \"q\", \"ख\\u093C\" to \"kh\", \"ग\\u093C\" to \"g\", \"ज\\u093C\" to \"z\", \"ड\\u093C\" to \"r\", \"ढ\\u093C\" to \"rh\", \"फ\\u093C\" to \"f\", \"य\\u093C\" to \"y\"\n    )\n\n    private val GURMUKHI_ROMAJI_MAP: Map<String, String> = mapOf(\n        \"ੳ\" to \"o\", \"ਅ\" to \"a\", \"ੲ\" to \"e\", \"ਸ\" to \"s\", \"ਹ\" to \"h\",\n        \"ਕ\" to \"k\", \"ਖ\" to \"kh\", \"ਗ\" to \"g\", \"ਘ\" to \"gh\", \"ਙ\" to \"ng\",\n        \"ਚ\" to \"ch\", \"ਛ\" to \"chh\", \"ਜ\" to \"j\", \"ਝ\" to \"jh\", \"ਞ\" to \"ny\",\n        \"ਟ\" to \"t\", \"ਠ\" to \"th\", \"ਡ\" to \"d\", \"ਢ\" to \"dh\", \"ਣ\" to \"n\",\n        \"ਤ\" to \"t\", \"ਥ\" to \"th\", \"ਦ\" to \"d\", \"ਧ\" to \"dh\", \"ਨ\" to \"n\",\n        \"ਪ\" to \"p\", \"ਫ\" to \"ph\", \"ਬ\" to \"b\", \"ਭ\" to \"bh\", \"ਮ\" to \"m\",\n        \"ਯ\" to \"y\", \"ਰ\" to \"r\", \"ਲ\" to \"l\", \"ਵ\" to \"v\", \"ੜ\" to \"r\",\n        \"ਸ਼\" to \"sh\", \"ਖ਼\" to \"kh\", \"ਗ਼\" to \"g\", \"ਜ਼\" to \"z\", \"ਫ਼\" to \"f\", \"ਲ਼\" to \"l\",\n        \"ਾ\" to \"aa\", \"ਿ\" to \"i\", \"ੀ\" to \"ee\", \"ੁ\" to \"u\", \"ੂ\" to \"oo\",\n        \"ੇ\" to \"e\", \"ੈ\" to \"ai\", \"ੋ\" to \"o\", \"ੌ\" to \"au\",\n        \"ੰ\" to \"n\", \"ਂ\" to \"n\", \"ੱ\" to \"\", \"੍\" to \"\", \"਼\" to \"\",\n        \"ੴ\" to \"Ek Onkar\",\n        \"੦\" to \"0\", \"੧\" to \"1\", \"੨\" to \"2\", \"੩\" to \"3\", \"੪\" to \"4\",\n        \"੫\" to \"5\", \"੬\" to \"6\", \"੭\" to \"7\", \"੮\" to \"8\", \"੯\" to \"9\"\n    )\n\n    private val GENERAL_CYRILLIC_ROMAJI_MAP: Map<String, String> = mapOf(\n        \"А\" to \"A\", \"Б\" to \"B\", \"В\" to \"V\", \"Г\" to \"G\", \"Ґ\" to \"G\", \"Д\" to \"D\",\n        \"Ѓ\" to \"Ǵ\", \"Ђ\" to \"Đ\", \"Е\" to \"E\", \"Ё\" to \"Yo\", \"Є\" to \"Ye\", \"Ж\" to \"Zh\",\n        \"З\" to \"Z\", \"Ѕ\" to \"Dz\", \"И\" to \"I\", \"І\" to \"I\", \"Ї\" to \"Yi\", \"Й\" to \"Y\",\n        \"Ј\" to \"Y\", \"К\" to \"K\", \"Л\" to \"L\", \"Љ\" to \"Ly\", \"М\" to \"M\", \"Н\" to \"N\",\n        \"Њ\" to \"Ny\", \"О\" to \"O\", \"П\" to \"P\", \"Р\" to \"R\", \"С\" to \"S\", \"Т\" to \"T\",\n        \"Ћ\" to \"Ć\", \"У\" to \"U\", \"Ў\" to \"Ŭ\", \"Ф\" to \"F\", \"Х\" to \"Kh\", \"Ц\" to \"Ts\",\n        \"Ч\" to \"Ch\", \"Џ\" to \"Dž\", \"Ш\" to \"Sh\", \"Щ\" to \"Shch\", \"Ъ\" to \"ʺ\", \"Ы\" to \"Y\",\n        \"Ь\" to \"ʹ\", \"Э\" to \"E\", \"Ю\" to \"Yu\", \"Я\" to \"Ya\",\n        \"Ѡ\" to \"O\", \"Ѣ\" to \"Ya\", \"Ѥ\" to \"Ye\", \"Ѧ\" to \"Ya\", \"Ѩ\" to \"Ya\",\n        \"Ѫ\" to \"U\", \"Ѭ\" to \"Yu\", \"Ѯ\" to \"Ks\", \"Ѱ\" to \"Ps\", \"Ѳ\" to \"F\",\n        \"Ѵ\" to \"I\", \"Ѷ\" to \"I\", \"Ғ\" to \"Gh\", \"Ҕ\" to \"G\", \"Җ\" to \"Zh\",\n        \"Ҙ\" to \"Dz\", \"Қ\" to \"Q\", \"Ҝ\" to \"K\", \"Ҟ\" to \"K\", \"Ҡ\" to \"K\",\n        \"Ң\" to \"Ng\", \"Ҥ\" to \"Ng\", \"Ҧ\" to \"P\", \"Ҩ\" to \"O\", \"Ҫ\" to \"S\",\n        \"Ҭ\" to \"T\", \"Ү\" to \"U\", \"Ұ\" to \"U\", \"Ҳ\" to \"Kh\", \"Ҵ\" to \"Ts\",\n        \"Ҷ\" to \"Ch\", \"Ҹ\" to \"Ch\", \"Һ\" to \"H\", \"Ҽ\" to \"Ch\", \"Ҿ\" to \"Ch\",\n        \"Ќ\" to \"Ḱ\", \"Ө\" to \"Ö\",\n\n        \"а\" to \"a\", \"б\" to \"b\", \"в\" to \"v\", \"г\" to \"g\", \"ґ\" to \"g\", \"д\" to \"d\",\n        \"ѓ\" to \"ǵ\", \"ђ\" to \"đ\", \"е\" to \"e\", \"ё\" to \"yo\", \"є\" to \"ye\", \"ж\" to \"zh\",\n        \"з\" to \"z\", \"ѕ\" to \"dz\", \"и\" to \"i\", \"і\" to \"i\", \"ї\" to \"yi\", \"й\" to \"y\",\n        \"ј\" to \"y\", \"к\" to \"k\", \"л\" to \"l\", \"љ\" to \"ly\", \"м\" to \"m\", \"н\" to \"n\",\n        \"њ\" to \"ny\", \"о\" to \"o\", \"п\" to \"p\", \"р\" to \"r\", \"с\" to \"s\", \"т\" to \"t\",\n        \"ћ\" to \"ć\", \"у\" to \"u\", \"ў\" to \"ŭ\", \"ф\" to \"f\", \"х\" to \"kh\", \"ц\" to \"ts\",\n        \"ч\" to \"ch\", \"џ\" to \"dž\", \"ш\" to \"sh\", \"щ\" to \"shch\", \"ъ\" to \"ʺ\", \"ы\" to \"y\",\n        \"ь\" to \"ʹ\", \"э\" to \"e\", \"ю\" to \"yu\", \"я\" to \"ya\",\n        \"ѡ\" to \"o\", \"ѣ\" to \"ya\", \"ѥ\" to \"ye\", \"ѧ\" to \"ya\", \"ѩ\" to \"ya\",\n        \"ѫ\" to \"u\", \"ѭ\" to \"yu\", \"ѯ\" to \"ks\", \"ѱ\" to \"ps\", \"ѳ\" to \"f\",\n        \"ѵ\" to \"i\", \"ѷ\" to \"i\", \"ғ\" to \"gh\", \"ҕ\" to \"g\", \"җ\" to \"zh\",\n        \"ҙ\" to \"dz\", \"қ\" to \"q\", \"ҝ\" to \"k\", \"ҟ\" to \"k\", \"ҡ\" to \"k\",\n        \"ң\" to \"ng\", \"ҥ\" to \"ng\", \"ҧ\" to \"p\", \"ҩ\" to \"o\", \"ҫ\" to \"s\",\n        \"ҭ\" to \"t\", \"ү\" to \"u\", \"ұ\" to \"u\", \"ҳ\" to \"kh\", \"ҵ\" to \"ts\",\n        \"ҷ\" to \"ch\", \"ҹ\" to \"ch\", \"һ\" to \"h\", \"ҽ\" to \"ch\", \"ҿ\" to \"ch\",\n        \"ќ\" to \"ḱ\", \"ө\" to \"ö\"\n    )\n\n    private val RUSSIAN_ROMAJI_MAP: Map<String, String> = mapOf(\n        \"ого\" to \"ovo\", \"Ого\" to \"Ovo\", \"его\" to \"evo\", \"Его\" to \"Evo\"\n    )\n\n    private val UKRAINIAN_ROMAJI_MAP: Map<String, String> = mapOf(\n        \"Г\" to \"H\", \"г\" to \"h\",\n        \"Ґ\" to \"G\", \"ґ\" to \"g\",\n        \"Є\" to \"Ye\", \"є\" to \"ye\",\n        \"І\" to \"I\", \"і\" to \"i\",\n        \"Ї\" to \"Yi\", \"ї\" to \"yi\"\n    )\n\n    private val SERBIAN_ROMAJI_MAP: Map<String, String> = mapOf(\n        \"Ж\" to \"Ž\", \"Љ\" to \"Lj\", \"Њ\" to \"Nj\", \"Ц\" to \"C\", \"Ч\" to \"Č\",\n        \"Џ\" to \"Dž\", \"Ш\" to \"Š\", \"Х\" to \"H\",\n\n        \"ж\" to \"ž\", \"љ\" to \"lj\", \"њ\" to \"nj\", \"ц\" to \"c\", \"ч\" to \"č\",\n        \"џ\" to \"dž\", \"ш\" to \"š\", \"х\" to \"h\"\n    )\n\n    private val BULGARIAN_ROMAJI_MAP: Map<String, String> = mapOf(\n        \"Ж\" to \"Zh\", \"Ц\" to \"Ts\", \"Ч\" to \"Ch\", \"Ш\" to \"Sh\", \"Щ\" to \"Sht\",\n        \"Ъ\" to \"A\", \"Ь\" to \"Y\", \"Ю\" to \"Yu\", \"Я\" to \"Ya\",\n\n        \"ж\" to \"zh\", \"ц\" to \"ts\", \"ч\" to \"ch\", \"ш\" to \"sh\", \"щ\" to \"sht\",\n        \"ъ\" to \"a\", \"ь\" to \"y\", \"ю\" to \"yu\", \"я\" to \"ya\"\n    )\n\n    private val BELARUSIAN_ROMAJI_MAP: Map<String, String> = mapOf(\n        \"Г\" to \"H\", \"г\" to \"h\", \"Ў\" to \"W\", \"ў\" to \"w\"\n    )\n\n    private val KYRGYZ_ROMAJI_MAP: Map<String, String> = mapOf(\n        \"Ү\" to \"Ü\", \"ү\" to \"ü\", \"Ы\" to \"Y\", \"ы\" to \"y\"\n    )\n\n    private val MACEDONIAN_ROMAJI_MAP: Map<String, String> = mapOf(\n        \"Ѓ\" to \"Gj\", \"Ѕ\" to \"Dz\", \"И\" to \"I\", \"Ј\" to \"J\", \"Љ\" to \"Lj\",\n        \"Њ\" to \"Nj\", \"Ќ\" to \"Kj\", \"Џ\" to \"Dž\", \"Ч\" to \"Č\", \"Ш\" to \"Sh\",\n        \"Ж\" to \"Zh\", \"Ц\" to \"C\", \"Х\" to \"H\",\n\n        \"ѓ\" to \"gj\", \"ѕ\" to \"dz\", \"и\" to \"i\", \"ј\" to \"j\", \"љ\" to \"lj\",\n        \"њ\" to \"nj\", \"ќ\" to \"kj\", \"џ\" to \"dž\", \"ч\" to \"č\", \"ш\" to \"sh\",\n        \"ж\" to \"zh\", \"ц\" to \"c\", \"х\" to \"h\"\n    )\n\n    private val RUSSIAN_CYRILLIC_LETTERS = setOf(\n        \"А\", \"Б\", \"В\", \"Г\", \"Д\", \"Е\", \"Ё\", \"Ж\", \"З\", \"И\", \"Й\", \"К\", \"Л\", \"М\", \"Н\",\n        \"О\", \"П\", \"Р\", \"С\", \"Т\", \"У\", \"Ф\", \"Х\", \"Ц\", \"Ч\", \"Ш\", \"Щ\", \"Ъ\", \"Ы\", \"Ь\",\n        \"Э\", \"Ю\", \"Я\",\n\n        \"а\", \"б\", \"в\", \"г\", \"д\", \"е\", \"ё\", \"ж\", \"з\", \"и\", \"й\", \"к\", \"л\", \"м\", \"н\",\n        \"о\", \"п\", \"р\", \"с\", \"т\", \"у\", \"ф\", \"х\", \"ц\", \"ч\", \"ш\", \"щ\", \"ъ\", \"ы\", \"ь\",\n        \"э\", \"ю\", \"я\"\n    )\n\n    private val UKRAINIAN_CYRILLIC_LETTERS = setOf(\n       \"А\", \"Б\", \"В\", \"Г\", \"Ґ\", \"Д\", \"Е\", \"Є\", \"Ж\", \"З\", \"И\", \"І\", \"Ї\", \"Й\",\n        \"К\", \"Л\", \"М\", \"Н\", \"О\", \"П\", \"Р\", \"С\", \"Т\", \"У\", \"Ф\", \"Х\", \"Ц\", \"Ч\",\n        \"Ш\", \"Щ\", \"Ь\", \"Ю\", \"Я\",\n\n        \"а\", \"б\", \"в\", \"г\", \"ґ\", \"д\", \"е\", \"є\", \"ж\", \"з\", \"и\", \"і\", \"ї\", \"й\",\n        \"к\", \"л\", \"м\", \"н\", \"о\", \"п\", \"р\", \"с\", \"т\", \"у\", \"ф\", \"х\", \"ц\", \"ч\",\n        \"ш\", \"щ\", \"ь\", \"ю\", \"я\"\n    )\n\n    private val SERBIAN_CYRILLIC_LETTERS = setOf(\n        \"А\", \"Б\", \"В\", \"Г\", \"Д\", \"Ђ\", \"Е\", \"Ж\", \"З\", \"И\", \"Ј\", \"К\", \"Л\", \"Љ\", \"М\",\n        \"Н\", \"Њ\", \"О\", \"П\", \"Р\", \"С\", \"Т\", \"Ћ\", \"У\", \"Ф\", \"Х\", \"Ц\", \"Ч\", \"Џ\", \"Ш\",\n\n        \"а\", \"б\", \"в\", \"г\", \"д\", \"ђ\", \"е\", \"ж\", \"з\", \"и\", \"ј\", \"к\", \"л\", \"љ\", \"м\",\n        \"н\", \"њ\", \"о\", \"п\", \"р\", \"с\", \"т\", \"ћ\", \"у\", \"ф\", \"х\", \"ц\", \"ч\", \"џ\", \"ш\"\n    )\n\n    private val BULGARIAN_CYRILLIC_LETTERS = setOf(\n        \"А\", \"Б\", \"В\", \"Г\", \"Д\", \"Е\", \"Ж\", \"З\", \"И\", \"Й\", \"К\", \"Л\", \"М\",\n        \"Н\", \"О\", \"П\", \"Р\", \"С\", \"Т\", \"У\", \"Ф\", \"Х\", \"Ц\", \"Ч\", \"Ш\", \"Щ\",\n        \"Ъ\", \"Ь\", \"Ю\", \"Я\",\n\n        \"а\", \"б\", \"в\", \"г\", \"д\", \"е\", \"ж\", \"з\", \"и\", \"й\", \"к\", \"л\", \"м\",\n        \"н\", \"о\", \"п\", \"р\", \"с\", \"т\", \"у\", \"ф\", \"х\", \"ц\", \"ч\", \"ш\", \"щ\",\n        \"ъ\", \"ь\", \"ю\", \"я\"\n    )\n\n    private val BELARUSIAN_CYRILLIC_LETTERS = setOf(\n        \"А\", \"Б\", \"В\", \"Г\", \"Д\", \"Е\", \"Ё\", \"Ж\", \"З\", \"І\", \"Й\", \"К\", \"Л\", \"М\", \"Н\",\n        \"О\", \"П\", \"Р\", \"С\", \"Т\", \"У\", \"Ў\", \"Ф\", \"Х\", \"Ц\", \"Ч\", \"Ш\", \"Ь\", \"Ю\", \"Я\",\n        \"Ы\", \"Э\",\n\n        \"а\", \"б\", \"в\", \"г\", \"д\", \"е\", \"ё\", \"ж\", \"з\", \"і\", \"й\", \"к\", \"л\", \"м\", \"н\",\n        \"о\", \"п\", \"р\", \"с\", \"т\", \"у\", \"ў\", \"ф\", \"х\", \"ц\", \"ч\", \"ш\", \"ь\", \"ю\", \"я\",\n        \"ы\", \"э\"\n    )\n\n    private val KYRGYZ_CYRILLIC_LETTERS = setOf(\n        \"А\", \"Б\", \"В\", \"Г\", \"Д\", \"Е\", \"Ё\", \"Ж\", \"З\", \"И\", \"Й\", \"К\", \"Л\", \"М\", \"Н\",\n        \"Ң\", \"О\", \"Ө\", \"П\", \"Р\", \"С\", \"Т\", \"У\", \"Ү\", \"Ф\", \"Х\", \"Ц\", \"Ч\", \"Ш\", \"Щ\",\n        \"Ъ\", \"Ы\", \"Ь\", \"Э\", \"Ю\", \"Я\",\n\n        \"а\", \"б\", \"в\", \"г\", \"д\", \"е\", \"ё\", \"ж\", \"з\", \"и\", \"й\", \"к\", \"л\", \"м\", \"н\",\n        \"ң\", \"о\", \"ө\", \"п\", \"р\", \"с\", \"т\", \"у\", \"ү\", \"ф\", \"х\", \"ц\", \"ч\", \"ш\", \"щ\",\n        \"ъ\", \"ы\", \"ь\", \"э\", \"ю\", \"я\"\n    )\n\n    private val MACEDONIAN_CYRILLIC_LETTERS = setOf(\n        \"А\", \"Б\", \"В\", \"Г\", \"Д\", \"Ѓ\", \"Е\", \"Ж\", \"З\", \"Ѕ\", \"И\", \"Ј\", \"К\", \"Л\",\n        \"Љ\", \"М\", \"Н\", \"Њ\", \"О\", \"П\", \"Р\", \"С\", \"Т\", \"Ќ\", \"У\", \"Ф\", \"Х\",\n        \"Ц\", \"Ч\", \"Џ\", \"Ш\",\n\n        \"а\", \"б\", \"в\", \"г\", \"д\", \"ѓ\", \"е\", \"ж\", \"з\", \"ѕ\", \"и\", \"ј\", \"к\", \"л\",\n        \"љ\", \"м\", \"н\", \"њ\", \"о\", \"п\", \"р\", \"с\", \"т\", \"ќ\", \"у\", \"ф\", \"х\",\n        \"ц\", \"ч\", \"џ\", \"ш\"\n    )\n\n    private val UKRAINIAN_SPECIFIC_CYRILLIC_LETTERS = setOf(\n        \"Ґ\", \"ґ\", \"Є\", \"є\", \"І\", \"і\", \"Ї\", \"ї\"\n    )\n\n    private val SERBIAN_SPECIFIC_CYRILLIC_LETTERS = setOf(\n        \"Ђ\", \"ђ\", \"Ј\", \"ј\", \"Љ\", \"љ\", \"Њ\", \"њ\", \"Ћ\", \"ћ\", \"Џ\", \"џ\"\n    )\n\n    private val BELARUSIAN_SPECIFIC_CYRILLIC_LETTERS = setOf(\n        \"Ў\", \"ў\", \"І\", \"і\"\n    )\n\n    private val KYRGYZ_SPECIFIC_CYRILLIC_LETTERS = setOf(\n        \"Ң\", \"ң\", \"Ө\", \"ө\", \"Ү\", \"ү\"\n    )\n\n    private val MACEDONIAN_SPECIFIC_CYRILLIC_LETTERS = setOf(\n        \"Ѓ\", \"ѓ\", \"Ѕ\", \"ѕ\", \"Ќ\", \"ќ\"\n    )\n\n    // Lazy initialized Tokenizer\n    private val kuromojiTokenizer: Tokenizer by lazy {\n        Tokenizer()\n    }\n\n    private val HEX_ENTITY_REGEX = \"&#x([0-9a-fA-F]+);\".toRegex()\n    private val DEC_ENTITY_REGEX = \"&#(\\\\d+);\".toRegex()\n\n    private fun decodeHtmlEntities(text: String): String =\n        text\n            .replace(HEX_ENTITY_REGEX) { match ->\n                match.groupValues[1].toIntOrNull(16)\n                    ?.takeIf { it in 0..0x10FFFF }\n                    ?.let { String(Character.toChars(it)) }\n                    ?: match.value\n            }\n            .replace(DEC_ENTITY_REGEX) { match ->\n                match.groupValues[1].toIntOrNull()\n                    ?.takeIf { it in 0..0x10FFFF }\n                    ?.let { String(Character.toChars(it)) }\n                    ?: match.value\n            }\n            .replace(\"&apos;\", \"'\")\n            .replace(\"&quot;\", \"\\\"\")\n            .replace(\"&lt;\", \"<\")\n            .replace(\"&gt;\", \">\")\n            .replace(\"&nbsp;\", \" \")\n            .replace(\"&amp;\", \"&\")\n\n    fun parseLyrics(lyrics: String): List<LyricsEntry> {\n        // Unescape JSON string if needed\n        val unescapedLyrics = lyrics\n            .trim()\n            .removePrefix(\"\\\"\")\n            .removeSuffix(\"\\\"\")\n            .replace(\"\\\\\\\\\", \"\\\\\")\n            .replace(\"\\\\n\", \"\\n\")\n            .replace(\"\\\\r\", \"\\r\")\n            .replace(\"\\\\t\", \"\\t\")\n\n        // Decode HTML entities (e.g. &#x27; -> ', &amp; -> &)\n        val decodedLyrics = decodeHtmlEntities(unescapedLyrics)\n\n        val lines = decodedLyrics.lines()\n            .filter { it.isNotBlank() && !it.trim().startsWith(\"[offset:\") }\n\n        // Check if this is rich sync format (contains <MM:SS.mm> patterns)\n        val isRichSync = lines.any { line ->\n            RICH_SYNC_LINE_REGEX.matches(line.trim()) &&\n            RICH_SYNC_WORD_REGEX.containsMatchIn(line)\n        }\n\n        return if (isRichSync) {\n            parseRichSyncLyrics(lines)\n        } else {\n            parseStandardLyrics(lines)\n        }\n    }\n\n    /**\n     * Parse rich sync lyrics format: [MM:SS.mm]<MM:SS.mm> word <MM:SS.mm> word ...\n     * This format provides word-by-word timing for karaoke-style highlighting\n     */\n    private fun parseRichSyncLyrics(lines: List<String>): List<LyricsEntry> {\n        val result = mutableListOf<LyricsEntry>()\n\n        lines.forEachIndexed { index, line ->\n            val matchResult = RICH_SYNC_LINE_REGEX.matchEntire(line.trim())\n            if (matchResult != null) {\n                val minutes = matchResult.groupValues[1].toLongOrNull() ?: 0L\n                val seconds = matchResult.groupValues[2].toLongOrNull() ?: 0L\n                val centiseconds = matchResult.groupValues[3].toLongOrNull() ?: 0L\n\n                // Convert to milliseconds\n                val millisPart = if (matchResult.groupValues[3].length == 3) centiseconds else centiseconds * 10\n                val lineTimeMs = minutes * DateUtils.MINUTE_IN_MILLIS + seconds * DateUtils.SECOND_IN_MILLIS + millisPart\n\n                var content = matchResult.groupValues[4].trimStart()\n\n                // Parse agent marker {agent:v1}\n                val agentMatch = AGENT_REGEX.find(content)\n                val agent = agentMatch?.groupValues?.get(1)\n                if (agentMatch != null) {\n                    content = content.replaceFirst(AGENT_REGEX, \"\")\n                }\n\n                // Parse background marker {bg}\n                val isBackground = BACKGROUND_REGEX.containsMatchIn(content)\n                if (isBackground) {\n                    content = content.replaceFirst(BACKGROUND_REGEX, \"\")\n                }\n\n                // Parse word-level timestamps from content\n                val wordTimings = parseRichSyncWords(content, index, lines)\n\n                // Extract plain text (remove all <MM:SS.mm> tags)\n                val plainText = content.replace(Regex(\"<\\\\d{1,2}:\\\\d{2}\\\\.\\\\d{2,3}>\\\\s*\"), \"\").trim()\n\n                if (plainText.isNotBlank()) {\n                    result.add(LyricsEntry(lineTimeMs, plainText, wordTimings, agent = agent, isBackground = isBackground))\n                }\n            }\n        }\n\n        return result.sorted()\n    }\n\n    /**\n     * Parse word timestamps from rich sync content\n     * Format: <MM:SS.mm> word <MM:SS.mm> word ...\n     */\n    private fun parseRichSyncWords(content: String, currentIndex: Int, allLines: List<String>): List<WordTimestamp>? {\n        val wordMatches = RICH_SYNC_WORD_REGEX.findAll(content).toList()\n\n        if (wordMatches.isEmpty()) return null\n\n        val wordTimings = mutableListOf<WordTimestamp>()\n\n        wordMatches.forEachIndexed { index, match ->\n            val minutes = match.groupValues[1].toLongOrNull() ?: 0L\n            val seconds = match.groupValues[2].toLongOrNull() ?: 0L\n            val fraction = match.groupValues[3].toLongOrNull() ?: 0L\n\n            // Convert to seconds (Double)\n            val fractionPart = if (match.groupValues[3].length == 3) fraction / 1000.0 else fraction / 100.0\n            val startTimeSeconds = minutes * 60.0 + seconds + fractionPart\n\n            val wordText = match.groupValues[4].trim()\n\n            // Calculate end time: use next word's start time, or estimate from next line\n            val endTimeSeconds = if (index < wordMatches.size - 1) {\n                val nextMatch = wordMatches[index + 1]\n                val nextMinutes = nextMatch.groupValues[1].toLongOrNull() ?: 0L\n                val nextSeconds = nextMatch.groupValues[2].toLongOrNull() ?: 0L\n                val nextFraction = nextMatch.groupValues[3].toLongOrNull() ?: 0L\n                val nextFractionPart = if (nextMatch.groupValues[3].length == 3) nextFraction / 1000.0 else nextFraction / 100.0\n                nextMinutes * 60.0 + nextSeconds + nextFractionPart\n            } else {\n                // For last word, try to get next line's start time or add a default duration\n                val nextLineTime = getNextLineStartTime(currentIndex, allLines)\n                nextLineTime ?: (startTimeSeconds + 0.5) // Default 500ms duration for last word\n            }\n\n            if (wordText.isNotBlank()) {\n                wordTimings.add(WordTimestamp(wordText, startTimeSeconds, endTimeSeconds))\n            }\n        }\n\n        return if (wordTimings.isNotEmpty()) wordTimings else null\n    }\n\n    /**\n     * Get the start time of the next line for calculating the last word's end time\n     */\n    private fun getNextLineStartTime(currentIndex: Int, allLines: List<String>): Double? {\n        if (currentIndex + 1 >= allLines.size) return null\n\n        val nextLine = allLines[currentIndex + 1].trim()\n        val matchResult = RICH_SYNC_LINE_REGEX.matchEntire(nextLine) ?: return null\n\n        val minutes = matchResult.groupValues[1].toLongOrNull() ?: return null\n        val seconds = matchResult.groupValues[2].toLongOrNull() ?: return null\n        val fraction = matchResult.groupValues[3].toLongOrNull() ?: 0L\n\n        val fractionPart = if (matchResult.groupValues[3].length == 3) fraction / 1000.0 else fraction / 100.0\n        return minutes * 60.0 + seconds + fractionPart\n    }\n\n    /**\n     * Parse standard synced lyrics format: [MM:SS.mm] text\n     */\n    private fun parseStandardLyrics(lines: List<String>): List<LyricsEntry> {\n        val result = mutableListOf<LyricsEntry>()\n\n        var i = 0\n        while (i < lines.size) {\n            val line = lines[i]\n            if (!line.trim().startsWith(\"<\") || !line.trim().endsWith(\">\")) {\n                val entries = parseLine(line, null)\n                if (entries != null) {\n                    val wordTimestamps = if (i + 1 < lines.size) {\n                        val nextLine = lines[i + 1]\n                        if (nextLine.trim().startsWith(\"<\") && nextLine.trim().endsWith(\">\")) {\n                            parseWordTimestamps(nextLine.trim().removeSurrounding(\"<\", \">\"))\n                        } else null\n                    } else null\n\n                    if (wordTimestamps != null) {\n                        result.addAll(entries.map { entry ->\n                            LyricsEntry(entry.time, entry.text, wordTimestamps, agent = entry.agent, isBackground = entry.isBackground)\n                        })\n                    } else {\n                        result.addAll(entries)\n                    }\n                }\n            }\n            i++\n        }\n        return result.sorted()\n    }\n\n    private fun parseWordTimestamps(data: String): List<WordTimestamp>? {\n        if (data.isBlank()) return null\n        return try {\n            data.split(\"|\").mapNotNull { wordData ->\n                val parts = wordData.split(\":\")\n                if (parts.size == 3) {\n                    WordTimestamp(\n                        text = parts[0],\n                        startTime = parts[1].toDouble(),\n                        endTime = parts[2].toDouble()\n                    )\n                } else null\n            }\n        } catch (e: Exception) {\n            null\n        }\n    }\n\n    private fun parseLine(line: String, words: List<WordTimestamp>? = null): List<LyricsEntry>? {\n        if (line.isEmpty()) {\n            return null\n        }\n        val matchResult = LINE_REGEX.matchEntire(line.trim()) ?: return null\n        val times = matchResult.groupValues[1]\n        var text = matchResult.groupValues[3]\n        val timeMatchResults = TIME_REGEX.findAll(times)\n\n        // Parse agent marker {agent:v1}\n        val agentMatch = AGENT_REGEX.find(text)\n        val agent = agentMatch?.groupValues?.get(1)\n        if (agentMatch != null) {\n            text = text.replaceFirst(AGENT_REGEX, \"\")\n        }\n\n        // Parse background marker {bg}\n        val isBackground = BACKGROUND_REGEX.containsMatchIn(text)\n        if (isBackground) {\n            text = text.replaceFirst(BACKGROUND_REGEX, \"\")\n        }\n\n        return timeMatchResults\n            .map { timeMatchResult ->\n                val min = timeMatchResult.groupValues[1].toLong()\n                val sec = timeMatchResult.groupValues[2].toLong()\n                val milString = timeMatchResult.groupValues[3]\n                var mil = milString.toLong()\n                if (milString.length == 2) {\n                    mil *= 10\n                }\n                val time = min * DateUtils.MINUTE_IN_MILLIS + sec * DateUtils.SECOND_IN_MILLIS + mil\n                LyricsEntry(time, text, words, agent = agent, isBackground = isBackground)\n            }.toList()\n    }\n\n    fun findCurrentLineIndex(\n        lines: List<LyricsEntry>,\n        position: Long,\n    ): Int {\n        for (index in lines.indices) {\n            if (lines[index].time >= position + 300L) {\n                return index - 1\n            }\n        }\n        return lines.lastIndex\n    }\n\n    // TODO: Will be useful if we let the user pick the language, useless for now\n    /* enum class CyrillicLanguage {\n        RUSSIAN,\n        UKRAINIAN,\n        SERBIAN,\n        BULGARIAN,\n        BELARUSIAN,\n        KYRGYZ,\n        MACEDONIAN\n    } */\n\n    fun katakanaToRomaji(katakana: String?): String {\n        if (katakana.isNullOrEmpty()) return \"\"\n\n        val romajiBuilder = StringBuilder(katakana.length)\n        var i = 0\n        val n = katakana.length\n        while (i < n) {\n            var consumed = false\n            if (i + 1 < n) {\n                val twoCharCandidate = katakana.substring(i, i + 2)\n                val mappedTwoChar = KANA_ROMAJI_MAP[twoCharCandidate]\n                if (mappedTwoChar != null) {\n                    romajiBuilder.append(mappedTwoChar)\n                    i += 2\n                    consumed = true\n                }\n            }\n\n            if (!consumed) {\n                val oneCharCandidate = katakana[i].toString()\n                val mappedOneChar = KANA_ROMAJI_MAP[oneCharCandidate]\n                if (mappedOneChar != null) {\n                    romajiBuilder.append(mappedOneChar)\n                } else {\n                    romajiBuilder.append(oneCharCandidate)\n                }\n                i += 1\n            }\n        }\n        return romajiBuilder.toString().lowercase()\n    }\n\n    suspend fun romanizeJapanese(text: String): String = withContext(Dispatchers.Default) {\n        val tokens = kuromojiTokenizer.tokenize(text)\n        val romanizedTokens = tokens.mapIndexed { index, token ->\n            val currentReading = if (token.reading.isNullOrEmpty() || token.reading == \"*\") {\n                token.surface\n            } else {\n                token.reading\n            }\n            val nextTokenReading = if (index + 1 < tokens.size) {\n                tokens[index + 1].reading?.takeIf { it.isNotEmpty() && it != \"*\" } ?: tokens[index + 1].surface\n            } else {\n                null\n            }\n            katakanaToRomaji(currentReading, nextTokenReading)\n        }\n        romanizedTokens.joinToString(\" \")\n    }\n\n    fun katakanaToRomaji(katakana: String?, nextKatakana: String? = null): String {\n        if (katakana.isNullOrEmpty()) return \"\"\n\n        val romajiBuilder = StringBuilder(katakana.length)\n        var i = 0\n        val n = katakana.length\n        while (i < n) {\n            var consumed = false\n            if (i + 1 < n) {\n                val twoCharCandidate = katakana.substring(i, i + 2)\n                val mappedTwoChar = KANA_ROMAJI_MAP[twoCharCandidate]\n                if (mappedTwoChar != null) {\n                    romajiBuilder.append(mappedTwoChar)\n                    i += 2\n                    consumed = true\n                }\n            }\n\n            if (!consumed && katakana[i] == 'ッ') {\n                val nextCharToDouble = nextKatakana?.getOrNull(0)\n                if (nextCharToDouble != null) {\n                    val nextCharRomaji = KANA_ROMAJI_MAP[nextCharToDouble.toString()]?.getOrNull(0)?.toString()\n                        ?: nextCharToDouble.toString()\n                    romajiBuilder.append(nextCharRomaji.lowercase().trim())\n                }\n                i += 1\n                consumed = true\n            }\n\n            if (!consumed) {\n                val oneCharCandidate = katakana[i].toString()\n                val mappedOneChar = KANA_ROMAJI_MAP[oneCharCandidate]\n                if (mappedOneChar != null) {\n                    romajiBuilder.append(mappedOneChar)\n                } else {\n                    romajiBuilder.append(oneCharCandidate)\n                }\n                i += 1\n            }\n        }\n        return romajiBuilder.toString().lowercase()\n    }\n\n    suspend fun romanizeKorean(text: String): String = withContext(Dispatchers.Default) {\n        val romajaBuilder = StringBuilder()\n        var prevFinal: String? = null\n\n        for (i in text.indices) {\n            val char = text[i]\n            if (char in '\\uAC00'..'\\uD7A3') {\n                val syllableIndex = char.code - 0xAC00\n                val choIndex = syllableIndex / (21 * 28)\n                val jungIndex = (syllableIndex % (21 * 28)) / 28\n                val jongIndex = syllableIndex % 28\n\n                val choChar = (0x1100 + choIndex).toChar().toString()\n                val jungChar = (0x1161 + jungIndex).toChar().toString()\n                val jongChar = if (jongIndex == 0) null else (0x11A7 + jongIndex).toChar().toString()\n\n                if (prevFinal != null) {\n                    val contextKey = prevFinal + choChar\n                    val jong = HANGUL_ROMAJA_MAP[\"jong\"]?.get(contextKey)\n                        ?: HANGUL_ROMAJA_MAP[\"jong\"]?.get(prevFinal)\n                        ?: prevFinal\n                    romajaBuilder.append(jong)\n                }\n\n                val cho = HANGUL_ROMAJA_MAP[\"cho\"]?.get(choChar) ?: choChar\n                val jung = HANGUL_ROMAJA_MAP[\"jung\"]?.get(jungChar) ?: jungChar\n                romajaBuilder.append(cho).append(jung)\n                prevFinal = jongChar\n            } else {\n                if (prevFinal != null) {\n                    val jong = HANGUL_ROMAJA_MAP[\"jong\"]?.get(prevFinal) ?: prevFinal\n                    romajaBuilder.append(jong)\n                    prevFinal = null\n                }\n                romajaBuilder.append(char)\n            }\n        }\n\n        if (prevFinal != null) {\n            val jong = HANGUL_ROMAJA_MAP[\"jong\"]?.get(prevFinal) ?: prevFinal\n            romajaBuilder.append(jong)\n        }\n\n        romajaBuilder.toString()\n    }\n\n    suspend fun romanizeChinese(text: String): String = withContext(Dispatchers.Default) {\n        if (text.isEmpty()) return@withContext \"\"\n        val builder = StringBuilder(text.length * 2)\n        for (ch in text) {\n            if (ch in '\\u4E00'..'\\u9FFF') {\n                val py = Pinyin.toPinyin(ch).lowercase(Locale.getDefault())\n                builder.append(py).append(' ')\n            } else {\n                builder.append(ch)\n            }\n        }\n        // Remove whitespaces before ASCII and CJK punctuations\n        builder.toString()\n            .replace(Regex(\"\\\\s+([,.!?;:])\"), \"$1\")\n            .replace(Regex(\"\\\\s+([，。！？；：、（）《》〈〉【】『』「」])\"), \"$1\")\n            .trim()\n    }\n\n    suspend fun romanizeCyrillic(text: String): String? = withContext(Dispatchers.Default) {\n        if (text.isEmpty()) return@withContext null\n\n        val cyrillicChars = text.filter { it in '\\u0400'..'\\u04FF' }\n\n        if (cyrillicChars.isEmpty() ||\n            (cyrillicChars.length == 1 && (cyrillicChars[0] == 'е' || cyrillicChars[0] == 'Е'))) {\n            return@withContext null\n        }\n\n        when {\n            isRussian(text) -> romanizeRussianInternal(text)\n            isUkrainian(text) -> romanizeUkrainianInternal(text)\n            isSerbian(text) -> romanizeSerbianInternal(text)\n            isBulgarian(text) -> romanizeBulgarianInternal(text)\n            isBelarusian(text) -> romanizeBelarusianInternal(text)\n            isKyrgyz(text) -> romanizeKyrgyzInternal(text)\n            isMacedonian(text) -> romanizeMacedonianInternal(text)\n            else -> null\n        }\n    }\n\n    private fun romanizeRussianInternal(text: String): String {\n        val romajiBuilder = StringBuilder(text.length)\n        val words = text.split(\"((?<=\\\\s|[.,!?;])|(?=\\\\s|[.,!?;]))\".toRegex())\n            .filter { it.isNotEmpty() }\n\n        words.forEachIndexed { _, word ->\n            if (word.matches(\"[.,!?;]\".toRegex()) || word.isBlank()) {\n                romajiBuilder.append(word)\n            } else {\n                var charIndex = 0\n                while (charIndex < word.length) {\n                    var consumed = false\n                    // Check for 3-character sequences\n                    if (charIndex + 2 < word.length) {\n                        val threeCharCandidate = word.substring(charIndex, charIndex + 3)\n                        if (RUSSIAN_ROMAJI_MAP.containsKey(threeCharCandidate)) {\n                            romajiBuilder.append(RUSSIAN_ROMAJI_MAP[threeCharCandidate])\n                            charIndex += 3\n                            consumed = true\n                        }\n                    }\n\n                    if (!consumed) {\n                        val charStr = word[charIndex].toString()\n                        // Special case for 'е' or 'Е' at the start of a word\n                        if ((charStr == \"е\" || charStr == \"Е\") && (charIndex == 0 || word[charIndex - 1].isWhitespace())) {\n                            romajiBuilder.append(if (charStr == \"е\") \"ye\" else \"Ye\")\n                        } else {\n                            // Apply general Cyrillic mapping (Russian is no different so there's no need to apply a russian map)\n                            val romanizedChar = GENERAL_CYRILLIC_ROMAJI_MAP[charStr] ?: charStr\n                            romajiBuilder.append(romanizedChar)\n                        }\n                        charIndex += 1\n                    }\n                }\n            }\n        }\n        return romajiBuilder.toString()\n    }\n\n    private fun romanizeUkrainianInternal(text: String): String {\n        val romajiBuilder = StringBuilder(text.length)\n        val words = text.split(\"((?<=\\\\s|[.,!?;])|(?=\\\\s|[.,!?;]))\".toRegex())\n            .filter { it.isNotEmpty() }\n\n        words.forEachIndexed { _, word ->\n            if (word.matches(\"[.,!?;]\".toRegex()) || word.isBlank()) {\n                romajiBuilder.append(word)\n            } else {\n                var charIndex = 0\n                while (charIndex < word.length) {\n                    val charStr = word[charIndex].toString()\n                    var processed = false\n\n                    if (charIndex > 0 && word[charIndex - 1].isLetter() && !isCyrillicVowel(word[charIndex - 1])) {\n                        // Check if the current character is Ю or Я and is preceded by a consonant\n                        if (charStr == \"Ю\") {\n                            romajiBuilder.append(\"Iu\")\n                            processed = true\n                        } else if (charStr == \"ю\") {\n                            romajiBuilder.append(\"iu\")\n                            processed = true\n                        } else if (charStr == \"Я\") {\n                            romajiBuilder.append(\"Ia\")\n                            processed = true\n                        } else if (charStr == \"я\") {\n                            romajiBuilder.append(\"ia\")\n                            processed = true\n                        }\n                    }\n\n                    if (!processed) {\n                        romajiBuilder.append(UKRAINIAN_ROMAJI_MAP[charStr] ?: GENERAL_CYRILLIC_ROMAJI_MAP[charStr] ?: charStr)\n                    }\n                    charIndex++\n                }\n            }\n        }\n        return romajiBuilder.toString()\n    }\n\n    private fun romanizeSerbianInternal(text: String): String {\n        val romajiBuilder = StringBuilder(text.length)\n        val words = text.split(\"((?<=\\\\s|[.,!?;])|(?=\\\\s|[.,!?;]))\".toRegex())\n            .filter { it.isNotEmpty() }\n\n        words.forEachIndexed { _, word ->\n            if (word.matches(\"[.,!?;]\".toRegex()) || word.isBlank()) {\n                romajiBuilder.append(word)\n            } else {\n                var charIndex = 0\n                while (charIndex < word.length) {\n                    val charStr = word[charIndex].toString()\n                    val romanizedChar = SERBIAN_ROMAJI_MAP[charStr] ?: GENERAL_CYRILLIC_ROMAJI_MAP[charStr] ?: charStr\n                    romajiBuilder.append(romanizedChar)\n                    charIndex++\n                }\n            }\n        }\n        return romajiBuilder.toString()\n    }\n\n    private fun romanizeBulgarianInternal(text: String): String {\n        val romajiBuilder = StringBuilder(text.length)\n        val words = text.split(\"((?<=\\\\s|[.,!?;])|(?=\\\\s|[.,!?;]))\".toRegex())\n            .filter { it.isNotEmpty() }\n\n        words.forEachIndexed { _, word ->\n            if (word.matches(\"[.,!?;]\".toRegex()) || word.isBlank()) {\n                romajiBuilder.append(word)\n            } else {\n                var charIndex = 0\n                while (charIndex < word.length) {\n                    val charStr = word[charIndex].toString()\n                    val romanizedChar = BULGARIAN_ROMAJI_MAP[charStr] ?: GENERAL_CYRILLIC_ROMAJI_MAP[charStr] ?: charStr\n                    romajiBuilder.append(romanizedChar)\n                    charIndex++\n                }\n            }\n        }\n        return romajiBuilder.toString()\n    }\n\n    private fun romanizeBelarusianInternal(text: String): String {\n        val romajiBuilder = StringBuilder(text.length)\n        val words = text.split(\"((?<=\\\\s|[.,!?;])|(?=\\\\s|[.,!?;]))\".toRegex())\n            .filter { it.isNotEmpty() }\n\n        words.forEach { word ->\n            if (word.matches(\"[.,!?;]\".toRegex()) || word.isBlank()) {\n                romajiBuilder.append(word)\n            } else {\n                var charIndex = 0\n                while (charIndex < word.length) {\n                    val charStr = word[charIndex].toString()\n                    // Special case for 'е' or 'Е' at the start of a word\n                    if ((charStr == \"е\" || charStr == \"Е\") && (charIndex == 0 || word[charIndex - 1].isWhitespace())) {\n                        romajiBuilder.append(if (charStr == \"е\") \"ye\" else \"Ye\")\n                    } else {\n                        // General mapping\n                        val romanizedChar = BELARUSIAN_ROMAJI_MAP[charStr] ?: GENERAL_CYRILLIC_ROMAJI_MAP[charStr] ?: charStr\n                        romajiBuilder.append(romanizedChar)\n                    }\n                    charIndex += 1\n                }\n            }\n        }\n\n        return romajiBuilder.toString()\n    }\n\n    private fun romanizeKyrgyzInternal(text: String): String {\n        val romajiBuilder = StringBuilder(text.length)\n        val words = text.split(\"((?<=\\\\s|[.,!?;])|(?=\\\\s|[.,!?;]))\".toRegex())\n            .filter { it.isNotEmpty() }\n\n        words.forEachIndexed { _, word ->\n            if (word.matches(\"[.,!?;]\".toRegex()) || word.isBlank()) {\n                romajiBuilder.append(word)\n            } else {\n                var charIndex = 0\n                while (charIndex < word.length) {\n                    val charStr = word[charIndex].toString()\n                    val romanizedChar = KYRGYZ_ROMAJI_MAP[charStr] ?: GENERAL_CYRILLIC_ROMAJI_MAP[charStr] ?: charStr\n                    romajiBuilder.append(romanizedChar)\n                    charIndex++\n                }\n            }\n        }\n        return romajiBuilder.toString()\n    }\n\n    private fun romanizeMacedonianInternal(text: String): String {\n        val romajiBuilder = StringBuilder(text.length)\n        val words = text.split(\"((?<=\\\\s|[.,!?;])|(?=\\\\s|[.,!?;]))\".toRegex())\n            .filter { it.isNotEmpty() }\n\n        words.forEachIndexed { _, word ->\n            if (word.matches(\"[.,!?;]\".toRegex()) || word.isBlank()) {\n                romajiBuilder.append(word)\n            } else {\n                var charIndex = 0\n                while (charIndex < word.length) {\n                    val charStr = word[charIndex].toString()\n                    val romanizedChar = MACEDONIAN_ROMAJI_MAP[charStr] ?: GENERAL_CYRILLIC_ROMAJI_MAP[charStr] ?: charStr\n                    romajiBuilder.append(romanizedChar)\n                    charIndex++\n                }\n            }\n        }\n        return romajiBuilder.toString()\n    }\n\n    // TODO: This function might be used later if we let the user choose the language manually\n    /** private suspend fun romanizeCyrillicWithLanguage(text: String, language: CyrillicLanguage): String = withContext(Dispatchers.Default) {\n        if (text.isEmpty()) return@withContext \"\"\n\n        val detectedLanguage = language ?: when {\n            isRussian(text) -> CyrillicLanguage.RUSSIAN\n            isUkrainian(text) -> CyrillicLanguage.UKRAINIAN\n            isSerbian(text) -> CyrillicLanguage.SERBIAN\n            isBelarusian(text) -> CyrillicLanguage.BELARUSIAN\n            isKyrgyz(text) -> CyrillicLanguage.KYRGYZ\n            isMacedonian(text) -> CyrillicLanguage.MACEDONIAN\n            else -> return@withContext text\n        }\n\n        val languageMap: Map<String, String> = when (detectedLanguage) {\n            CyrillicLanguage.RUSSIAN -> RUSSIAN_ROMAJI_MAP\n            CyrillicLanguage.UKRAINIAN -> UKRAINIAN_ROMAJI_MAP\n            CyrillicLanguage.SERBIAN -> SERBIAN_ROMAJI_MAP\n            CyrillicLanguage.BELARUSIAN -> BELARUSIAN_ROMAJI_MAP\n            CyrillicLanguage.KYRGYZ -> KYRGYZ_ROMAJI_MAP\n            CyrillicLanguage.MACEDONIAN -> MACEDONIAN_ROMAJI_MAP\n            // else -> emptyMap()\n        }\n        val languageLetters = when (language) {\n            CyrillicLanguage.RUSSIAN -> RUSSIAN_CYRILLIC_LETTERS\n            CyrillicLanguage.UKRAINIAN -> UKRAINIAN_CYRILLIC_LETTERS\n            CyrillicLanguage.SERBIAN -> SERBIAN_CYRILLIC_LETTERS\n            CyrillicLanguage.BELARUSIAN -> BELARUSIAN_CYRILLIC_LETTERS\n            CyrillicLanguage.KYRGYZ -> KYRGYZ_CYRILLIC_LETTERS\n            CyrillicLanguage.MACEDONIAN -> MACEDONIAN_CYRILLIC_LETTERS\n            else -> GENERAL_CYRILLIC_ROMAJI_MAP.keys\n        }\n\n        val romajiBuilder = StringBuilder(text.length)\n        val words = text.split(\"((?<=\\\\s|[.,!?;])|(?=\\\\s|[.,!?;]))\".toRegex())\n            .filter { it.isNotEmpty() }\n\n        words.forEachIndexed { _, word ->\n            if (word.matches(\"[.,!?;]\".toRegex()) || word.isBlank()) {\n                // Preserve punctuation or spaces as is\n                romajiBuilder.append(word)\n            } else {\n                // Process word\n                var charIndex = 0\n                while (charIndex < word.length) {\n                    var consumed = false\n                    // Check for 3-character sequences (language-specific, e.g., Russian)\n                    if (detectedLanguage == CyrillicLanguage.RUSSIAN && charIndex + 2 < word.length) {\n                        val threeCharCandidate = word.substring(charIndex, charIndex + 3)\n                        if (languageLetters is Set<*> && languageLetters.containsAll(threeCharCandidate.toList().map { it.toString() })) {\n                            val mappedThreeChar = languageMap[threeCharCandidate]\n                            if (mappedThreeChar != null) {\n                                romajiBuilder.append(mappedThreeChar)\n                                charIndex += 3\n                                consumed = true\n                            }\n                        }\n                    }\n                    if (!consumed) {\n                        val charStr = word[charIndex].toString()\n                        val isSpecificLanguageChar = languageLetters is Set<*> && languageLetters.contains(charStr)\n                        val isGeneralCyrillicChar = GENERAL_CYRILLIC_ROMAJI_MAP.containsKey(charStr)\n\n                        if (isSpecificLanguageChar || isGeneralCyrillicChar) {\n                            if (detectedLanguage == CyrillicLanguage.RUSSIAN && (charStr == \"е\" || charStr == \"Е\") && charIndex == 0 && (charIndex == 0 || word[charIndex-1].isWhitespace())) {\n                                romajiBuilder.append(if (charStr == \"е\") \"ye\" else \"Ye\")\n                            } else {\n                                val romanizedChar = languageMap[charStr] ?: GENERAL_CYRILLIC_ROMAJI_MAP[charStr]\n                                if (romanizedChar != null) {\n                                    romajiBuilder.append(romanizedChar)\n                                } else {\n                                    romajiBuilder.append(charStr)\n                                }\n                            }\n                        } else {\n                            romajiBuilder.append(charStr)\n                        }\n                        charIndex += 1\n                    }\n                }\n            }\n        }\n        romajiBuilder.toString()\n    } */\n\n    fun isRussian(text: String): Boolean {\n        return text.any { char ->\n            RUSSIAN_CYRILLIC_LETTERS.contains(char.toString())\n        } && text.all { char ->\n            val charStr = char.toString()\n            RUSSIAN_CYRILLIC_LETTERS.contains(charStr) || !charStr.matches(\"[\\\\u0400-\\\\u04FF]\".toRegex())\n        }\n    }\n\n    fun isUkrainian(text: String): Boolean {\n        return text.any { char ->\n            UKRAINIAN_CYRILLIC_LETTERS.contains(char.toString()) || UKRAINIAN_SPECIFIC_CYRILLIC_LETTERS.contains(char.toString())\n        } && text.all { char ->\n            UKRAINIAN_CYRILLIC_LETTERS.contains(char.toString()) || UKRAINIAN_SPECIFIC_CYRILLIC_LETTERS.contains(char.toString()) || !char.toString().matches(\"[\\\\u0400-\\\\u04FF]\".toRegex())\n        }\n    }\n\n    fun isSerbian(text: String): Boolean {\n        return text.any { char ->\n            SERBIAN_CYRILLIC_LETTERS.contains(char.toString()) || SERBIAN_SPECIFIC_CYRILLIC_LETTERS.contains(char.toString())\n        } && text.all { char ->\n            SERBIAN_CYRILLIC_LETTERS.contains(char.toString()) || SERBIAN_SPECIFIC_CYRILLIC_LETTERS.contains(char.toString()) || !char.toString().matches(\"[\\\\u0400-\\\\u04FF]\".toRegex())\n        }\n    }\n\n    fun isBulgarian(text: String): Boolean {\n        return text.any { char ->\n            BULGARIAN_CYRILLIC_LETTERS.contains(char.toString()) // Bulgarian doesn't have any language specific letters\n        } && text.all { char ->\n            BULGARIAN_CYRILLIC_LETTERS.contains(char.toString()) || !char.toString().matches(\"[\\\\u0400-\\\\u04FF]\".toRegex())\n        }\n    }\n\n    fun isBelarusian(text: String): Boolean {\n        return text.any { char ->\n            BELARUSIAN_CYRILLIC_LETTERS.contains(char.toString()) || BELARUSIAN_SPECIFIC_CYRILLIC_LETTERS.contains(char.toString())\n        } && text.all { char ->\n            BELARUSIAN_CYRILLIC_LETTERS.contains(char.toString()) || BELARUSIAN_SPECIFIC_CYRILLIC_LETTERS.contains(char.toString()) || !char.toString().matches(\"[\\\\u0400-\\\\u04FF]\".toRegex())\n        }\n    }\n\n    fun isKyrgyz(text: String): Boolean {\n        return text.any { char ->\n            KYRGYZ_CYRILLIC_LETTERS.contains(char.toString()) || KYRGYZ_SPECIFIC_CYRILLIC_LETTERS.contains(char.toString())\n        } && text.all { char ->\n            KYRGYZ_CYRILLIC_LETTERS.contains(char.toString()) || KYRGYZ_SPECIFIC_CYRILLIC_LETTERS.contains(char.toString()) || !char.toString().matches(\"[\\\\u0400-\\\\u04FF]\".toRegex())\n        }\n    }\n\n    fun isMacedonian(text: String): Boolean {\n        return text.any { char ->\n            MACEDONIAN_CYRILLIC_LETTERS.contains(char.toString()) || MACEDONIAN_SPECIFIC_CYRILLIC_LETTERS.contains(char.toString())\n        } && text.all { char ->\n            MACEDONIAN_CYRILLIC_LETTERS.contains(char.toString()) || MACEDONIAN_SPECIFIC_CYRILLIC_LETTERS.contains(char.toString()) || !char.toString().matches(\"[\\\\u0400-\\\\u04FF]\".toRegex())\n        }\n    }\n\n    fun isJapanese(text: String): Boolean {\n        return text.any { char ->\n            (char in '\\u3040'..'\\u309F') || // Hiragana\n                    (char in '\\u30A0'..'\\u30FF') || // Katakana\n                    (char in '\\u4E00'..'\\u9FFF') // CJK Unified Ideographs\n        }\n    }\n\n    fun isKorean(text: String): Boolean {\n        return text.any { char ->\n            (char in '\\uAC00'..'\\uD7A3') // Hangul Syllables\n        }\n    }\n\n    fun isChinese(text: String): Boolean {\n        if (text.isEmpty()) return false\n        val cjkCharCount = text.count { char -> char in '\\u4E00'..'\\u9FFF' }\n        val hiraganaKatakanaCount = text.count { char -> (char in '\\u3040'..'\\u309F') || (char in '\\u30A0'..'\\u30FF') }\n        return cjkCharCount > 0 && (hiraganaKatakanaCount.toDouble() / text.length.toDouble()) < 0.1\n    }\n\n    fun isHindi(text: String): Boolean {\n        return text.any { char ->\n            char in '\\u0900'..'\\u097F'\n        }\n    }\n\n    suspend fun romanizeHindi(text: String): String = withContext(Dispatchers.Default) {\n        val sb = StringBuilder(text.length)\n        var i = 0\n        while (i < text.length) {\n            var consumed = false\n            // Check for 2-character sequences (e.g. char + nukta)\n            if (i + 1 < text.length) {\n                val twoCharCandidate = text.substring(i, i + 2)\n                val mappedTwoChar = DEVANAGARI_ROMAJI_MAP[twoCharCandidate]\n                if (mappedTwoChar != null) {\n                    sb.append(mappedTwoChar)\n                    i += 2\n                    consumed = true\n                }\n            }\n\n            if (!consumed) {\n                val charStr = text[i].toString()\n                sb.append(DEVANAGARI_ROMAJI_MAP[charStr] ?: charStr)\n                i += 1\n            }\n        }\n        sb.toString()\n    }\n\n    fun isPunjabi(text: String): Boolean {\n        return text.any { char ->\n            char in '\\u0A00'..'\\u0A7F'\n        }\n    }\n\n    suspend fun romanizePunjabi(text: String): String = withContext(Dispatchers.Default) {\n        val sb = StringBuilder(text.length)\n        var i = 0\n        while (i < text.length) {\n            val char = text[i]\n            var consumed = false\n\n            // Check for Adhak (Gemination)\n            if (char == '\\u0A71') {\n                 // Double next consonant if possible\n                 if (i + 1 < text.length) {\n                     val nextCharStr = text[i+1].toString()\n                     val nextMapped = GURMUKHI_ROMAJI_MAP[nextCharStr]\n                     if (nextMapped != null && nextMapped.isNotEmpty()) {\n                         sb.append(nextMapped[0])\n                     }\n                 }\n                 i++\n                 continue\n            }\n\n            // Check for 2-character sequences (e.g. char + nukta)\n            if (i + 1 < text.length) {\n                val twoCharCandidate = text.substring(i, i + 2)\n                val mappedTwoChar = GURMUKHI_ROMAJI_MAP[twoCharCandidate]\n                if (mappedTwoChar != null) {\n                    sb.append(mappedTwoChar)\n                    i += 2\n                    consumed = true\n                }\n            }\n\n            if (!consumed) {\n                val str = char.toString()\n                sb.append(GURMUKHI_ROMAJI_MAP[str] ?: str)\n                i++\n            }\n        }\n        sb.toString()\n    }\n\n    private fun isCyrillicVowel(char: Char): Boolean {\n        return \"АаЕеЄєИиІіЇїОоУуЮюЯяЫыЭэ\".contains(char)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/lyrics/SimpMusicLyricsProvider.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.lyrics\n\nimport android.content.Context\nimport com.metrolist.music.constants.EnableSimpMusicKey\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\nimport com.metrolist.simpmusic.SimpMusicLyrics\n\nobject SimpMusicLyricsProvider : LyricsProvider {\n    override val name = \"SimpMusic\"\n\n    override fun isEnabled(context: Context): Boolean = context.dataStore[EnableSimpMusicKey] ?: true\n\n    override suspend fun getLyrics(\n        id: String,\n        title: String,\n        artist: String,\n        duration: Int,\n        album: String?,\n    ): Result<String> = SimpMusicLyrics.getLyrics(id, duration)\n\n    override suspend fun getAllLyrics(\n        id: String,\n        title: String,\n        artist: String,\n        duration: Int,\n        album: String?,\n        callback: (String) -> Unit,\n    ) {\n        SimpMusicLyrics.getAllLyrics(id, duration, callback)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/lyrics/YouTubeLyricsProvider.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.lyrics\n\nimport android.content.Context\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.WatchEndpoint\n\nobject YouTubeLyricsProvider : LyricsProvider {\n    override val name = \"YouTube Music\"\n\n    override fun isEnabled(context: Context) = true\n\n    override suspend fun getLyrics(\n        id: String,\n        title: String,\n        artist: String,\n        duration: Int,\n        album: String?,\n    ): Result<String> =\n        runCatching {\n            val nextResult = YouTube.next(WatchEndpoint(videoId = id)).getOrThrow()\n            YouTube\n                .lyrics(\n                    endpoint = nextResult.lyricsEndpoint\n                        ?: throw IllegalStateException(\"Lyrics endpoint not found\"),\n                ).getOrThrow() ?: throw IllegalStateException(\"Lyrics unavailable\")\n        }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/lyrics/YouTubeSubtitleLyricsProvider.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.lyrics\n\nimport android.content.Context\nimport com.metrolist.innertube.YouTube\n\nobject YouTubeSubtitleLyricsProvider : LyricsProvider {\n    override val name = \"YouTube Subtitle\"\n\n    override fun isEnabled(context: Context) = true\n\n    override suspend fun getLyrics(\n        id: String,\n        title: String,\n        artist: String,\n        duration: Int,\n        album: String?,\n    ): Result<String> = YouTube.transcript(id)\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/models/ItemsPage.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.models\n\nimport com.metrolist.innertube.models.YTItem\n\ndata class ItemsPage(\n    val items: List<YTItem>,\n    val continuation: String?,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/models/MediaMetadata.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.models\n\nimport androidx.compose.runtime.Immutable\nimport com.metrolist.innertube.models.EpisodeItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.WatchEndpoint.WatchEndpointMusicSupportedConfigs.WatchEndpointMusicConfig.Companion.MUSIC_VIDEO_TYPE_ATV\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.db.entities.SongEntity\nimport com.metrolist.music.ui.utils.resize\nimport java.io.Serializable\nimport java.time.LocalDateTime\n\n@Immutable\ndata class MediaMetadata(\n    val id: String,\n    val title: String,\n    val artists: List<Artist>,\n    val duration: Int,\n    val thumbnailUrl: String? = null,\n    val album: Album? = null,\n    val setVideoId: String? = null,\n    val musicVideoType: String? = null,\n    val explicit: Boolean = false,\n    val liked: Boolean = false,\n    val likedDate: LocalDateTime? = null,\n    val inLibrary: LocalDateTime? = null,\n    val libraryAddToken: String? = null,\n    val libraryRemoveToken: String? = null,\n    val suggestedBy: String? = null,\n    val isEpisode: Boolean = false,\n    val uploadEntityId: String? = null,\n) : Serializable {\n    val isVideoSong: Boolean\n        get() = musicVideoType != null && musicVideoType != MUSIC_VIDEO_TYPE_ATV\n\n    data class Artist(\n        val id: String?,\n        val name: String,\n    ) : Serializable\n\n    data class Album(\n        val id: String,\n        val title: String,\n    ) : Serializable\n\n    fun toSongEntity() =\n        SongEntity(\n            id = id,\n            title = title,\n            duration = duration,\n            thumbnailUrl = thumbnailUrl,\n            albumId = album?.id,\n            albumName = album?.title,\n            explicit = explicit,\n            liked = liked,\n            likedDate = likedDate,\n            inLibrary = inLibrary,\n            libraryAddToken = libraryAddToken,\n            libraryRemoveToken = libraryRemoveToken,\n            isVideo = isVideoSong,\n            isEpisode = isEpisode,\n            uploadEntityId = uploadEntityId\n        )\n}\n\nfun Song.toMediaMetadata() =\n    MediaMetadata(\n        id = song.id,\n        title = song.title,\n        artists =\n        orderedArtists.map {\n            MediaMetadata.Artist(\n                id = it.id,\n                name = it.name,\n            )\n        },\n        duration = song.duration,\n        thumbnailUrl = song.thumbnailUrl,\n        album =\n        album?.let {\n            MediaMetadata.Album(\n                id = it.id,\n                title = it.title,\n            )\n        } ?: song.albumId?.let { albumId ->\n            MediaMetadata.Album(\n                id = albumId,\n                title = song.albumName.orEmpty(),\n            )\n        },\n        explicit = song.explicit,\n        // Use a non-ATV type if isVideo is true to indicate it's a video song\n        musicVideoType = if (song.isVideo) \"MUSIC_VIDEO_TYPE_OMV\" else null,\n        suggestedBy = null,\n        isEpisode = song.isEpisode,\n    )\n\nfun SongItem.toMediaMetadata() =\n    MediaMetadata(\n        id = id,\n        title = title,\n        artists =\n        artists.map {\n            MediaMetadata.Artist(\n                id = it.id,\n                name = it.name,\n            )\n        },\n        duration = duration ?: -1,\n        thumbnailUrl = thumbnail.resize(544, 544),\n        album =\n        album?.let {\n            MediaMetadata.Album(\n                id = it.id,\n                title = it.name,\n            )\n        },\n        explicit = explicit,\n        setVideoId = setVideoId,\n        musicVideoType = musicVideoType,\n        libraryAddToken = libraryAddToken,\n        libraryRemoveToken = libraryRemoveToken,\n        suggestedBy = null,\n        isEpisode = isEpisode,\n        uploadEntityId = uploadEntityId\n    )\n\nfun EpisodeItem.toMediaMetadata() =\n    MediaMetadata(\n        id = id,\n        title = title,\n        artists = listOfNotNull(author).map {\n            MediaMetadata.Artist(\n                id = it.id,\n                name = it.name,\n            )\n        },\n        duration = duration ?: -1,\n        thumbnailUrl = thumbnail.resize(544, 544),\n        album = podcast?.let {\n            MediaMetadata.Album(\n                id = it.id,\n                title = it.name,\n            )\n        },\n        explicit = explicit,\n        suggestedBy = null,\n        isEpisode = true,\n        libraryAddToken = libraryAddToken,\n        libraryRemoveToken = libraryRemoveToken,\n    )\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/models/PersistPlayerState.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.models\n\nimport java.io.Serializable\n\ndata class PersistPlayerState(\n    val playWhenReady: Boolean,\n    val repeatMode: Int,\n    val shuffleModeEnabled: Boolean,\n    val volume: Float,\n    val currentPosition: Long,\n    val currentMediaItemIndex: Int,\n    val playbackState: Int,\n    val timestamp: Long = System.currentTimeMillis()\n) : Serializable\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/models/PersistQueue.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.models\n\nimport java.io.Serializable\n\ndata class PersistQueue(\n    val title: String?,\n    val items: List<MediaMetadata>,\n    val mediaItemIndex: Int,\n    val position: Long,\n    val queueType: QueueType = QueueType.LIST,\n    val queueData: QueueData? = null,\n) : Serializable\n\nsealed class QueueType : Serializable {\n    object LIST : QueueType()\n    object YOUTUBE : QueueType()\n    object YOUTUBE_ALBUM_RADIO : QueueType()\n    object LOCAL_ALBUM_RADIO : QueueType()\n}\n\nsealed class QueueData : Serializable {\n    data class YouTubeData(\n        val endpoint: String,\n        val continuation: String? = null\n    ) : QueueData()\n    \n    data class YouTubeAlbumRadioData(\n        val playlistId: String,\n        val albumSongCount: Int = 0,\n        val continuation: String? = null,\n        val firstTimeLoaded: Boolean = false\n    ) : QueueData()\n    \n    data class LocalAlbumRadioData(\n        val albumId: String,\n        val startIndex: Int = 0,\n        val playlistId: String? = null,\n        val continuation: String? = null,\n        val firstTimeLoaded: Boolean = false\n    ) : QueueData()\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/models/SimilarRecommendation.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.models\n\nimport com.metrolist.innertube.models.YTItem\nimport com.metrolist.music.db.entities.LocalItem\n\ndata class SimilarRecommendation(\n    val title: LocalItem,\n    val items: List<YTItem>,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/playback/DownloadUtil.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.playback\n\nimport android.content.Context\nimport android.net.ConnectivityManager\nimport androidx.core.content.getSystemService\nimport androidx.core.net.toUri\nimport androidx.media3.database.DatabaseProvider\nimport androidx.media3.datasource.ResolvingDataSource\nimport androidx.media3.datasource.cache.CacheDataSource\nimport androidx.media3.datasource.cache.SimpleCache\nimport androidx.media3.datasource.okhttp.OkHttpDataSource\nimport androidx.media3.exoplayer.offline.Download\nimport androidx.media3.exoplayer.offline.DownloadManager\nimport androidx.media3.exoplayer.offline.DownloadNotificationHelper\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.music.constants.AudioQuality\nimport com.metrolist.music.constants.AudioQualityKey\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.db.entities.FormatEntity\nimport com.metrolist.music.db.entities.SongEntity\nimport com.metrolist.music.di.DownloadCache\nimport com.metrolist.music.di.PlayerCache\nimport com.metrolist.music.utils.YTPlayerUtils\nimport com.metrolist.music.utils.enumPreference\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.DelicateCoroutinesApi\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Runnable\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.cancel\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.runBlocking\nimport okhttp3.OkHttpClient\nimport java.time.LocalDateTime\nimport java.util.concurrent.Executor\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n@Singleton\nclass DownloadUtil\n@Inject\nconstructor(\n    @ApplicationContext context: Context,\n    val database: MusicDatabase,\n    val databaseProvider: DatabaseProvider,\n    @DownloadCache val downloadCache: SimpleCache,\n    @PlayerCache val playerCache: SimpleCache,\n) {\n    private val connectivityManager = context.getSystemService<ConnectivityManager>()!!\n    private val audioQuality by enumPreference(context, AudioQualityKey, AudioQuality.AUTO)\n    private val songUrlCache = HashMap<String, Pair<String, Long>>()\n\n    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)\n\n    val downloads = MutableStateFlow<Map<String, Download>>(emptyMap())\n\n    private val dataSourceFactory =\n        ResolvingDataSource.Factory(\n            CacheDataSource\n                .Factory()\n                .setCache(playerCache)\n                .setUpstreamDataSourceFactory(\n                    OkHttpDataSource.Factory(\n                        OkHttpClient.Builder()\n                            .proxy(YouTube.proxy)\n                            .proxyAuthenticator { _, response ->\n                                YouTube.proxyAuth?.let { auth ->\n                                    response.request.newBuilder()\n                                        .header(\"Proxy-Authorization\", auth)\n                                        .build()\n                                } ?: response.request\n                            }\n                            .build(),\n                    ),\n                ),\n        ) { dataSpec ->\n            val mediaId = dataSpec.key ?: error(\"No media id\")\n            val length = if (dataSpec.length >= 0) dataSpec.length else 1\n\n            if (playerCache.isCached(mediaId, dataSpec.position, length)) {\n                return@Factory dataSpec\n            }\n\n            songUrlCache[mediaId]?.takeIf { it.second < System.currentTimeMillis() }?.let {\n                return@Factory dataSpec.withUri(it.first.toUri())\n            }\n\n            val playbackData = runBlocking(Dispatchers.IO) {\n                YTPlayerUtils.playerResponseForPlayback(\n                    mediaId,\n                    audioQuality = audioQuality,\n                    connectivityManager = connectivityManager,\n                )\n            }.getOrThrow()\n            val format = playbackData.format\n\n            database.query {\n                upsert(\n                    FormatEntity(\n                        id = mediaId,\n                        itag = format.itag,\n                        mimeType = format.mimeType.split(\";\")[0],\n                        codecs = format.mimeType.split(\"codecs=\")[1].removeSurrounding(\"\\\"\"),\n                        bitrate = format.bitrate,\n                        sampleRate = format.audioSampleRate,\n                        contentLength = format.contentLength!!,\n                        loudnessDb = playbackData.audioConfig?.loudnessDb,\n                        perceptualLoudnessDb = playbackData.audioConfig?.perceptualLoudnessDb,\n                        playbackUrl = playbackData.playbackTracking?.videostatsPlaybackUrl?.baseUrl\n                    ),\n                )\n\n                val now = LocalDateTime.now()\n                val existing = getSongByIdBlocking(mediaId)?.song\n\n                val updatedSong = if (existing != null) {\n                    if (existing.dateDownload == null) {\n                        existing.copy(dateDownload = now)\n                    } else {\n                        existing\n                    }\n                } else {\n                    SongEntity(\n                        id = mediaId,\n                        title = playbackData.videoDetails?.title ?: \"Unknown\",\n                        duration = playbackData.videoDetails?.lengthSeconds?.toIntOrNull() ?: 0,\n                        thumbnailUrl = playbackData.videoDetails?.thumbnail?.thumbnails?.lastOrNull()?.url,\n                        dateDownload = now,\n                        isDownloaded = false\n                    )\n                }\n\n                upsert(updatedSong)\n            }\n\n            val streamUrl = playbackData.streamUrl.let {\n                \"${it}&range=0-${format.contentLength ?: 10000000}\"\n            }\n\n            songUrlCache[mediaId] = streamUrl to playbackData.streamExpiresInSeconds * 1000L\n            dataSpec.withUri(streamUrl.toUri())\n        }\n\n    val downloadNotificationHelper =\n        DownloadNotificationHelper(context, ExoDownloadService.CHANNEL_ID)\n\n    @OptIn(DelicateCoroutinesApi::class)\n    val downloadManager: DownloadManager =\n        DownloadManager(\n            context,\n            databaseProvider,\n            downloadCache,\n            dataSourceFactory,\n            Executor(Runnable::run)\n        ).apply {\n            maxParallelDownloads = 3\n            addListener(\n                object : DownloadManager.Listener {\n                    override fun onDownloadChanged(\n                        downloadManager: DownloadManager,\n                        download: Download,\n                        finalException: Exception?,\n                    ) {\n                        downloads.update { map ->\n                            map.toMutableMap().apply {\n                                set(download.request.id, download)\n                            }\n                        }\n\n                        scope.launch {\n                            when (download.state) {\n                                Download.STATE_COMPLETED -> {\n                                    database.updateDownloadedInfo(download.request.id, true, LocalDateTime.now())\n                                }\n                                Download.STATE_FAILED,\n                                Download.STATE_STOPPED,\n                                Download.STATE_REMOVING -> {\n                                    database.updateDownloadedInfo(download.request.id, false, null)\n                                }\n                                else -> {\n                                }\n                            }\n                        }\n                    }\n                }\n            )\n        }\n\n    init {\n        val result = mutableMapOf<String, Download>()\n        val cursor = downloadManager.downloadIndex.getDownloads()\n        while (cursor.moveToNext()) {\n            result[cursor.download.request.id] = cursor.download\n        }\n        downloads.value = result\n    }\n\n    fun getDownload(songId: String): Flow<Download?> = downloads.map { it[songId] }\n\n    fun release() {\n        scope.cancel()\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/playback/ExoDownloadService.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.playback\n\nimport android.app.Notification\nimport android.app.PendingIntent\nimport android.content.Context\nimport android.content.Intent\nimport android.graphics.drawable.Icon\nimport androidx.media3.common.util.NotificationUtil\nimport androidx.media3.common.util.Util\nimport androidx.media3.exoplayer.offline.Download\nimport androidx.media3.exoplayer.offline.DownloadManager\nimport androidx.media3.exoplayer.offline.DownloadNotificationHelper\nimport androidx.media3.exoplayer.offline.DownloadService\nimport androidx.media3.exoplayer.scheduler.PlatformScheduler\nimport androidx.media3.exoplayer.scheduler.Scheduler\nimport com.metrolist.music.R\nimport dagger.hilt.android.AndroidEntryPoint\nimport javax.inject.Inject\n\n\n@AndroidEntryPoint\nclass ExoDownloadService : DownloadService(\n    NOTIFICATION_ID,\n    1000L,\n    CHANNEL_ID,\n    R.string.downloading,\n    0\n) {\n    @Inject\n    lateinit var downloadUtil: DownloadUtil\n\n    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\n        if (intent?.action == REMOVE_ALL_PENDING_DOWNLOADS) {\n            downloadManager.currentDownloads.forEach { download ->\n                downloadManager.removeDownload(download.request.id)\n            }\n        }\n        return super.onStartCommand(intent, flags, startId)\n    }\n\n    override fun getDownloadManager() = downloadUtil.downloadManager\n\n    override fun getScheduler(): Scheduler = PlatformScheduler(this, JOB_ID)\n\n    override fun getForegroundNotification(\n        downloads: MutableList<Download>,\n        notMetRequirements: Int\n    ): Notification =\n        Notification.Builder.recoverBuilder(\n            this, downloadUtil.downloadNotificationHelper.buildProgressNotification(\n                this,\n                R.drawable.download,\n                null,\n                if (downloads.size == 1) Util.fromUtf8Bytes(downloads[0].request.data)\n                else resources.getQuantityString(R.plurals.n_song, downloads.size, downloads.size),\n                downloads,\n                notMetRequirements\n            )\n        ).addAction(\n            Notification.Action.Builder(\n                Icon.createWithResource(this, R.drawable.close),\n                getString(android.R.string.cancel),\n                PendingIntent.getService(\n                    this,\n                    0,\n                    Intent(this, ExoDownloadService::class.java).setAction(\n                        REMOVE_ALL_PENDING_DOWNLOADS\n                    ),\n                    PendingIntent.FLAG_IMMUTABLE\n                )\n            ).build()\n        ).build()\n\n\n    /**\n     * This helper will outlive the lifespan of a single instance of [ExoDownloadService]\n     */\n    class TerminalStateNotificationHelper(\n        private val context: Context,\n        private val notificationHelper: DownloadNotificationHelper,\n        private var nextNotificationId: Int,\n    ) : DownloadManager.Listener {\n        override fun onDownloadChanged(\n            downloadManager: DownloadManager,\n            download: Download,\n            finalException: Exception?,\n        ) {\n            if (download.state == Download.STATE_FAILED) {\n                val notification = notificationHelper.buildDownloadFailedNotification(\n                    context,\n                    R.drawable.error,\n                    null,\n                    Util.fromUtf8Bytes(download.request.data)\n                )\n                NotificationUtil.setNotification(context, nextNotificationId++, notification)\n            }\n        }\n    }\n\n    companion object {\n        const val CHANNEL_ID = \"download\"\n        const val NOTIFICATION_ID = 1\n        const val JOB_ID = 1\n        const val REMOVE_ALL_PENDING_DOWNLOADS = \"REMOVE_ALL_PENDING_DOWNLOADS\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/playback/MediaLibrarySessionCallback.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.playback\n\nimport android.content.ContentResolver\nimport android.content.Context\nimport android.net.Uri\nimport android.os.Bundle\nimport androidx.annotation.DrawableRes\nimport androidx.core.net.toUri\nimport androidx.media3.common.C\nimport androidx.media3.common.MediaItem\nimport androidx.media3.common.MediaMetadata\nimport androidx.media3.exoplayer.offline.Download\nimport androidx.media3.session.LibraryResult\nimport androidx.media3.session.MediaLibraryService\nimport androidx.media3.session.MediaLibraryService.MediaLibrarySession\nimport androidx.media3.session.MediaSession\nimport androidx.media3.session.MediaSession.MediaItemsWithStartPosition\nimport androidx.media3.session.SessionCommand\nimport androidx.media3.session.SessionError\nimport androidx.media3.session.SessionResult\nimport coil3.imageLoader\nimport com.google.common.collect.ImmutableList\nimport com.google.common.util.concurrent.Futures\nimport com.google.common.util.concurrent.ListenableFuture\nimport com.google.common.util.concurrent.SettableFuture\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.filterExplicit\nimport com.metrolist.innertube.models.filterVideoSongs\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.HideExplicitKey\nimport com.metrolist.music.constants.HideVideoSongsKey\nimport com.metrolist.music.constants.MediaSessionConstants\nimport com.metrolist.music.constants.SongSortType\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.db.entities.PlaylistEntity\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.extensions.toggleRepeatMode\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\nimport com.metrolist.music.utils.reportException\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.flowOn\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.guava.future\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.plus\nimport javax.inject.Inject\nimport com.metrolist.music.constants.AndroidAutoSectionsOrderKey\nimport com.metrolist.music.constants.AndroidAutoYouTubePlaylistsKey\nimport com.metrolist.music.ui.screens.settings.AndroidAutoSection\nimport com.metrolist.music.ui.screens.settings.deserializeSections\nimport com.metrolist.music.ui.screens.settings.serializeSections\n\nclass MediaLibrarySessionCallback\n@Inject\nconstructor(\n    @ApplicationContext val context: Context,\n    val database: MusicDatabase,\n    val downloadUtil: DownloadUtil,\n) : MediaLibrarySession.Callback {\n    private val scope = CoroutineScope(Dispatchers.Main) + Job()\n    lateinit var service: MusicService\n    var toggleLike: () -> Unit = {}\n    var toggleStartRadio: () -> Unit = {}\n    var toggleLibrary: () -> Unit = {}\n    var addToTargetPlaylist: () -> Unit = {}\n\n    override fun onConnect(\n        session: MediaSession,\n        controller: MediaSession.ControllerInfo,\n    ): MediaSession.ConnectionResult {\n        val connectionResult = super.onConnect(session, controller)\n        return MediaSession.ConnectionResult.accept(\n            connectionResult.availableSessionCommands\n                .buildUpon()\n                .add(MediaSessionConstants.CommandToggleLike)\n                .add(MediaSessionConstants.CommandToggleStartRadio)\n                .add(MediaSessionConstants.CommandToggleLibrary)\n                .add(MediaSessionConstants.CommandToggleShuffle)\n                .add(MediaSessionConstants.CommandToggleRepeatMode)\n                .add(MediaSessionConstants.CommandAddToTargetPlaylist)\n                .build(),\n            connectionResult.availablePlayerCommands,\n        )\n    }\n\n    override fun onCustomCommand(\n        session: MediaSession,\n        controller: MediaSession.ControllerInfo,\n        customCommand: SessionCommand,\n        args: Bundle,\n    ): ListenableFuture<SessionResult> {\n        when (customCommand.customAction) {\n            MediaSessionConstants.ACTION_TOGGLE_LIKE -> toggleLike()\n            MediaSessionConstants.ACTION_TOGGLE_START_RADIO -> toggleStartRadio()\n            MediaSessionConstants.ACTION_TOGGLE_LIBRARY -> toggleLibrary()\n            MediaSessionConstants.ACTION_TOGGLE_SHUFFLE -> session.player.shuffleModeEnabled =\n                !session.player.shuffleModeEnabled\n\n            MediaSessionConstants.ACTION_TOGGLE_REPEAT_MODE -> session.player.toggleRepeatMode()\n            MediaSessionConstants.ACTION_ADD_TO_TARGET_PLAYLIST -> addToTargetPlaylist()\n        }\n        return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))\n    }\n\n    @Deprecated(\"Deprecated in MediaLibrarySession.Callback\")\n    override fun onPlaybackResumption(\n        mediaSession: MediaSession,\n        controller: MediaSession.ControllerInfo\n    ): ListenableFuture<MediaItemsWithStartPosition> {\n        return SettableFuture.create<MediaItemsWithStartPosition>()\n    }\n\n    override fun onGetLibraryRoot(\n        session: MediaLibrarySession,\n        browser: MediaSession.ControllerInfo,\n        params: MediaLibraryService.LibraryParams?,\n    ): ListenableFuture<LibraryResult<MediaItem>> =\n        Futures.immediateFuture(\n            LibraryResult.ofItem(\n                MediaItem\n                    .Builder()\n                    .setMediaId(MusicService.ROOT)\n                    .setMediaMetadata(\n                        MediaMetadata\n                            .Builder()\n                            .setIsPlayable(false)\n                            .setIsBrowsable(false)\n                            .setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_MIXED)\n                            .build(),\n                    ).build(),\n                params,\n            ),\n        )\n\n    override fun onGetChildren(\n        session: MediaLibrarySession,\n        browser: MediaSession.ControllerInfo,\n        parentId: String,\n        page: Int,\n        pageSize: Int,\n        params: MediaLibraryService.LibraryParams?,\n    ): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> =\n        scope.future(Dispatchers.IO) {\n            LibraryResult.ofItemList(\n                when (parentId) {\n                    MusicService.ROOT -> {\n                        val sectionsRaw = context.dataStore.get(\n                            AndroidAutoSectionsOrderKey,\n                            serializeSections(AndroidAutoSection.values().map { it to true })\n                        )\n                        val sections = deserializeSections(sectionsRaw)\n                        sections\n                            .filter { (_, enabled) -> enabled }\n                            .ifEmpty { listOf(AndroidAutoSection.LIKED to true) }\n                            .map { (section, _) ->\n                                when (section) {\n                                    AndroidAutoSection.LIKED -> browsableMediaItem(\n                                        \"${MusicService.PLAYLIST}/${PlaylistEntity.LIKED_PLAYLIST_ID}\",\n                                        context.getString(R.string.liked_songs),\n                                        null,\n                                        drawableUri(R.drawable.favorite),\n                                        MediaMetadata.MEDIA_TYPE_PLAYLIST,\n                                    )\n                                   AndroidAutoSection.SONGS -> browsableMediaItem(\n                                        MusicService.SONG,\n                                        context.getString(R.string.songs),\n                                        null,\n                                        drawableUri(R.drawable.music_note),\n                                        MediaMetadata.MEDIA_TYPE_PLAYLIST,\n                                    )\n                                    AndroidAutoSection.ARTISTS -> browsableMediaItem(\n                                        MusicService.ARTIST,\n                                        context.getString(R.string.artists),\n                                        null,\n                                        drawableUri(R.drawable.artist),\n                                        MediaMetadata.MEDIA_TYPE_FOLDER_ARTISTS,\n                                    )\n                                    AndroidAutoSection.ALBUMS -> browsableMediaItem(\n                                        MusicService.ALBUM,\n                                        context.getString(R.string.albums),\n                                        null,\n                                        drawableUri(R.drawable.album),\n                                        MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS,\n                                    )\n                                    AndroidAutoSection.PLAYLISTS -> browsableMediaItem(\n                                        MusicService.PLAYLIST,\n                                        context.getString(R.string.playlists),\n                                        null,\n                                        drawableUri(R.drawable.queue_music),\n                                        MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS,\n                                    )\n                                }\n                            }\n                    }\n\n\n                    MusicService.SONG -> database.songsByCreateDateAsc().first()\n                        .map { it.toMediaItem(parentId) }\n\n                    MusicService.ARTIST ->\n                        database.artistsByCreateDateAsc().first().map { artist ->\n                            browsableMediaItem(\n                                \"${MusicService.ARTIST}/${artist.id}\",\n                                artist.artist.name,\n                                context.resources.getQuantityString(\n                                    R.plurals.n_song,\n                                    artist.songCount,\n                                    artist.songCount\n                                ),\n                                artist.artist.thumbnailUrl?.toUri(),\n                                MediaMetadata.MEDIA_TYPE_ARTIST,\n                            )\n                        }\n\n                    MusicService.ALBUM ->\n                        database.albumsByCreateDateAsc().first().map { album ->\n                            browsableMediaItem(\n                                \"${MusicService.ALBUM}/${album.id}\",\n                                album.album.title,\n                                album.artists.joinToString {\n                                    it.name\n                                },\n                                album.album.thumbnailUrl?.toUri(),\n                                MediaMetadata.MEDIA_TYPE_ALBUM,\n                            )\n                        }\n\n                    MusicService.PLAYLIST -> {\n                        val likedSongCount = database.likedSongsCount().first()\n                        val downloadedSongCount = downloadUtil.downloads.value.size\n                        val showYoutubePlaylists = context.dataStore.get(AndroidAutoYouTubePlaylistsKey, false)\n\n                        // Build local playlists immediately\n                        val localItems = listOf(\n                            browsableMediaItem(\n                                \"${MusicService.PLAYLIST}/${PlaylistEntity.LIKED_PLAYLIST_ID}\",\n                                context.getString(R.string.liked_songs),\n                                context.resources.getQuantityString(R.plurals.n_song, likedSongCount, likedSongCount),\n                                drawableUri(R.drawable.favorite),\n                                MediaMetadata.MEDIA_TYPE_PLAYLIST,\n                            ),\n                            browsableMediaItem(\n                                \"${MusicService.PLAYLIST}/${PlaylistEntity.DOWNLOADED_PLAYLIST_ID}\",\n                                context.getString(R.string.downloaded_songs),\n                                context.resources.getQuantityString(R.plurals.n_song, downloadedSongCount, downloadedSongCount),\n                                drawableUri(R.drawable.download),\n                                MediaMetadata.MEDIA_TYPE_PLAYLIST,\n                            ),\n                        ) + database.playlistsByCreateDateAsc().first().map { playlist ->\n                            browsableMediaItem(\n                                \"${MusicService.PLAYLIST}/${playlist.id}\",\n                                playlist.playlist.name,\n                                context.resources.getQuantityString(R.plurals.n_song, playlist.songCount, playlist.songCount),\n                                playlist.thumbnails.firstOrNull()?.toUri(),\n                                MediaMetadata.MEDIA_TYPE_PLAYLIST,\n                            )\n                        }\n\n                        // Fetch YouTube playlists asynchronously if enabled\n                        if (showYoutubePlaylists) {\n                            GlobalScope.launch(Dispatchers.IO) {\n                               try {\n                                    val youtubePlaylists = YouTube.home().getOrNull()?.sections\n                                        ?.flatMap { it.items }\n                                        ?.filterIsInstance<PlaylistItem>()\n                                        ?.take(10)\n                                        ?: emptyList()\n\n                                    if (youtubePlaylists.isNotEmpty()) {\n                                        session.notifyChildrenChanged(\n                                            MusicService.PLAYLIST,\n                                            localItems.size + youtubePlaylists.size,\n                                            null\n                                        )\n                                    }\n                                } catch (e: Exception) {\n                                    reportException(e)\n                                }\n                            }\n                        }\n                        localItems\n                    }\n\n                    else ->\n                        when {\n                            parentId.startsWith(\"${MusicService.ARTIST}/\") ->\n                                database.artistSongsByCreateDateAsc(parentId.removePrefix(\"${MusicService.ARTIST}/\"))\n                                    .first().map {\n                                    it.toMediaItem(parentId)\n                                }\n\n                            parentId.startsWith(\"${MusicService.ALBUM}/\") ->\n                                database.albumSongs(parentId.removePrefix(\"${MusicService.ALBUM}/\"))\n                                    .first().map {\n                                    it.toMediaItem(parentId)\n                                }\n\n                            parentId.startsWith(\"${MusicService.PLAYLIST}/\") -> {\n                                val playlistId = parentId.removePrefix(\"${MusicService.PLAYLIST}/\")\n                                val songs = when (playlistId) {\n                                    PlaylistEntity.LIKED_PLAYLIST_ID -> database.likedSongs(\n                                        SongSortType.CREATE_DATE,\n                                        true\n                                    )\n\n                                    PlaylistEntity.DOWNLOADED_PLAYLIST_ID -> {\n                                        val downloads = downloadUtil.downloads.value\n                                        database\n                                            .allSongs()\n                                            .flowOn(Dispatchers.IO)\n                                            .map { songs ->\n                                                songs.filter {\n                                                    downloads[it.id]?.state == Download.STATE_COMPLETED\n                                                }\n                                            }.map { songs ->\n                                                songs\n                                                    .map { it to downloads[it.id] }\n                                                    .sortedBy { it.second?.updateTimeMs ?: 0L }\n                                                    .map { it.first }\n                                            }\n                                    }\n\n                                    else ->\n                                        database.playlistSongs(playlistId).map { list ->\n                                            list.map { it.song }\n                                        }\n                                }.first()\n\n                                // Add shuffle item at the top\n                                listOf(\n                                    MediaItem.Builder()\n                                        .setMediaId(\"$parentId/${MusicService.SHUFFLE_ACTION}\")\n                                        .setMediaMetadata(\n                                            MediaMetadata.Builder()\n                                                .setTitle(context.getString(R.string.shuffle))\n                                                .setArtworkUri(drawableUri(R.drawable.shuffle))\n                                                .setIsPlayable(true)\n                                                .setIsBrowsable(false)\n                                                .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)\n                                                .build()\n                                        ).build()\n                                ) + songs.map { it.toMediaItem(parentId) }\n                            }\n\n                            parentId.startsWith(\"${MusicService.YOUTUBE_PLAYLIST}/\") -> {\n                                val playlistId = parentId.removePrefix(\"${MusicService.YOUTUBE_PLAYLIST}/\")\n                                try {\n                                    val songs = YouTube.playlist(playlistId).getOrNull()?.songs\n                                        ?.take(100)\n                                        ?.filterExplicit(context.dataStore.get(HideExplicitKey, false))\n                                        ?.filterVideoSongs(context.dataStore.get(HideVideoSongsKey, false))\n                                        ?: emptyList()\n\n                                    // Add shuffle item at the top\n                                    listOf(\n                                        MediaItem.Builder()\n                                            .setMediaId(\"$parentId/${MusicService.SHUFFLE_ACTION}\")\n                                            .setMediaMetadata(\n                                                MediaMetadata.Builder()\n                                                    .setTitle(context.getString(R.string.shuffle))\n                                                    .setArtworkUri(drawableUri(R.drawable.shuffle))\n                                                    .setIsPlayable(true)\n                                                    .setIsBrowsable(false)\n                                                    .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)\n                                                    .build()\n                                            ).build()\n                                    ) + songs.map { songItem ->\n                                        MediaItem.Builder()\n                                            .setMediaId(\"$parentId/${songItem.id}\")\n                                            .setMediaMetadata(\n                                                MediaMetadata.Builder()\n                                                    .setTitle(songItem.title)\n                                                    .setSubtitle(songItem.artists.joinToString(\", \") { it.name })\n                                                    .setArtist(songItem.artists.joinToString(\", \") { it.name })\n                                                    .setArtworkUri(songItem.thumbnail.toUri())\n                                                    .setIsPlayable(true)\n                                                    .setIsBrowsable(false)\n                                                    .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)\n                                                    .build()\n                                            )\n                                            .build()\n                                    }\n                                } catch (e: Exception) {\n                                    reportException(e)\n                                    emptyList()\n                                }\n                            }\n\n                            else -> emptyList()\n                        }\n                },\n                params,\n            )\n        }\n\n    override fun onGetItem(\n        session: MediaLibrarySession,\n        browser: MediaSession.ControllerInfo,\n        mediaId: String,\n    ): ListenableFuture<LibraryResult<MediaItem>> =\n        scope.future(Dispatchers.IO) {\n            database.song(mediaId).first()?.toMediaItem()?.let {\n                LibraryResult.ofItem(it, null)\n            } ?: LibraryResult.ofError(SessionError.ERROR_UNKNOWN)\n        }\n\n    override fun onSearch(\n        session: MediaLibrarySession,\n        browser: MediaSession.ControllerInfo,\n        query: String,\n        params: MediaLibraryService.LibraryParams?\n    ): ListenableFuture<LibraryResult<Void>> {\n        session.notifySearchResultChanged(browser, query, 1, params)\n        return Futures.immediateFuture(LibraryResult.ofVoid())\n    }\n\n    override fun onGetSearchResult(\n        session: MediaLibrarySession,\n        browser: MediaSession.ControllerInfo,\n        query: String,\n        page: Int,\n        pageSize: Int,\n        params: MediaLibraryService.LibraryParams?\n    ): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {\n        return scope.future(Dispatchers.IO) {\n            if (query.isEmpty()) {\n                return@future LibraryResult.ofItemList(emptyList(), params)\n            }\n\n            try {\n                val searchResults = mutableListOf<MediaItem>()\n\n                val localSongs = database.allSongs().first().filter { song ->\n                    song.song.title.contains(query, ignoreCase = true) ||\n                    song.artists.any { it.name.contains(query, ignoreCase = true) } ||\n                    song.album?.title?.contains(query, ignoreCase = true) == true\n                }\n                \n                val artistSongs = database.searchArtists(query).first().flatMap { artist ->\n                    database.artistSongsByCreateDateAsc(artist.id).first()\n                }\n                \n                val albumSongs = database.searchAlbums(query).first().flatMap { album ->\n                    database.albumSongs(album.id).first()\n                }\n                \n                val playlistSongs = database.searchPlaylists(query).first().flatMap { playlist ->\n                    database.playlistSongs(playlist.id).first().map { it.song }\n                }\n\n                val allLocalSongs = (localSongs + artistSongs + albumSongs + playlistSongs)\n                    .distinctBy { it.id }\n                \n                allLocalSongs.forEach { song ->\n                    searchResults.add(song.toMediaItem(\n                        path = \"${MusicService.SEARCH}/$query\",\n                        isPlayable = true,\n                        isBrowsable = true\n                    ))\n                }\n\n                try {\n                    val onlineResults = YouTube.search(query, YouTube.SearchFilter.FILTER_SONG)\n                        .getOrNull()\n                        ?.items\n                        ?.filterIsInstance<SongItem>()\n                        ?.filterExplicit(context.dataStore.get(HideExplicitKey, false))\n                        ?.filterVideoSongs(context.dataStore.get(HideVideoSongsKey, false))\n                        ?.filter { onlineSong ->\n                            !allLocalSongs.any { localSong ->\n                                localSong.id == onlineSong.id ||\n                                (localSong.song.title.equals(onlineSong.title, ignoreCase = true) &&\n                                 localSong.artists.any { artist ->\n                                     onlineSong.artists.any {\n                                         it.name.equals(artist.name, ignoreCase = true)\n                                     }\n                                 })\n                            }\n                        } ?: emptyList()\n\n                    onlineResults.forEach { songItem ->\n                        try {\n                            database.query { insert(songItem.toMediaMetadata()) }\n                        } catch (e: Exception) {\n                        }\n                        \n                        searchResults.add(\n                            MediaItem.Builder()\n                                .setMediaId(\"${MusicService.SEARCH}/$query/${songItem.id}\")\n                                .setMediaMetadata(\n                                    MediaMetadata.Builder()\n                                        .setTitle(songItem.title)\n                                        .setSubtitle(songItem.artists.joinToString(\", \") { it.name })\n                                        .setArtist(songItem.artists.joinToString(\", \") { it.name })\n                                        .setArtworkUri(songItem.thumbnail.toUri())\n                                        .setIsPlayable(true)\n                                        .setIsBrowsable(true)\n                                        .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)\n                                        .build()\n                                )\n                                .build()\n                        )\n                    }\n                } catch (e: Exception) {\n                    reportException(e)\n                }\n                \n                LibraryResult.ofItemList(searchResults, params)\n                \n            } catch (e: Exception) {\n                reportException(e)\n                LibraryResult.ofItemList(emptyList(), params)\n            }\n        }\n    }\n\n    override fun onSetMediaItems(\n        mediaSession: MediaSession,\n        controller: MediaSession.ControllerInfo,\n        mediaItems: MutableList<MediaItem>,\n        startIndex: Int,\n        startPositionMs: Long,\n    ): ListenableFuture<MediaItemsWithStartPosition> =\n        scope.future {\n            val defaultResult = MediaItemsWithStartPosition(emptyList(), startIndex, startPositionMs)\n            val path = mediaItems.firstOrNull()?.mediaId?.split(\"/\")\n                ?: return@future defaultResult\n\n            when (path.firstOrNull()) {\n                MusicService.SONG -> {\n                    val songId = path.getOrNull(1) ?: return@future defaultResult\n                    val allSongs = database.songsByCreateDateAsc().first()\n                    MediaItemsWithStartPosition(\n                        allSongs.map { it.toMediaItem() },\n                        allSongs.indexOfFirst { it.id == songId }.takeIf { it != -1 } ?: 0,\n                        startPositionMs\n                    )\n                }\n\n                MusicService.ARTIST -> {\n                    val songId = path.getOrNull(2) ?: return@future defaultResult\n                    val artistId = path.getOrNull(1) ?: return@future defaultResult\n                    val songs = database.artistSongsByCreateDateAsc(artistId).first()\n                    MediaItemsWithStartPosition(\n                        songs.map { it.toMediaItem() },\n                        songs.indexOfFirst { it.id == songId }.takeIf { it != -1 } ?: 0,\n                        startPositionMs\n                    )\n                }\n\n                MusicService.ALBUM -> {\n                    val songId = path.getOrNull(2) ?: return@future defaultResult\n                    val albumId = path.getOrNull(1) ?: return@future defaultResult\n                    val albumWithSongs = database.albumWithSongs(albumId).first() ?: return@future defaultResult\n                    MediaItemsWithStartPosition(\n                        albumWithSongs.songs.map { it.toMediaItem() },\n                        albumWithSongs.songs.indexOfFirst { it.id == songId }.takeIf { it != -1 } ?: 0,\n                        startPositionMs\n                    )\n                }\n\n                MusicService.PLAYLIST -> {\n                    val songId = path.getOrNull(2) ?: return@future defaultResult\n                    val playlistId = path.getOrNull(1) ?: return@future defaultResult\n                    val songs = when (playlistId) {\n                        PlaylistEntity.LIKED_PLAYLIST_ID -> database.likedSongs(SongSortType.CREATE_DATE, descending = true)\n                        PlaylistEntity.DOWNLOADED_PLAYLIST_ID -> {\n                            val downloads = downloadUtil.downloads.value\n                            database\n                                .allSongs()\n                                .flowOn(Dispatchers.IO)\n                                .map { songs ->\n                                    songs.filter {\n                                        downloads[it.id]?.state == Download.STATE_COMPLETED\n                                    }\n                                }.map { songs ->\n                                    songs\n                                        .map { it to downloads[it.id] }\n                                        .sortedBy { it.second?.updateTimeMs ?: 0L }\n                                        .map { it.first }\n                                }\n                        }\n                        else -> database.playlistSongs(playlistId).map { list ->\n                            list.map { it.song }\n                        }\n                    }.first()\n\n                    // Check if this is a shuffle action\n                    if (songId == MusicService.SHUFFLE_ACTION) {\n                        MediaItemsWithStartPosition(\n                            songs.shuffled().map { it.toMediaItem() },\n                            0,\n                            C.TIME_UNSET\n                        )\n                    } else {\n                        MediaItemsWithStartPosition(\n                            songs.map { it.toMediaItem() },\n                            songs.indexOfFirst { it.id == songId }.takeIf { it != -1 } ?: 0,\n                            startPositionMs\n                        )\n                    }\n                }\n\n                MusicService.YOUTUBE_PLAYLIST -> {\n                    val songId = path.getOrNull(2) ?: return@future defaultResult\n                    val playlistId = path.getOrNull(1) ?: return@future defaultResult\n\n                    val songs = try {\n                        YouTube.playlist(playlistId).getOrNull()?.songs?.map {\n                            it.toMediaItem()\n                        } ?: emptyList()\n                    } catch (e: Exception) {\n                        reportException(e)\n                        return@future defaultResult\n                    }\n\n                    // Check if this is a shuffle action\n                    if (songId == MusicService.SHUFFLE_ACTION) {\n                        MediaItemsWithStartPosition(\n                            songs.shuffled(),\n                            0,\n                            C.TIME_UNSET\n                        )\n                    } else {\n                        MediaItemsWithStartPosition(\n                            songs,\n                            songs.indexOfFirst { it.mediaId.endsWith(songId) }.takeIf { it != -1 } ?: 0,\n                            C.TIME_UNSET\n                        )\n                    }\n                }\n\n                MusicService.SEARCH -> {\n                    val songId = path.getOrNull(2) ?: return@future defaultResult\n                    val searchQuery = path.getOrNull(1) ?: return@future defaultResult\n                    \n                    val searchResults = mutableListOf<Song>()\n\n                    val localSongs = database.allSongs().first().filter { song ->\n                        song.song.title.contains(searchQuery, ignoreCase = true) ||\n                        song.artists.any { it.name.contains(searchQuery, ignoreCase = true) } ||\n                        song.album?.title?.contains(searchQuery, ignoreCase = true) == true\n                    }\n                    \n                    val artistSongs = database.searchArtists(searchQuery).first().flatMap { artist ->\n                        database.artistSongsByCreateDateAsc(artist.id).first()\n                    }\n                    \n                    val albumSongs = database.searchAlbums(searchQuery).first().flatMap { album ->\n                        database.albumSongs(album.id).first()\n                    }\n                    \n                    val playlistSongs = database.searchPlaylists(searchQuery).first().flatMap { playlist ->\n                        database.playlistSongs(playlist.id).first().map { it.song }\n                    }\n\n                    val allLocalSongs = (localSongs + artistSongs + albumSongs + playlistSongs)\n                        .distinctBy { it.id }\n                    \n                    searchResults.addAll(allLocalSongs)\n                    \n                    try {\n                        val onlineResults = YouTube.search(searchQuery, YouTube.SearchFilter.FILTER_SONG)\n                            .getOrNull()\n                            ?.items\n                            ?.filterIsInstance<SongItem>()\n                            ?.filterExplicit(context.dataStore.get(HideExplicitKey, false))\n                            ?.filterVideoSongs(context.dataStore.get(HideVideoSongsKey, false))\n                            ?.filter { onlineSong ->\n                                !allLocalSongs.any { localSong ->\n                                    localSong.id == onlineSong.id ||\n                                    (localSong.song.title.equals(onlineSong.title, ignoreCase = true) &&\n                                     localSong.artists.any { artist ->\n                                         onlineSong.artists.any {\n                                             it.name.equals(artist.name, ignoreCase = true)\n                                         }\n                                     })\n                                }\n                            } ?: emptyList()\n\n                        onlineResults.forEach { songItem ->\n                            try {\n                                database.query { insert(songItem.toMediaMetadata()) }\n                                database.song(songItem.id).first()?.let { newSong ->\n                                    searchResults.add(newSong)\n                                }\n                            } catch (e: Exception) {\n                            }\n                        }\n                    } catch (e: Exception) {\n                        reportException(e)\n                    }\n                    \n                    if (searchResults.isEmpty()) {\n                        return@future defaultResult\n                    }\n                    \n                    val targetIndex = searchResults.indexOfFirst { it.id == songId }\n                    \n                    MediaItemsWithStartPosition(\n                        searchResults.map { it.toMediaItem() },\n                        if (targetIndex >= 0) targetIndex else 0,\n                        C.TIME_UNSET\n                    )\n                }\n\n                else -> defaultResult\n            }\n        }\n\n    private fun drawableUri(\n        @DrawableRes id: Int,\n    ) = Uri\n        .Builder()\n        .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)\n        .authority(context.resources.getResourcePackageName(id))\n        .appendPath(context.resources.getResourceTypeName(id))\n        .appendPath(context.resources.getResourceEntryName(id))\n        .build()\n\n    private fun browsableMediaItem(\n        id: String,\n        title: String,\n        subtitle: String?,\n        iconUri: Uri?,\n        mediaType: Int = MediaMetadata.MEDIA_TYPE_MUSIC,\n    ) = MediaItem\n        .Builder()\n        .setMediaId(id)\n        .setMediaMetadata(\n            MediaMetadata\n                .Builder()\n                .setTitle(title)\n                .setSubtitle(subtitle)\n                .setArtist(subtitle)\n                .setArtworkUri(iconUri)\n                .setIsPlayable(false)\n                .setIsBrowsable(true)\n                .setMediaType(mediaType)\n                .build(),\n        ).build()\n\n    private fun Song.toMediaItem(path: String, isPlayable: Boolean = true, isBrowsable: Boolean = false): MediaItem {\n        val artworkUri = song.thumbnailUrl?.let {\n            val snapshot = context.imageLoader.diskCache?.openSnapshot(it)\n            if (snapshot != null) {\n                snapshot.use { snapshot -> snapshot.data.toFile().toUri() }\n            } else {\n                it.toUri()\n            }\n        }\n\n        return MediaItem\n            .Builder()\n            .setMediaId(\"$path/$id\")\n            .setMediaMetadata(\n                MediaMetadata\n                    .Builder()\n                    .setTitle(song.title)\n                    .setSubtitle(artists.joinToString { it.name })\n                    .setArtist(artists.joinToString { it.name })\n                    .setArtworkUri(artworkUri)\n                    .setIsPlayable(isPlayable)\n                    .setIsBrowsable(isBrowsable)\n                    .setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)\n                    .build(),\n            ).build()\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/playback/MetrolistCacheEvictor.kt",
    "content": "package com.metrolist.music.playback\n\nimport androidx.media3.datasource.cache.Cache\nimport androidx.media3.datasource.cache.CacheEvictor\nimport androidx.media3.datasource.cache.CacheSpan\nimport com.metrolist.music.db.MusicDatabase\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\n\nclass MetrolistCacheEvictor(\n    private val wrappedEvictor: CacheEvictor,\n    private val database: MusicDatabase\n) : CacheEvictor {\n\n    private val scope = CoroutineScope(Dispatchers.IO)\n    private var cache: Cache? = null\n\n    override fun requiresCacheSpanTouches(): Boolean {\n        return wrappedEvictor.requiresCacheSpanTouches()\n    }\n\n    override fun onCacheInitialized() {\n        wrappedEvictor.onCacheInitialized()\n    }\n\n    override fun onStartFile(cache: Cache, key: String, position: Long, length: Long) {\n        this.cache = cache\n        wrappedEvictor.onStartFile(cache, key, position, length)\n    }\n\n    override fun onSpanAdded(cache: Cache, span: CacheSpan) {\n        this.cache = cache\n        wrappedEvictor.onSpanAdded(cache, span)\n        checkSpanAndSync(cache, span)\n    }\n\n    override fun onSpanRemoved(cache: Cache, span: CacheSpan) {\n        this.cache = cache\n        wrappedEvictor.onSpanRemoved(cache, span)\n        checkSpanAndSync(cache, span)\n    }\n\n    override fun onSpanTouched(cache: Cache, oldSpan: CacheSpan, newSpan: CacheSpan) {\n        this.cache = cache\n        wrappedEvictor.onSpanTouched(cache, oldSpan, newSpan)\n    }\n\n    private fun checkSpanAndSync(cache: Cache, span: CacheSpan) {\n        val mediaId = span.key\n        scope.launch {\n            try {\n                val entity = database.getSongById(mediaId)\n                    if (entity != null) {\n                        val length = if (entity.song.duration > 0) {\n                            androidx.media3.datasource.cache.ContentMetadata.getContentLength(\n                                cache.getContentMetadata(mediaId)\n                            )\n                        } else {\n                            -1L\n                        }\n\n                        val contentLength = androidx.media3.datasource.cache.ContentMetadata.getContentLength(\n                                cache.getContentMetadata(mediaId)\n                            )\n\n                        if (contentLength != androidx.media3.common.C.LENGTH_UNSET.toLong()) {\n                            val cachedSpans = cache.getCachedSpans(mediaId)\n                            var cachedBytes = 0L\n                            for (s in cachedSpans) {\n                                cachedBytes += s.length\n                            }\n                            val isCached = cachedBytes > 0 && cachedBytes >= (contentLength * 0.99).toLong()\n                            if (entity.song.isCached != isCached) {\n                                database.updateCachedInfo(mediaId, isCached)\n                            }\n                        }\n                    }\n                } catch (e: Exception) {\n                    e.printStackTrace()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/playback/MusicService.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\n@file:Suppress(\"DEPRECATION\")\n\npackage com.metrolist.music.playback\n\nimport android.app.ForegroundServiceStartNotAllowedException\nimport android.app.Notification\nimport android.app.NotificationChannel\nimport android.app.NotificationManager\nimport android.app.PendingIntent\nimport android.content.BroadcastReceiver\nimport android.content.ComponentName\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport android.content.pm.ServiceInfo\nimport android.database.SQLException\nimport android.media.AudioDeviceCallback\nimport android.media.AudioDeviceInfo\nimport android.media.AudioFocusRequest\nimport android.media.audiofx.AudioEffect\nimport android.media.audiofx.LoudnessEnhancer\nimport android.media.AudioManager\nimport android.net.ConnectivityManager\nimport android.os.Binder\nimport android.os.Build\nimport android.os.Handler\nimport android.os.Looper\nimport android.widget.Toast\nimport androidx.core.app.NotificationCompat\nimport androidx.core.content.getSystemService\nimport androidx.core.net.toUri\nimport androidx.datastore.preferences.core.edit\nimport androidx.media3.common.audio.SonicAudioProcessor\nimport androidx.media3.common.AudioAttributes\nimport androidx.media3.common.C\nimport androidx.media3.common.MediaItem\nimport androidx.media3.common.PlaybackException\nimport androidx.media3.common.PlaybackParameters\nimport androidx.media3.common.Player\nimport androidx.media3.common.Player.EVENT_POSITION_DISCONTINUITY\nimport androidx.media3.common.Player.EVENT_TIMELINE_CHANGED\nimport androidx.media3.common.Player.REPEAT_MODE_ALL\nimport androidx.media3.common.Player.REPEAT_MODE_OFF\nimport androidx.media3.common.Player.REPEAT_MODE_ONE\nimport androidx.media3.common.Player.STATE_IDLE\nimport androidx.media3.common.Timeline\nimport androidx.media3.common.util.UnstableApi\nimport androidx.media3.datasource.cache.CacheDataSource\nimport androidx.media3.datasource.cache.CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR\nimport androidx.media3.datasource.cache.SimpleCache\nimport androidx.media3.datasource.DataSource\nimport androidx.media3.datasource.DefaultDataSource\nimport androidx.media3.datasource.HttpDataSource\nimport androidx.media3.datasource.okhttp.OkHttpDataSource\nimport androidx.media3.datasource.ResolvingDataSource\nimport androidx.media3.exoplayer.analytics.AnalyticsListener\nimport androidx.media3.exoplayer.analytics.PlaybackStats\nimport androidx.media3.exoplayer.analytics.PlaybackStatsListener\nimport androidx.media3.exoplayer.audio.DefaultAudioSink\nimport androidx.media3.exoplayer.audio.SilenceSkippingAudioProcessor\nimport androidx.media3.exoplayer.DefaultRenderersFactory\nimport androidx.media3.exoplayer.ExoPlayer\nimport androidx.media3.exoplayer.source.DefaultMediaSourceFactory\nimport androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder\nimport androidx.media3.extractor.ExtractorsFactory\nimport androidx.media3.extractor.mkv.MatroskaExtractor\nimport androidx.media3.extractor.mp4.FragmentedMp4Extractor\nimport androidx.media3.session.CommandButton\nimport androidx.media3.session.DefaultMediaNotificationProvider\nimport androidx.media3.session.MediaController\nimport androidx.media3.session.MediaLibraryService\nimport androidx.media3.session.MediaSession\nimport androidx.media3.session.SessionToken\nimport com.google.common.util.concurrent.MoreExecutors\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.WatchEndpoint\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.lastfm.LastFM\nimport com.metrolist.music.constants.AndroidAutoTargetPlaylistKey\nimport com.metrolist.music.constants.AudioNormalizationKey\nimport com.metrolist.music.constants.AudioOffload\nimport com.metrolist.music.constants.AudioQualityKey\nimport com.metrolist.music.constants.AutoDownloadOnLikeKey\nimport com.metrolist.music.constants.AutoLoadMoreKey\nimport com.metrolist.music.constants.AutoSkipNextOnErrorKey\nimport com.metrolist.music.constants.CrossfadeDurationKey\nimport com.metrolist.music.constants.CrossfadeEnabledKey\nimport com.metrolist.music.constants.CrossfadeGaplessKey\nimport com.metrolist.music.constants.DisableLoadMoreWhenRepeatAllKey\nimport com.metrolist.music.constants.DiscordActivityNameKey\nimport com.metrolist.music.constants.DiscordActivityTypeKey\nimport com.metrolist.music.constants.DiscordAdvancedModeKey\nimport com.metrolist.music.constants.DiscordAvatarKey\nimport com.metrolist.music.constants.DiscordButton1TextKey\nimport com.metrolist.music.constants.DiscordButton1VisibleKey\nimport com.metrolist.music.constants.DiscordButton2TextKey\nimport com.metrolist.music.constants.DiscordButton2VisibleKey\nimport com.metrolist.music.constants.DiscordStatusKey\nimport com.metrolist.music.constants.DiscordTokenKey\nimport com.metrolist.music.constants.DiscordUseDetailsKey\nimport com.metrolist.music.constants.EnableDiscordRPCKey\nimport com.metrolist.music.constants.EnableLastFMScrobblingKey\nimport com.metrolist.music.constants.EnableSongCacheKey\nimport com.metrolist.music.constants.HideExplicitKey\nimport com.metrolist.music.constants.HideVideoSongsKey\nimport com.metrolist.music.constants.HistoryDuration\nimport com.metrolist.music.constants.LastFMUseNowPlaying\nimport com.metrolist.music.constants.MediaSessionConstants\nimport com.metrolist.music.constants.MediaSessionConstants.CommandAddToTargetPlaylist\nimport com.metrolist.music.constants.MediaSessionConstants.CommandToggleLike\nimport com.metrolist.music.constants.MediaSessionConstants.CommandToggleRepeatMode\nimport com.metrolist.music.constants.MediaSessionConstants.CommandToggleShuffle\nimport com.metrolist.music.constants.MediaSessionConstants.CommandToggleStartRadio\nimport com.metrolist.music.constants.PauseListenHistoryKey\nimport com.metrolist.music.constants.PauseOnMute\nimport com.metrolist.music.constants.PersistentQueueKey\nimport com.metrolist.music.constants.PersistentShuffleAcrossQueuesKey\nimport com.metrolist.music.constants.PlayerVolumeKey\nimport com.metrolist.music.constants.PreventDuplicateTracksInQueueKey\nimport com.metrolist.music.constants.RememberShuffleAndRepeatKey\nimport com.metrolist.music.constants.RepeatModeKey\nimport com.metrolist.music.constants.ResumeOnBluetoothConnectKey\nimport com.metrolist.music.constants.ScrobbleDelayPercentKey\nimport com.metrolist.music.constants.ScrobbleDelaySecondsKey\nimport com.metrolist.music.constants.ScrobbleMinSongDurationKey\nimport com.metrolist.music.constants.ShowLyricsKey\nimport com.metrolist.music.constants.ShuffleModeKey\nimport com.metrolist.music.constants.ShufflePlaylistFirstKey\nimport com.metrolist.music.constants.SimilarContent\nimport com.metrolist.music.constants.SkipSilenceInstantKey\nimport com.metrolist.music.constants.SkipSilenceKey\nimport com.metrolist.music.db.entities.Event\nimport com.metrolist.music.db.entities.FormatEntity\nimport com.metrolist.music.db.entities.LyricsEntity\nimport com.metrolist.music.db.entities.PlaylistEntity\nimport com.metrolist.music.db.entities.RelatedSongMap\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.di.DownloadCache\nimport com.metrolist.music.di.PlayerCache\nimport com.metrolist.music.eq.audio.CustomEqualizerAudioProcessor\nimport com.metrolist.music.eq.data.EQProfileRepository\nimport com.metrolist.music.eq.EqualizerService\nimport com.metrolist.music.extensions.collect\nimport com.metrolist.music.extensions.collectLatest\nimport com.metrolist.music.extensions.currentMetadata\nimport com.metrolist.music.extensions.findNextMediaItemById\nimport com.metrolist.music.extensions.mediaItems\nimport com.metrolist.music.extensions.metadata\nimport com.metrolist.music.extensions.setOffloadEnabled\nimport com.metrolist.music.extensions.SilentHandler\nimport com.metrolist.music.extensions.toEnum\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.extensions.toPersistQueue\nimport com.metrolist.music.extensions.toQueue\nimport com.metrolist.music.lyrics.LyricsHelper\nimport com.metrolist.music.MainActivity\nimport com.metrolist.music.models.PersistPlayerState\nimport com.metrolist.music.models.PersistQueue\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.playback.alarm.MusicAlarmScheduler\nimport com.metrolist.music.playback.alarm.MusicAlarmStore\nimport com.metrolist.music.playback.audio.SilenceDetectorAudioProcessor\nimport com.metrolist.music.playback.queues.EmptyQueue\nimport com.metrolist.music.playback.queues.filterExplicit\nimport com.metrolist.music.playback.queues.filterVideoSongs\nimport com.metrolist.music.playback.queues.ListQueue\nimport com.metrolist.music.playback.queues.Queue\nimport com.metrolist.music.playback.queues.YouTubeQueue\nimport com.metrolist.music.R\nimport com.metrolist.music.utils.CoilBitmapLoader\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.DiscordRPC\nimport com.metrolist.music.utils.get\nimport com.metrolist.music.utils.NetworkConnectivityObserver\nimport com.metrolist.music.utils.reportException\nimport com.metrolist.music.utils.ScrobbleManager\nimport com.metrolist.music.utils.SyncUtils\nimport com.metrolist.music.utils.YTPlayerUtils\nimport com.metrolist.music.widget.MetrolistWidgetManager\nimport com.metrolist.music.widget.MusicWidgetReceiver\nimport dagger.hilt.android.AndroidEntryPoint\nimport java.io.ObjectInputStream\nimport java.io.ObjectOutputStream\nimport java.time.LocalDateTime\nimport javax.inject.Inject\nimport kotlin.coroutines.coroutineContext\nimport kotlin.random.Random\nimport kotlin.time.Duration.Companion.seconds\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.distinctUntilChangedBy\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.FlowPreview\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.plus\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.withContext\nimport okhttp3.OkHttpClient\nimport timber.log.Timber\n\nprivate const val INSTANT_SILENCE_SKIP_STEP_MS = 15_000L\nprivate const val INSTANT_SILENCE_SKIP_SETTLE_MS = 350L\n\n@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)\n@androidx.annotation.OptIn(UnstableApi::class)\n@AndroidEntryPoint\nclass MusicService :\n    MediaLibraryService(),\n    Player.Listener,\n    PlaybackStatsListener.Callback {\n    @Inject\n    lateinit var database: MusicDatabase\n\n    @Inject\n    lateinit var lyricsHelper: LyricsHelper\n\n    @Inject\n    lateinit var syncUtils: SyncUtils\n\n    @Inject\n    lateinit var mediaLibrarySessionCallback: MediaLibrarySessionCallback\n\n    @Inject\n    lateinit var equalizerService: EqualizerService\n\n    @Inject\n    lateinit var eqProfileRepository: EQProfileRepository\n\n    @Inject\n    lateinit var widgetManager: MetrolistWidgetManager\n\n    @Inject\n    lateinit var listenTogetherManager: com.metrolist.music.listentogether.ListenTogetherManager\n\n    private lateinit var audioManager: AudioManager\n    private var audioFocusRequest: AudioFocusRequest? = null\n    private var lastAudioFocusState = AudioManager.AUDIOFOCUS_NONE\n    private var wasPlayingBeforeAudioFocusLoss = false\n    private var hasAudioFocus = false\n    private var reentrantFocusGain = false\n    private var wasPlayingBeforeVolumeMute = false\n    private var isPausedByVolumeMute = false\n\n    private var crossfadeEnabled = false\n    private var crossfadeDuration = 5000f\n    private var crossfadeGapless = true\n    private var crossfadeTriggerJob: Job? = null\n\n    private val secondaryPlayerListener = object : Player.Listener {\n        override fun onPlayerError(error: PlaybackException) {\n            Timber.tag(TAG).e(error, \"Secondary player error\")\n            secondaryPlayer?.stop()\n            secondaryPlayer?.clearMediaItems()\n            secondaryPlayer = null\n        }\n    }\n\n    private var scope = CoroutineScope(Dispatchers.Main) + Job()\n\n    private val binder = MusicBinder()\n\n    inner class MusicBinder : Binder() {\n        val service: MusicService\n            get() = this@MusicService\n    }\n\n    private lateinit var connectivityManager: ConnectivityManager\n    lateinit var connectivityObserver: NetworkConnectivityObserver\n    val waitingForNetworkConnection = MutableStateFlow(false)\n    private val isNetworkConnected = MutableStateFlow(false)\n\n    private lateinit var audioQuality: com.metrolist.music.constants.AudioQuality\n\n    private var currentQueue: Queue = EmptyQueue\n    var queueTitle: String? = null\n\n    val currentMediaMetadata = MutableStateFlow<com.metrolist.music.models.MediaMetadata?>(null)\n    private val currentSong =\n        currentMediaMetadata\n            .flatMapLatest { mediaMetadata ->\n                database.song(mediaMetadata?.id)\n            }.stateIn(scope, SharingStarted.Lazily, null)\n    private val currentFormat =\n        currentMediaMetadata.flatMapLatest { mediaMetadata ->\n            database.format(mediaMetadata?.id)\n        }\n\n    lateinit var playerVolume: MutableStateFlow<Float>\n    val isMuted = MutableStateFlow(false)\n    private val sleepTimerVolumeMultiplier = MutableStateFlow(1f)\n    private val audioFocusVolumeMultiplier = MutableStateFlow(1f)\n\n    fun toggleMute() {\n        val newMutedState = !isMuted.value\n        isMuted.value = newMutedState\n        applyEffectiveVolume()\n    }\n\n    fun setMuted(muted: Boolean) {\n        isMuted.value = muted\n        applyEffectiveVolume()\n    }\n\n    private fun calculateEffectiveVolume(\n        volume: Float = playerVolume.value,\n        muted: Boolean = isMuted.value,\n        sleepTimerMultiplier: Float = sleepTimerVolumeMultiplier.value,\n        focusMultiplier: Float = audioFocusVolumeMultiplier.value,\n    ): Float {\n        if (muted) return 0f\n        return (volume * sleepTimerMultiplier * focusMultiplier).coerceIn(0f, 1f)\n    }\n\n    private fun applyEffectiveVolume() {\n        if (!::player.isInitialized || isCrossfading) return\n        player.volume = calculateEffectiveVolume()\n    }\n\n\n    lateinit var sleepTimer: SleepTimer\n\n    @Inject\n    @PlayerCache\n    lateinit var playerCache: SimpleCache\n\n    @Inject\n    @DownloadCache\n    lateinit var downloadCache: SimpleCache\n\n    lateinit var player: ExoPlayer\n        private set\n    private var secondaryPlayer: ExoPlayer? = null\n    private var fadingPlayer: ExoPlayer? = null\n    private var isCrossfading = false\n    private var crossfadeJob: Job? = null\n\n    private lateinit var mediaSession: MediaLibrarySession\n\n    // Tracks if player has been properly initilized\n    private val playerInitialized = MutableStateFlow(false)\n    val isPlayerReady: kotlinx.coroutines.flow.StateFlow<Boolean> = playerInitialized.asStateFlow()\n\n    // Expose active player flow for UI/Connection updates\n    private val _playerFlow = MutableStateFlow<ExoPlayer?>(null)\n    val playerFlow = _playerFlow.asStateFlow()\n\n    private val playerSilenceProcessors = HashMap<Player, SilenceDetectorAudioProcessor>()\n\n\n    private val instantSilenceSkipEnabled = MutableStateFlow(false)\n\n    private var isAudioEffectSessionOpened = false\n    private var loudnessEnhancer: LoudnessEnhancer? = null\n\n    private var discordRpc: DiscordRPC? = null\n    private var lastPlaybackSpeed = 1.0f\n    private var discordUpdateJob: kotlinx.coroutines.Job? = null\n\n    private var scrobbleManager: ScrobbleManager? = null\n\n    val automixItems = MutableStateFlow<List<MediaItem>>(emptyList())\n\n    // Tracks the original queue size to distinguish original items from auto-added ones\n    private var originalQueueSize: Int = 0\n\n    private var consecutivePlaybackErr = 0\n    private var retryJob: Job? = null\n    private var retryCount = 0\n    private var silenceSkipJob: Job? = null\n\n    // URL cache for stream URLs - class-level so it can be invalidated on errors\n    private val songUrlCache = HashMap<String, Pair<String, Long>>()\n\n    // Flag to bypass cache when quality changes - forces fresh stream fetch\n    private val bypassCacheForQualityChange = mutableSetOf<String>()\n\n    // Enhanced error tracking for strict retry management\n    private var currentMediaIdRetryCount = mutableMapOf<String, Int>()\n    private val MAX_RETRY_PER_SONG = 3\n    private val RETRY_DELAY_MS = 1000L\n\n    // Track failed songs to prevent infinite retry loops\n    private val recentlyFailedSongs = mutableSetOf<String>()\n    private var failedSongsClearJob: Job? = null\n\n    // Google Cast support\n    var castConnectionHandler: CastConnectionHandler? = null\n        private set\n\n    private val screenStateReceiver = object : BroadcastReceiver() {\n        override fun onReceive(context: Context, intent: Intent) {\n            when (intent.action) {\n                Intent.ACTION_SCREEN_OFF -> {\n                    if (!player.isPlaying) {\n                        scope.launch(Dispatchers.IO) {\n                            discordRpc?.closeRPC()\n                        }\n                    }\n                }\n\n                Intent.ACTION_SCREEN_ON -> {\n                    if (player.isPlaying) {\n                        scope.launch {\n                            currentSong.value?.let { song ->\n                                updateDiscordRPC(song)\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    private val audioDeviceCallback = object : AudioDeviceCallback() {\n        override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {\n            super.onAudioDevicesAdded(addedDevices)\n            val hasBluetooth = addedDevices?.any {\n                it.type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP ||\n                        it.type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO\n            } == true\n\n            if (hasBluetooth) {\n                if (dataStore.get(ResumeOnBluetoothConnectKey, false)) {\n                    if (player.playbackState == Player.STATE_READY && !player.isPlaying) {\n                        player.play()\n                    }\n                }\n            }\n        }\n    }\n\n    override fun onCreate() {\n        super.onCreate()\n        isRunning = true\n\n        // Player rediness reset to false\n        playerInitialized.value = false\n\n        // 3. Connect the processor to the service\n        // handled in createExoPlayer\n\n        try {\n            val nm = getSystemService(NotificationManager::class.java)\n            nm?.createNotificationChannel(\n                NotificationChannel(\n                    CHANNEL_ID,\n                    getString(R.string.music_player),\n                    NotificationManager.IMPORTANCE_LOW\n                )\n            )\n            val pending = PendingIntent.getActivity(\n                this,\n                0,\n                Intent(this, MainActivity::class.java),\n                PendingIntent.FLAG_IMMUTABLE\n            )\n            val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)\n                .setContentTitle(getString(R.string.music_player))\n                .setContentText(\"\")\n                .setSmallIcon(R.drawable.small_icon)\n                .setContentIntent(pending)\n                .setOngoing(true)\n                .build()\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n                startForeground(\n                    NOTIFICATION_ID,\n                    notification,\n                    ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK\n                )\n            } else {\n                startForeground(NOTIFICATION_ID, notification)\n            }\n        } catch (e: Exception) {\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&\n                e is ForegroundServiceStartNotAllowedException\n            ) {\n                Timber.tag(TAG).w(\"Foreground service start not allowed (likely app in background)\")\n            } else {\n                Timber.tag(TAG).e(e, \"Failed to create foreground notification\")\n                reportException(e)\n            }\n        }\n\n        setMediaNotificationProvider(\n            DefaultMediaNotificationProvider(\n                this,\n                { NOTIFICATION_ID },\n                CHANNEL_ID,\n                R.string.music_player\n            )\n                .apply {\n                    setSmallIcon(R.drawable.small_icon)\n                },\n        )\n        player = createExoPlayer()\n        player.addListener(this@MusicService)\n        sleepTimer = SleepTimer(scope, player) { multiplier ->\n            sleepTimerVolumeMultiplier.value = multiplier\n        }\n        player.addListener(sleepTimer)\n\n        // Mark player as initialized after successful creation\n        playerInitialized.value = true\n        Timber.tag(TAG).d(\"Player successfully initialized\")\n\n        // Sync initial cache state\n        scope.launch(Dispatchers.IO) {\n            try {\n                val cachedIds = playerCache.keys.toList()\n                if (cachedIds.isNotEmpty()) {\n                    val fullyCachedIds = cachedIds.filter { mediaId ->\n                        val contentLength = playerCache.getContentMetadata(mediaId)\n                            .get(androidx.media3.datasource.cache.ContentMetadata.KEY_CONTENT_LENGTH, -1L)\n                        if (contentLength > 0) {\n                            val cachedBytes = playerCache.getCachedSpans(mediaId).sumOf { it.length }\n                            cachedBytes >= contentLength * 0.99\n                        } else {\n                            false\n                        }\n                    }\n                    if (fullyCachedIds.isNotEmpty()) {\n                        val chunkSize = 500\n                        for (i in fullyCachedIds.indices step chunkSize) {\n                            val chunk = fullyCachedIds.subList(i, minOf(i + chunkSize, fullyCachedIds.size))\n                            database.updateCachedInfoMany(chunk)\n                        }\n                    }\n                }\n            } catch (e: Exception) {\n                Timber.tag(TAG).e(e, \"Failed to sync initial cache state\")\n            }\n        }\n\n        audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager\n        setupAudioFocusRequest()\n\n        mediaLibrarySessionCallback.apply {\n            toggleLike = ::toggleLike\n            toggleStartRadio = ::toggleStartRadio\n            toggleLibrary = ::toggleLibrary\n            addToTargetPlaylist = ::addToTargetPlaylist\n        }\n        mediaSession =\n            MediaLibrarySession\n                .Builder(this, player, mediaLibrarySessionCallback)\n                .setSessionActivity(\n                    PendingIntent.getActivity(\n                        this,\n                        0,\n                        Intent(this, MainActivity::class.java),\n                        PendingIntent.FLAG_IMMUTABLE,\n                    ),\n                ).setBitmapLoader(CoilBitmapLoader(this, scope))\n                .build()\n        player.repeatMode = dataStore.get(RepeatModeKey, REPEAT_MODE_OFF)\n\n        // Restore shuffle mode if remember option is enabled\n        if (dataStore.get(RememberShuffleAndRepeatKey, true)) {\n            player.shuffleModeEnabled = dataStore.get(ShuffleModeKey, false)\n        }\n\n        // Keep a connected controller so that notification works\n        val sessionToken = SessionToken(this, ComponentName(this, MusicService::class.java))\n        val controllerFuture = MediaController.Builder(this, sessionToken).buildAsync()\n        controllerFuture.addListener({ controllerFuture.get() }, MoreExecutors.directExecutor())\n\n        connectivityManager = getSystemService()!!\n        connectivityObserver = NetworkConnectivityObserver(this)\n\n        val screenStateFilter = IntentFilter().apply {\n            addAction(Intent.ACTION_SCREEN_ON)\n            addAction(Intent.ACTION_SCREEN_OFF)\n        }\n        registerReceiver(screenStateReceiver, screenStateFilter)\n\n        audioManager.registerAudioDeviceCallback(audioDeviceCallback, null)\n\n        audioQuality = dataStore.get(AudioQualityKey).toEnum(com.metrolist.music.constants.AudioQuality.AUTO)\n        playerVolume = MutableStateFlow(dataStore.get(PlayerVolumeKey, 1f).coerceIn(0f, 1f))\n\n        // Initialize Google Cast\n        initializeCast()\n\n        // 4. Watch for EQ profile changes\n        scope.launch {\n            eqProfileRepository.activeProfile.collect { profile ->\n                if (profile != null) {\n                    val result = equalizerService.applyProfile(profile)\n                    if (result.isSuccess && player.playbackState == Player.STATE_READY && player.isPlaying) {\n                        // Instant update: flush buffers and seek slightly to re-process audio\n                        // Small seek to force re-buffer through the new EQ settings\n                        // Seek to current position effectively resets the pipeline\n                        player.seekTo(player.currentPosition)\n                    }\n                } else {\n                    equalizerService.disable()\n                    if (player.playbackState == Player.STATE_READY && player.isPlaying) {\n                        player.seekTo(player.currentPosition)\n                    }\n                }\n            }\n        }\n\n        scope.launch {\n            connectivityObserver.networkStatus.collect { isConnected ->\n                isNetworkConnected.value = isConnected\n                if (isConnected && waitingForNetworkConnection.value) {\n                    triggerRetry()\n                }\n                // Update Discord RPC when network becomes available\n                if (isConnected && discordRpc != null && player.isPlaying) {\n                    val mediaId = player.currentMetadata?.id\n                    if (mediaId != null) {\n                        database.song(mediaId).first()?.let { song ->\n                            updateDiscordRPC(song)\n                        }\n                    }\n                }\n            }\n        }\n\n        // Watch for audio quality setting changes\n        var isFirstQualityEmit = true\n        scope.launch {\n            dataStore.data\n                .map {\n                    it[AudioQualityKey]?.let { value ->\n                        com.metrolist.music.constants.AudioQuality.entries.find { it.name == value }\n                    } ?: com.metrolist.music.constants.AudioQuality.AUTO\n                }\n                .distinctUntilChanged()\n                .collect { newQuality ->\n                    val oldQuality = audioQuality\n                    audioQuality = newQuality\n\n                    // Skip reload on first emit (app startup)\n                    if (isFirstQualityEmit) {\n                        isFirstQualityEmit = false\n                        Timber.tag(\"MusicService\").i(\"QUALITY INIT: $newQuality\")\n                        return@collect\n                    }\n\n                    Timber.tag(\"MusicService\").i(\"QUALITY CHANGED: $oldQuality -> $newQuality\")\n\n                    // Reload current song with new quality\n                    val mediaId = player.currentMediaItem?.mediaId ?: return@collect\n                    val currentPosition = player.currentPosition\n                    val wasPlaying = player.isPlaying\n                    val currentIndex = player.currentMediaItemIndex\n\n                    Timber.tag(\"MusicService\").i(\"RELOADING STREAM: $mediaId at position ${currentPosition}ms\")\n\n                    // Clear cached URL to force fresh fetch\n                    songUrlCache.remove(mediaId)\n\n                    // CRITICAL: Clear caches synchronously to prevent format parsing errors\n                    runBlocking(Dispatchers.IO) {\n                        try {\n                            playerCache.removeResource(mediaId)\n                            downloadCache.removeResource(mediaId)\n                            Timber.tag(\"MusicService\").d(\"Cleared player and download cache for $mediaId\")\n                        } catch (e: Exception) {\n                            Timber.tag(\"MusicService\").e(e, \"Failed to clear cache for $mediaId\")\n                        }\n                    }\n\n                    // Set bypass flag so resolver skips cache checks\n                    bypassCacheForQualityChange.add(mediaId)\n                    Timber.tag(\"MusicService\").d(\"Set bypass cache flag for $mediaId\")\n\n                    // Reload player at same position\n                    player.stop()\n                    player.seekTo(currentIndex, currentPosition)\n                    player.prepare()\n                    if (wasPlaying) {\n                        player.play()\n                    }\n                }\n        }\n\n        combine(\n            playerVolume,\n            isMuted,\n            sleepTimerVolumeMultiplier,\n            audioFocusVolumeMultiplier,\n        ) { volume, muted, timerMultiplier, focusMultiplier ->\n            calculateEffectiveVolume(\n                volume = volume,\n                muted = muted,\n                sleepTimerMultiplier = timerMultiplier,\n                focusMultiplier = focusMultiplier,\n            )\n        }.collectLatest(scope) {\n            if (!isCrossfading) {\n                player.volume = it\n            }\n        }\n\n        playerVolume.debounce(1000).collect(scope) { volume ->\n            dataStore.edit { settings ->\n                settings[PlayerVolumeKey] = volume\n            }\n        }\n\n        currentSong.debounce(1000).collect(scope) { song ->\n            updateNotification()\n            updateWidgetUI(player.isPlaying)\n        }\n\n        combine(\n            currentMediaMetadata.distinctUntilChangedBy { it?.id },\n            dataStore.data.map { it[ShowLyricsKey] ?: false }.distinctUntilChanged(),\n        ) { mediaMetadata, showLyrics ->\n            mediaMetadata to showLyrics\n        }.collectLatest(scope) { (mediaMetadata, showLyrics) ->\n            if (showLyrics && mediaMetadata != null && database.lyrics(mediaMetadata.id)\n                    .first() == null\n            ) {\n                val lyricsWithProvider = lyricsHelper.getLyrics(mediaMetadata)\n                database.query {\n                    upsert(\n                        LyricsEntity(\n                            id = mediaMetadata.id,\n                            lyrics = lyricsWithProvider.lyrics,\n                            provider = lyricsWithProvider.provider,\n                        ),\n                    )\n                }\n            }\n        }\n\n        dataStore.data\n            .map { (it[SkipSilenceKey] ?: false) to (it[SkipSilenceInstantKey] ?: false) }\n            .distinctUntilChanged()\n            .collectLatest(scope) { (skipSilence, instantSkip) ->\n                player.skipSilenceEnabled = skipSilence\n                secondaryPlayer?.skipSilenceEnabled = skipSilence\n\n                val enableInstant = skipSilence && instantSkip\n                instantSilenceSkipEnabled.value = enableInstant\n\n                playerSilenceProcessors.values.forEach { processor ->\n                    processor.instantModeEnabled = enableInstant\n                    if (!enableInstant) {\n                        processor.resetTracking()\n                    }\n                }\n\n                if (!enableInstant) {\n                    silenceSkipJob?.cancel()\n                }\n            }\n\n        combine(\n            currentFormat,\n            dataStore.data\n                .map { it[AudioNormalizationKey] ?: true }\n                .distinctUntilChanged(),\n        ) { format, normalizeAudio ->\n            format to normalizeAudio\n        }.collectLatest(scope) { (format, normalizeAudio) -> setupLoudnessEnhancer() }\n\n        combine(\n            dataStore.data.map { it[AudioOffload] ?: false },\n            dataStore.data.map { it[CrossfadeEnabledKey] ?: false }\n        ) { offloadPref, crossfadeEnabled ->\n            // Force disable offload if crossfade is enabled to prevent volume ramp issues\n            if (crossfadeEnabled) false else offloadPref\n        }.distinctUntilChanged()\n            .collectLatest(scope) { useOffload ->\n                player.setOffloadEnabled(useOffload)\n                secondaryPlayer?.setOffloadEnabled(useOffload)\n            }\n\n        dataStore.data\n            .map { it[DiscordTokenKey] to (it[EnableDiscordRPCKey] ?: true) }\n            .debounce(300)\n            .distinctUntilChanged()\n            .collect(scope) { (key, enabled) ->\n                if (discordRpc?.isRpcRunning() == true) {\n                    discordRpc?.closeRPC()\n                }\n                discordRpc = null\n                if (key != null && enabled) {\n                    discordRpc = DiscordRPC(this, key)\n                    if (player.playbackState == Player.STATE_READY && player.playWhenReady) {\n                        currentSong.value?.let {\n                            updateDiscordRPC(it, true)\n                        }\n                    }\n                }\n            }\n\n        // Watch all Discord customization preferences\n        dataStore.data\n            .map {\n                listOf(\n                    it[DiscordUseDetailsKey],\n                    it[DiscordAdvancedModeKey],\n                    it[DiscordStatusKey],\n                    it[DiscordButton1TextKey],\n                    it[DiscordButton1VisibleKey],\n                    it[DiscordButton2TextKey],\n                    it[DiscordButton2VisibleKey],\n                    it[DiscordActivityTypeKey],\n                    it[DiscordActivityNameKey]\n                )\n            }\n            .debounce(300)\n            .distinctUntilChanged()\n            .collect(scope) {\n                if (player.playbackState == Player.STATE_READY) {\n                    currentSong.value?.let { song ->\n                        updateDiscordRPC(song, true)\n                    }\n                }\n            }\n\n        dataStore.data\n            .map { it[EnableLastFMScrobblingKey] ?: false }\n            .debounce(300)\n            .distinctUntilChanged()\n            .collect(scope) { enabled ->\n                if (enabled && scrobbleManager == null) {\n                    val delayPercent = dataStore.get(ScrobbleDelayPercentKey, LastFM.DEFAULT_SCROBBLE_DELAY_PERCENT)\n                    val minSongDuration =\n                        dataStore.get(ScrobbleMinSongDurationKey, LastFM.DEFAULT_SCROBBLE_MIN_SONG_DURATION)\n                    val delaySeconds = dataStore.get(ScrobbleDelaySecondsKey, LastFM.DEFAULT_SCROBBLE_DELAY_SECONDS)\n                    scrobbleManager = ScrobbleManager(\n                        scope,\n                        minSongDuration = minSongDuration,\n                        scrobbleDelayPercent = delayPercent,\n                        scrobbleDelaySeconds = delaySeconds\n                    )\n                    scrobbleManager?.useNowPlaying = dataStore.get(LastFMUseNowPlaying, false)\n                } else if (!enabled && scrobbleManager != null) {\n                    scrobbleManager?.destroy()\n                    scrobbleManager = null\n                }\n            }\n\n        dataStore.data\n            .map { it[LastFMUseNowPlaying] ?: false }\n            .distinctUntilChanged()\n            .collectLatest(scope) {\n                scrobbleManager?.useNowPlaying = it\n            }\n\n        dataStore.data\n            .map { prefs ->\n                Triple(\n                    prefs[ScrobbleDelayPercentKey] ?: LastFM.DEFAULT_SCROBBLE_DELAY_PERCENT,\n                    prefs[ScrobbleMinSongDurationKey] ?: LastFM.DEFAULT_SCROBBLE_MIN_SONG_DURATION,\n                    prefs[ScrobbleDelaySecondsKey] ?: LastFM.DEFAULT_SCROBBLE_DELAY_SECONDS\n                )\n            }\n            .distinctUntilChanged()\n            .collect(scope) { (delayPercent, minSongDuration, delaySeconds) ->\n                scrobbleManager?.let {\n                    it.scrobbleDelayPercent = delayPercent\n                    it.minSongDuration = minSongDuration\n                    it.scrobbleDelaySeconds = delaySeconds\n                }\n            }\n\n        combine(\n            dataStore.data.map { prefs ->\n                Triple(\n                    prefs[CrossfadeEnabledKey] ?: false,\n                    prefs[CrossfadeDurationKey] ?: 5f,\n                    prefs[CrossfadeGaplessKey] ?: true\n                )\n            },\n            listenTogetherManager.roomState\n        ) { (enabled, duration, gapless), roomState ->\n            // Disable crossfade if user is in a listen together room\n            Triple(enabled && roomState == null, duration, gapless)\n        }\n            .distinctUntilChanged()\n            .collect(scope) { (enabled, duration, gapless) ->\n                crossfadeEnabled = enabled\n                crossfadeDuration = duration * 1000f // Convert to ms\n                crossfadeGapless = gapless\n            }\n\n        if (dataStore.get(PersistentQueueKey, true)) {\n            val queueFile = filesDir.resolve(PERSISTENT_QUEUE_FILE)\n            if (queueFile.exists()) {\n                runCatching {\n                    queueFile.inputStream().use { fis ->\n                        ObjectInputStream(fis).use { oos ->\n                            oos.readObject() as PersistQueue\n                        }\n                    }\n                }.onSuccess { queue ->\n                    runCatching {\n                        // Convert back to proper queue type\n                        val restoredQueue = queue.toQueue()\n                        // Wait for player initialization before playing\n                        scope.launch {\n                            playerInitialized.first { it }\n                            if (isActive) {\n                                playQueue(\n                                    queue = restoredQueue,\n                                    playWhenReady = false,\n                                )\n                            }\n                        }\n                    }.onFailure { error ->\n                        Timber.tag(TAG).w(error, \"Failed to restore persisted queue, clearing data\")\n                        clearPersistedQueueFiles()\n                    }\n                }.onFailure { error ->\n                    Timber.tag(TAG).w(error, \"Failed to read persisted queue, clearing data\")\n                    clearPersistedQueueFiles()\n                }\n            }\n\n            val automixFile = filesDir.resolve(PERSISTENT_AUTOMIX_FILE)\n            if (automixFile.exists()) {\n                runCatching {\n                    automixFile.inputStream().use { fis ->\n                        ObjectInputStream(fis).use { oos ->\n                            oos.readObject() as PersistQueue\n                        }\n                    }\n                }.onSuccess { queue ->\n                    runCatching {\n                        automixItems.value = queue.items.map { it.toMediaItem() }\n                    }.onFailure { error ->\n                        Timber.tag(TAG).w(error, \"Failed to restore automix queue, clearing data\")\n                        clearPersistedQueueFiles()\n                    }\n                }.onFailure { error ->\n                    Timber.tag(TAG).w(error, \"Failed to read automix queue, clearing data\")\n                    clearPersistedQueueFiles()\n                }\n            }\n\n            // Restore player state\n            val playerStateFile = filesDir.resolve(PERSISTENT_PLAYER_STATE_FILE)\n            if (playerStateFile.exists()) {\n                runCatching {\n                    playerStateFile.inputStream().use { fis ->\n                        ObjectInputStream(fis).use { oos ->\n                            oos.readObject() as PersistPlayerState\n                        }\n                    }\n                }.onSuccess { playerState ->\n                    // Restore player settings after queue is loaded\n                    scope.launch {\n                        delay(1000) // Wait for queue to be loaded\n                        // Don't restore repeat/shuffle from playerState as they are already set from DataStore (source of truth)\n                        // player.repeatMode = playerState.repeatMode\n                        // player.shuffleModeEnabled = playerState.shuffleModeEnabled\n                        playerVolume.value = playerState.volume\n\n                        // Restore position if it's still valid\n                        if (playerState.currentMediaItemIndex < player.mediaItemCount) {\n                            player.seekTo(playerState.currentMediaItemIndex, playerState.currentPosition)\n                        }\n                    }\n                }.onFailure { error ->\n                    Timber.tag(TAG).w(error, \"Failed to read player state, clearing data\")\n                    clearPersistedQueueFiles()\n                }\n            }\n        }\n\n        // Save queue periodically to prevent queue loss from crash or force kill\n        scope.launch {\n            while (isActive) {\n                delay(15.seconds)\n                if (dataStore.get(PersistentQueueKey, true)) {\n                    saveQueueToDisk()\n                }\n                // Also save episode position periodically\n                val currentMetadata = player.currentMediaItem?.metadata\n                if (currentMetadata?.isEpisode == true && player.isPlaying && player.currentPosition > 0) {\n                    previousEpisodePosition = player.currentPosition\n                    saveEpisodePosition(currentMetadata.id, player.currentPosition)\n                }\n            }\n        }\n\n        // Save queue more frequently when playing to ensure state is preserved\n        scope.launch {\n            while (isActive) {\n                delay(10.seconds)\n                if (dataStore.get(PersistentQueueKey, true) && player.isPlaying) {\n                    saveQueueToDisk()\n                }\n            }\n        }\n    }\n\n    private fun createExoPlayer(): ExoPlayer {\n        val eqProcessor = CustomEqualizerAudioProcessor()\n        equalizerService.addAudioProcessor(eqProcessor)\n\n        val silenceProcessor = SilenceDetectorAudioProcessor { handleLongSilenceDetected() }\n\n        // Set initial state\n        runBlocking {\n            val skipSilence = dataStore.get(SkipSilenceKey, false)\n            val instantSkip = dataStore.get(SkipSilenceInstantKey, false)\n            silenceProcessor.instantModeEnabled = skipSilence && instantSkip\n        }\n\n        val player = ExoPlayer.Builder(this)\n            .setMediaSourceFactory(createMediaSourceFactory())\n            .setRenderersFactory(createRenderersFactory(eqProcessor, silenceProcessor))\n            .setHandleAudioBecomingNoisy(true)\n            .setWakeMode(C.WAKE_MODE_NETWORK)\n            .setAudioAttributes(\n                AudioAttributes.Builder()\n                    .setUsage(C.USAGE_MEDIA)\n                    .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)\n                    .build(),\n                false,\n            )\n            .setSeekBackIncrementMs(5000)\n            .setSeekForwardIncrementMs(5000)\n            .setDeviceVolumeControlEnabled(true)\n            .build()\n\n        playerSilenceProcessors[player] = silenceProcessor\n\n        player.apply {\n            runBlocking {\n                val offload = dataStore.get(AudioOffload, false)\n                val crossfade = dataStore.get(CrossfadeEnabledKey, false)\n                setOffloadEnabled(if (crossfade) false else offload)\n                skipSilenceEnabled = dataStore.get(SkipSilenceKey, false)\n            }\n            addAnalyticsListener(PlaybackStatsListener(false, this@MusicService))\n\n            // Cleanup handled manually in onDestroy/release\n        }\n        _playerFlow.value = player\n        return player\n    }\n\n    private fun setupAudioFocusRequest() {\n        audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)\n            .setAudioAttributes(\n                android.media.AudioAttributes.Builder()\n                    .setUsage(android.media.AudioAttributes.USAGE_MEDIA)\n                    .setContentType(android.media.AudioAttributes.CONTENT_TYPE_MUSIC)\n                    .build()\n            )\n            .setOnAudioFocusChangeListener { focusChange ->\n                handleAudioFocusChange(focusChange)\n            }\n            .setAcceptsDelayedFocusGain(true)\n            .build()\n    }\n\n    private fun handleAudioFocusChange(focusChange: Int) {\n        when (focusChange) {\n\n            AudioManager.AUDIOFOCUS_GAIN,\n            AudioManager.AUDIOFOCUS_GAIN_TRANSIENT -> {\n                hasAudioFocus = true\n                audioFocusVolumeMultiplier.value = 1f\n\n                if (wasPlayingBeforeAudioFocusLoss && !player.isPlaying && !reentrantFocusGain) {\n                    reentrantFocusGain = true\n                    scope.launch {\n                        delay(300)\n                        if (hasAudioFocus && wasPlayingBeforeAudioFocusLoss && !player.isPlaying) {\n                            // Don't start local playback if casting\n                            if (castConnectionHandler?.isCasting?.value != true) {\n                                player.play()\n                            }\n                            wasPlayingBeforeAudioFocusLoss = false\n                        }\n                        reentrantFocusGain = false\n                    }\n                }\n\n                applyEffectiveVolume()\n                lastAudioFocusState = focusChange\n            }\n\n            AudioManager.AUDIOFOCUS_LOSS -> {\n                hasAudioFocus = false\n                audioFocusVolumeMultiplier.value = 1f\n                wasPlayingBeforeAudioFocusLoss = player.isPlaying\n                if (player.isPlaying) {\n                    player.pause()\n                }\n                abandonAudioFocus()\n                lastAudioFocusState = focusChange\n            }\n\n            AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {\n                hasAudioFocus = false\n                audioFocusVolumeMultiplier.value = 1f\n                wasPlayingBeforeAudioFocusLoss = player.isPlaying\n                if (player.isPlaying) {\n                    player.pause()\n                }\n                lastAudioFocusState = focusChange\n            }\n\n            AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {\n                hasAudioFocus = false\n                audioFocusVolumeMultiplier.value = 0.2f\n                wasPlayingBeforeAudioFocusLoss = player.isPlaying\n                if (player.isPlaying) {\n                    applyEffectiveVolume()\n                }\n                lastAudioFocusState = focusChange\n            }\n\n            AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK -> {\n                hasAudioFocus = true\n                audioFocusVolumeMultiplier.value = 1f\n                applyEffectiveVolume()\n                lastAudioFocusState = focusChange\n            }\n        }\n    }\n\n    private fun requestAudioFocus(): Boolean {\n        if (hasAudioFocus) return true\n\n        audioFocusRequest?.let { request ->\n            val result = audioManager.requestAudioFocus(request)\n            hasAudioFocus = result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED\n            return hasAudioFocus\n        }\n        return false\n    }\n\n    private fun abandonAudioFocus() {\n        if (hasAudioFocus) {\n            audioFocusRequest?.let { request ->\n                audioManager.abandonAudioFocusRequest(request)\n                hasAudioFocus = false\n            }\n        }\n    }\n\n    private fun clearPersistedQueueFiles() {\n        runCatching { filesDir.resolve(PERSISTENT_QUEUE_FILE).delete() }\n        runCatching { filesDir.resolve(PERSISTENT_AUTOMIX_FILE).delete() }\n        runCatching { filesDir.resolve(PERSISTENT_PLAYER_STATE_FILE).delete() }\n    }\n\n    fun hasAudioFocusForPlayback(): Boolean {\n        return hasAudioFocus\n    }\n\n    private fun waitOnNetworkError() {\n        if (waitingForNetworkConnection.value) return\n\n        // Check if we've exceeded max retry attempts\n        if (retryCount >= MAX_RETRY_COUNT) {\n            Timber.tag(TAG).w(\"Max retry count ($MAX_RETRY_COUNT) reached, stopping playback\")\n            stopOnError()\n            retryCount = 0\n            return\n        }\n\n        waitingForNetworkConnection.value = true\n\n        // Start a retry timer with exponential backoff\n        retryJob?.cancel()\n        retryJob = scope.launch {\n            // Exponential backoff: 3s, 6s, 12s, 24s... max 30s\n            val delayMs = minOf(3000L * (1 shl retryCount), 30000L)\n            Timber.tag(TAG).d(\"Waiting ${delayMs}ms before retry attempt ${retryCount + 1}/$MAX_RETRY_COUNT\")\n            delay(delayMs)\n\n            if (isNetworkConnected.value && waitingForNetworkConnection.value) {\n                retryCount++\n                triggerRetry()\n            }\n        }\n    }\n\n    private fun triggerRetry() {\n        waitingForNetworkConnection.value = false\n        retryJob?.cancel()\n\n        if (player.currentMediaItem != null) {\n            // After 3+ failed retries, try to refresh the stream URL by seeking to current position\n            // This forces ExoPlayer to re-resolve the data source and get a fresh URL\n            if (retryCount > 3) {\n                Timber.tag(TAG).d(\"Retry count > 3, attempting to refresh stream URL\")\n                val currentPosition = player.currentPosition\n                player.seekTo(player.currentMediaItemIndex, currentPosition)\n            }\n            player.prepare()\n            // Don't call play() here - let the player auto-resume via playWhenReady\n            // This avoids stealing audio focus during retry attempts\n        }\n    }\n\n    private fun skipOnError() {\n        /**\n         * Auto skip to the next media item on error.\n         *\n         * To prevent a \"runaway diesel engine\" scenario, force the user to take action after\n         * too many errors come up too quickly. Pause to show player \"stopped\" state\n         */\n        consecutivePlaybackErr += 2\n        val nextWindowIndex = player.nextMediaItemIndex\n\n        if (consecutivePlaybackErr <= MAX_CONSECUTIVE_ERR && nextWindowIndex != C.INDEX_UNSET) {\n            player.seekTo(nextWindowIndex, C.TIME_UNSET)\n            player.prepare()\n            // Don't start local playback if casting\n            if (castConnectionHandler?.isCasting?.value != true) {\n                player.play()\n            }\n            return\n        }\n\n        player.pause()\n        consecutivePlaybackErr = 0\n    }\n\n    private fun stopOnError() {\n        player.pause()\n    }\n\n    private fun updateNotification() {\n        mediaSession.setCustomLayout(\n            listOf(\n                CommandButton\n                    .Builder()\n                    .setDisplayName(\n                        getString(\n                            if (currentSong.value?.song?.liked ==\n                                true\n                            ) {\n                                R.string.action_remove_like\n                            } else {\n                                R.string.action_like\n                            },\n                        ),\n                    )\n                    .setIconResId(if (currentSong.value?.song?.liked == true) R.drawable.ic_heart else R.drawable.ic_heart_outline)\n                    .setSessionCommand(CommandToggleLike)\n                    .setEnabled(currentSong.value != null)\n                    .build(),\n                CommandButton\n                    .Builder()\n                    .setDisplayName(\n                        getString(\n                            when (player.repeatMode) {\n                                REPEAT_MODE_OFF -> R.string.repeat_mode_off\n                                REPEAT_MODE_ONE -> R.string.repeat_mode_one\n                                REPEAT_MODE_ALL -> R.string.repeat_mode_all\n                                else -> throw IllegalStateException()\n                            },\n                        ),\n                    ).setIconResId(\n                        when (player.repeatMode) {\n                            REPEAT_MODE_OFF -> R.drawable.repeat\n                            REPEAT_MODE_ONE -> R.drawable.repeat_one_on\n                            REPEAT_MODE_ALL -> R.drawable.repeat_on\n                            else -> throw IllegalStateException()\n                        },\n                    ).setSessionCommand(CommandToggleRepeatMode)\n                    .build(),\n                CommandButton\n                    .Builder()\n                    .setDisplayName(getString(if (player.shuffleModeEnabled) R.string.action_shuffle_off else R.string.action_shuffle_on))\n                    .setIconResId(if (player.shuffleModeEnabled) R.drawable.shuffle_on else R.drawable.shuffle)\n                    .setSessionCommand(CommandToggleShuffle)\n                    .build(),\n                CommandButton.Builder()\n                    .setDisplayName(getString(R.string.start_radio))\n                    .setIconResId(R.drawable.radio)\n                    .setSessionCommand(CommandToggleStartRadio)\n                    .setEnabled(currentSong.value != null)\n                    .build(),\n                CommandButton.Builder()\n                    .setDisplayName(getString(R.string.android_auto_target_playlist))\n                    .setIconResId(R.drawable.playlist_add)\n                    .setSessionCommand(CommandAddToTargetPlaylist)\n                    .setEnabled(currentSong.value != null)\n                    .build(),\n            ),\n        )\n    }\n\n    private suspend fun recoverSong(\n        mediaId: String,\n        playbackData: YTPlayerUtils.PlaybackData? = null\n    ) {\n        val song = database.song(mediaId).first()\n        val mediaMetadata = withContext(Dispatchers.Main) {\n            player.findNextMediaItemById(mediaId)?.metadata\n        } ?: return\n        val duration = song?.song?.duration?.takeIf { it != -1 }\n            ?: mediaMetadata.duration.takeIf { it != -1 }\n            ?: (playbackData?.videoDetails ?: YTPlayerUtils.playerResponseForMetadata(mediaId)\n                .getOrNull()?.videoDetails)?.lengthSeconds?.toInt()\n            ?: -1\n        database.query {\n            if (song == null) insert(mediaMetadata.copy(duration = duration))\n            else {\n                var updatedSong = song.song\n                if (song.song.duration == -1) {\n                    updatedSong = updatedSong.copy(duration = duration)\n                }\n                // Update isVideo flag if it's different from the current value\n                if (song.song.isVideo != mediaMetadata.isVideoSong) {\n                    updatedSong = updatedSong.copy(isVideo = mediaMetadata.isVideoSong)\n                }\n                if (updatedSong != song.song) {\n                    update(updatedSong)\n                }\n            }\n        }\n        if (!database.hasRelatedSongs(mediaId)) {\n            val relatedEndpoint =\n                YouTube.next(WatchEndpoint(videoId = mediaId)).getOrNull()?.relatedEndpoint\n                    ?: return\n            val relatedPage = YouTube.related(relatedEndpoint).getOrNull() ?: return\n            database.query {\n                relatedPage.songs\n                    .map(SongItem::toMediaMetadata)\n                    .onEach(::insert)\n                    .map {\n                        RelatedSongMap(\n                            songId = mediaId,\n                            relatedSongId = it.id\n                        )\n                    }\n                    .forEach(::insert)\n            }\n        }\n    }\n\n    fun playQueue(\n        queue: Queue,\n        playWhenReady: Boolean = true,\n    ) {\n        if (!scope.isActive) scope = CoroutineScope(Dispatchers.Main) + Job()\n\n        // Safety Check : Ensuring player is initilized\n        if (!playerInitialized.value) {\n            Timber.tag(TAG).w(\"playQueue called before player initialization, queuing request\")\n            scope.launch {\n                playerInitialized.first { it }\n                playQueue(queue, playWhenReady)\n            }\n            return\n        }\n\n        currentQueue = queue\n        queueTitle = null\n        val persistShuffleAcrossQueues = dataStore.get(PersistentShuffleAcrossQueuesKey, false)\n        val previousShuffleEnabled = player.shuffleModeEnabled\n        if (!persistShuffleAcrossQueues) {\n            player.shuffleModeEnabled = false\n        }\n        // Reset original queue size when starting a new queue\n        originalQueueSize = 0\n        if (queue.preloadItem != null) {\n            player.setMediaItem(queue.preloadItem!!.toMediaItem())\n            player.prepare()\n            player.playWhenReady = playWhenReady\n        }\n        scope.launch(SilentHandler) {\n            val initialStatus =\n                withContext(Dispatchers.IO) {\n                    queue.getInitialStatus()\n                        .filterExplicit(dataStore.get(HideExplicitKey, false))\n                        .filterVideoSongs(dataStore.get(HideVideoSongsKey, false))\n                }\n            if (queue.preloadItem != null && player.playbackState == STATE_IDLE) return@launch\n            if (initialStatus.title != null) {\n                queueTitle = initialStatus.title\n            }\n            if (initialStatus.items.isEmpty()) return@launch\n            // Track original queue size for shuffle playlist first feature\n            originalQueueSize = initialStatus.items.size\n            if (queue.preloadItem != null) {\n                player.addMediaItems(\n                    0,\n                    initialStatus.items.subList(0, initialStatus.mediaItemIndex)\n                )\n                player.addMediaItems(\n                    initialStatus.items.subList(\n                        initialStatus.mediaItemIndex + 1,\n                        initialStatus.items.size\n                    )\n                )\n            } else {\n                player.setMediaItems(\n                    initialStatus.items,\n                    if (initialStatus.mediaItemIndex >\n                        0\n                    ) {\n                        initialStatus.mediaItemIndex\n                    } else {\n                        0\n                    },\n                    initialStatus.position,\n                )\n                player.prepare()\n                player.playWhenReady = playWhenReady\n            }\n\n            // Rebuild shuffle order if shuffle is enabled\n            if (player.shuffleModeEnabled) {\n                val shufflePlaylistFirst = dataStore.get(ShufflePlaylistFirstKey, false)\n                applyShuffleOrder(player.currentMediaItemIndex, player.mediaItemCount, shufflePlaylistFirst)\n            }\n        }\n    }\n\n    fun startRadioSeamlessly() {\n        // Safety Check: Ensure Player is initilized\n        if (!playerInitialized.value) {\n            Timber.tag(TAG).w(\"startRadioSeamlessly called before player initialization\")\n            return\n        }\n\n        val currentMediaMetadata = player.currentMetadata ?: return\n\n        val currentIndex = player.currentMediaItemIndex\n        val currentMediaId = currentMediaMetadata.id\n\n        scope.launch(SilentHandler) {\n            // Use simple videoId to let YouTube personalize recommendations\n            val radioQueue = YouTubeQueue(\n                endpoint = WatchEndpoint(\n                    videoId = currentMediaId\n                )\n            )\n\n            try {\n                val initialStatus = withContext(Dispatchers.IO) {\n                    radioQueue.getInitialStatus()\n                        .filterExplicit(dataStore.get(HideExplicitKey, false))\n                        .filterVideoSongs(dataStore.get(HideVideoSongsKey, false))\n                }\n\n                if (initialStatus.title != null) {\n                    queueTitle = initialStatus.title\n                }\n\n                // Filter radio items to exclude current media item\n                val radioItems = initialStatus.items.filter { item ->\n                    item.mediaId != currentMediaId\n                }\n\n                if (radioItems.isNotEmpty()) {\n                    val itemCount = player.mediaItemCount\n\n                    if (itemCount > currentIndex + 1) {\n                        player.removeMediaItems(currentIndex + 1, itemCount)\n                    }\n\n                    player.addMediaItems(currentIndex + 1, radioItems)\n                    if (player.shuffleModeEnabled) {\n                        val shufflePlaylistFirst = dataStore.get(ShufflePlaylistFirstKey, false)\n                        applyShuffleOrder(player.currentMediaItemIndex, player.mediaItemCount, shufflePlaylistFirst)\n                    }\n                }\n\n                currentQueue = radioQueue\n            } catch (e: Exception) {\n                // Fallback: try with related endpoint\n                try {\n                    val nextResult = withContext(Dispatchers.IO) {\n                        YouTube.next(WatchEndpoint(videoId = currentMediaId)).getOrNull()\n                    }\n                    nextResult?.relatedEndpoint?.let { relatedEndpoint ->\n                        val relatedPage = withContext(Dispatchers.IO) {\n                            YouTube.related(relatedEndpoint).getOrNull()\n                        }\n                        relatedPage?.songs?.let { songs ->\n                            val radioItems = songs\n                                .filter { it.id != currentMediaId }\n                                .map { it.toMediaItem() }\n                                .filterExplicit(dataStore.get(HideExplicitKey, false))\n                                .filterVideoSongs(dataStore.get(HideVideoSongsKey, false))\n\n                            if (radioItems.isNotEmpty()) {\n                                val itemCount = player.mediaItemCount\n                                if (itemCount > currentIndex + 1) {\n                                    player.removeMediaItems(currentIndex + 1, itemCount)\n                                }\n                                player.addMediaItems(currentIndex + 1, radioItems)\n                                if (player.shuffleModeEnabled) {\n                                    val shufflePlaylistFirst = dataStore.get(ShufflePlaylistFirstKey, false)\n                                    applyShuffleOrder(\n                                        player.currentMediaItemIndex,\n                                        player.mediaItemCount,\n                                        shufflePlaylistFirst\n                                    )\n                                }\n                            }\n                        }\n                    }\n                } catch (_: Exception) {\n                    // Silent fail\n                }\n            }\n        }\n    }\n\n    fun getAutomixAlbum(albumId: String) {\n        scope.launch(SilentHandler) {\n            YouTube\n                .album(albumId)\n                .onSuccess {\n                    getAutomix(it.album.playlistId)\n                }\n        }\n    }\n\n    fun getAutomix(playlistId: String) {\n        if (dataStore.get(SimilarContent, true) &&\n            !(dataStore.get(DisableLoadMoreWhenRepeatAllKey, false) && player.repeatMode == REPEAT_MODE_ALL)\n        ) {\n            scope.launch(SilentHandler) {\n                try {\n                    // Try primary method\n                    YouTube.next(WatchEndpoint(playlistId = playlistId))\n                        .onSuccess { firstResult ->\n                            YouTube.next(WatchEndpoint(playlistId = firstResult.endpoint.playlistId))\n                                .onSuccess { secondResult ->\n                                    automixItems.value = secondResult.items.map { song ->\n                                        song.toMediaItem()\n                                    }\n                                }\n                                .onFailure {\n                                    // Fallback: use first result items\n                                    if (firstResult.items.isNotEmpty()) {\n                                        automixItems.value = firstResult.items.map { song ->\n                                            song.toMediaItem()\n                                        }\n                                    }\n                                }\n                        }\n                        .onFailure {\n                            // Fallback: try with radio format\n                            val currentSong = player.currentMetadata\n                            if (currentSong != null) {\n                                // Use simple videoId for better personalized recommendations\n                                YouTube.next(\n                                    WatchEndpoint(\n                                        videoId = currentSong.id\n                                    )\n                                ).onSuccess { radioResult ->\n                                    val filteredItems = radioResult.items\n                                        .filter { it.id != currentSong.id }\n                                        .map { it.toMediaItem() }\n                                    if (filteredItems.isNotEmpty()) {\n                                        automixItems.value = filteredItems\n                                    }\n                                }.onFailure {\n                                    // Final fallback: try related endpoint\n                                    YouTube.next(WatchEndpoint(videoId = currentSong.id))\n                                        .getOrNull()?.relatedEndpoint?.let { relatedEndpoint ->\n                                            YouTube.related(relatedEndpoint).onSuccess { relatedPage ->\n                                                val relatedItems = relatedPage.songs\n                                                    .filter { it.id != currentSong.id }\n                                                    .map { it.toMediaItem() }\n                                                if (relatedItems.isNotEmpty()) {\n                                                    automixItems.value = relatedItems\n\n                                                }\n                                            }\n                                        }\n                                }\n                            }\n                        }\n                } catch (_: Exception) {\n                    // Silent fail\n                }\n            }\n        }\n    }\n\n    fun addToQueueAutomix(\n        item: MediaItem,\n        position: Int,\n    ) {\n        automixItems.value =\n            automixItems.value.toMutableList().apply {\n                removeAt(position)\n            }\n        addToQueue(listOf(item))\n    }\n\n    fun playNextAutomix(\n        item: MediaItem,\n        position: Int,\n    ) {\n        automixItems.value =\n            automixItems.value.toMutableList().apply {\n                removeAt(position)\n            }\n        playNext(listOf(item))\n    }\n\n    fun clearAutomix() {\n        automixItems.value = emptyList()\n    }\n\n    fun playNext(items: List<MediaItem>) {\n        // If queue is empty or player is idle, play immediately instead\n        if (player.mediaItemCount == 0 || player.playbackState == STATE_IDLE) {\n            player.setMediaItems(items)\n            player.prepare()\n            // Don't start local playback if casting\n            if (castConnectionHandler?.isCasting?.value != true) {\n                player.play()\n            }\n            return\n        }\n\n        // Remove duplicates if enabled\n        if (dataStore.get(PreventDuplicateTracksInQueueKey, false)) {\n            val itemIds = items.map { it.mediaId }.toSet()\n            val indicesToRemove = mutableListOf<Int>()\n            val currentIndex = player.currentMediaItemIndex\n\n            for (i in 0 until player.mediaItemCount) {\n                if (i != currentIndex && player.getMediaItemAt(i).mediaId in itemIds) {\n                    indicesToRemove.add(i)\n                }\n            }\n\n            // Remove from highest index to lowest to maintain index stability\n            indicesToRemove.sortedDescending().forEach { index ->\n                player.removeMediaItem(index)\n            }\n        }\n\n        val insertIndex = player.currentMediaItemIndex + 1\n        val shuffleEnabled = player.shuffleModeEnabled\n\n        // Insert items immediately after the current item in the window/index space\n        player.addMediaItems(insertIndex, items)\n        player.prepare()\n\n        if (shuffleEnabled) {\n            // Rebuild shuffle order so that newly inserted items are played next\n            val timeline = player.currentTimeline\n            if (!timeline.isEmpty) {\n                val size = timeline.windowCount\n                val currentIndex = player.currentMediaItemIndex\n\n                // Newly inserted indices are a contiguous range [insertIndex, insertIndex + items.size)\n                val newIndices = (insertIndex until (insertIndex + items.size)).toSet()\n\n                // Collect existing shuffle traversal order excluding current index\n                val orderAfter = mutableListOf<Int>()\n                var idx = currentIndex\n                while (true) {\n                    idx = timeline.getNextWindowIndex(idx, Player.REPEAT_MODE_OFF, /*shuffleModeEnabled=*/true)\n                    if (idx == C.INDEX_UNSET) break\n                    if (idx != currentIndex) orderAfter.add(idx)\n                }\n\n                val prevList = mutableListOf<Int>()\n                var pIdx = currentIndex\n                while (true) {\n                    pIdx = timeline.getPreviousWindowIndex(pIdx, Player.REPEAT_MODE_OFF, /*shuffleModeEnabled=*/true)\n                    if (pIdx == C.INDEX_UNSET) break\n                    if (pIdx != currentIndex) prevList.add(pIdx)\n                }\n                prevList.reverse() // preserve original forward order\n\n                val existingOrder = (prevList + orderAfter).filter { it != currentIndex && it !in newIndices }\n\n                // Build new shuffle order: current -> newly inserted (in insertion order) -> rest\n                val nextBlock = (insertIndex until (insertIndex + items.size)).toList()\n                val finalOrder = IntArray(size)\n                var pos = 0\n                finalOrder[pos++] = currentIndex\n                nextBlock.forEach { if (it in 0 until size) finalOrder[pos++] = it }\n                existingOrder.forEach { if (pos < size) finalOrder[pos++] = it }\n\n                // Fill any missing indices (safety) to ensure a full permutation\n                if (pos < size) {\n                    for (i in 0 until size) {\n                        if (!finalOrder.contains(i)) {\n                            finalOrder[pos++] = i\n                            if (pos == size) break\n                        }\n                    }\n                }\n\n                player.setShuffleOrder(DefaultShuffleOrder(finalOrder, System.currentTimeMillis()))\n            }\n        }\n    }\n\n    fun addToQueue(items: List<MediaItem>) {\n        // Remove duplicates if enabled\n        if (dataStore.get(PreventDuplicateTracksInQueueKey, false)) {\n            val itemIds = items.map { it.mediaId }.toSet()\n            val indicesToRemove = mutableListOf<Int>()\n            val currentIndex = player.currentMediaItemIndex\n\n            for (i in 0 until player.mediaItemCount) {\n                if (i != currentIndex && player.getMediaItemAt(i).mediaId in itemIds) {\n                    indicesToRemove.add(i)\n                }\n            }\n\n            // Remove from highest index to lowest to maintain index stability\n            indicesToRemove.sortedDescending().forEach { index ->\n                player.removeMediaItem(index)\n            }\n        }\n\n        player.addMediaItems(items)\n        if (player.shuffleModeEnabled) {\n            val shufflePlaylistFirst = dataStore.get(ShufflePlaylistFirstKey, false)\n            applyShuffleOrder(player.currentMediaItemIndex, player.mediaItemCount, shufflePlaylistFirst)\n        }\n        player.prepare()\n    }\n\n    fun toggleLibrary() {\n        scope.launch {\n            val songToToggle = currentSong.first()\n            songToToggle?.let {\n                val isInLibrary = it.song.inLibrary != null\n                val token = if (isInLibrary) it.song.libraryRemoveToken else it.song.libraryAddToken\n\n                // Call YouTube API with feedback token if available\n                token?.let { feedbackToken ->\n                    YouTube.feedback(listOf(feedbackToken))\n                }\n\n                // Update local database\n                database.query {\n                    update(it.song.toggleLibrary())\n                }\n                currentMediaMetadata.value = player.currentMetadata\n            }\n        }\n    }\n\n    fun toggleLike() {\n        scope.launch {\n            val songToToggle = currentSong.first()\n            songToToggle?.let { librarySong ->\n                val songEntity = librarySong.song\n\n                // For podcast episodes, toggle save for later instead of like\n                if (songEntity.isEpisode) {\n                    toggleEpisodeSaveForLater(songEntity)\n                    return@let\n                }\n\n                val song = songEntity.toggleLike()\n                database.query {\n                    update(song)\n                    syncUtils.likeSong(song)\n\n                    // Check if auto-download on like is enabled and the song is now liked\n                    if (dataStore.get(AutoDownloadOnLikeKey, false) && song.liked) {\n                        // Trigger download for the liked song\n                        val downloadRequest =\n                            androidx.media3.exoplayer.offline.DownloadRequest\n                                .Builder(song.id, song.id.toUri())\n                                .setCustomCacheKey(song.id)\n                                .setData(song.title.toByteArray())\n                                .build()\n                        androidx.media3.exoplayer.offline.DownloadService.sendAddDownload(\n                            this@MusicService,\n                            ExoDownloadService::class.java,\n                            downloadRequest,\n                            false\n                        )\n                    }\n                }\n                currentMediaMetadata.value = player.currentMetadata\n            }\n        }\n    }\n\n    fun addToTargetPlaylist() {\n        scope.launch {\n            val currentSong = currentSong.first() ?: return@launch\n            val targetPlaylistId = dataStore.get(AndroidAutoTargetPlaylistKey, MediaSessionConstants.TARGET_PLAYLIST_AUTO)\n\n            if (targetPlaylistId == MediaSessionConstants.TARGET_PLAYLIST_AUTO) {\n                Handler(Looper.getMainLooper()).post {\n                    Toast.makeText(\n                        this@MusicService,\n                        getString(R.string.android_auto_target_playlist_not_set),\n                        Toast.LENGTH_SHORT\n                    ).show()\n                }\n                return@launch\n            }\n\n            database.query {\n                insert(\n                    com.metrolist.music.db.entities.PlaylistSongMap(\n                        playlistId = targetPlaylistId,\n                        songId = currentSong.id,\n                        position = Int.MAX_VALUE\n                    )\n                )\n            }\n        }\n    }\n    \n    private suspend fun toggleEpisodeSaveForLater(songEntity: com.metrolist.music.db.entities.SongEntity) {\n        val isCurrentlySaved = songEntity.inLibrary != null\n        val shouldBeSaved = !isCurrentlySaved\n\n        // Update database first (optimistic update)\n        // Also ensure isEpisode = true so it appears in saved episodes list\n        database.query {\n            update(songEntity.copy(\n                inLibrary = if (isCurrentlySaved) null else java.time.LocalDateTime.now(),\n                isEpisode = true\n            ))\n        }\n        currentMediaMetadata.value = player.currentMetadata\n\n        // Sync with YouTube (handles login check internally)\n        val setVideoId = if (isCurrentlySaved) database.getSetVideoId(songEntity.id)?.setVideoId else null\n        syncUtils.saveEpisode(songEntity.id, shouldBeSaved, setVideoId)\n    }\n\n    fun toggleStartRadio() {\n        startRadioSeamlessly()\n    }\n\n    private fun setupLoudnessEnhancer() {\n        val audioSessionId = player.audioSessionId\n\n        if (audioSessionId == C.AUDIO_SESSION_ID_UNSET || audioSessionId <= 0) {\n            Timber.tag(TAG)\n                .w(\"setupLoudnessEnhancer: invalid audioSessionId ($audioSessionId), cannot create effect yet\")\n            return\n        }\n\n        // Create or recreate enhancer if needed\n        if (loudnessEnhancer == null) {\n            try {\n                loudnessEnhancer = LoudnessEnhancer(audioSessionId)\n                Timber.tag(TAG).d(\"LoudnessEnhancer created for sessionId=$audioSessionId\")\n            } catch (e: Exception) {\n                reportException(e)\n                loudnessEnhancer = null\n                return\n            }\n        }\n\n        scope.launch {\n            try {\n                val currentMediaId = withContext(Dispatchers.Main) {\n                    player.currentMediaItem?.mediaId\n                }\n\n                val normalizeAudio = withContext(Dispatchers.IO) {\n                    dataStore.data.map { it[AudioNormalizationKey] ?: true }.first()\n                }\n\n                if (normalizeAudio && currentMediaId != null) {\n                    val format = withContext(Dispatchers.IO) {\n                        database.format(currentMediaId).first()\n                    }\n\n                    Timber.tag(TAG).d(\"Audio normalization enabled: $normalizeAudio\")\n                    Timber.tag(TAG)\n                        .d(\"Format loudnessDb: ${format?.loudnessDb}, perceptualLoudnessDb: ${format?.perceptualLoudnessDb}\")\n\n                    // Use loudnessDb if available, otherwise fall back to perceptualLoudnessDb\n                    val loudness = format?.loudnessDb ?: format?.perceptualLoudnessDb\n\n                    withContext(Dispatchers.Main) {\n                        if (loudness != null) {\n                            val loudnessDb = loudness.toFloat()\n                            val targetGain = (-loudnessDb * 100).toInt()\n                            val clampedGain = targetGain.coerceIn(MIN_GAIN_MB, MAX_GAIN_MB)\n\n                            Timber.tag(TAG)\n                                .d(\"Calculated raw normalization gain: $targetGain mB (from loudness: $loudnessDb)\")\n\n                            try {\n                                loudnessEnhancer?.setTargetGain(clampedGain)\n                                loudnessEnhancer?.enabled = true\n                                Timber.tag(TAG).i(\"LoudnessEnhancer gain applied: $clampedGain mB\")\n                            } catch (e: Exception) {\n                                Timber.tag(TAG).e(e, \"Failed to apply loudness enhancement\")\n                                reportException(e)\n                                releaseLoudnessEnhancer()\n                            }\n                        } else {\n                            loudnessEnhancer?.enabled = false\n                            Timber.tag(TAG)\n                                .w(\"Normalization enabled but no loudness data available - no normalization applied\")\n                        }\n                    }\n                } else {\n                    withContext(Dispatchers.Main) {\n                        loudnessEnhancer?.enabled = false\n                        Timber.tag(TAG).d(\"setupLoudnessEnhancer: normalization disabled or mediaId unavailable\")\n                    }\n                }\n            } catch (e: Exception) {\n                reportException(e)\n                releaseLoudnessEnhancer()\n            }\n        }\n    }\n\n    private fun releaseLoudnessEnhancer() {\n        try {\n            loudnessEnhancer?.release()\n            Timber.tag(TAG).d(\"LoudnessEnhancer released\")\n        } catch (e: Exception) {\n            reportException(e)\n            Timber.tag(TAG).e(e, \"Error releasing LoudnessEnhancer: ${e.message}\")\n        } finally {\n            loudnessEnhancer = null\n        }\n    }\n\n    private fun openAudioEffectSession() {\n        if (isAudioEffectSessionOpened) return\n        isAudioEffectSessionOpened = true\n        setupLoudnessEnhancer()\n        sendBroadcast(\n            Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION).apply {\n                putExtra(AudioEffect.EXTRA_AUDIO_SESSION, player.audioSessionId)\n                putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)\n                putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC)\n            },\n        )\n    }\n\n    private fun closeAudioEffectSession() {\n        if (!isAudioEffectSessionOpened) return\n        isAudioEffectSessionOpened = false\n        releaseLoudnessEnhancer()\n        sendBroadcast(\n            Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION).apply {\n                putExtra(AudioEffect.EXTRA_AUDIO_SESSION, player.audioSessionId)\n                putExtra(AudioEffect.EXTRA_PACKAGE_NAME, packageName)\n            },\n        )\n    }\n\n    private var previousMediaItemIndex = C.INDEX_UNSET\n    private var previousEpisodeId: String? = null\n    private var previousEpisodePosition: Long = 0L\n\n    /**\n     * Save podcast episode playback position to database.\n     * Only saves if the item is an episode and position is meaningful (> 3 seconds).\n     */\n    private fun saveEpisodePosition(episodeId: String, positionMs: Long) {\n        if (positionMs < 3000) return // Don't save if less than 3 seconds played\n        scope.launch(Dispatchers.IO + SilentHandler) {\n            database.updatePlaybackPosition(episodeId, positionMs)\n            Timber.tag(TAG).d(\"Saved episode position: $episodeId at ${positionMs}ms\")\n        }\n    }\n\n    /**\n     * Restore podcast episode playback position from database.\n     * Seeks to saved position if available.\n     */\n    private fun restoreEpisodePosition(episodeId: String) {\n        scope.launch(Dispatchers.IO + SilentHandler) {\n            val savedPosition = database.getPlaybackPosition(episodeId)\n            if (savedPosition != null && savedPosition > 0) {\n                withContext(Dispatchers.Main) {\n                    // Only seek if we're still on the same episode\n                    if (player.currentMediaItem?.mediaId == episodeId) {\n                        player.seekTo(savedPosition)\n                        Timber.tag(TAG).d(\"Restored episode position: $episodeId to ${savedPosition}ms\")\n                    }\n                }\n            }\n        }\n    }\n\n    override fun onMediaItemTransition(\n        mediaItem: MediaItem?,\n        reason: Int,\n    ) {\n        // Save previous episode position if it was an episode\n        previousEpisodeId?.let { episodeId ->\n            if (previousEpisodePosition > 0) {\n                saveEpisodePosition(episodeId, previousEpisodePosition)\n            }\n        }\n        previousEpisodeId = null\n        previousEpisodePosition = 0L\n\n        // Check if new item is an episode and restore its position\n        val newMetadata = mediaItem?.metadata\n        if (newMetadata?.isEpisode == true) {\n            previousEpisodeId = newMetadata.id\n            // Delay restoration to let playback start\n            scope.launch {\n                delay(100)\n                restoreEpisodePosition(newMetadata.id)\n            }\n        }\n\n        // Force Repeat One if the player ignored it and auto-advanced\n        if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {\n            val repeatMode = runBlocking { dataStore.get(RepeatModeKey, REPEAT_MODE_OFF) }\n            if (repeatMode == REPEAT_MODE_ONE &&\n                previousMediaItemIndex != C.INDEX_UNSET &&\n                previousMediaItemIndex != player.currentMediaItemIndex\n            ) {\n\n                player.seekTo(previousMediaItemIndex, 0)\n            }\n        }\n        previousMediaItemIndex = player.currentMediaItemIndex\n\n        lastPlaybackSpeed = -1.0f // force update song\n\n        setupLoudnessEnhancer()\n\n        discordUpdateJob?.cancel()\n\n        scrobbleManager?.onSongStop()\n        if (player.playWhenReady && player.playbackState == Player.STATE_READY) {\n            scrobbleManager?.onSongStart(player.currentMetadata, duration = player.duration)\n        }\n\n        // Sync Cast when media changes and Cast is connected\n        // Skip if this change was triggered by Cast sync (to prevent loops)\n        if (castConnectionHandler?.isCasting?.value == true &&\n            castConnectionHandler?.isSyncingFromCast != true &&\n            mediaItem != null\n        ) {\n            val metadata = mediaItem.metadata\n            if (metadata != null) {\n                // Try to navigate to the item if it's already in Cast queue\n                // This avoids a full reload which causes the widget to refresh\n                val navigated = castConnectionHandler?.navigateToMediaIfInQueue(metadata.id) ?: false\n                if (!navigated) {\n                    // Item not in Cast queue, need to reload\n                    castConnectionHandler?.loadMedia(metadata)\n                }\n            }\n        }\n\n        // Auto load more songs from queue\n        if (dataStore.get(AutoLoadMoreKey, true) &&\n            reason != Player.MEDIA_ITEM_TRANSITION_REASON_REPEAT &&\n            player.mediaItemCount - player.currentMediaItemIndex <= 5 &&\n            currentQueue.hasNextPage() &&\n            !(dataStore.get(DisableLoadMoreWhenRepeatAllKey, false) && player.repeatMode == REPEAT_MODE_ALL)\n        ) {\n            scope.launch(SilentHandler) {\n                val mediaItems = withContext(Dispatchers.IO) {\n                    currentQueue.nextPage()\n                        .filterExplicit(dataStore.get(HideExplicitKey, false))\n                        .filterVideoSongs(dataStore.get(HideVideoSongsKey, false))\n                }\n                if (player.playbackState != STATE_IDLE && mediaItems.isNotEmpty()) {\n                    player.addMediaItems(mediaItems)\n                    if (player.shuffleModeEnabled) {\n                        val shufflePlaylistFirst = dataStore.get(ShufflePlaylistFirstKey, false)\n                        applyShuffleOrder(player.currentMediaItemIndex, player.mediaItemCount, shufflePlaylistFirst)\n                    }\n                }\n            }\n        }\n\n        // Save state when media item changes\n        if (dataStore.get(PersistentQueueKey, true)) {\n            saveQueueToDisk()\n        }\n    }\n\n    override fun onPlaybackStateChanged(\n        @Player.State playbackState: Int,\n    ) {\n        // Force Repeat All if the player ignored it and ended playback\n        if (playbackState == Player.STATE_ENDED) {\n            val repeatMode = runBlocking { dataStore.get(RepeatModeKey, REPEAT_MODE_OFF) }\n            if (repeatMode == REPEAT_MODE_ALL && player.mediaItemCount > 0) {\n                player.seekTo(0, 0)\n                player.prepare()\n                player.play()\n            }\n        }\n\n        // Save state when playback state changes (but not during silence skipping)\n        if (dataStore.get(PersistentQueueKey, true) && !isSilenceSkipping) {\n            saveQueueToDisk()\n        }\n\n        if (playbackState == Player.STATE_READY) {\n            consecutivePlaybackErr = 0\n            retryCount = 0\n            waitingForNetworkConnection.value = false\n            retryJob?.cancel()\n\n            // Reset retry count for current song on successful playback\n            player.currentMediaItem?.mediaId?.let { mediaId ->\n                resetRetryCount(mediaId)\n                Timber.tag(TAG).d(\"Playback successful for $mediaId, reset retry count\")\n            }\n            scheduleCrossfade()\n        }\n\n        if (playbackState == Player.STATE_IDLE || playbackState == Player.STATE_ENDED) {\n            scrobbleManager?.onSongStop()\n        }\n    }\n\n    override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {\n        // Safety net: if local player tries to start while casting, immediately pause it\n        if (playWhenReady && castConnectionHandler?.isCasting?.value == true) {\n            player.pause()\n            return\n        }\n\n        if (reason == Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST) {\n            if (playWhenReady) {\n                isPausedByVolumeMute = false\n            }\n\n            if (!playWhenReady && !isPausedByVolumeMute) {\n                wasPlayingBeforeVolumeMute = false\n            }\n        }\n\n        // Save episode position when pausing\n        if (!playWhenReady) {\n            val currentMetadata = player.currentMediaItem?.metadata\n            if (currentMetadata?.isEpisode == true && player.currentPosition > 0) {\n                saveEpisodePosition(currentMetadata.id, player.currentPosition)\n                previousEpisodePosition = player.currentPosition\n            }\n        }\n\n        if (playWhenReady) {\n            setupLoudnessEnhancer()\n        }\n    }\n\n    override fun onEvents(\n        player: Player,\n        events: Player.Events,\n    ) {\n        if (events.containsAny(\n                Player.EVENT_PLAYBACK_STATE_CHANGED,\n                Player.EVENT_PLAY_WHEN_READY_CHANGED\n            )\n        ) {\n            scheduleCrossfade()\n            val isBufferingOrReady =\n                player.playbackState == Player.STATE_BUFFERING || player.playbackState == Player.STATE_READY\n            if (isBufferingOrReady && player.playWhenReady) {\n                val focusGranted = requestAudioFocus()\n                if (focusGranted) {\n                    openAudioEffectSession()\n                }\n            } else {\n                closeAudioEffectSession()\n            }\n        }\n        if (events.containsAny(EVENT_TIMELINE_CHANGED, EVENT_POSITION_DISCONTINUITY)) {\n            currentMediaMetadata.value = player.currentMetadata\n        }\n\n        // Widget and Discord RPC updates\n        if (events.containsAny(Player.EVENT_IS_PLAYING_CHANGED)) {\n            updateWidgetUI(player.isPlaying)\n            if (player.isPlaying) {\n                startWidgetUpdates()\n            } else {\n                stopWidgetUpdates()\n            }\n            if (!player.isPlaying && !events.containsAny(\n                    Player.EVENT_POSITION_DISCONTINUITY,\n                    Player.EVENT_MEDIA_ITEM_TRANSITION\n                )\n            ) {\n                scope.launch {\n                    discordRpc?.close()\n                }\n            }\n        }\n\n        // Update Discord RPC when media item changes or playback starts\n        if (events.containsAny(\n                Player.EVENT_MEDIA_ITEM_TRANSITION,\n                Player.EVENT_IS_PLAYING_CHANGED\n            ) && player.isPlaying\n        ) {\n            val mediaId = player.currentMetadata?.id\n            if (mediaId != null) {\n                scope.launch {\n                    // Fetch song from database to get full info\n                    database.song(mediaId).first()?.let { song ->\n                        updateDiscordRPC(song)\n                    }\n                }\n            }\n        }\n\n        // Scrobbling\n        if (events.containsAny(Player.EVENT_IS_PLAYING_CHANGED)) {\n            scrobbleManager?.onPlayerStateChanged(player.isPlaying, player.currentMetadata, duration = player.duration)\n        }\n\n    }\n\n    override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {\n        updateNotification()\n        if (shuffleModeEnabled) {\n            // If queue is empty, don't shuffle\n            if (player.mediaItemCount == 0) return\n\n            val shufflePlaylistFirst = dataStore.get(ShufflePlaylistFirstKey, false)\n            val currentIndex = player.currentMediaItemIndex\n            val totalCount = player.mediaItemCount\n\n            applyShuffleOrder(currentIndex, totalCount, shufflePlaylistFirst)\n        }\n\n        // Save shuffle mode to preferences\n        if (dataStore.get(RememberShuffleAndRepeatKey, true)) {\n            scope.launch {\n                dataStore.edit { settings ->\n                    settings[ShuffleModeKey] = shuffleModeEnabled\n                }\n            }\n        }\n\n        // Save state when shuffle mode changes\n        if (dataStore.get(PersistentQueueKey, true)) {\n            saveQueueToDisk()\n        }\n    }\n\n    override fun onRepeatModeChanged(repeatMode: Int) {\n        updateNotification()\n        scope.launch {\n            dataStore.edit { settings ->\n                settings[RepeatModeKey] = repeatMode\n            }\n        }\n\n        // Save state when repeat mode changes\n        if (dataStore.get(PersistentQueueKey, true)) {\n            saveQueueToDisk()\n        }\n    }\n\n    /**\n     * Applies a new shuffle order to the player, maintaining the current item's position.\n     * If `shufflePlaylistFirst` is true, it attempts to shuffle original items separately from added items.\n     */\n    private fun applyShuffleOrder(\n        currentIndex: Int,\n        totalCount: Int,\n        shufflePlaylistFirst: Boolean\n    ) {\n        if (totalCount == 0) return\n\n        if (shufflePlaylistFirst && originalQueueSize > 0 && originalQueueSize < totalCount) {\n            // Shuffle original items and added items separately\n            val originalIndices = (0 until originalQueueSize).filter { it != currentIndex }.toMutableList()\n            val addedIndices = (originalQueueSize until totalCount).filter { it != currentIndex }.toMutableList()\n\n            originalIndices.shuffle()\n            addedIndices.shuffle()\n\n            val shuffledIndices = IntArray(totalCount)\n            var pos = 0\n            shuffledIndices[pos++] = currentIndex\n\n            if (currentIndex < originalQueueSize) {\n                originalIndices.forEach { shuffledIndices[pos++] = it }\n                addedIndices.forEach { shuffledIndices[pos++] = it }\n            } else {\n                (0 until originalQueueSize).shuffled().forEach { shuffledIndices[pos++] = it }\n                addedIndices.forEach { shuffledIndices[pos++] = it }\n            }\n            player.setShuffleOrder(DefaultShuffleOrder(shuffledIndices, System.currentTimeMillis()))\n        } else {\n            val shuffledIndices = IntArray(totalCount) { it }\n            shuffledIndices.shuffle()\n            // Ensure current item is first in the shuffle order\n            val currentItemIndexInShuffled = shuffledIndices.indexOf(currentIndex)\n            if (currentItemIndexInShuffled != -1) { // Should always be true if totalCount > 0\n                val temp = shuffledIndices[0]\n                shuffledIndices[0] = shuffledIndices[currentItemIndexInShuffled]\n                shuffledIndices[currentItemIndexInShuffled] = temp\n            }\n            player.setShuffleOrder(DefaultShuffleOrder(shuffledIndices, System.currentTimeMillis()))\n        }\n    }\n\n    override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) {\n        super.onPlaybackParametersChanged(playbackParameters)\n        if (playbackParameters.speed != lastPlaybackSpeed) {\n            lastPlaybackSpeed = playbackParameters.speed\n            discordUpdateJob?.cancel()\n\n            // update scheduling thingy\n            discordUpdateJob = scope.launch {\n                delay(1000)\n                if (player.playWhenReady && player.playbackState == Player.STATE_READY) {\n                    currentSong.value?.let { song ->\n                        updateDiscordRPC(song)\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Extracts the HTTP response code from an error's cause chain.\n     * Returns null if no HTTP response code is found.\n     */\n    private fun getHttpResponseCode(error: PlaybackException): Int? {\n        var cause: Throwable? = error.cause\n        while (cause != null) {\n            if (cause is HttpDataSource.InvalidResponseCodeException) {\n                return cause.responseCode\n            }\n            cause = cause.cause\n        }\n        return null\n    }\n\n    /**\n     * Checks if the error is caused by an expired/forbidden URL (HTTP 403).\n     * This typically happens when a YouTube stream URL expires.\n     */\n    private fun isExpiredUrlError(error: PlaybackException): Boolean {\n        val responseCode = getHttpResponseCode(error)\n        return responseCode == 403\n    }\n\n    /**\n     * Checks if the error is a Range Not Satisfiable error (HTTP 416).\n     * This happens when cached data doesn't match the actual stream size.\n     */\n    private fun isRangeNotSatisfiableError(error: PlaybackException): Boolean {\n        val responseCode = getHttpResponseCode(error)\n        return responseCode == 416\n    }\n\n    /**\n     * Checks if the error is a \"page needs to be reloaded\" error.\n     * This is a YouTube-specific error that requires refreshing the stream.\n     */\n    private fun isPageReloadError(error: PlaybackException): Boolean {\n        val errorMessage = error.message?.lowercase() ?: \"\"\n        val causeMessage = error.cause?.message?.lowercase() ?: \"\"\n        val innerCauseMessage = error.cause?.cause?.message?.lowercase() ?: \"\"\n\n        val reloadKeywords = listOf(\n            \"page needs to be reloaded\",\n            \"pagina deve essere ricaricata\",\n            \"la pagina deve essere ricaricata\",\n            \"page must be reloaded\",\n            \"reload\",\n            \"ricaricata\"\n        )\n\n        return reloadKeywords.any { keyword ->\n            errorMessage.contains(keyword) ||\n                    causeMessage.contains(keyword) ||\n                    innerCauseMessage.contains(keyword)\n        }\n    }\n\n    private fun isNetworkRelatedError(error: PlaybackException): Boolean {\n        // Don't treat specific errors as network errors - they need special handling\n        if (isExpiredUrlError(error) || isRangeNotSatisfiableError(error) || isPageReloadError(error)) {\n            return false\n        }\n        return error.errorCode == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED ||\n                error.errorCode == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT ||\n                error.errorCode == PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE ||\n                error.cause is java.net.ConnectException ||\n                error.cause is java.net.UnknownHostException ||\n                (error.cause as? PlaybackException)?.errorCode == PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED\n    }\n\n    /**\n     * Checks if the error is caused by AudioTrack write or initialization failures.\n     * These errors indicate the audio renderer is in a corrupted/invalid state.\n     */\n    private fun isAudioRendererError(error: PlaybackException): Boolean {\n        return error.errorCode == PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED ||\n                error.errorCode == PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED ||\n                (error.cause as? PlaybackException)?.errorCode == PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED ||\n                (error.cause as? PlaybackException)?.errorCode == PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED\n    }\n\n    override fun onPlayerError(error: PlaybackException) {\n        super.onPlayerError(error)\n\n        // Safety check : ensuring player is still initialized\n        if (!playerInitialized.value) {\n            Timber.tag(TAG).e(error, \"Player error occurred but player not initialized\")\n            return\n        }\n\n        val mediaId = player.currentMediaItem?.mediaId\n        Timber.tag(TAG)\n            .w(error, \"Player error occurred for $mediaId: errorCode=${error.errorCode}, message=${error.message}\")\n        reportException(error)\n\n        // Check if this song has failed too many times\n        if (mediaId != null && hasExceededRetryLimit(mediaId)) {\n            Timber.tag(TAG).w(\"Song $mediaId has exceeded retry limit, skipping\")\n            markSongAsFailed(mediaId)\n            handleFinalFailure()\n            return\n        }\n\n        // Aggressive cache clearing for all playback errors\n        if (mediaId != null) {\n            performAggressiveCacheClear(mediaId)\n        }\n\n        // Handle specific error types with strict strategies\n        when {\n            isAudioRendererError(error) -> {\n                Timber.tag(TAG).d(\"AudioTrack error detected (${error.errorCode}), performing safe recovery\")\n                handleAudioRendererError(mediaId)\n                return\n            }\n\n            isRangeNotSatisfiableError(error) -> {\n                Timber.tag(TAG).d(\"Range Not Satisfiable (416) detected, performing strict recovery\")\n                handleRangeNotSatisfiableError(mediaId)\n                return\n            }\n\n            isPageReloadError(error) -> {\n                Timber.tag(TAG).d(\"Page reload error detected, performing strict recovery\")\n                handlePageReloadError(mediaId)\n                return\n            }\n\n            isExpiredUrlError(error) -> {\n                Timber.tag(TAG).d(\"Expired URL (403) detected, refreshing stream URL\")\n                handleExpiredUrlError(mediaId)\n                return\n            }\n\n            !isNetworkConnected.value || isNetworkRelatedError(error) -> {\n                Timber.tag(TAG).d(\"Network-related error detected, waiting for connection\")\n                waitOnNetworkError()\n                return\n            }\n        }\n\n        // For IO_UNSPECIFIED and IO_BAD_HTTP_STATUS, try recovery first\n        if (error.errorCode == PlaybackException.ERROR_CODE_IO_UNSPECIFIED ||\n            error.errorCode == PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS\n        ) {\n            Timber.tag(TAG).d(\"IO error detected (${error.errorCode}), attempting recovery\")\n            handleGenericIOError(mediaId)\n            return\n        }\n\n        // Final fallback\n        if (dataStore.get(AutoSkipNextOnErrorKey, false)) {\n            Timber.tag(TAG).d(\"Auto-skipping to next track due to unrecoverable error\")\n            skipOnError()\n        } else {\n            Timber.tag(TAG).d(\"Stopping playback due to unrecoverable error\")\n            stopOnError()\n        }\n    }\n\n    /**\n     * Performs aggressive cache clearing for a media item.\n     * Clears both player cache and download cache, plus URL cache.\n     */\n    private fun performAggressiveCacheClear(mediaId: String) {\n        Timber.tag(TAG).d(\"Performing aggressive cache clear for $mediaId\")\n\n        // Clear URL cache\n        songUrlCache.remove(mediaId)\n\n        // Clear player cache\n        try {\n            playerCache.removeResource(mediaId)\n            Timber.tag(TAG).d(\"Cleared player cache for $mediaId\")\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Failed to clear player cache for $mediaId\")\n        }\n\n        // Clear decryption caches\n        try {\n            YTPlayerUtils.forceRefreshForVideo(mediaId)\n            Timber.tag(TAG).d(\"Cleared decryption caches for $mediaId\")\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Failed to clear decryption caches for $mediaId\")\n        }\n    }\n\n    /**\n     * Checks if a song has exceeded the retry limit.\n     */\n    private fun hasExceededRetryLimit(mediaId: String): Boolean {\n        val currentRetries = currentMediaIdRetryCount[mediaId] ?: 0\n        return currentRetries >= MAX_RETRY_PER_SONG\n    }\n\n    /**\n     * Increments the retry count for a song.\n     */\n    private fun incrementRetryCount(mediaId: String) {\n        val currentRetries = currentMediaIdRetryCount[mediaId] ?: 0\n        currentMediaIdRetryCount[mediaId] = currentRetries + 1\n        Timber.tag(TAG).d(\"Retry count for $mediaId: ${currentRetries + 1}/$MAX_RETRY_PER_SONG\")\n    }\n\n    /**\n     * Resets the retry count for a song (called on successful playback).\n     */\n    private fun resetRetryCount(mediaId: String) {\n        currentMediaIdRetryCount.remove(mediaId)\n        recentlyFailedSongs.remove(mediaId)\n    }\n\n    /**\n     * Marks a song as failed to prevent further retry attempts.\n     */\n    private fun markSongAsFailed(mediaId: String) {\n        recentlyFailedSongs.add(mediaId)\n        currentMediaIdRetryCount.remove(mediaId)\n\n        // Schedule cleanup of failed songs list after 5 minutes\n        failedSongsClearJob?.cancel()\n        failedSongsClearJob = scope.launch {\n            delay(5 * 60 * 1000L) // 5 minutes\n            recentlyFailedSongs.clear()\n            Timber.tag(TAG).d(\"Cleared recently failed songs list\")\n        }\n    }\n\n    /**\n     * Handles AudioTrack errors (write failed, init failed) with safe recovery.\n     * These errors indicate the audio renderer is corrupted and needs careful reset.\n     */\n    private fun handleAudioRendererError(mediaId: String?) {\n        if (mediaId == null) {\n            handleFinalFailure()\n            return\n        }\n\n        incrementRetryCount(mediaId)\n\n        retryJob?.cancel()\n        retryJob = scope.launch {\n            try {\n                // Pause playback immediately to stop the renderer\n                player.pause()\n                Timber.tag(TAG).d(\"Paused playback due to AudioTrack error\")\n\n                // Wait longer for audio renderer to settle before retry\n                // This prevents the renderer from continuing to fail in a loop\n                delay(RETRY_DELAY_MS * 3) // 3 seconds instead of 1 second\n\n                // Check if player is still initialized before attempting recovery\n                if (!playerInitialized.value) {\n                    Timber.tag(TAG).w(\"Player no longer initialized, aborting AudioTrack recovery\")\n                    return@launch\n                }\n\n                val currentIndex = player.currentMediaItemIndex\n                if (currentIndex != C.INDEX_UNSET) {\n                    // Seek to current position to force a clean audio renderer reinit\n                    val currentPosition = player.currentPosition\n                    player.seekTo(currentIndex, currentPosition)\n                    player.prepare()\n\n                    Timber.tag(TAG).d(\"Retrying playback for $mediaId after AudioTrack error\")\n\n                    // Resume playback if it wasn't paused by user\n                    if (wasPlayingBeforeAudioFocusLoss) {\n                        delay(500) // Brief delay to allow renderer to be ready\n                        if (hasAudioFocus && playerInitialized.value) {\n                            if (castConnectionHandler?.isCasting?.value != true) {\n                                player.play()\n                            }\n                        }\n                    }\n                } else {\n                    Timber.tag(TAG).w(\"Invalid media item index during AudioTrack recovery\")\n                    handleFinalFailure()\n                }\n            } catch (e: Exception) {\n                Timber.tag(TAG).e(e, \"Error during AudioTrack error recovery\")\n                handleFinalFailure()\n            }\n        }\n    }\n\n    /**\n     * Handles Range Not Satisfiable (416) errors with strict recovery.\n     * This error occurs when cached data doesn't match the actual stream size.\n     */\n    private fun handleRangeNotSatisfiableError(mediaId: String?) {\n        if (mediaId == null) {\n            handleFinalFailure()\n            return\n        }\n\n        incrementRetryCount(mediaId)\n\n        retryJob?.cancel()\n        retryJob = scope.launch {\n            // Clear all caches aggressively\n            performAggressiveCacheClear(mediaId)\n\n            // Wait before retry\n            delay(RETRY_DELAY_MS)\n\n            // Force re-prepare from position 0 to avoid range issues\n            val currentIndex = player.currentMediaItemIndex\n            player.seekTo(currentIndex, 0)\n            player.prepare()\n\n            Timber.tag(TAG).d(\"Retrying playback for $mediaId after 416 error (from position 0)\")\n        }\n    }\n\n    /**\n     * Handles \"page needs to be reloaded\" errors with strict recovery.\n     * This requires clearing decryption caches and getting fresh stream URLs.\n     */\n    private fun handlePageReloadError(mediaId: String?) {\n        if (mediaId == null) {\n            handleFinalFailure()\n            return\n        }\n\n        incrementRetryCount(mediaId)\n\n        retryJob?.cancel()\n        retryJob = scope.launch {\n            Timber.tag(TAG).d(\"Handling page reload error for $mediaId\")\n\n            // Clear all caches including decryption caches\n            performAggressiveCacheClear(mediaId)\n\n            // Additional delay for page reload errors as they may be rate-limited\n            delay(RETRY_DELAY_MS * 2)\n\n            // Re-prepare the player\n            val currentPosition = player.currentPosition\n            val currentIndex = player.currentMediaItemIndex\n            player.seekTo(currentIndex, currentPosition)\n            player.prepare()\n\n            Timber.tag(TAG).d(\"Retrying playback for $mediaId after page reload error\")\n        }\n    }\n\n    /**\n     * Handles expired URL (403) errors by clearing caches and retrying.\n     */\n    private fun handleExpiredUrlError(mediaId: String?) {\n        if (mediaId == null) {\n            handleFinalFailure()\n            return\n        }\n\n        incrementRetryCount(mediaId)\n\n        // Clear the cached URL\n        songUrlCache.remove(mediaId)\n        Timber.tag(TAG).d(\"Cleared cached URL for $mediaId\")\n\n        // Clear decryption caches\n        try {\n            YTPlayerUtils.forceRefreshForVideo(mediaId)\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Failed to clear decryption caches\")\n        }\n\n        retryJob?.cancel()\n        retryJob = scope.launch {\n            delay(RETRY_DELAY_MS)\n\n            // Seek to current position to force URL re-resolution\n            val currentPosition = player.currentPosition\n            val currentIndex = player.currentMediaItemIndex\n            player.seekTo(currentIndex, currentPosition)\n            player.prepare()\n\n            Timber.tag(TAG).d(\"Retrying playback for $mediaId after 403 error\")\n        }\n    }\n\n    /**\n     * Handles generic IO errors with recovery attempt.\n     */\n    private fun handleGenericIOError(mediaId: String?) {\n        if (mediaId == null) {\n            handleFinalFailure()\n            return\n        }\n\n        incrementRetryCount(mediaId)\n\n        retryJob?.cancel()\n        retryJob = scope.launch {\n            performAggressiveCacheClear(mediaId)\n            delay(RETRY_DELAY_MS)\n\n            val currentPosition = player.currentPosition\n            val currentIndex = player.currentMediaItemIndex\n            player.seekTo(currentIndex, currentPosition)\n            player.prepare()\n\n            Timber.tag(TAG).d(\"Retrying playback for $mediaId after generic IO error\")\n        }\n    }\n\n    /**\n     * Handles final failure when all recovery attempts have been exhausted.\n     */\n    private fun handleFinalFailure() {\n        if (dataStore.get(AutoSkipNextOnErrorKey, false)) {\n            Timber.tag(TAG).d(\"All recovery attempts exhausted, auto-skipping to next track\")\n            skipOnError()\n        } else {\n            Timber.tag(TAG).d(\"All recovery attempts exhausted, stopping playback\")\n            stopOnError()\n        }\n    }\n\n    override fun onDeviceVolumeChanged(volume: Int, muted: Boolean) {\n        super.onDeviceVolumeChanged(volume, muted)\n        val pauseOnMute = dataStore.get(PauseOnMute, false)\n\n        if ((volume == 0 || muted) && pauseOnMute) {\n            if (player.isPlaying) {\n                wasPlayingBeforeVolumeMute = true\n                isPausedByVolumeMute = true\n                player.pause()\n            }\n        } else if (volume > 0 && !muted && pauseOnMute) {\n            if (wasPlayingBeforeVolumeMute && !player.isPlaying && castConnectionHandler?.isCasting?.value != true) {\n                wasPlayingBeforeVolumeMute = false\n                isPausedByVolumeMute = false\n                player.play()\n            }\n        }\n    }\n\n    private fun createCacheDataSource(): DataSource.Factory {\n        val baseFactory = DefaultDataSource.Factory(\n            this,\n            OkHttpDataSource.Factory(\n                OkHttpClient\n                    .Builder()\n                    .proxy(YouTube.proxy)\n                    .proxyAuthenticator { _, response ->\n                        YouTube.proxyAuth?.let { auth ->\n                            response.request.newBuilder()\n                                .header(\"Proxy-Authorization\", auth)\n                                .build()\n                        } ?: response.request\n                    }\n                    .build(),\n            ),\n        )\n\n        return DataSource.Factory {\n            val usePlayerCache = dataStore.get(EnableSongCacheKey, true)\n\n            val upstreamFactory = if (usePlayerCache) {\n                CacheDataSource.Factory()\n                    .setCache(playerCache)\n                    .setUpstreamDataSourceFactory(baseFactory)\n            } else {\n                baseFactory\n            }\n\n            CacheDataSource.Factory()\n                .setCache(downloadCache)\n                .setUpstreamDataSourceFactory(upstreamFactory)\n                .setCacheWriteDataSinkFactory(null)\n                .setFlags(FLAG_IGNORE_CACHE_ON_ERROR)\n                .createDataSource()\n        }\n    }\n\n    // Flag to prevent queue saving during silence skip operations\n    private var isSilenceSkipping = false\n\n    private fun handleLongSilenceDetected() {\n        if (!instantSilenceSkipEnabled.value) return\n        if (silenceSkipJob?.isActive == true) return\n\n        silenceSkipJob = scope.launch {\n            // Debounce so short fades or transitions do not trigger a jump.\n            delay(200)\n            performInstantSilenceSkip()\n        }\n    }\n\n    private suspend fun performInstantSilenceSkip() {\n        val duration = player.duration.takeIf { it != C.TIME_UNSET && it > 0 } ?: return\n        if (duration <= INSTANT_SILENCE_SKIP_STEP_MS) return\n\n        isSilenceSkipping = true\n        try {\n            var hops = 0\n            val silenceProcessor = playerSilenceProcessors[player] ?: return\n            while (coroutineContext.isActive && instantSilenceSkipEnabled.value && silenceProcessor.isCurrentlySilent()) {\n                val current = player.currentPosition\n                val target = (current + INSTANT_SILENCE_SKIP_STEP_MS).coerceAtMost(duration - 500)\n\n                if (target <= current) break\n\n                // Reset silence tracking before seeking to prevent immediate re-trigger\n                silenceProcessor.resetTracking()\n                player.seekTo(target)\n                hops++\n\n                if (hops >= 80 || target >= duration - 500) break\n\n                delay(INSTANT_SILENCE_SKIP_SETTLE_MS)\n            }\n            if (hops > 0) {\n                Timber.tag(TAG).d(\"Silence skip: jumped $hops times\")\n            }\n        } finally {\n            isSilenceSkipping = false\n        }\n    }\n\n    private fun updateDiscordRPC(song: Song, showFeedback: Boolean = false) {\n        val useDetails = dataStore.get(DiscordUseDetailsKey, false)\n        val advancedMode = dataStore.get(DiscordAdvancedModeKey, false)\n\n        val status = if (advancedMode) dataStore.get(DiscordStatusKey, \"online\") else \"online\"\n        val b1Text = if (advancedMode) dataStore.get(DiscordButton1TextKey, \"\") else \"\"\n        val b1Visible = if (advancedMode) dataStore.get(DiscordButton1VisibleKey, true) else true\n        val b2Text = if (advancedMode) dataStore.get(DiscordButton2TextKey, \"\") else \"\"\n        val b2Visible = if (advancedMode) dataStore.get(DiscordButton2VisibleKey, true) else true\n        val activityType = if (advancedMode) dataStore.get(DiscordActivityTypeKey, \"listening\") else \"listening\"\n        val activityName = if (advancedMode) dataStore.get(DiscordActivityNameKey, \"\") else \"\"\n\n        discordUpdateJob?.cancel()\n        discordUpdateJob = scope.launch {\n            discordRpc?.updateSong(\n                song,\n                player.currentPosition,\n                player.playbackParameters.speed,\n                useDetails,\n                status,\n                b1Text,\n                b1Visible,\n                b2Text,\n                b2Visible,\n                activityType,\n                activityName\n            )?.onFailure {\n                // Rate limited or error\n                if (showFeedback) {\n                    Handler(Looper.getMainLooper()).post {\n                        Toast.makeText(\n                            this@MusicService,\n                            \"Discord RPC update failed: ${it.message}\",\n                            Toast.LENGTH_SHORT\n                        ).show()\n                    }\n                }\n            }\n        }\n    }\n\n    private fun createDataSourceFactory(): DataSource.Factory {\n        return ResolvingDataSource.Factory(createCacheDataSource()) { dataSpec ->\n            val mediaId = dataSpec.key ?: error(\"No media id\")\n\n            // Check if we need to bypass cache for quality change\n            val shouldBypassCache = bypassCacheForQualityChange.contains(mediaId)\n\n            if (!shouldBypassCache) {\n                val usePlayerCache = dataStore.get(EnableSongCacheKey, true)\n                if (downloadCache.isCached(\n                        mediaId,\n                        dataSpec.position,\n                        if (dataSpec.length >= 0) dataSpec.length else 1\n                    ) ||\n                    (usePlayerCache && playerCache.isCached(mediaId, dataSpec.position, CHUNK_LENGTH))\n                ) {\n                    scope.launch(Dispatchers.IO) { recoverSong(mediaId) }\n                    return@Factory dataSpec\n                }\n\n                songUrlCache[mediaId]?.takeIf { it.second > System.currentTimeMillis() }?.let {\n                    scope.launch(Dispatchers.IO) { recoverSong(mediaId) }\n                    return@Factory dataSpec.withUri(it.first.toUri())\n                }\n            } else {\n                Timber.tag(\"MusicService\").i(\"BYPASSING CACHE for $mediaId due to quality change\")\n            }\n\n            Timber.tag(\"MusicService\").i(\"FETCHING STREAM: $mediaId | quality=$audioQuality\")\n            val playbackData = runBlocking(Dispatchers.IO) {\n                YTPlayerUtils.playerResponseForPlayback(\n                    mediaId,\n                    audioQuality = audioQuality,\n                    connectivityManager = connectivityManager,\n                )\n            }.getOrElse { throwable ->\n                when (throwable) {\n                    is PlaybackException -> throw throwable\n\n                    is java.net.ConnectException, is java.net.UnknownHostException -> {\n                        throw PlaybackException(\n                            getString(R.string.error_no_internet),\n                            throwable,\n                            PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED\n                        )\n                    }\n\n                    is java.net.SocketTimeoutException -> {\n                        throw PlaybackException(\n                            getString(R.string.error_timeout),\n                            throwable,\n                            PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT\n                        )\n                    }\n\n                    else -> throw PlaybackException(\n                        getString(R.string.error_unknown),\n                        throwable,\n                        PlaybackException.ERROR_CODE_REMOTE_ERROR\n                    )\n                }\n            }\n\n            val nonNullPlayback = requireNotNull(playbackData) {\n                getString(R.string.error_unknown)\n            }\n            run {\n                val format = nonNullPlayback.format\n                val loudnessDb = nonNullPlayback.audioConfig?.loudnessDb\n                val perceptualLoudnessDb = nonNullPlayback.audioConfig?.perceptualLoudnessDb\n\n                Timber.tag(TAG)\n                    .d(\"Storing format for $mediaId with loudnessDb: $loudnessDb, perceptualLoudnessDb: $perceptualLoudnessDb\")\n                if (loudnessDb == null && perceptualLoudnessDb == null) {\n                    Timber.tag(TAG).w(\"No loudness data available from YouTube for video: $mediaId\")\n                }\n\n                database.query {\n                    upsert(\n                        FormatEntity(\n                            id = mediaId,\n                            itag = format.itag,\n                            mimeType = format.mimeType.split(\";\")[0],\n                            codecs = format.mimeType.split(\"codecs=\")[1].removeSurrounding(\"\\\"\"),\n                            bitrate = format.bitrate,\n                            sampleRate = format.audioSampleRate,\n                            contentLength = format.contentLength!!,\n                            loudnessDb = loudnessDb,\n                            perceptualLoudnessDb = perceptualLoudnessDb,\n                            playbackUrl = nonNullPlayback.playbackTracking?.videostatsPlaybackUrl?.baseUrl\n                        )\n                    )\n                }\n                scope.launch(Dispatchers.IO) { recoverSong(mediaId, nonNullPlayback) }\n\n                // Clear bypass flag now that we've fetched fresh stream\n                if (bypassCacheForQualityChange.remove(mediaId)) {\n                    Timber.tag(\"MusicService\").d(\"Cleared bypass cache flag for $mediaId after fresh fetch\")\n                }\n\n                val streamUrl = nonNullPlayback.streamUrl\n\n                songUrlCache[mediaId] =\n                    streamUrl to System.currentTimeMillis() + (nonNullPlayback.streamExpiresInSeconds * 1000L)\n                return@Factory dataSpec.withUri(streamUrl.toUri()).subrange(dataSpec.uriPositionOffset, CHUNK_LENGTH)\n            }\n        }\n    }\n\n    private fun createMediaSourceFactory() =\n        DefaultMediaSourceFactory(\n            createDataSourceFactory(),\n            ExtractorsFactory {\n                arrayOf(MatroskaExtractor(), FragmentedMp4Extractor())\n            },\n        )\n\n    private fun createRenderersFactory(\n        eqProcessor: CustomEqualizerAudioProcessor,\n        silenceProcessor: SilenceDetectorAudioProcessor\n    ) =\n        object : DefaultRenderersFactory(this) {\n            override fun buildAudioSink(\n                context: Context,\n                enableFloatOutput: Boolean,\n                enableAudioTrackPlaybackParams: Boolean,\n            ) = DefaultAudioSink\n                .Builder(this@MusicService)\n                .setEnableFloatOutput(enableFloatOutput)\n                .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)\n                .setAudioProcessorChain(\n                    DefaultAudioSink.DefaultAudioProcessorChain(\n                        // 2. Inject processor into audio pipeline\n                        arrayOf(\n                            eqProcessor,\n                            silenceProcessor,\n                        ),\n                        SilenceSkippingAudioProcessor(2_000_000, 20_000, 256),\n                        SonicAudioProcessor(),\n                    ),\n                ).build()\n        }\n\n    override fun onPlaybackStatsReady(\n        eventTime: AnalyticsListener.EventTime,\n        playbackStats: PlaybackStats,\n    ) {\n        val mediaItem = eventTime.timeline.getWindow(eventTime.windowIndex, Timeline.Window()).mediaItem\n        val historyDurationMs = dataStore[HistoryDuration]?.times(1000f) ?: 30000f\n\n        if (playbackStats.totalPlayTimeMs >= historyDurationMs &&\n            !dataStore.get(PauseListenHistoryKey, false)\n        ) {\n            database.query {\n                incrementTotalPlayTime(mediaItem.mediaId, playbackStats.totalPlayTimeMs)\n                try {\n                    insert(\n                        Event(\n                            songId = mediaItem.mediaId,\n                            timestamp = LocalDateTime.now(),\n                            playTime = playbackStats.totalPlayTimeMs,\n                        ),\n                    )\n                } catch (_: SQLException) {\n                }\n            }\n        }\n\n        if (playbackStats.totalPlayTimeMs >= historyDurationMs) {\n            CoroutineScope(Dispatchers.IO).launch {\n                val playbackUrl = database.format(mediaItem.mediaId).first()?.playbackUrl\n                    ?: YTPlayerUtils.playerResponseForMetadata(mediaItem.mediaId, null)\n                        .getOrNull()?.playbackTracking?.videostatsPlaybackUrl?.baseUrl\n                playbackUrl?.let {\n                    YouTube.registerPlayback(null, playbackUrl)\n                        .onFailure {\n                            reportException(it)\n                        }\n                }\n            }\n        }\n    }\n\n    private fun saveQueueToDisk() {\n        if (player.mediaItemCount == 0) {\n            Timber.tag(TAG).d(\"Skipping queue save - no media items\")\n            return\n        }\n\n        try {\n            // Save current queue with proper type information\n            val persistQueue = currentQueue.toPersistQueue(\n                title = queueTitle,\n                items = player.mediaItems.mapNotNull { it.metadata },\n                mediaItemIndex = player.currentMediaItemIndex,\n                position = player.currentPosition\n            )\n\n            val persistAutomix =\n                PersistQueue(\n                    title = \"automix\",\n                    items = automixItems.value.mapNotNull { it.metadata },\n                    mediaItemIndex = 0,\n                    position = 0,\n                )\n\n            // Save player state\n            val persistPlayerState = PersistPlayerState(\n                playWhenReady = player.playWhenReady,\n                repeatMode = player.repeatMode,\n                shuffleModeEnabled = player.shuffleModeEnabled,\n                volume = playerVolume.value,\n                currentPosition = player.currentPosition,\n                currentMediaItemIndex = player.currentMediaItemIndex,\n                playbackState = player.playbackState\n            )\n\n            runCatching {\n                filesDir.resolve(PERSISTENT_QUEUE_FILE).outputStream().use { fos ->\n                    ObjectOutputStream(fos).use { oos ->\n                        oos.writeObject(persistQueue)\n                    }\n                }\n                Timber.tag(TAG).d(\"Queue saved successfully\")\n            }.onFailure {\n                Timber.tag(TAG).e(it, \"Failed to save queue\")\n                reportException(it)\n            }\n\n            runCatching {\n                filesDir.resolve(PERSISTENT_AUTOMIX_FILE).outputStream().use { fos ->\n                    ObjectOutputStream(fos).use { oos ->\n                        oos.writeObject(persistAutomix)\n                    }\n                }\n                Timber.tag(TAG).d(\"Automix saved successfully\")\n            }.onFailure {\n                Timber.tag(TAG).e(it, \"Failed to save automix\")\n                reportException(it)\n            }\n\n            runCatching {\n                filesDir.resolve(PERSISTENT_PLAYER_STATE_FILE).outputStream().use { fos ->\n                    ObjectOutputStream(fos).use { oos ->\n                        oos.writeObject(persistPlayerState)\n                    }\n                }\n                Timber.tag(TAG).d(\"Player state saved successfully\")\n            }.onFailure {\n                Timber.tag(TAG).e(it, \"Failed to save player state\")\n                reportException(it)\n            }\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Error during queue save operation\")\n            reportException(e)\n        }\n    }\n\n    override fun onDestroy() {\n        isRunning = false\n\n        // Save episode position before destroying\n        val currentMetadata = player.currentMediaItem?.metadata\n        if (currentMetadata?.isEpisode == true && player.currentPosition > 0) {\n            runBlocking(Dispatchers.IO) {\n                database.updatePlaybackPosition(currentMetadata.id, player.currentPosition)\n            }\n        }\n\n        try {\n            unregisterReceiver(screenStateReceiver)\n        } catch (e: Exception) {\n            // Ignore\n        }\n        audioManager.unregisterAudioDeviceCallback(audioDeviceCallback)\n        castConnectionHandler?.release()\n        if (dataStore.get(PersistentQueueKey, true)) {\n            saveQueueToDisk()\n        }\n        if (discordRpc?.isRpcRunning() == true) {\n            discordRpc?.closeRPC()\n        }\n        discordRpc = null\n        connectivityObserver.unregister()\n        abandonAudioFocus()\n        releaseLoudnessEnhancer()\n        mediaSession.release()\n        player.removeListener(this)\n        player.removeListener(sleepTimer)\n        playerSilenceProcessors.remove(player)\n        // Note: equalizerService audio processors are cleared in equalizerService.release() if needed,\n        // or we can't easily reference the specific processor created in createExoPlayer here without storing it.\n        // But since we are destroying the service, it's fine.\n        player.release()\n        discordUpdateJob?.cancel()\n        super.onDestroy()\n    }\n\n    override fun onBind(intent: Intent?) = super.onBind(intent) ?: binder\n\n    override fun onTaskRemoved(rootIntent: Intent?) {\n        super.onTaskRemoved(rootIntent)\n    }\n\n    override fun onGetSession(controllerInfo: MediaSession.ControllerInfo) = mediaSession\n\n    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\n        when (intent?.action) {\n            ACTION_ALARM_TRIGGER -> {\n                handleAlarmTrigger(intent)\n            }\n\n            MusicWidgetReceiver.ACTION_PLAY_PAUSE -> {\n                if (player.isPlaying) player.pause() else player.play()\n                updateWidgetUI(player.isPlaying)\n            }\n\n            MusicWidgetReceiver.ACTION_LIKE -> {\n                toggleLike()\n            }\n\n            MusicWidgetReceiver.ACTION_NEXT -> {\n                player.seekToNext()\n                updateWidgetUI(player.isPlaying)\n            }\n\n            MusicWidgetReceiver.ACTION_PREVIOUS -> {\n                player.seekToPrevious()\n                updateWidgetUI(player.isPlaying)\n            }\n\n            MusicWidgetReceiver.ACTION_UPDATE_WIDGET -> {\n                updateWidgetUI(player.isPlaying)\n            }\n        }\n\n        return super.onStartCommand(intent, flags, startId)\n    }\n\n    private fun handleAlarmTrigger(intent: Intent) {\n        scope.launch(Dispatchers.IO) {\n            try {\n                MusicAlarmScheduler.scheduleFromPreferences(this@MusicService)\n            } catch (t: Throwable) {\n                Timber.tag(TAG).e(t, \"Failed to reschedule alarms after trigger\")\n            }\n        }\n        val playlistId = intent.getStringExtra(EXTRA_ALARM_PLAYLIST_ID).orEmpty()\n        val alarmId = intent.getStringExtra(EXTRA_ALARM_ID).orEmpty()\n        if (playlistId.isBlank()) {\n            if (alarmId.isNotBlank()) {\n                scope.launch(Dispatchers.IO) {\n                    try {\n                        val alarms = MusicAlarmStore.load(this@MusicService)\n                        val updated = alarms.map { alarm ->\n                            if (alarm.id == alarmId) alarm.copy(enabled = false, nextTriggerAt = -1L) else alarm\n                        }\n                        MusicAlarmScheduler.scheduleAll(this@MusicService, updated)\n                    } catch (t: Throwable) {\n                        Timber.tag(TAG).e(t, \"Failed to disable alarm with invalid playlist\")\n                    }\n                }\n            }\n            return\n        }\n        val randomSong = intent.getBooleanExtra(EXTRA_ALARM_RANDOM_SONG, false)\n        scope.launch {\n            try {\n                val playlistSongs = withContext(Dispatchers.IO) {\n                    database.playlistSongs(playlistId).first()\n                }\n                if (playlistSongs.isEmpty()) {\n                    if (alarmId.isNotBlank()) {\n                        withContext(Dispatchers.IO) {\n                            val alarms = MusicAlarmStore.load(this@MusicService)\n                            val updated = alarms.map { alarm ->\n                                if (alarm.id == alarmId) alarm.copy(enabled = false, nextTriggerAt = -1L) else alarm\n                            }\n                            MusicAlarmScheduler.scheduleAll(this@MusicService, updated)\n                        }\n                    }\n                    return@launch\n                }\n                val items = playlistSongs.map { it.song.toMediaItem() }\n                val playlistName = withContext(Dispatchers.IO) {\n                    database.playlist(playlistId).first()?.playlist?.name\n                }\n                withContext(Dispatchers.IO) {\n                    MusicAlarmScheduler.scheduleFromPreferences(this@MusicService)\n                }\n\n                val alarmItems =\n                    if (randomSong) {\n                        val firstIndex = Random.nextInt(items.size)\n                        buildList(items.size) {\n                            add(items[firstIndex])\n                            items.forEachIndexed { index, item ->\n                                if (index != firstIndex) add(item)\n                            }\n                        }\n                    } else {\n                        items\n                    }\n\n                player.stop()\n                player.clearMediaItems()\n                playQueue(\n                    ListQueue(\n                        title = playlistName,\n                        items = alarmItems,\n                        startIndex = 0,\n                        position = 0L\n                    ),\n                    playWhenReady = true\n                )\n            } catch (t: Throwable) {\n                Timber.tag(TAG).e(t, \"Failed to start alarm playback\")\n            }\n        }\n    }\n\n    /**\n     * Updates all app widgets with current playback state\n     */\n    private fun updateWidgetUI(isPlaying: Boolean) {\n        scope.launch {\n            try {\n                val songData = currentSong.value\n                val song = songData?.song\n                val songTitle = song?.title ?: getString(R.string.no_song_playing)\n                val artistName = songData?.artists?.joinToString(\", \") { it.name } ?: getString(R.string.tap_to_open)\n                val isLiked = songData?.song?.liked == true\n\n                widgetManager.updateWidgets(\n                    title = songTitle,\n                    artist = artistName,\n                    artworkUri = song?.thumbnailUrl,\n                    isPlaying = isPlaying,\n                    isLiked = isLiked,\n                    duration = if (player.duration != C.TIME_UNSET) player.duration else 0,\n                    currentPosition = player.currentPosition\n                )\n            } catch (e: Exception) {\n                // Widget not added to home screen or other error\n            }\n        }\n    }\n\n    private var widgetUpdateJob: Job? = null\n\n    private fun startWidgetUpdates() {\n        widgetUpdateJob?.cancel()\n        widgetUpdateJob = scope.launch {\n            while (isActive) {\n                if (player.isPlaying) {\n                    updateWidgetUI(true)\n                }\n                delay(200)\n            }\n        }\n    }\n\n    private fun stopWidgetUpdates() {\n        widgetUpdateJob?.cancel()\n        widgetUpdateJob = null\n    }\n\n    private fun shareSong() {\n        val songData = currentSong.value\n        val songId = songData?.song?.id ?: return\n\n        val shareIntent = Intent(Intent.ACTION_SEND).apply {\n            type = \"text/plain\"\n            putExtra(Intent.EXTRA_TEXT, \"https://music.youtube.com/watch?v=$songId\")\n            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n        }\n        startActivity(Intent.createChooser(shareIntent, null).apply {\n            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n        })\n    }\n\n    /**\n     * Get the stream URL for a given media ID.\n     * This is used for Google Cast to send the audio URL to Chromecast.\n     */\n    suspend fun getStreamUrl(mediaId: String): String? {\n        return withContext(Dispatchers.IO) {\n            try {\n                val playbackData = YTPlayerUtils.playerResponseForPlayback(\n                    videoId = mediaId,\n                    audioQuality = audioQuality,\n                    connectivityManager = connectivityManager,\n                ).getOrNull()\n                playbackData?.streamUrl\n            } catch (e: Exception) {\n                timber.log.Timber.e(e, \"Failed to get stream URL for Cast\")\n                null\n            }\n        }\n    }\n\n    /**\n     * Initialize Google Cast support\n     */\n    private fun initializeCast() {\n        if (dataStore.get(com.metrolist.music.constants.EnableGoogleCastKey, true)) {\n            try {\n                castConnectionHandler = CastConnectionHandler(this, scope, this)\n                castConnectionHandler?.initialize()\n                timber.log.Timber.d(\"Google Cast initialized\")\n            } catch (e: Exception) {\n                timber.log.Timber.e(e, \"Failed to initialize Google Cast\")\n            }\n        }\n    }\n\n\n    override fun onPositionDiscontinuity(\n        oldPosition: Player.PositionInfo,\n        newPosition: Player.PositionInfo,\n        reason: Int\n    ) {\n        if (reason == Player.DISCONTINUITY_REASON_SEEK) {\n            scheduleCrossfade()\n        }\n    }\n\n    private fun scheduleCrossfade() {\n        crossfadeTriggerJob?.cancel()\n        crossfadeTriggerJob = null\n        if (!crossfadeEnabled || player.duration == C.TIME_UNSET || player.duration <= crossfadeDuration) return\n        if (crossfadeGapless && isNextItemGapless()) return\n        if (!player.hasNextMediaItem() && player.repeatMode != REPEAT_MODE_ONE) return\n\n        val triggerTime = player.duration - crossfadeDuration.toLong()\n        val delayMs = triggerTime - player.currentPosition\n        if (delayMs <= 0) return\n\n        val targetMediaId = player.currentMediaItem?.mediaId\n\n        crossfadeTriggerJob = scope.launch {\n            delay(delayMs)\n            if (isActive && player.isPlaying && player.currentMediaItem?.mediaId == targetMediaId && !sleepTimer.pauseWhenSongEnd) {\n                startCrossfade()\n            }\n        }\n    }\n\n    private fun isNextItemGapless(): Boolean {\n        val current = player.currentMediaItem?.mediaMetadata ?: return false\n        val nextIndex = player.nextMediaItemIndex\n        if (nextIndex == C.INDEX_UNSET) return false\n        val next = player.getMediaItemAt(nextIndex).mediaMetadata\n        return current.albumTitle != null && current.albumTitle == next.albumTitle\n    }\n\n    private fun startCrossfade() {\n        if (isCrossfading) return\n\n        // Preserve player state before creating the secondary player\n        // Use runBlocking to ensure we get the correct state from DataStore\n        val savedRepeatMode = runBlocking { dataStore.get(RepeatModeKey, REPEAT_MODE_OFF) }\n        val savedShuffleEnabled = runBlocking { dataStore.get(ShuffleModeKey, false) }\n\n        // For repeat-one, crossfade back into the same track\n        val targetIndex = if (savedRepeatMode == REPEAT_MODE_ONE) {\n            player.currentMediaItemIndex\n        } else {\n            player.nextMediaItemIndex\n        }\n        if (targetIndex == C.INDEX_UNSET) return\n\n        secondaryPlayer = createExoPlayer()\n        val secPlayer = secondaryPlayer!!\n        secPlayer.addListener(secondaryPlayerListener)\n\n        val itemCount = player.mediaItemCount\n        val items = mutableListOf<MediaItem>()\n        // Copy entire queue history + future\n        for (i in 0 until itemCount) {\n            items.add(player.getMediaItemAt(i))\n        }\n\n        secPlayer.setMediaItems(items)\n        // Seek to target track (next track, or current track for repeat-one)\n        secPlayer.seekTo(targetIndex, 0)\n        secPlayer.volume = 0f\n\n        // Copy repeat and shuffle state to the new player\n        secPlayer.repeatMode = savedRepeatMode\n        secPlayer.shuffleModeEnabled = savedShuffleEnabled\n\n        secPlayer.prepare()\n        secPlayer.playWhenReady = true\n\n        performCrossfadeSwap()\n\n        // Rebuild shuffle order on the new primary player if shuffle was active\n        if (savedShuffleEnabled) {\n            val shufflePlaylistFirst = dataStore.get(ShufflePlaylistFirstKey, false)\n            applyShuffleOrder(player.currentMediaItemIndex, player.mediaItemCount, shufflePlaylistFirst)\n        }\n    }\n\n    private fun performCrossfadeSwap() {\n        isCrossfading = true\n        val nextPlayer = secondaryPlayer ?: return\n        val currentPlayer = player\n\n        fadingPlayer = currentPlayer\n        player = nextPlayer\n        _playerFlow.value = player\n        secondaryPlayer = null\n\n        fadingPlayer?.removeListener(this)\n        fadingPlayer?.removeListener(sleepTimer)\n\n        // Add listener to sync play/pause state\n        player.addListener(object : Player.Listener {\n            override fun onIsPlayingChanged(isPlaying: Boolean) {\n                if (isCrossfading && fadingPlayer != null) {\n                    if (isPlaying) {\n                        fadingPlayer?.play()\n                    } else {\n                        fadingPlayer?.pause()\n                    }\n                } else {\n                    player.removeListener(this)\n                }\n            }\n        })\n\n        nextPlayer.removeListener(secondaryPlayerListener)\n        nextPlayer.addListener(this)\n        nextPlayer.addListener(sleepTimer)\n\n        sleepTimer.player = player\n\n        try {\n            (mediaSession as MediaSession).player = player\n        } catch (e: Exception) {\n            timber.log.Timber.e(e, \"Failed to swap player in MediaSession\")\n        }\n\n        crossfadeJob = scope.launch {\n            val duration = crossfadeDuration.toLong()\n            val steps = 20\n            val stepTime = duration / steps\n            val startVolume = try {\n                fadingPlayer?.volume ?: 1f\n            } catch (e: Exception) {\n                1f\n            }\n\n            for (i in 0..steps) {\n                if (!isActive) break\n                // Pause volume ramp if player is paused\n                while (!player.isPlaying && isActive) {\n                    delay(100)\n                }\n\n                val progress = i / steps.toFloat()\n                val fadeIn = 1.0f - (1.0f - progress) * (1.0f - progress)\n                val fadeOut = (1.0f - progress) * (1.0f - progress)\n\n                try {\n                    player.volume = startVolume * fadeIn\n                    fadingPlayer?.volume = startVolume * fadeOut\n                } catch (e: Exception) {\n                    break\n                }\n\n                delay(stepTime)\n            }\n\n            try {\n                fadingPlayer?.volume = 0f\n                player.volume = startVolume\n                cleanupCrossfade()\n            } catch (e: Exception) {\n            }\n        }\n    }\n\n    private fun cleanupCrossfade() {\n        fadingPlayer?.stop()\n        fadingPlayer?.clearMediaItems()\n        fadingPlayer?.release()\n        fadingPlayer = null\n        isCrossfading = false\n        applyEffectiveVolume()\n        sleepTimer.notifySongTransition()\n    }\n\n    companion object {\n        const val ACTION_ALARM_TRIGGER = \"com.metrolist.music.action.ALARM_TRIGGER\"\n        const val EXTRA_ALARM_ID = \"extra_alarm_id\"\n        const val EXTRA_ALARM_PLAYLIST_ID = \"extra_alarm_playlist_id\"\n        const val EXTRA_ALARM_RANDOM_SONG = \"extra_alarm_random_song\"\n\n        const val ROOT = \"root\"\n        const val SONG = \"song\"\n        const val ARTIST = \"artist\"\n        const val ALBUM = \"album\"\n        const val PLAYLIST = \"playlist\"\n        const val YOUTUBE_PLAYLIST = \"youtube_playlist\"\n        const val SEARCH = \"search\"\n        const val SHUFFLE_ACTION = \"__shuffle__\"\n\n        const val CHANNEL_ID = \"music_channel_01\"\n        const val NOTIFICATION_ID = 888\n        const val ERROR_CODE_NO_STREAM = 1000001\n        const val CHUNK_LENGTH = 512 * 1024L\n        const val PERSISTENT_QUEUE_FILE = \"persistent_queue.data\"\n        const val PERSISTENT_AUTOMIX_FILE = \"persistent_automix.data\"\n        const val PERSISTENT_PLAYER_STATE_FILE = \"persistent_player_state.data\"\n        const val MAX_CONSECUTIVE_ERR = 5\n        const val MAX_RETRY_COUNT = 10\n\n        // Constants for audio normalization\n        private const val MAX_GAIN_MB = 300 // Maximum gain in millibels (3 dB)\n        private const val MIN_GAIN_MB = -1500 // Minimum gain in millibels (-15 dB)\n\n        private const val TAG = \"MusicService\"\n\n        @Volatile\n        var isRunning = false\n            private set\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/playback/PlayerConnection.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.playback\n\nimport android.content.Context\nimport androidx.media3.common.MediaItem\nimport androidx.media3.common.PlaybackException\nimport androidx.media3.common.Player\nimport androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM\nimport androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM\nimport androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM\nimport androidx.media3.common.Player.REPEAT_MODE_OFF\nimport androidx.media3.common.Player.STATE_ENDED\nimport androidx.media3.common.Timeline\nimport androidx.media3.exoplayer.ExoPlayer\nimport com.metrolist.music.constants.SleepTimerCustomDaysKey\nimport com.metrolist.music.constants.SleepTimerDayTimesKey\nimport com.metrolist.music.constants.SleepTimerDefaultKey\nimport com.metrolist.music.constants.SleepTimerEnabledKey\nimport com.metrolist.music.constants.SleepTimerEndTimeKey\nimport com.metrolist.music.constants.SleepTimerRepeatKey\nimport com.metrolist.music.constants.SleepTimerStartTimeKey\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.extensions.currentMetadata\nimport com.metrolist.music.extensions.getCurrentQueueIndex\nimport com.metrolist.music.extensions.getQueueWindows\nimport com.metrolist.music.extensions.metadata\nimport com.metrolist.music.extensions.togglePlayPause\nimport com.metrolist.music.playback.MusicService.MusicBinder\nimport com.metrolist.music.playback.queues.Queue\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\nimport com.metrolist.music.utils.reportException\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.launch\nimport timber.log.Timber\nimport java.time.LocalDate\nimport java.time.LocalTime\nimport java.time.format.DateTimeFormatter\nimport kotlin.math.roundToInt\n\n@OptIn(ExperimentalCoroutinesApi::class)\nclass PlayerConnection(\n    context: Context,\n    binder: MusicBinder,\n    val database: MusicDatabase,\n    scope: CoroutineScope,\n) : Player.Listener {\n    private companion object {\n        private const val TAG = \"PlayerConnection\"\n    }\n\n    val service = binder.service\n    private val playerReadinessFlow = service.isPlayerReady\n\n    /**\n     * Safe player accessor checks readiness & handles errors.\n     * Should be used by all player access within this class.\n     */\n    private fun getPlayerSafe(): ExoPlayer =\n        try {\n            if (!playerReadinessFlow.value) {\n                Timber.tag(TAG).w(\"Player accessed before service initialization complete; returning best-effort reference\")\n            }\n            service.player\n        } catch (e: UninitializedPropertyAccessException) {\n            Timber.tag(TAG).e(e, \"Fatal: player property accessed but not initialized\")\n            throw IllegalStateException(\"MusicService.player not initialized; possible race condition in service startup\", e)\n        }\n\n    /**\n     * Public accessor for player. Throws if player not ready.\n     * Callers should check [isPlayerInitialized] before calling, or handle exceptions.\n     */\n    val player: ExoPlayer\n        get() = getPlayerSafe()\n\n    /** Tracks whether player initialization completed successfully */\n    private val isPlayerInitialized = MutableStateFlow(service.isPlayerReady.value)\n\n    val playbackState: MutableStateFlow<Int>\n    private val playWhenReady: MutableStateFlow<Boolean>\n    val isPlaying: kotlinx.coroutines.flow.StateFlow<Boolean>\n\n    init {\n        Timber.tag(TAG).d(\"PlayerConnection init: playerReady=${playerReadinessFlow.value}\")\n\n        // Initialize with player state or safe defaults if player not ready\n        val initialState =\n            try {\n                val initialPlayer = getPlayerSafe()\n                Triple(\n                    initialPlayer.playbackState,\n                    initialPlayer.playWhenReady,\n                    initialPlayer.playWhenReady && initialPlayer.playbackState != STATE_ENDED,\n                )\n            } catch (e: Exception) {\n                Timber.tag(TAG).e(e, \"Error during PlayerConnection initialization, using defaults\")\n                Triple(Player.STATE_IDLE, false, false)\n            }\n\n        playbackState = MutableStateFlow(initialState.first)\n        playWhenReady = MutableStateFlow(initialState.second)\n        isPlaying =\n            combine(playbackState, playWhenReady) { state, ready ->\n                ready && state != STATE_ENDED\n            }.stateIn(\n                scope,\n                SharingStarted.Lazily,\n                initialState.third,\n            )\n\n        // Track service readiness changes in background.\n        scope.launch {\n            playerReadinessFlow.collect { ready ->\n                isPlayerInitialized.value = ready\n                if (ready) {\n                    Timber.tag(TAG).d(\"Service player initialization detected by PlayerConnection\")\n                }\n            }\n        }\n\n        Timber.tag(TAG).d(\"PlayerConnection state flows initialized successfully\")\n    }\n\n    // Effective playing state, considers Cast when active\n    val isEffectivelyPlaying =\n        combine(\n            isPlaying,\n            service.castConnectionHandler?.isCasting ?: MutableStateFlow(false),\n            service.castConnectionHandler?.castIsPlaying ?: MutableStateFlow(false),\n        ) { localPlaying, isCasting, castPlaying ->\n            if (isCasting) castPlaying else localPlaying\n        }.stateIn(\n            scope,\n            SharingStarted.Lazily,\n            player.playbackState != STATE_ENDED && player.playWhenReady,\n        )\n\n    val mediaMetadata = MutableStateFlow(player.currentMetadata)\n    val currentSong =\n        mediaMetadata.flatMapLatest {\n            database.song(it?.id)\n        }\n    val currentLyrics =\n        mediaMetadata.flatMapLatest { mediaMetadata ->\n            database.lyrics(mediaMetadata?.id)\n        }\n    val currentFormat =\n        mediaMetadata.flatMapLatest { mediaMetadata ->\n            database.format(mediaMetadata?.id)\n        }\n\n    val queueTitle = MutableStateFlow<String?>(null)\n    val queueWindows = MutableStateFlow<List<Timeline.Window>>(emptyList())\n    val currentMediaItemIndex = MutableStateFlow(-1)\n    val currentWindowIndex = MutableStateFlow(-1)\n\n    val shuffleModeEnabled = MutableStateFlow(false)\n    val repeatMode = MutableStateFlow(REPEAT_MODE_OFF)\n\n    val canSkipPrevious = MutableStateFlow(true)\n    val canSkipNext = MutableStateFlow(true)\n\n    val error = MutableStateFlow<PlaybackException?>(null)\n    val isMuted = service.isMuted\n\n    val waitingForNetworkConnection = service.waitingForNetworkConnection\n\n    // Callback to check if playback changes should be blocked (e.g., Listen Together guest)\n    var shouldBlockPlaybackChanges: (() -> Boolean)? = null\n\n    // Flag to allow internal sync operations to bypass blocking (set by ListenTogetherManager)\n    @Volatile\n    var allowInternalSync: Boolean = false\n\n    var onSkipPrevious: (() -> Unit)? = null\n    var onSkipNext: (() -> Unit)? = null\n\n    private var attachedPlayer: Player? = null\n\n    init {\n        try {\n            // Observe player changes (e.g. crossfade swap)\n            scope.launch {\n                service.playerFlow.collect { newPlayer ->\n                    if (newPlayer != null && newPlayer != attachedPlayer) {\n                        updateAttachedPlayer(newPlayer)\n                    }\n                }\n            }\n            // Initial setup if flow hasn't emitted yet but service is ready\n            if (attachedPlayer == null && service.isPlayerReady.value) {\n                updateAttachedPlayer(player)\n            }\n\n            Timber.tag(TAG).d(\"PlayerConnection flow observer registered\")\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Failed to initialize PlayerConnection listener or state\")\n            // Propagate the error so MainActivity can retry\n            throw e\n        }\n    }\n\n    private fun updateAttachedPlayer(newPlayer: Player) {\n        attachedPlayer?.removeListener(this)\n        attachedPlayer = newPlayer\n        newPlayer.addListener(this)\n        // Refresh all state from new player\n        playbackState.value = newPlayer.playbackState\n        playWhenReady.value = newPlayer.playWhenReady\n        mediaMetadata.value = newPlayer.currentMetadata\n        queueTitle.value = service.queueTitle\n        queueWindows.value = newPlayer.getQueueWindows()\n        currentWindowIndex.value = newPlayer.getCurrentQueueIndex()\n        currentMediaItemIndex.value = newPlayer.currentMediaItemIndex\n        shuffleModeEnabled.value = newPlayer.shuffleModeEnabled\n        repeatMode.value = newPlayer.repeatMode\n        Timber.tag(TAG).d(\"Attached to new player instance: $newPlayer\")\n    }\n\n    fun playQueue(queue: Queue) {\n        // Block if Listen Together guest (unless internal sync)\n        if (!allowInternalSync && shouldBlockPlaybackChanges?.invoke() == true) {\n            Timber.tag(\"PlayerConnection\").d(\"playQueue blocked - Listen Together guest\")\n            return\n        }\n        if (!playerReadinessFlow.value) {\n            Timber.tag(TAG).w(\"playQueue called before player ready; delegating to service\")\n        }\n        try {\n            service.playQueue(queue)\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Error in playQueue\")\n            throw e\n        }\n    }\n\n    fun startRadioSeamlessly() {\n        // Block if Listen Together guest\n        if (shouldBlockPlaybackChanges?.invoke() == true) {\n            Timber.tag(\"PlayerConnection\").d(\"startRadioSeamlessly blocked - Listen Together guest\")\n            return\n        }\n        if (!playerReadinessFlow.value) {\n            Timber.tag(TAG).w(\"startRadioSeamlessly called before player ready; delegating to service\")\n        }\n        try {\n            service.startRadioSeamlessly()\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Error in startRadioSeamlessly\")\n            throw e\n        }\n    }\n\n    fun playNext(item: MediaItem) = playNext(listOf(item))\n\n    fun playNext(items: List<MediaItem>) {\n        // Block if Listen Together guest (unless internal sync)\n        if (!allowInternalSync && shouldBlockPlaybackChanges?.invoke() == true) {\n            Timber.tag(\"PlayerConnection\").d(\"playNext blocked - Listen Together guest\")\n            return\n        }\n        try {\n            service.playNext(items)\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Error in playNext\")\n            throw e\n        }\n    }\n\n    fun addToQueue(item: MediaItem) = addToQueue(listOf(item))\n\n    fun addToQueue(items: List<MediaItem>) {\n        // Block if Listen Together guest (unless internal sync)\n        if (!allowInternalSync && shouldBlockPlaybackChanges?.invoke() == true) {\n            Timber.tag(\"PlayerConnection\").d(\"addToQueue blocked - Listen Together guest\")\n            return\n        }\n        try {\n            service.addToQueue(items)\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Error in addToQueue\")\n            throw e\n        }\n    }\n\n    fun toggleLike() {\n        try {\n            service.toggleLike()\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Error in toggleLike\")\n        }\n    }\n\n    fun toggleMute() {\n        service.toggleMute()\n    }\n\n    fun setMuted(muted: Boolean) {\n        service.setMuted(muted)\n    }\n\n    fun toggleLibrary() {\n        try {\n            service.toggleLibrary()\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Error in toggleLibrary\")\n        }\n    }\n\n    /**\n     * Toggle play/pause - handles Cast when active\n     */\n    fun togglePlayPause() {\n        if (!allowInternalSync && shouldBlockPlaybackChanges?.invoke() == true) return\n        try {\n            val castHandler = service.castConnectionHandler\n            if (castHandler?.isCasting?.value == true) {\n                if (castHandler.castIsPlaying.value) {\n                    castHandler.pause()\n                } else {\n                    castHandler.play()\n                }\n            } else {\n                player.togglePlayPause()\n            }\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Error in togglePlayPause\")\n        }\n    }\n\n    /**\n     * Start playback - handles Cast when active\n     */\n    fun play() {\n        try {\n            val castHandler = service.castConnectionHandler\n            if (castHandler?.isCasting?.value == true) {\n                castHandler.play()\n            } else {\n                if (player.playbackState == Player.STATE_IDLE) {\n                    player.prepare()\n                }\n                player.playWhenReady = true\n            }\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Error in play\")\n        }\n    }\n\n    /**\n     * Pause playback - handles Cast when active\n     */\n    fun pause() {\n        try {\n            val castHandler = service.castConnectionHandler\n            if (castHandler?.isCasting?.value == true) {\n                castHandler.pause()\n            } else {\n                player.playWhenReady = false\n            }\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Error in pause\")\n        }\n    }\n\n    /**\n     * Seek to position - handles Cast when active\n     */\n    fun seekTo(position: Long) {\n        try {\n            val castHandler = service.castConnectionHandler\n            if (castHandler?.isCasting?.value == true) {\n                castHandler.seekTo(position)\n            } else {\n                player.seekTo(position)\n            }\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Error in seekTo\")\n        }\n    }\n\n    fun seekToNext() {\n        try {\n            // When casting, use Cast skip instead of local player\n            val castHandler = service.castConnectionHandler\n            if (castHandler?.isCasting?.value == true) {\n                castHandler.skipToNext()\n                return\n            }\n            player.seekToNext()\n            if (player.playbackState == Player.STATE_IDLE || player.playbackState == Player.STATE_ENDED) {\n                player.prepare()\n            }\n            player.playWhenReady = true\n            onSkipNext?.invoke()\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Error in seekToNext\")\n        }\n    }\n\n    var onRestartSong: (() -> Unit)? = null\n\n    fun seekToPrevious() {\n        try {\n            // When casting, use Cast skip instead of local player\n            val castHandler = service.castConnectionHandler\n            if (castHandler?.isCasting?.value == true) {\n                castHandler.skipToPrevious()\n                return\n            }\n\n            // Logic to mimic standard seekToPrevious behavior but with explicit callbacks\n            // If we are more than 3 seconds in, just restart the song\n            if (player.currentPosition > 3000 || !player.hasPreviousMediaItem()) {\n                player.seekTo(0)\n                if (player.playbackState == Player.STATE_IDLE || player.playbackState == Player.STATE_ENDED) {\n                    player.prepare()\n                }\n                player.playWhenReady = true\n                onRestartSong?.invoke()\n            } else {\n                // Otherwise go to previous media item\n                player.seekToPreviousMediaItem()\n                if (player.playbackState == Player.STATE_IDLE || player.playbackState == Player.STATE_ENDED) {\n                    player.prepare()\n                }\n                player.playWhenReady = true\n                onSkipPrevious?.invoke()\n            }\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Error in seekToPrevious\")\n        }\n    }\n\n    /** Parses \"0=09:00-23:00;1=22:00-06:00\" into Map<dayIndex, Pair<start, end>>. */\n    private fun parseDayTimes(raw: String): Map<Int, Pair<String, String>> {\n        if (raw.isBlank()) return emptyMap()\n        return raw\n            .split(\";\")\n            .mapNotNull { entry ->\n                val parts = entry.split(\"=\")\n                if (parts.size != 2) return@mapNotNull null\n                val dayIndex = parts[0].toIntOrNull() ?: return@mapNotNull null\n                val times = parts[1].split(\"-\")\n                if (times.size != 2) return@mapNotNull null\n                dayIndex to (times[0] to times[1])\n            }.toMap()\n    }\n\n    private fun checkAndStartAutomaticSleepTimer(): Boolean {\n        return try {\n            val sleepTimerEnabled = service.applicationContext.dataStore.get(SleepTimerEnabledKey) ?: false\n            Timber.tag(TAG).d(\"✓ Sleep Timer Check: enabled=$sleepTimerEnabled\")\n\n            if (!sleepTimerEnabled) {\n                Timber.tag(TAG).d(\"✗ Sleep Timer disabled - skipping\")\n                return false\n            }\n\n            if (service.sleepTimer.isActive) {\n                Timber.tag(TAG).d(\"✗ Sleep Timer already active - skipping\")\n                return false\n            }\n\n            val sleepTimerRepeat = service.applicationContext.dataStore.get(SleepTimerRepeatKey) ?: \"daily\"\n            val sleepTimerStartTime = service.applicationContext.dataStore.get(SleepTimerStartTimeKey) ?: \"09:00\"\n            val sleepTimerEndTime = service.applicationContext.dataStore.get(SleepTimerEndTimeKey) ?: \"23:00\"\n            val sleepTimerDefaultMinutes = (service.applicationContext.dataStore.get(SleepTimerDefaultKey) ?: 30f).roundToInt()\n            val sleepTimerCustomDaysStr = service.applicationContext.dataStore.get(SleepTimerCustomDaysKey) ?: \"0,1,2,3,4\"\n            val sleepTimerDayTimesStr = service.applicationContext.dataStore.get(SleepTimerDayTimesKey) ?: \"\"\n\n            Timber\n                .tag(\n                    TAG,\n                ).d(\n                    \"Sleep Timer Config: repeat=$sleepTimerRepeat start=$sleepTimerStartTime end=$sleepTimerEndTime default=$sleepTimerDefaultMinutes custom=$sleepTimerCustomDaysStr\",\n                )\n\n            val currentTime = LocalTime.now()\n            val today = LocalDate.now()\n            val dayOfWeek = today.dayOfWeek.value % 7\n            val adjustedDayOfWeek = if (dayOfWeek == 0) 6 else dayOfWeek - 1\n\n            Timber.tag(TAG).d(\"Current: time=$currentTime dayOfWeek=$adjustedDayOfWeek\")\n\n            val isDayAllowed =\n                when (sleepTimerRepeat) {\n                    \"daily\" -> {\n                        true\n                    }\n\n                    \"weekdays\" -> {\n                        adjustedDayOfWeek in 0..4\n                    }\n\n                    \"weekends\" -> {\n                        adjustedDayOfWeek in 5..6\n                    }\n\n                    \"weekdays_weekends\" -> {\n                        true\n                    }\n\n                    // both groups active; per-day time handles the distinction\n                    \"custom\" -> {\n                        val customDays = sleepTimerCustomDaysStr.split(\",\").mapNotNull { it.trim().toIntOrNull() }\n                        Timber.tag(TAG).d(\"Custom days: $customDays, adjustedDayOfWeek=$adjustedDayOfWeek\")\n                        adjustedDayOfWeek in customDays\n                    }\n\n                    else -> {\n                        false\n                    }\n                }\n\n            if (!isDayAllowed) {\n                Timber.tag(TAG).d(\"✗ Day not allowed for Sleep Timer\")\n                return false\n            }\n\n// \"daily\" uses the single global time window.\n// All other modes store per-day times in the dayTimes map so that\n// e.g. weekdays and weekends can have different windows.\n            val timeFormatter = DateTimeFormatter.ofPattern(\"HH:mm\")\n            val usesDayTimesMap = sleepTimerRepeat != \"daily\"\n            val (startStr, endStr) =\n                if (usesDayTimesMap) {\n                    parseDayTimes(sleepTimerDayTimesStr)[adjustedDayOfWeek]\n                        ?: (sleepTimerStartTime to sleepTimerEndTime)\n                } else {\n                    sleepTimerStartTime to sleepTimerEndTime\n                }\n\n            val startTime = LocalTime.parse(startStr, timeFormatter)\n            val endTime = LocalTime.parse(endStr, timeFormatter)\n\n            // Support overnight ranges (e.g. 22:00–06:00) in addition to normal ranges\n            val isTimeInRange =\n                if (endTime.isAfter(startTime)) {\n                    currentTime.isAfter(startTime) && currentTime.isBefore(endTime)\n                } else {\n                    currentTime.isAfter(startTime) || currentTime.isBefore(endTime)\n                }\n\n            Timber.tag(TAG).d(\"Time check: $currentTime between $startStr-$endStr? $isTimeInRange\")\n\n            if (isTimeInRange) {\n                Timber.tag(TAG).i(\"AUTO SLEEP TIMER STARTED: $sleepTimerDefaultMinutes minutes\")\n                service.sleepTimer.start(sleepTimerDefaultMinutes)\n                return true\n            }\n\n            Timber.tag(TAG).d(\"✗ Time not in range\")\n            return false\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Sleep Timer error\")\n            return false\n        }\n    }\n\n    override fun onPlaybackStateChanged(state: Int) {\n        playbackState.value = state\n        error.value = player.playerError\n    }\n\n    override fun onPlayWhenReadyChanged(\n        newPlayWhenReady: Boolean,\n        reason: Int,\n    ) {\n        val wasPlaying = playWhenReady.value\n        playWhenReady.value = newPlayWhenReady\n\n        // Central sleep timer trigger: fires on every paused -> playing transition,\n        if (newPlayWhenReady && !wasPlaying) {\n            checkAndStartAutomaticSleepTimer()\n        }\n    }\n\n    override fun onMediaItemTransition(\n        mediaItem: MediaItem?,\n        reason: Int,\n    ) {\n        mediaMetadata.value = mediaItem?.metadata\n        currentMediaItemIndex.value = player.currentMediaItemIndex\n        currentWindowIndex.value = player.getCurrentQueueIndex()\n        updateCanSkipPreviousAndNext()\n    }\n\n    override fun onTimelineChanged(\n        timeline: Timeline,\n        reason: Int,\n    ) {\n        queueWindows.value = player.getQueueWindows()\n        queueTitle.value = service.queueTitle\n        currentMediaItemIndex.value = player.currentMediaItemIndex\n        currentWindowIndex.value = player.getCurrentQueueIndex()\n        updateCanSkipPreviousAndNext()\n    }\n\n    override fun onShuffleModeEnabledChanged(enabled: Boolean) {\n        shuffleModeEnabled.value = enabled\n        queueWindows.value = player.getQueueWindows()\n        currentWindowIndex.value = player.getCurrentQueueIndex()\n        updateCanSkipPreviousAndNext()\n    }\n\n    override fun onRepeatModeChanged(mode: Int) {\n        repeatMode.value = mode\n        updateCanSkipPreviousAndNext()\n    }\n\n    override fun onPlayerErrorChanged(playbackError: PlaybackException?) {\n        if (playbackError != null) {\n            reportException(playbackError)\n        }\n        error.value = playbackError\n    }\n\n    private fun updateCanSkipPreviousAndNext() {\n        if (!player.currentTimeline.isEmpty) {\n            val window =\n                player.currentTimeline.getWindow(player.currentMediaItemIndex, Timeline.Window())\n            canSkipPrevious.value = player.isCommandAvailable(COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM) ||\n                !window.isLive ||\n                player.isCommandAvailable(COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM)\n            canSkipNext.value = window.isLive &&\n                window.isDynamic ||\n                player.isCommandAvailable(COMMAND_SEEK_TO_NEXT_MEDIA_ITEM)\n        } else {\n            canSkipPrevious.value = false\n            canSkipNext.value = false\n        }\n    }\n\n    fun dispose() {\n        try {\n            attachedPlayer?.removeListener(this)\n            attachedPlayer = null\n            Timber.tag(TAG).d(\"PlayerConnection disposed successfully\")\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Error during PlayerConnection disposal\")\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/playback/SleepTimer.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.playback\n\nimport androidx.media3.common.C\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.media3.common.MediaItem\nimport androidx.media3.common.Player\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport kotlin.time.Duration.Companion.minutes\n\nclass SleepTimer(\n    private val scope: CoroutineScope,\n    var player: Player,\n    private val onVolumeMultiplierChanged: (Float) -> Unit = {},\n) : Player.Listener {\n    private companion object {\n        private const val TIMER_TICK_MS = 1000L\n        private const val FADE_OUT_WINDOW_MS = 60_000L\n    }\n\n    private var sleepTimerJob: Job? = null\n    var triggerTime by mutableLongStateOf(-1L)\n        private set\n    var pauseWhenSongEnd by mutableStateOf(false)\n        private set\n    var stopAfterCurrentSongOnTimeout by mutableStateOf(false)\n        private set\n    var fadeOutEnabled by mutableStateOf(false)\n        private set\n    val isActive: Boolean\n        get() = triggerTime != -1L || pauseWhenSongEnd\n\n    fun start(minute: Int) {\n        start(\n            minute = minute,\n            stopAfterCurrentSong = false,\n            fadeOut = false,\n        )\n    }\n\n    fun start(\n        minute: Int,\n        stopAfterCurrentSong: Boolean,\n        fadeOut: Boolean,\n    ) {\n        sleepTimerJob?.cancel()\n        sleepTimerJob = null\n        updateVolumeMultiplier(1f)\n        fadeOutEnabled = fadeOut\n\n        if (minute == -1) {\n            pauseWhenSongEnd = true\n            stopAfterCurrentSongOnTimeout = false\n            triggerTime = -1L\n            if (fadeOutEnabled) {\n                sleepTimerJob =\n                    scope.launch {\n                        while (this@SleepTimer.isActive) {\n                            updateVolumeMultiplierForCurrentSong()\n                            delay(TIMER_TICK_MS)\n                        }\n                    }\n            }\n        } else {\n            pauseWhenSongEnd = false\n            stopAfterCurrentSongOnTimeout = stopAfterCurrentSong\n            triggerTime = System.currentTimeMillis() + minute.minutes.inWholeMilliseconds\n            sleepTimerJob =\n                scope.launch {\n                    while (this@SleepTimer.isActive) {\n                        if (triggerTime != -1L) {\n                            val remainingMs = triggerTime - System.currentTimeMillis()\n                            if (remainingMs <= 0L) {\n                                triggerTime = -1L\n                                if (stopAfterCurrentSongOnTimeout) {\n                                    pauseWhenSongEnd = true\n                                    stopAfterCurrentSongOnTimeout = false\n                                    if (!fadeOutEnabled) {\n                                        break\n                                    }\n                                } else {\n                                    completeTimerAndPause()\n                                    break\n                                }\n                            } else if (fadeOutEnabled && !stopAfterCurrentSongOnTimeout) {\n                                updateVolumeMultiplierForRemainingTime(remainingMs)\n                            }\n                        } else if (pauseWhenSongEnd && fadeOutEnabled) {\n                            updateVolumeMultiplierForCurrentSong()\n                        }\n\n                        delay(TIMER_TICK_MS)\n                    }\n                }\n        }\n    }\n\n    /**\n     * Notify the sleep timer that a song transition has occurred outside of normal\n     * player callbacks (e.g. during crossfade player swap). If \"end of song\" mode\n     * is active, this will pause the player and deactivate the timer.\n     */\n    fun notifySongTransition() {\n        if (pauseWhenSongEnd) {\n            completeTimerAndPause()\n        }\n    }\n\n    fun clear() {\n        sleepTimerJob?.cancel()\n        sleepTimerJob = null\n        pauseWhenSongEnd = false\n        stopAfterCurrentSongOnTimeout = false\n        fadeOutEnabled = false\n        triggerTime = -1L\n        updateVolumeMultiplier(1f)\n    }\n\n    override fun onMediaItemTransition(\n        mediaItem: MediaItem?,\n        reason: Int,\n    ) {\n        if (pauseWhenSongEnd) {\n            completeTimerAndPause()\n        }\n    }\n\n    override fun onPlaybackStateChanged(\n        @Player.State playbackState: Int,\n    ) {\n        if (playbackState == Player.STATE_ENDED && pauseWhenSongEnd) {\n            completeTimerAndPause()\n        }\n    }\n\n    private fun completeTimerAndPause() {\n        sleepTimerJob?.cancel()\n        sleepTimerJob = null\n        pauseWhenSongEnd = false\n        stopAfterCurrentSongOnTimeout = false\n        fadeOutEnabled = false\n        triggerTime = -1L\n        updateVolumeMultiplier(1f)\n        player.pause()\n    }\n\n    private fun updateVolumeMultiplierForRemainingTime(remainingMs: Long) {\n        updateVolumeMultiplier(volumeMultiplierForRemainingTime(remainingMs))\n    }\n\n    private fun updateVolumeMultiplierForCurrentSong() {\n        val duration = player.duration\n        if (duration == C.TIME_UNSET || duration <= 0) {\n            updateVolumeMultiplier(1f)\n            return\n        }\n\n        val remainingMs = (duration - player.currentPosition).coerceAtLeast(0L)\n        updateVolumeMultiplierForRemainingTime(remainingMs)\n    }\n\n    private fun volumeMultiplierForRemainingTime(remainingMs: Long): Float {\n        if (remainingMs >= FADE_OUT_WINDOW_MS) return 1f\n        return (remainingMs.toFloat() / FADE_OUT_WINDOW_MS).coerceIn(0f, 1f)\n    }\n\n    private fun updateVolumeMultiplier(multiplier: Float) {\n        onVolumeMultiplierChanged(multiplier)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/playback/alarm/MusicAlarmReceiver.kt",
    "content": "package com.metrolist.music.playback.alarm\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport androidx.core.content.ContextCompat\nimport com.metrolist.music.playback.MusicService\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\n\nclass MusicAlarmReceiver : BroadcastReceiver() {\n    override fun onReceive(context: Context, intent: Intent?) {\n        if (intent?.action != ACTION_TRIGGER_ALARM) return\n        val pendingResult = goAsync()\n        val alarmId = intent.getStringExtra(MusicService.EXTRA_ALARM_ID).orEmpty()\n        val playlistId = intent.getStringExtra(MusicService.EXTRA_ALARM_PLAYLIST_ID).orEmpty()\n        val randomSong = intent.getBooleanExtra(MusicService.EXTRA_ALARM_RANDOM_SONG, false)\n        val serviceIntent = Intent(context, MusicService::class.java)\n            .setAction(MusicService.ACTION_ALARM_TRIGGER)\n            .putExtra(MusicService.EXTRA_ALARM_ID, alarmId)\n            .putExtra(MusicService.EXTRA_ALARM_PLAYLIST_ID, playlistId)\n            .putExtra(MusicService.EXTRA_ALARM_RANDOM_SONG, randomSong)\n        ContextCompat.startForegroundService(context, serviceIntent)\n        CoroutineScope(Dispatchers.IO).launch {\n            try {\n                MusicAlarmScheduler.scheduleFromPreferences(context)\n            } finally {\n                pendingResult.finish()\n            }\n        }\n    }\n\n    companion object {\n        const val ACTION_TRIGGER_ALARM = \"com.metrolist.music.action.TRIGGER_ALARM\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/playback/alarm/MusicAlarmRescheduleReceiver.kt",
    "content": "package com.metrolist.music.playback.alarm\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\n\nclass MusicAlarmRescheduleReceiver : BroadcastReceiver() {\n    override fun onReceive(context: Context, intent: Intent?) {\n        when (intent?.action) {\n            Intent.ACTION_LOCKED_BOOT_COMPLETED,\n            Intent.ACTION_BOOT_COMPLETED,\n            Intent.ACTION_TIME_CHANGED,\n            Intent.ACTION_TIMEZONE_CHANGED,\n            Intent.ACTION_MY_PACKAGE_REPLACED -> {\n                val pendingResult = goAsync()\n                CoroutineScope(Dispatchers.IO).launch {\n                    try {\n                        MusicAlarmScheduler.scheduleFromPreferences(context)\n                    } finally {\n                        pendingResult.finish()\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/playback/alarm/MusicAlarmScheduler.kt",
    "content": "package com.metrolist.music.playback.alarm\n\nimport android.app.AlarmManager\nimport android.app.PendingIntent\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Build\nimport com.metrolist.music.playback.MusicService\nimport java.util.Calendar\n\nobject MusicAlarmScheduler {\n    fun scheduleFromPreferences(context: Context) {\n        val alarms = MusicAlarmStore.loadBlocking(context)\n        scheduleAll(context, alarms)\n    }\n\n    fun scheduleAll(context: Context, alarms: List<MusicAlarmEntry>) {\n        val alarmManager = context.getSystemService(AlarmManager::class.java) ?: return\n        val knownAlarmIds = (MusicAlarmStore.loadBlocking(context).map { it.id } + alarms.map { it.id }).distinct()\n        knownAlarmIds.forEach { alarmId ->\n            cancel(context, alarmId)\n        }\n        val updated = alarms.map { alarm ->\n            if (!alarm.enabled || alarm.playlistId.isBlank()) {\n                alarm.copy(nextTriggerAt = -1L)\n            } else {\n                val triggerAtMillis = nextTriggerMillis(alarm.hour, alarm.minute)\n                val pendingIntent = alarmPendingIntent(context, alarm)\n\n                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) {\n                    alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent)\n                } else {\n                    alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAtMillis, pendingIntent)\n                }\n                alarm.copy(nextTriggerAt = triggerAtMillis)\n            }\n        }\n        MusicAlarmStore.saveBlocking(context, updated)\n    }\n\n    fun cancel(context: Context, alarmId: String) {\n        val alarmManager = context.getSystemService(AlarmManager::class.java) ?: return\n        alarmManager.cancel(alarmPendingIntent(context, alarmId))\n        alarmManager.cancel(legacyAlarmPendingIntent(context, alarmId))\n    }\n\n    private fun alarmPendingIntent(\n        context: Context,\n        alarm: MusicAlarmEntry\n    ): PendingIntent {\n        val intent = Intent(context, MusicService::class.java)\n            .setAction(MusicService.ACTION_ALARM_TRIGGER)\n            .putExtra(MusicService.EXTRA_ALARM_ID, alarm.id)\n            .putExtra(MusicService.EXTRA_ALARM_PLAYLIST_ID, alarm.playlistId)\n            .putExtra(MusicService.EXTRA_ALARM_RANDOM_SONG, alarm.randomSong)\n\n        return foregroundServicePendingIntent(context, alarm.id, intent)\n    }\n\n    private fun alarmPendingIntent(context: Context, alarmId: String): PendingIntent {\n        val intent = Intent(context, MusicService::class.java)\n            .setAction(MusicService.ACTION_ALARM_TRIGGER)\n            .putExtra(MusicService.EXTRA_ALARM_ID, alarmId)\n\n        return foregroundServicePendingIntent(context, alarmId, intent)\n    }\n\n    private fun foregroundServicePendingIntent(\n        context: Context,\n        alarmId: String,\n        intent: Intent\n    ): PendingIntent {\n        return PendingIntent.getForegroundService(\n            context,\n            requestCode(alarmId),\n            intent,\n            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n        )\n    }\n\n    private fun legacyAlarmPendingIntent(context: Context, alarmId: String): PendingIntent {\n        val intent = Intent(context, MusicAlarmReceiver::class.java)\n            .setAction(MusicAlarmReceiver.ACTION_TRIGGER_ALARM)\n            .putExtra(MusicService.EXTRA_ALARM_ID, alarmId)\n\n        return PendingIntent.getBroadcast(\n            context,\n            requestCode(alarmId),\n            intent,\n            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n        )\n    }\n\n    private fun requestCode(alarmId: String): Int {\n        return alarmId.hashCode() and Int.MAX_VALUE\n    }\n\n    private fun nextTriggerMillis(hour: Int, minute: Int): Long {\n        val calendar = Calendar.getInstance().apply {\n            set(Calendar.SECOND, 0)\n            set(Calendar.MILLISECOND, 0)\n            set(Calendar.HOUR_OF_DAY, hour)\n            set(Calendar.MINUTE, minute)\n        }\n        if (calendar.timeInMillis <= System.currentTimeMillis()) {\n            calendar.add(Calendar.DAY_OF_YEAR, 1)\n        }\n        return calendar.timeInMillis\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/playback/alarm/MusicAlarmStore.kt",
    "content": "package com.metrolist.music.playback.alarm\n\nimport android.content.Context\nimport android.os.Build\nimport androidx.datastore.preferences.core.edit\nimport com.metrolist.music.constants.AlarmEnabledKey\nimport com.metrolist.music.constants.AlarmEntriesKey\nimport com.metrolist.music.constants.AlarmHourKey\nimport com.metrolist.music.constants.AlarmMinuteKey\nimport com.metrolist.music.constants.AlarmNextTriggerAtKey\nimport com.metrolist.music.constants.AlarmPlaylistIdKey\nimport com.metrolist.music.constants.AlarmRandomSongKey\nimport com.metrolist.music.utils.dataStore\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.runBlocking\nimport org.json.JSONArray\nimport org.json.JSONObject\nimport java.util.UUID\n\ndata class MusicAlarmEntry(\n    val id: String,\n    val enabled: Boolean,\n    val hour: Int,\n    val minute: Int,\n    val playlistId: String,\n    val randomSong: Boolean,\n    val nextTriggerAt: Long = -1L\n)\n\nobject MusicAlarmStore {\n    private const val ALARM_PREFS = \"alarm_store\"\n    private const val ALARM_PREFS_ENTRIES = \"alarm_entries\"\n\n    suspend fun load(context: Context): List<MusicAlarmEntry> {\n        val protectedRaw = alarmPrefsContext(context).getSharedPreferences(ALARM_PREFS, Context.MODE_PRIVATE)\n            .getString(ALARM_PREFS_ENTRIES, null)\n            .orEmpty()\n        if (protectedRaw.isNotBlank()) {\n            parse(protectedRaw)?.let { return it }\n        }\n\n        return runCatching {\n            val prefs = context.dataStore.data.first()\n            val raw = prefs[AlarmEntriesKey].orEmpty()\n            if (raw.isNotBlank()) {\n                parse(raw)\n                    ?: migrateLegacy(\n                        prefs[AlarmEnabledKey] ?: false,\n                        prefs[AlarmHourKey] ?: 7,\n                        prefs[AlarmMinuteKey] ?: 0,\n                        prefs[AlarmPlaylistIdKey].orEmpty(),\n                        prefs[AlarmRandomSongKey] ?: false,\n                        prefs[AlarmNextTriggerAtKey] ?: -1L\n                    )\n            } else {\n                migrateLegacy(prefs[AlarmEnabledKey] ?: false, prefs[AlarmHourKey] ?: 7, prefs[AlarmMinuteKey] ?: 0, prefs[AlarmPlaylistIdKey].orEmpty(), prefs[AlarmRandomSongKey] ?: false, prefs[AlarmNextTriggerAtKey] ?: -1L)\n            }\n        }.getOrElse {\n            emptyList()\n        }.also { entries ->\n            if (entries.isNotEmpty()) {\n                saveProtected(context, entries)\n            }\n        }\n    }\n\n    fun loadBlocking(context: Context): List<MusicAlarmEntry> {\n        return runBlocking {\n            load(context)\n        }\n    }\n\n    suspend fun save(context: Context, entries: List<MusicAlarmEntry>) {\n        saveProtected(context, entries)\n        context.dataStore.edit { prefs ->\n            prefs[AlarmEntriesKey] = serialize(entries)\n            prefs[AlarmNextTriggerAtKey] = entries.filter { it.enabled }.minOfOrNull { it.nextTriggerAt.takeIf { time -> time > 0L } ?: Long.MAX_VALUE }\n                ?.takeIf { it != Long.MAX_VALUE } ?: -1L\n        }\n    }\n\n    fun saveBlocking(context: Context, entries: List<MusicAlarmEntry>) {\n        runBlocking {\n            save(context, entries)\n        }\n    }\n\n    fun createEmpty(): MusicAlarmEntry {\n        return MusicAlarmEntry(\n            id = UUID.randomUUID().toString(),\n            enabled = true,\n            hour = 7,\n            minute = 0,\n            playlistId = \"\",\n            randomSong = false,\n            nextTriggerAt = -1L\n        )\n    }\n\n    private fun migrateLegacy(\n        enabled: Boolean,\n        hour: Int,\n        minute: Int,\n        playlistId: String,\n        randomSong: Boolean,\n        nextTriggerAt: Long\n    ): List<MusicAlarmEntry> {\n        if (playlistId.isBlank()) return emptyList()\n        return listOf(\n            MusicAlarmEntry(\n                id = \"legacy-main-alarm\",\n                enabled = enabled,\n                hour = hour,\n                minute = minute,\n                playlistId = playlistId,\n                randomSong = randomSong,\n                nextTriggerAt = nextTriggerAt\n            )\n        )\n    }\n\n    private fun serialize(entries: List<MusicAlarmEntry>): String {\n        val array = JSONArray()\n        entries.forEach { entry ->\n            array.put(\n                JSONObject()\n                    .put(\"id\", entry.id)\n                    .put(\"enabled\", entry.enabled)\n                    .put(\"hour\", entry.hour)\n                    .put(\"minute\", entry.minute)\n                    .put(\"playlistId\", entry.playlistId)\n                    .put(\"randomSong\", entry.randomSong)\n                    .put(\"nextTriggerAt\", entry.nextTriggerAt)\n            )\n        }\n        return array.toString()\n    }\n\n    private fun parse(raw: String): List<MusicAlarmEntry>? {\n        val array = runCatching { JSONArray(raw) }.getOrElse { return null }\n        return buildList {\n            for (index in 0 until array.length()) {\n                val item = array.optJSONObject(index) ?: continue\n                runCatching {\n                    MusicAlarmEntry(\n                        id = item.optString(\"id\").ifBlank { UUID.randomUUID().toString() },\n                        enabled = item.optBoolean(\"enabled\", true),\n                        hour = item.optInt(\"hour\", 7).coerceIn(0, 23),\n                        minute = item.optInt(\"minute\", 0).coerceIn(0, 59),\n                        playlistId = item.optString(\"playlistId\"),\n                        randomSong = item.optBoolean(\"randomSong\", false),\n                        nextTriggerAt = item.optLong(\"nextTriggerAt\", -1L)\n                    )\n                }.getOrNull()?.let(::add)\n            }\n        }\n    }\n\n    private fun saveProtected(context: Context, entries: List<MusicAlarmEntry>) {\n        alarmPrefsContext(context)\n            .getSharedPreferences(ALARM_PREFS, Context.MODE_PRIVATE)\n            .edit()\n            .putString(ALARM_PREFS_ENTRIES, serialize(entries))\n            .apply()\n    }\n\n    private fun alarmPrefsContext(context: Context): Context {\n        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {\n            context.createDeviceProtectedStorageContext()\n        } else {\n            context\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/playback/audio/SilenceDetectorAudioProcessor.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.playback.audio\n\nimport androidx.media3.common.C\nimport androidx.media3.common.audio.AudioProcessor\nimport androidx.media3.common.util.UnstableApi\nimport java.nio.ByteBuffer\nimport java.nio.ByteOrder\nimport kotlin.math.abs\n\n/**\n * Lightweight PCM pass-through processor that detects long stretches of near-silence.\n * When [instantModeEnabled] is true and a silence block longer than [minSilenceDurationUs]\n * is detected, [onLongSilence] is invoked exactly once per silent segment.\n */\n@UnstableApi\n@Suppress(\"DEPRECATION\")\nclass SilenceDetectorAudioProcessor(\n    private val minSilenceDurationUs: Long = 2_000_000L,\n    private val silenceThreshold: Int = 256,\n    private val onLongSilence: () -> Unit,\n) : AudioProcessor {\n\n    private var sampleRate = 0\n    private var channelCount = 0\n    private var encoding = C.ENCODING_INVALID\n\n    private var outputBuffer: ByteBuffer = EMPTY_BUFFER\n    private var inputEnded = false\n\n    @Volatile\n    var instantModeEnabled: Boolean = false\n\n    @Volatile\n    private var consecutiveSilentFrames: Long = 0\n\n    @Volatile\n    private var inSilence: Boolean = false\n\n    private var notifiedThisSilence = false\n\n    override fun configure(inputAudioFormat: AudioProcessor.AudioFormat): AudioProcessor.AudioFormat {\n        sampleRate = inputAudioFormat.sampleRate\n        channelCount = inputAudioFormat.channelCount\n        encoding = inputAudioFormat.encoding\n\n        if (encoding != C.ENCODING_PCM_16BIT) {\n            throw AudioProcessor.UnhandledAudioFormatException(inputAudioFormat)\n        }\n\n        return inputAudioFormat\n    }\n\n    override fun isActive(): Boolean = true\n\n    override fun queueInput(inputBuffer: ByteBuffer) {\n        if (!inputBuffer.hasRemaining()) {\n            outputBuffer = EMPTY_BUFFER\n            return\n        }\n\n        // Analyze the incoming PCM for silence without mutating the buffer position.\n        if (instantModeEnabled && sampleRate > 0 && channelCount > 0) {\n            detectSilence(inputBuffer)\n        } else {\n            clearSilenceState()\n        }\n\n        val out = replaceOutputBuffer(inputBuffer.remaining())\n        out.put(inputBuffer)\n        out.flip()\n    }\n\n    private fun detectSilence(inputBuffer: ByteBuffer) {\n        // Ensure predictable endian access for getShort(index).\n        inputBuffer.order(ByteOrder.LITTLE_ENDIAN)\n\n        val frameCount = inputBuffer.remaining() / 2 / channelCount\n        val basePosition = inputBuffer.position()\n\n        repeat(frameCount) { frameIndex ->\n            var framePeak = 0\n            repeat(channelCount) { channelIndex ->\n                val sampleIndex = basePosition + (frameIndex * channelCount + channelIndex) * 2\n                val sampleValue = abs(inputBuffer.getShort(sampleIndex).toInt())\n                if (sampleValue > framePeak) {\n                    framePeak = sampleValue\n                }\n            }\n\n            if (framePeak < silenceThreshold) {\n                consecutiveSilentFrames++\n                val silentDurationUs = (consecutiveSilentFrames * 1_000_000L) / sampleRate\n                if (silentDurationUs >= minSilenceDurationUs) {\n                    inSilence = true\n                    if (!notifiedThisSilence) {\n                        notifiedThisSilence = true\n                        onLongSilence()\n                    }\n                }\n            } else {\n                clearSilenceState()\n            }\n        }\n    }\n\n    private fun clearSilenceState() {\n        consecutiveSilentFrames = 0\n        inSilence = false\n        notifiedThisSilence = false\n    }\n\n    fun resetTracking() {\n        clearSilenceState()\n    }\n\n    fun isCurrentlySilent(): Boolean = inSilence\n\n    override fun queueEndOfStream() {\n        inputEnded = true\n    }\n\n    override fun getOutput(): ByteBuffer {\n        val output = outputBuffer\n        outputBuffer = EMPTY_BUFFER\n        return output\n    }\n\n    override fun isEnded(): Boolean = inputEnded && outputBuffer === EMPTY_BUFFER\n\n    @Deprecated(\"Deprecated in AudioProcessor\")\n    override fun flush() {\n        outputBuffer = EMPTY_BUFFER\n        inputEnded = false\n        clearSilenceState()\n    }\n\n    @Deprecated(\"Deprecated in AudioProcessor\")\n    override fun reset() {\n        flush()\n        sampleRate = 0\n        channelCount = 0\n        encoding = C.ENCODING_INVALID\n    }\n\n    private fun replaceOutputBuffer(size: Int): ByteBuffer {\n        if (outputBuffer.capacity() < size) {\n            outputBuffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder())\n        } else {\n            outputBuffer.clear()\n        }\n        return outputBuffer\n    }\n\n    companion object {\n        private val EMPTY_BUFFER: ByteBuffer = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder())\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/playback/queues/EmptyQueue.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.playback.queues\n\nimport androidx.media3.common.MediaItem\nimport com.metrolist.music.models.MediaMetadata\n\nobject EmptyQueue : Queue {\n    override val preloadItem: MediaMetadata? = null\n\n    override suspend fun getInitialStatus() = Queue.Status(null, emptyList(), -1)\n\n    override fun hasNextPage() = false\n\n    override suspend fun nextPage() = emptyList<MediaItem>()\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/playback/queues/ListQueue.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.playback.queues\n\nimport androidx.media3.common.MediaItem\nimport com.metrolist.music.models.MediaMetadata\n\nclass ListQueue(\n    val title: String? = null,\n    val items: List<MediaItem>,\n    val startIndex: Int = 0,\n    val position: Long = 0L,\n) : Queue {\n    override val preloadItem: MediaMetadata? = null\n\n    override suspend fun getInitialStatus() = Queue.Status(title, items, startIndex, position)\n\n    override fun hasNextPage(): Boolean = false\n\n    override suspend fun nextPage() = throw UnsupportedOperationException()\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/playback/queues/LocalAlbumRadio.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.playback.queues\n\nimport androidx.media3.common.MediaItem\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.WatchEndpoint\nimport com.metrolist.music.db.entities.AlbumWithSongs\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.models.MediaMetadata\nimport kotlinx.coroutines.Dispatchers.IO\nimport kotlinx.coroutines.withContext\n\nclass LocalAlbumRadio(\n    private val albumWithSongs: AlbumWithSongs,\n    private val startIndex: Int = 0,\n) : Queue {\n    override val preloadItem: MediaMetadata? = null\n\n    private lateinit var playlistId: String\n    private val endpoint: WatchEndpoint\n        get() = WatchEndpoint(\n            playlistId = playlistId\n        )\n\n    private var continuation: String? = null\n    private var firstTimeLoaded: Boolean = false\n\n    override suspend fun getInitialStatus(): Queue.Status = withContext(IO) {\n        Queue.Status(\n            title = albumWithSongs.album.title,\n            items = albumWithSongs.songs.map { it.toMediaItem() },\n            mediaItemIndex = startIndex\n        )\n    }\n\n    override fun hasNextPage(): Boolean = !firstTimeLoaded || continuation != null\n\n    override suspend fun nextPage(): List<MediaItem> = withContext(IO) {\n        if (!firstTimeLoaded) {\n            playlistId = YouTube.album(albumWithSongs.album.id).getOrThrow().album.playlistId\n            val nextResult = YouTube.next(endpoint, continuation).getOrThrow()\n            continuation = nextResult.continuation\n            firstTimeLoaded = true\n            return@withContext nextResult.items.subList(\n                albumWithSongs.songs.size,\n                nextResult.items.size\n            ).map { it.toMediaItem() }\n        }\n        val nextResult = YouTube.next(endpoint, continuation).getOrThrow()\n        continuation = nextResult.continuation\n        nextResult.items.map { it.toMediaItem() }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/playback/queues/Queue.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.playback.queues\n\nimport androidx.media3.common.MediaItem\nimport com.metrolist.music.extensions.metadata\nimport com.metrolist.music.models.MediaMetadata\n\ninterface Queue {\n    val preloadItem: MediaMetadata?\n\n    suspend fun getInitialStatus(): Status\n\n    fun hasNextPage(): Boolean\n\n    suspend fun nextPage(): List<MediaItem>\n\n    data class Status(\n        val title: String?,\n        val items: List<MediaItem>,\n        val mediaItemIndex: Int,\n        val position: Long = 0L,\n    ) {\n        fun filterExplicit(enabled: Boolean = true) =\n            if (enabled) {\n                copy(\n                    items = items.filterExplicit(),\n                )\n            } else {\n                this\n            }\n\n        fun filterVideoSongs(disableVideos: Boolean = false) =\n            if (disableVideos) {\n                copy(\n                    items = items.filterVideoSongs(true),\n                )\n            } else {\n                this\n            }\n    }\n}\n\nfun List<MediaItem>.filterExplicit(enabled: Boolean = true) =\n    if (enabled) {\n        filterNot {\n            it.metadata?.explicit == true\n        }\n    } else {\n        this\n    }\n\nfun List<MediaItem>.filterVideoSongs(disableVideos: Boolean = false) =\n    if (disableVideos) {\n        filterNot { it.metadata?.isVideoSong == true }\n    } else {\n        this\n    }\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/playback/queues/YouTubeAlbumRadio.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.playback.queues\n\nimport androidx.media3.common.MediaItem\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.WatchEndpoint\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.models.MediaMetadata\nimport kotlinx.coroutines.Dispatchers.IO\nimport kotlinx.coroutines.withContext\n\nclass YouTubeAlbumRadio(\n    private var playlistId: String,\n) : Queue {\n    override val preloadItem: MediaMetadata? = null\n\n    private val endpoint: WatchEndpoint\n        get() = WatchEndpoint(\n            playlistId = playlistId\n        )\n\n    private var albumSongCount = 0\n    private var continuation: String? = null\n    private var firstTimeLoaded: Boolean = false\n\n    override suspend fun getInitialStatus(): Queue.Status = withContext(IO) {\n        val albumSongs = YouTube.albumSongs(playlistId).getOrThrow()\n        albumSongCount = albumSongs.size\n        Queue.Status(\n            title = albumSongs.first().album?.name.orEmpty(),\n            items = albumSongs.map { it.toMediaItem() },\n            mediaItemIndex = 0\n        )\n    }\n\n    override fun hasNextPage(): Boolean = !firstTimeLoaded || continuation != null\n\n    override suspend fun nextPage(): List<MediaItem> = withContext(IO) {\n        val nextResult = YouTube.next(endpoint, continuation).getOrThrow()\n        continuation = nextResult.continuation\n        if (!firstTimeLoaded) {\n            firstTimeLoaded = true\n            nextResult.items.subList(albumSongCount, nextResult.items.size).map { it.toMediaItem() }\n        } else {\n            nextResult.items.map { it.toMediaItem() }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/playback/queues/YouTubePlaylistQueue.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.playback.queues\n\nimport androidx.media3.common.MediaItem\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.models.MediaMetadata\nimport kotlinx.coroutines.Dispatchers.IO\nimport kotlinx.coroutines.withContext\n\nclass YouTubePlaylistQueue(\n    private val playlistId: String,\n    private val playlistTitle: String? = null,\n    private val initialSongs: List<SongItem> = emptyList(),\n    private val initialContinuation: String? = null,\n    private val startIndex: Int = 0,\n    override val preloadItem: MediaMetadata? = null,\n) : Queue {\n    private var continuation: String? = initialContinuation\n    private var retryCount = 0\n    private val maxRetries = 3\n\n    override suspend fun getInitialStatus(): Queue.Status {\n        return withContext(IO) {\n            if (initialSongs.isNotEmpty()) {\n                Queue.Status(\n                    title = playlistTitle,\n                    items = initialSongs.map { it.toMediaItem() },\n                    mediaItemIndex = startIndex,\n                )\n            } else {\n                val playlistPage = YouTube.playlist(playlistId).getOrThrow()\n                continuation = playlistPage.songsContinuation\n                Queue.Status(\n                    title = playlistPage.playlist.title,\n                    items = playlistPage.songs.map { it.toMediaItem() },\n                    mediaItemIndex = startIndex,\n                )\n            }\n        }\n    }\n\n    override fun hasNextPage(): Boolean = continuation != null\n\n    override suspend fun nextPage(): List<MediaItem> {\n        return withContext(IO) {\n            val currentContinuation = continuation ?: return@withContext emptyList()\n            var lastException: Throwable? = null\n            \n            for (attempt in 0..maxRetries) {\n                try {\n                    val continuationPage = YouTube.playlistContinuation(currentContinuation).getOrThrow()\n                    continuation = continuationPage.continuation\n                    retryCount = 0\n                    return@withContext continuationPage.songs.map { it.toMediaItem() }\n                } catch (e: Exception) {\n                    lastException = e\n                    retryCount++\n                    if (retryCount >= maxRetries) {\n                        continuation = null\n                    }\n                }\n            }\n            throw lastException ?: Exception(\"Failed to get next page\")\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/playback/queues/YouTubeQueue.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.playback.queues\n\nimport androidx.media3.common.MediaItem\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.WatchEndpoint\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.models.MediaMetadata\nimport kotlinx.coroutines.Dispatchers.IO\nimport kotlinx.coroutines.withContext\n\nclass YouTubeQueue(\n    private var endpoint: WatchEndpoint,\n    override val preloadItem: MediaMetadata? = null,\n) : Queue {\n    private var continuation: String? = null\n    private var retryCount = 0\n    private val maxRetries = 3\n\n    override suspend fun getInitialStatus(): Queue.Status {\n        return withContext(IO) {\n            var lastException: Throwable? = null\n\n            if (endpoint.videoId != null && endpoint.playlistId == null) {\n                endpoint = WatchEndpoint(\n                    videoId = endpoint.videoId,\n                    playlistId = \"RDAMVM${endpoint.videoId}\"\n                )\n            }\n\n            for (attempt in 0..maxRetries) {\n                try {\n                    val nextResult = YouTube.next(endpoint, continuation).getOrThrow()\n                    endpoint = nextResult.endpoint\n                    continuation = nextResult.continuation\n                    retryCount = 0\n                    return@withContext Queue.Status(\n                        title = nextResult.title,\n                        items = nextResult.items.map { it.toMediaItem() },\n                        mediaItemIndex = nextResult.currentIndex ?: 0,\n                    )\n                } catch (e: Exception) {\n                    lastException = e\n                }\n            }\n            throw lastException ?: Exception(\"Failed to get initial status\")\n        }\n    }\n\n    override fun hasNextPage(): Boolean = continuation != null\n\n    override suspend fun nextPage(): List<MediaItem> {\n        return withContext(IO) {\n            var lastException: Throwable? = null\n\n            for (attempt in 0..maxRetries) {\n                try {\n                    val nextResult = YouTube.next(endpoint, continuation).getOrThrow()\n                    endpoint = nextResult.endpoint\n                    continuation = nextResult.continuation\n                    retryCount = 0\n                    return@withContext nextResult.items.map { it.toMediaItem() }\n                } catch (e: Exception) {\n                    lastException = e\n                    retryCount++\n                    if (retryCount >= maxRetries) {\n                        continuation = null // Stop trying to load more\n                    }\n                }\n            }\n            throw lastException ?: Exception(\"Failed to get next page\")\n        }\n    }\n\n    companion object {\n        /**\n         * Creates a radio queue based on a song.\n         * Explicitly requests the RDAMVM playlist to trigger automotive/radio mixing.\n         */\n        fun radio(song: MediaMetadata): YouTubeQueue {\n            return YouTubeQueue(\n                WatchEndpoint(\n                    videoId = song.id,\n                    playlistId = \"RDAMVM${song.id}\"\n                ),\n                song\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/quicksettings/MusicRecognizerTileService.kt",
    "content": "package com.metrolist.music.quicksettings\n\nimport android.app.PendingIntent\nimport android.content.Intent\nimport android.graphics.drawable.Icon\nimport android.os.Build\nimport android.service.quicksettings.Tile\nimport android.service.quicksettings.TileService\nimport com.metrolist.music.R\nimport com.metrolist.music.recognition.RecognitionLaunchActivity\n\nclass MusicRecognizerTileService : TileService() {\n    override fun onStartListening() {\n        super.onStartListening()\n        qsTile?.apply {\n            icon = Icon.createWithResource(this@MusicRecognizerTileService, R.drawable.mic)\n            state = Tile.STATE_INACTIVE\n            updateTile()\n        }\n    }\n\n    override fun onClick() {\n        super.onClick()\n        val launchIntent =\n            Intent(this, RecognitionLaunchActivity::class.java).apply {\n                flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_ANIMATION\n            }\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {\n            val pendingIntent =\n                PendingIntent.getActivity(\n                    this,\n                    0,\n                    launchIntent,\n                    PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,\n                )\n            startActivityAndCollapse(pendingIntent)\n        } else {\n            @Suppress(\"DEPRECATION\")\n            startActivityAndCollapse(launchIntent)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/recognition/AudioResampler.kt",
    "content": "package com.metrolist.music.recognition\n\nimport androidx.annotation.OptIn\nimport androidx.media3.common.util.UnstableApi\nimport androidx.media3.common.audio.AudioProcessor\nimport androidx.media3.common.audio.SonicAudioProcessor\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.ensureActive\nimport kotlinx.coroutines.withContext\nimport java.nio.ByteBuffer\nimport java.nio.ByteOrder\n\n/**\n * Data class representing decoded audio data with its properties.\n */\ndata class DecodedAudio(\n    val data: ByteArray,\n    val channelCount: Int,\n    val sampleRate: Int,\n    val pcmEncoding: Int,\n) {\n    override fun equals(other: Any?): Boolean {\n        if (this === other) return true\n        if (javaClass != other?.javaClass) return false\n        other as DecodedAudio\n        return data.contentEquals(other.data) &&\n                channelCount == other.channelCount &&\n                sampleRate == other.sampleRate &&\n                pcmEncoding == other.pcmEncoding\n    }\n\n    override fun hashCode(): Int {\n        var result = data.contentHashCode()\n        result = 31 * result + channelCount\n        result = 31 * result + sampleRate\n        result = 31 * result + pcmEncoding\n        return result\n    }\n}\n\n/**\n * Audio resampler using Media3 SonicAudioProcessor.\n * Resamples audio to the required sample rate for fingerprinting.\n */\n@OptIn(UnstableApi::class)\nobject AudioResampler {\n\n    suspend fun resample(\n        decodedAudio: DecodedAudio,\n        outputSampleRate: Int\n    ): Result<DecodedAudio> = withContext(Dispatchers.Default) {\n        if (decodedAudio.sampleRate == outputSampleRate) {\n            return@withContext Result.success(decodedAudio)\n        }\n        \n        var sonicRef: AudioProcessor? = null\n        try {\n            val sonic: AudioProcessor = SonicAudioProcessor().apply {\n                setOutputSampleRateHz(outputSampleRate)\n            }\n            sonicRef = sonic\n            \n            val inputFormat = AudioProcessor.AudioFormat(\n                decodedAudio.sampleRate,\n                decodedAudio.channelCount,\n                decodedAudio.pcmEncoding\n            )\n            val outputFormat = sonic.configure(inputFormat)\n            sonic.flush()\n\n            val inputBuf = ByteBuffer.wrap(decodedAudio.data).order(ByteOrder.nativeOrder())\n            sonic.queueInput(inputBuf)\n            sonic.queueEndOfStream()\n\n            val outputChunks = mutableListOf<ByteArray>()\n            var outputChunksByteSize = 0\n\n            while (!sonic.isEnded) {\n                ensureActive()\n                val outputBuffer = sonic.output\n                if (!outputBuffer.hasRemaining()) continue\n                val chunk = ByteArray(outputBuffer.remaining())\n                outputBuffer.get(chunk)\n                outputChunks.add(chunk)\n                outputChunksByteSize += chunk.size\n            }\n            sonic.reset()\n\n            val resampledData = if (outputChunks.size == 1) {\n                outputChunks[0]\n            } else {\n                ByteArray(outputChunksByteSize).also {\n                    var dest = 0\n                    for (chunk in outputChunks) {\n                        System.arraycopy(chunk, 0, it, dest, chunk.size)\n                        dest += chunk.size\n                    }\n                }\n            }\n            \n            Result.success(DecodedAudio(\n                data = resampledData,\n                channelCount = outputFormat.channelCount,\n                sampleRate = outputFormat.sampleRate,\n                pcmEncoding = outputFormat.encoding,\n            ))\n        } catch (e: Exception) {\n            ensureActive()\n            Result.failure(e)\n        } finally {\n            sonicRef?.reset()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/recognition/MusicRecognitionService.kt",
    "content": "/**\n * Music Recognition Feature\n * \n * This feature is based on the original MusicRecognizer project by Aleksey Saenko.\n * Original project: https://github.com/aleksey-saenko/MusicRecognizer\n * \n * Special thanks to Aleksey Saenko for the music recognition implementation.\n */\n\npackage com.metrolist.music.recognition\n\nimport android.Manifest\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.content.pm.PackageManager\nimport android.media.AudioFormat\nimport android.media.AudioRecord\nimport android.media.MediaRecorder\nimport androidx.core.content.ContextCompat\nimport com.metrolist.shazamkit.Shazam\nimport com.metrolist.shazamkit.models.RecognitionResult\nimport com.metrolist.shazamkit.models.RecognitionStatus\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.withContext\nimport java.io.ByteArrayOutputStream\nimport java.nio.ByteOrder\n\n/**\n * Service for recognizing music using audio fingerprinting.\n * Records audio from the microphone, generates a Shazam-compatible fingerprint,\n * and sends it to the Shazam API for recognition.\n */\nobject MusicRecognitionService {\n    \n    // Recording parameters\n    private const val RECORDING_SAMPLE_RATE = 44100\n    private const val CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO\n    private const val AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT\n    // Recording duration: 12 seconds for better recognition accuracy\n    // We use 12s directly to match the fallback duration for maximum compatibility\n    private const val RECORDING_DURATION_MS = 12000L\n    \n    private val _recognitionStatus = MutableStateFlow<RecognitionStatus>(RecognitionStatus.Ready)\n    val recognitionStatus: StateFlow<RecognitionStatus> = _recognitionStatus.asStateFlow()\n\n    /**\n     * Set to true by the widget service after it has already persisted the result to the\n     * database, so that [RecognitionScreen] skips the duplicate insert.\n     * Reset to false by [reset].\n     */\n    var resultSavedExternally: Boolean = false\n    \n    fun hasRecordPermission(context: Context): Boolean {\n        return ContextCompat.checkSelfPermission(\n            context, \n            Manifest.permission.RECORD_AUDIO\n        ) == PackageManager.PERMISSION_GRANTED\n    }\n    \n    /**\n     * Start the music recognition process.\n     * Records audio, generates fingerprint, and queries Shazam API.\n     */\n    @SuppressLint(\"MissingPermission\")\n    suspend fun recognize(context: Context): RecognitionStatus = withContext(Dispatchers.IO) {\n        if (!hasRecordPermission(context)) {\n            return@withContext RecognitionStatus.Error(\"Microphone permission not granted\")\n        }\n        \n        _recognitionStatus.value = RecognitionStatus.Listening\n        \n        try {\n            // Step 1: Record audio\n            val audioData = recordAudio()\n            \n            _recognitionStatus.value = RecognitionStatus.Processing\n            \n            // Step 2: Convert to mono if needed and resample to 16kHz\n            val decodedAudio = DecodedAudio(\n                data = audioData,\n                channelCount = 1,\n                sampleRate = RECORDING_SAMPLE_RATE,\n                pcmEncoding = AUDIO_FORMAT\n            )\n            \n            val resampledAudio = AudioResampler.resample(\n                decodedAudio, \n                VibraSignature.REQUIRED_SAMPLE_RATE\n            ).getOrElse { error ->\n                _recognitionStatus.value = RecognitionStatus.Error(\"Failed to resample audio: ${error.message}\")\n                return@withContext _recognitionStatus.value\n            }\n            \n            // Verify format\n            require(\n                resampledAudio.channelCount == 1 &&\n                resampledAudio.sampleRate == VibraSignature.REQUIRED_SAMPLE_RATE &&\n                resampledAudio.pcmEncoding == AudioFormat.ENCODING_PCM_16BIT &&\n                ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN &&\n                resampledAudio.data.isNotEmpty() && \n                resampledAudio.data.size % 2 == 0\n            ) { \"Invalid audio format for fingerprint generation\" }\n            \n            // Step 3: Generate fingerprint using native library\n            val signature = try {\n                VibraSignature.fromI16(resampledAudio.data)\n            } catch (e: Exception) {\n                _recognitionStatus.value = RecognitionStatus.Error(\"Failed to generate fingerprint: ${e.message}\")\n                return@withContext _recognitionStatus.value\n            }\n            \n            // Step 4: Send to Shazam API\n            val sampleDurationMs = (resampledAudio.data.size / 2) * 1000L / VibraSignature.REQUIRED_SAMPLE_RATE\n            \n            val result = Shazam.recognize(signature, sampleDurationMs)\n            \n            result.fold(\n                onSuccess = { recognitionResult ->\n                    _recognitionStatus.value = RecognitionStatus.Success(recognitionResult)\n                },\n                onFailure = { error ->\n                    val message = error.message ?: \"Unknown error\"\n                    _recognitionStatus.value = if (message.contains(\"No match\", ignoreCase = true)) {\n                        RecognitionStatus.NoMatch(\"No matches found. Try again with clearer audio.\")\n                    } else {\n                        RecognitionStatus.Error(message)\n                    }\n                }\n            )\n            \n            _recognitionStatus.value\n        } catch (e: Exception) {\n            _recognitionStatus.value = RecognitionStatus.Error(e.message ?: \"Recognition failed\")\n            _recognitionStatus.value\n        }\n    }\n    \n    @SuppressLint(\"MissingPermission\")\n    private suspend fun recordAudio(): ByteArray = withContext(Dispatchers.IO) {\n        val bufferSize = AudioRecord.getMinBufferSize(\n            RECORDING_SAMPLE_RATE, \n            CHANNEL_CONFIG, \n            AUDIO_FORMAT\n        )\n        \n        val audioRecord = AudioRecord(\n            MediaRecorder.AudioSource.MIC,\n            RECORDING_SAMPLE_RATE,\n            CHANNEL_CONFIG,\n            AUDIO_FORMAT,\n            bufferSize\n        )\n        \n        val outputStream = ByteArrayOutputStream()\n        val buffer = ByteArray(bufferSize)\n        val startTime = System.currentTimeMillis()\n        \n        try {\n            audioRecord.startRecording()\n            \n            while (System.currentTimeMillis() - startTime < RECORDING_DURATION_MS && isActive) {\n                val bytesRead = audioRecord.read(buffer, 0, bufferSize)\n                if (bytesRead > 0) {\n                    outputStream.write(buffer, 0, bytesRead)\n                }\n            }\n        } finally {\n            audioRecord.stop()\n            audioRecord.release()\n        }\n        \n        outputStream.toByteArray()\n    }\n    \n    fun reset() {\n        _recognitionStatus.value = RecognitionStatus.Ready\n        resultSavedExternally = false\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/recognition/RecognitionForegroundService.kt",
    "content": "package com.metrolist.music.recognition\n\nimport android.app.NotificationChannel\nimport android.app.NotificationManager\nimport android.app.PendingIntent\nimport android.app.Service\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.content.Intent\nimport android.content.pm.ServiceInfo\nimport android.os.Build\nimport android.os.IBinder\nimport android.util.Log\nimport androidx.core.app.NotificationCompat\nimport androidx.core.app.NotificationManagerCompat\nimport com.metrolist.music.MainActivity\nimport com.metrolist.music.R\nimport com.metrolist.shazamkit.models.RecognitionResult\nimport com.metrolist.shazamkit.models.RecognitionStatus\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.cancel\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport kotlinx.coroutines.withTimeoutOrNull\nimport java.net.HttpURLConnection\nimport java.net.URL\n\nclass RecognitionForegroundService : Service() {\n    private val serviceScope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob())\n    private var recognitionJob: Job? = null\n    private var statusJob: Job? = null\n    private var keepNotificationOnStop = false\n    private var terminalStateHandled = false\n\n    override fun onBind(intent: Intent?): IBinder? = null\n\n    override fun onCreate() {\n        super.onCreate()\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            createNotificationChannel()\n        }\n    }\n\n    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\n        if (!startInForeground()) return START_NOT_STICKY\n        startRecognitionIfNeeded()\n        return START_NOT_STICKY\n    }\n\n    override fun onDestroy() {\n        recognitionJob?.cancel()\n        statusJob?.cancel()\n        serviceScope.cancel()\n        if (!keepNotificationOnStop) {\n            stopForeground(STOP_FOREGROUND_REMOVE)\n        }\n        super.onDestroy()\n    }\n\n    private fun startInForeground(): Boolean {\n        val notification =\n            buildNotification(\n                title = getString(R.string.recognize_music),\n                contentText = getString(R.string.recognition_notification_listening),\n                isTerminal = false,\n                contentIntent = null,\n                largeIcon = null,\n                actionIntent = null,\n                actionTitle = null,\n            )\n\n        try {\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n                startForeground(\n                    NOTIFICATION_ID,\n                    notification,\n                    ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE,\n                )\n            } else {\n                startForeground(NOTIFICATION_ID, notification)\n            }\n            return true\n        } catch (foregroundTypeException: SecurityException) {\n            Log.w(TAG, \"Unable to start microphone foreground service\", foregroundTypeException)\n            stopSelf()\n            return false\n        } catch (runtimeException: RuntimeException) {\n            if (\n                Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&\n                    runtimeException::class.java.name ==\n                    \"android.app.ForegroundServiceStartNotAllowedException\"\n            ) {\n                Log.w(TAG, \"Unable to start microphone foreground service\", runtimeException)\n                stopSelf()\n                return false\n            }\n            throw runtimeException\n        }\n    }\n\n    private fun startRecognitionIfNeeded() {\n        if (recognitionJob?.isActive == true) return\n\n        keepNotificationOnStop = false\n        terminalStateHandled = false\n        MusicRecognitionService.reset()\n\n        statusJob?.cancel()\n        statusJob =\n            serviceScope.launch {\n                MusicRecognitionService.recognitionStatus.collect { status ->\n                    when (status) {\n                        is RecognitionStatus.Ready -> Unit\n                        else -> renderStatus(status)\n                    }\n                }\n            }\n\n        recognitionJob =\n            serviceScope.launch {\n                val result = MusicRecognitionService.recognize(this@RecognitionForegroundService)\n                if (result is RecognitionStatus.Error &&\n                    MusicRecognitionService.recognitionStatus.value !is RecognitionStatus.Error\n                ) {\n                    renderStatus(result)\n                }\n            }\n    }\n\n    private fun renderStatus(status: RecognitionStatus) {\n        when (status) {\n            is RecognitionStatus.Listening -> {\n                updateNotification(\n                    title = getString(R.string.recognize_music),\n                    contentText = getString(R.string.recognition_notification_listening),\n                    isTerminal = false,\n                    contentIntent = null,\n                    largeIcon = null,\n                    actionIntent = null,\n                    actionTitle = null,\n                )\n            }\n\n            is RecognitionStatus.Processing -> {\n                updateNotification(\n                    title = getString(R.string.recognize_music),\n                    contentText = getString(R.string.recognition_notification_processing),\n                    isTerminal = false,\n                    contentIntent = null,\n                    largeIcon = null,\n                    actionIntent = null,\n                    actionTitle = null,\n                )\n            }\n\n            is RecognitionStatus.Success -> {\n                handleSuccess(status.result)\n            }\n\n            is RecognitionStatus.NoMatch -> {\n                if (terminalStateHandled) return\n                terminalStateHandled = true\n                updateNotification(\n                    title = getString(R.string.recognize_music),\n                    contentText = getString(R.string.recognition_notification_no_match),\n                    isTerminal = true,\n                    contentIntent = null,\n                    largeIcon = null,\n                    actionIntent = null,\n                    actionTitle = null,\n                )\n                finishWithPersistentResult()\n            }\n\n            is RecognitionStatus.Error -> {\n                if (terminalStateHandled) return\n                terminalStateHandled = true\n                updateNotification(\n                    title = getString(R.string.recognize_music),\n                    contentText = getString(R.string.recognition_notification_failed),\n                    isTerminal = true,\n                    contentIntent = null,\n                    largeIcon = null,\n                    actionIntent = null,\n                    actionTitle = null,\n                )\n                finishWithPersistentResult()\n            }\n\n            is RecognitionStatus.Ready -> Unit\n        }\n    }\n\n    private fun updateNotification(\n        title: String,\n        contentText: String,\n        isTerminal: Boolean,\n        contentIntent: PendingIntent?,\n        largeIcon: Bitmap?,\n        actionIntent: PendingIntent?,\n        actionTitle: String?,\n    ) {\n        NotificationManagerCompat.from(this).notify(\n            NOTIFICATION_ID,\n            buildNotification(\n                title = title,\n                contentText = contentText,\n                isTerminal = isTerminal,\n                contentIntent = contentIntent,\n                largeIcon = largeIcon,\n                actionIntent = actionIntent,\n                actionTitle = actionTitle,\n            ),\n        )\n    }\n\n    private fun buildNotification(\n        title: String,\n        contentText: String,\n        isTerminal: Boolean,\n        contentIntent: PendingIntent?,\n        largeIcon: Bitmap?,\n        actionIntent: PendingIntent?,\n        actionTitle: String?,\n    ) =\n        NotificationCompat.Builder(this, CHANNEL_ID)\n            .setSmallIcon(R.drawable.ic_widget_mic)\n            .setContentTitle(title)\n            .setContentText(contentText)\n            .setPriority(NotificationCompat.PRIORITY_LOW)\n            .setOnlyAlertOnce(true)\n            .setOngoing(!isTerminal)\n            .setAutoCancel(isTerminal)\n            .setContentIntent(contentIntent)\n            .setLargeIcon(largeIcon)\n            .apply {\n                if (actionIntent != null && actionTitle != null) {\n                    addAction(0, actionTitle, actionIntent)\n                }\n            }\n            .build()\n\n    private fun handleSuccess(result: RecognitionResult) {\n        if (terminalStateHandled) return\n        terminalStateHandled = true\n\n        val pendingIntent = createResultPendingIntent(result)\n        updateNotification(\n            title = result.title,\n            contentText = result.artist,\n            isTerminal = true,\n            contentIntent = pendingIntent,\n            largeIcon = null,\n            actionIntent = pendingIntent,\n            actionTitle = getString(R.string.listen_on_metrolist),\n        )\n\n        serviceScope.launch {\n            val coverUrl = result.coverArtHqUrl ?: result.coverArtUrl\n            val coverBitmap =\n                if (coverUrl == null) {\n                    null\n                } else {\n                    withTimeoutOrNull(1_500L) {\n                        loadBitmap(coverUrl)\n                    }\n                }\n\n            if (coverBitmap != null) {\n                updateNotification(\n                    title = result.title,\n                    contentText = result.artist,\n                    isTerminal = true,\n                    contentIntent = pendingIntent,\n                    largeIcon = coverBitmap,\n                    actionIntent = pendingIntent,\n                    actionTitle = getString(R.string.listen_on_metrolist),\n                )\n            }\n            finishWithPersistentResult()\n        }\n    }\n\n    private suspend fun loadBitmap(url: String): Bitmap? =\n        withContext(Dispatchers.IO) {\n            runCatching {\n                val connection = (URL(url).openConnection() as? HttpURLConnection)\n                    ?: return@runCatching null\n                try {\n                    connection.connectTimeout = BITMAP_CONNECT_TIMEOUT_MS\n                    connection.readTimeout = BITMAP_READ_TIMEOUT_MS\n                    connection.instanceFollowRedirects = true\n                    connection.doInput = true\n                    connection.connect()\n                    connection.inputStream.use(BitmapFactory::decodeStream)\n                } finally {\n                    connection.disconnect()\n                }\n            }.getOrNull()\n        }\n\n    private fun createResultPendingIntent(result: RecognitionResult): PendingIntent {\n        val launchIntent =\n            Intent(this, MainActivity::class.java).apply {\n                action = MainActivity.ACTION_RECOGNITION\n                putExtra(EXTRA_RECOGNITION_TRACK_ID, result.trackId)\n                putExtra(EXTRA_RECOGNITION_TITLE, result.title)\n                putExtra(EXTRA_RECOGNITION_ARTIST, result.artist)\n                flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP\n            }\n\n        return PendingIntent.getActivity(\n            this,\n            RESULT_PENDING_INTENT_REQUEST_CODE,\n            launchIntent,\n            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,\n        )\n    }\n\n    private fun finishWithPersistentResult() {\n        keepNotificationOnStop = true\n        stopForeground(STOP_FOREGROUND_DETACH)\n        stopSelf()\n    }\n\n    private fun createNotificationChannel() {\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return\n\n        val channel =\n            NotificationChannel(\n                CHANNEL_ID,\n                getString(R.string.recognition_notification_channel_name),\n                NotificationManager.IMPORTANCE_LOW,\n            ).apply {\n                description = getString(R.string.recognition_notification_channel_desc)\n                setShowBadge(false)\n            }\n\n        getSystemService(NotificationManager::class.java).createNotificationChannel(channel)\n    }\n\n    companion object {\n        const val EXTRA_RECOGNITION_TRACK_ID = \"recognition_track_id\"\n        const val EXTRA_RECOGNITION_TITLE = \"recognition_title\"\n        const val EXTRA_RECOGNITION_ARTIST = \"recognition_artist\"\n\n        private const val CHANNEL_ID = \"recognition_channel\"\n        private const val NOTIFICATION_ID = 9100\n        private const val RESULT_PENDING_INTENT_REQUEST_CODE = 9101\n        private const val TAG = \"RecognitionFgService\"\n        private const val BITMAP_CONNECT_TIMEOUT_MS = 1_200\n        private const val BITMAP_READ_TIMEOUT_MS = 1_200\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/recognition/RecognitionLaunchActivity.kt",
    "content": "package com.metrolist.music.recognition\n\nimport android.Manifest\nimport android.app.Activity\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.os.Build\nimport android.os.Bundle\nimport androidx.core.content.ContextCompat\nimport com.metrolist.music.MainActivity\n\nclass RecognitionLaunchActivity : Activity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        handleRecognitionLaunch()\n    }\n\n    override fun onNewIntent(intent: Intent?) {\n        super.onNewIntent(intent)\n        handleRecognitionLaunch()\n    }\n\n    private fun handleRecognitionLaunch() {\n        if (hasRecordPermission()) {\n            startRecognitionService()\n        } else {\n            openRecognitionPermissionFlow()\n        }\n        finish()\n    }\n\n    private fun hasRecordPermission(): Boolean {\n        return ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) ==\n            PackageManager.PERMISSION_GRANTED\n    }\n\n    private fun openRecognitionPermissionFlow() {\n        val intent =\n            Intent(this, MainActivity::class.java).apply {\n                action = MainActivity.ACTION_RECOGNITION\n                putExtra(MainActivity.EXTRA_AUTO_START_RECOGNITION, true)\n                flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP\n            }\n        startActivity(intent)\n    }\n\n    private fun startRecognitionService() {\n        if (!hasRecordPermission()) return\n\n        val serviceIntent = Intent(this, RecognitionForegroundService::class.java)\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            startForegroundService(serviceIntent)\n        } else {\n            startService(serviceIntent)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/recognition/ShazamSignatureGenerator.kt",
    "content": "package com.metrolist.music.recognition\n\nimport android.util.Base64\nimport java.io.ByteArrayOutputStream\nimport java.nio.ByteBuffer\nimport java.nio.ByteOrder\nimport java.util.zip.CRC32\nimport kotlin.math.PI\nimport kotlin.math.cos\nimport kotlin.math.ln\nimport kotlin.math.max\n\n/**\n * Pure Kotlin implementation of the Shazam audio fingerprinting algorithm.\n *\n * Ported from the vibra C++ library (https://github.com/marin-m/SongRec) which implements\n * the Shazam signature algorithm using FFT-based audio fingerprinting.\n *\n * This replaces the native C++ + FFTW3 implementation with a pure JVM solution.\n */\ninternal object ShazamSignatureGenerator {\n\n    private const val SAMPLE_RATE = 16_000\n    private const val FFT_SIZE = 2048\n    private const val FFT_OUTPUT_SIZE = FFT_SIZE / 2 + 1  // 1025\n    private const val MAX_PEAKS = 255\n    private const val MAX_TIME_SECONDS = 12.0\n\n    // Spread ring buffer size\n    private const val RING_BUF_SIZE = 256\n\n    // Band IDs matching FrequencyBand enum in C++ (0=250-520Hz, 1=520-1450Hz, 2=1450-3500Hz, 3=3500-5500Hz)\n    private const val BAND_250_520 = 0\n    private const val BAND_520_1450 = 1\n    private const val BAND_1450_3500 = 2\n    private const val BAND_3500_5500 = 3\n\n    /**\n     * Hanning window: w[i] = 0.5 * (1 - cos(2π*(i+1)/2049)) for i=0..2047.\n     *\n     * This matches the precomputed HANNIG_MATRIX values in the C++ hanning.h header.\n     */\n    private val HANNING = DoubleArray(FFT_SIZE) { i ->\n        0.5 * (1.0 - cos(2.0 * PI * (i + 1).toDouble() / 2049.0))\n    }\n\n    /**\n     * Generates a Shazam-compatible audio fingerprint from raw 16-bit PCM samples.\n     *\n     * @param samples ByteArray of mono PCM audio (16-bit signed little-endian, 16kHz)\n     * @return Signature URI string (data:audio/vnd.shazam.sig;base64,...)\n     */\n    fun fromI16(samples: ByteArray): String {\n        require(samples.size >= 2 && samples.size % 2 == 0) {\n            \"samples must be a non-empty byte array with even length (16-bit PCM)\"\n        }\n        val pcm = ShortArray(samples.size / 2)\n        ByteBuffer.wrap(samples).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(pcm)\n        return SignatureGeneratorState().process(pcm)\n    }\n\n    private class SignatureGeneratorState {\n        // Circular buffer for 2048 raw samples (as Shorts stored in Int for speed)\n        private val samplesRing = IntArray(FFT_SIZE)\n        private var samplesPos = 0\n\n        // Circular buffer of FFT magnitude outputs (RING_BUF_SIZE x FFT_OUTPUT_SIZE)\n        private val fftOutputs = Array(RING_BUF_SIZE) { DoubleArray(FFT_OUTPUT_SIZE) }\n        private var fftPos = 0\n        private var fftNumWritten = 0\n\n        // Circular buffer of time-spread FFT outputs (RING_BUF_SIZE x FFT_OUTPUT_SIZE)\n        private val spreadFfts = Array(RING_BUF_SIZE) { DoubleArray(FFT_OUTPUT_SIZE) }\n        private var spreadPos = 0\n        private var spreadNumWritten = 0\n\n        // Accumulated samples count (for signature header)\n        private var numSamples = 0\n\n        // Band → list of peaks (bands 0..3)\n        private val bandPeaks = Array(4) { mutableListOf<FrequencyPeak>() }\n        private var totalPeaks = 0\n\n        fun process(pcm: ShortArray): String {\n            var offset = 0\n            while (offset + 128 <= pcm.size) {\n                // Match C++ stopping condition: stop when BOTH time≥max AND peaks≥max\n                val elapsedSec = numSamples.toDouble() / SAMPLE_RATE\n                if (elapsedSec >= MAX_TIME_SECONDS && totalPeaks >= MAX_PEAKS) break\n\n                numSamples += 128\n                feedSamples(pcm, offset, 128)\n                doFFT()\n                doPeakSpreadingAndRecognition()\n                offset += 128\n            }\n            return encodeSignature()\n        }\n\n        private fun feedSamples(pcm: ShortArray, start: Int, count: Int) {\n            for (k in start until start + count) {\n                samplesRing[samplesPos] = pcm[k].toInt()\n                samplesPos = (samplesPos + 1) % FFT_SIZE\n            }\n        }\n\n        private fun doFFT() {\n            // Build windowed excerpt from ring buffer (oldest → newest)\n            val windowed = DoubleArray(FFT_SIZE) { i ->\n                samplesRing[(samplesPos + i) % FFT_SIZE].toDouble() * HANNING[i]\n            }\n            val result = computeRfft(windowed)\n            result.copyInto(fftOutputs[fftPos])\n            fftPos = (fftPos + 1) % RING_BUF_SIZE\n            fftNumWritten++\n        }\n\n        private fun doPeakSpreadingAndRecognition() {\n            doPeakSpreading()\n            if (spreadNumWritten >= 47) {\n                doPeakRecognition()\n            }\n        }\n\n        private fun doPeakSpreading() {\n            // Start with a copy of the last FFT output\n            val lastFftIdx = (fftPos - 1 + RING_BUF_SIZE) % RING_BUF_SIZE\n            val spread = fftOutputs[lastFftIdx].copyOf()\n\n            // Frequency spreading: 3-point running max (in-place, forward pass)\n            for (pos in 0 until FFT_OUTPUT_SIZE - 2) {\n                spread[pos] = maxOf(spread[pos], spread[pos + 1], spread[pos + 2])\n            }\n\n            // Time spreading: propagate max to/from older spread entries at offsets -1, -3, -6\n            // Only older entries are updated; the new entry keeps only frequency spreading (matches C++).\n            for (pos in 0 until FFT_OUTPUT_SIZE) {\n                var maxVal = spread[pos]\n                for (offset in intArrayOf(-1, -3, -6)) {\n                    val idx = ((spreadPos + offset) % RING_BUF_SIZE + RING_BUF_SIZE) % RING_BUF_SIZE\n                    val oldVal = spreadFfts[idx][pos]\n                    if (oldVal > maxVal) maxVal = oldVal\n                    spreadFfts[idx][pos] = maxVal\n                }\n                // Note: spread[pos] is intentionally NOT updated here.\n                // The new entry stored in spreadFfts should only have frequency spreading applied,\n                // not time spreading. This matches the original C++ vibra implementation.\n            }\n\n            spread.copyInto(spreadFfts[spreadPos])\n            spreadPos = (spreadPos + 1) % RING_BUF_SIZE\n            spreadNumWritten++\n        }\n\n        private fun doPeakRecognition() {\n            val fftMinus46 = fftOutputs[(fftPos - 46 + RING_BUF_SIZE * 2) % RING_BUF_SIZE]\n            val spreadMinus49 = spreadFfts[(spreadPos - 49 + RING_BUF_SIZE * 2) % RING_BUF_SIZE]\n\n            val otherOffsets = intArrayOf(-53, -45, 165, 172, 179, 186, 193, 200, 214, 221, 228, 235, 242, 249)\n\n            for (binPos in 10 until FFT_OUTPUT_SIZE - 8) {\n                val fftVal = fftMinus46[binPos]\n                if (fftVal < 1.0 / 64.0 || fftVal < spreadMinus49[binPos]) continue\n\n                // Check 8 neighbors in spreadMinus49\n                var maxNeighborSpread49 = 0.0\n                for (neighborOffset in intArrayOf(-10, -7, -4, -3, 1, 2, 5, 8)) {\n                    val v = spreadMinus49[binPos + neighborOffset]\n                    if (v > maxNeighborSpread49) maxNeighborSpread49 = v\n                }\n                if (fftVal <= maxNeighborSpread49) continue\n\n                // Check 14 other spread FFT offsets\n                var maxNeighborOther = maxNeighborSpread49\n                for (otherOffset in otherOffsets) {\n                    val spreadIdx = ((spreadPos + otherOffset) % RING_BUF_SIZE + RING_BUF_SIZE) % RING_BUF_SIZE\n                    val v = spreadFfts[spreadIdx][binPos - 1]\n                    if (v > maxNeighborOther) maxNeighborOther = v\n                }\n                if (fftVal <= maxNeighborOther) continue\n\n                // Valid peak found: compute corrected bin and frequency\n                val fftNumber = spreadNumWritten - 46\n\n                val peakMag = ln(max(1.0 / 64.0, fftVal)) * 1477.3 + 6144\n                val peakMagBefore = ln(max(1.0 / 64.0, fftMinus46[binPos - 1])) * 1477.3 + 6144\n                val peakMagAfter = ln(max(1.0 / 64.0, fftMinus46[binPos + 1])) * 1477.3 + 6144\n\n                val peakVariation1 = peakMag * 2 - peakMagBefore - peakMagAfter\n                val peakVariation2 = (peakMagAfter - peakMagBefore) * 32 / peakVariation1\n\n                val correctedBin = binPos * 64.0 + peakVariation2\n                val frequencyHz = correctedBin * (16000.0 / 2.0 / 1024.0 / 64.0)\n\n                val band = when {\n                    frequencyHz < 250.0  -> continue\n                    frequencyHz < 520.0  -> BAND_250_520\n                    frequencyHz < 1450.0 -> BAND_520_1450\n                    frequencyHz < 3500.0 -> BAND_1450_3500\n                    frequencyHz <= 5500.0 -> BAND_3500_5500\n                    else -> continue\n                }\n\n                bandPeaks[band].add(\n                    FrequencyPeak(\n                        fftPassNumber = fftNumber,\n                        peakMagnitude = peakMag.toInt(),\n                        correctedPeakFrequencyBin = correctedBin.toInt()\n                    )\n                )\n                totalPeaks++\n            }\n        }\n\n        private fun encodeSignature(): String {\n            val contentsStream = ByteArrayOutputStream()\n\n            // Write each frequency band's peaks in ascending band order (matches C++ std::map iteration)\n            for (bandId in 0..3) {\n                val peaks = bandPeaks[bandId]\n                if (peaks.isEmpty()) continue\n\n                val peakBuf = ByteArrayOutputStream()\n                var prevFftPassNumber = 0\n\n                for (peak in peaks) {\n                    val diff = peak.fftPassNumber - prevFftPassNumber\n                    if (diff >= 255) {\n                        // Encode absolute position with 0xFF marker\n                        peakBuf.write(0xFF)\n                        writeLittleEndian32(peakBuf, peak.fftPassNumber)\n                        prevFftPassNumber = peak.fftPassNumber\n                    }\n                    peakBuf.write(peak.fftPassNumber - prevFftPassNumber)\n                    writeLittleEndian16(peakBuf, peak.peakMagnitude)\n                    writeLittleEndian16(peakBuf, peak.correctedPeakFrequencyBin)\n                    prevFftPassNumber = peak.fftPassNumber\n                }\n\n                val peakBytes = peakBuf.toByteArray()\n\n                // Band tag: 0x60030040 + bandId\n                writeLittleEndian32(contentsStream, 0x60030040 + bandId)\n                writeLittleEndian32(contentsStream, peakBytes.size)\n                contentsStream.write(peakBytes)\n\n                // Pad to 4-byte alignment\n                val padBytes = (4 - peakBytes.size % 4) % 4\n                repeat(padBytes) { contentsStream.write(0) }\n            }\n\n            val contents = contentsStream.toByteArray()\n            val sizeMinusHeader = contents.size + 8\n            val samplesAndOffset = (numSamples + SAMPLE_RATE * 0.24).toInt()\n\n            // Build 48-byte header struct (all fields little-endian)\n            val headerBytes = ByteBuffer.allocate(48).order(ByteOrder.LITTLE_ENDIAN).apply {\n                putInt(0xcafe2580.toInt())     // magic1\n                putInt(0)                      // crc32 placeholder\n                putInt(sizeMinusHeader)        // size_minus_header\n                putInt(0x94119c00.toInt())     // magic2\n                putInt(0); putInt(0); putInt(0) // void1[3]\n                putInt(3 shl 27)               // shifted_sample_rate_id\n                putInt(0); putInt(0)           // void2[2]\n                putInt(samplesAndOffset)       // number_samples_plus_divided_sample_rate\n                putInt((15 shl 19) + 0x40000) // fixed_value\n            }.array()\n\n            // Assemble full buffer: header(48) + 0x40000000(4) + sizeMinusHeader(4) + contents\n            val fullBuf = ByteArrayOutputStream(56 + contents.size)\n            fullBuf.write(headerBytes)\n            writeLittleEndian32(fullBuf, 0x40000000)\n            writeLittleEndian32(fullBuf, contents.size + 8)\n            fullBuf.write(contents)\n\n            val fullBytes = fullBuf.toByteArray()\n\n            // CRC32 over bytes from offset 8 to end (skipping magic1 and the crc32 field itself)\n            val crc = CRC32()\n            crc.update(fullBytes, 8, fullBytes.size - 8)\n            val crc32Value = crc.value.toInt()\n\n            // Write CRC32 at offset 4 (little-endian)\n            fullBytes[4] = (crc32Value and 0xFF).toByte()\n            fullBytes[5] = ((crc32Value shr 8) and 0xFF).toByte()\n            fullBytes[6] = ((crc32Value shr 16) and 0xFF).toByte()\n            fullBytes[7] = ((crc32Value shr 24) and 0xFF).toByte()\n\n            val base64 = Base64.encodeToString(fullBytes, Base64.NO_WRAP)\n            return \"data:audio/vnd.shazam.sig;base64,$base64\"\n        }\n    }\n\n    private data class FrequencyPeak(\n        val fftPassNumber: Int,\n        val peakMagnitude: Int,\n        val correctedPeakFrequencyBin: Int\n    )\n\n    private fun writeLittleEndian32(out: ByteArrayOutputStream, value: Int) {\n        out.write(value and 0xFF)\n        out.write((value ushr 8) and 0xFF)\n        out.write((value ushr 16) and 0xFF)\n        out.write((value ushr 24) and 0xFF)\n    }\n\n    private fun writeLittleEndian16(out: ByteArrayOutputStream, value: Int) {\n        out.write(value and 0xFF)\n        out.write((value ushr 8) and 0xFF)\n    }\n\n    /**\n     * Computes the real-input FFT of [windowed] (size 2048) using an iterative\n     * Cooley-Tukey radix-2 DIT algorithm.\n     *\n     * Returns FFT_OUTPUT_SIZE (1025) magnitude values:\n     *   magnitude[k] = max((re[k]² + im[k]²) / 2^17, 1e-10)\n     *\n     * This matches the FFTW3 r2c output format used in the C++ vibra library.\n     */\n    private fun computeRfft(windowed: DoubleArray): DoubleArray {\n        val n = windowed.size  // 2048\n        val re = windowed.copyOf()\n        val im = DoubleArray(n)\n\n        // Bit-reversal permutation\n        var j = 0\n        for (i in 1 until n) {\n            var bit = n ushr 1\n            while (j and bit != 0) {\n                j = j xor bit\n                bit = bit ushr 1\n            }\n            j = j xor bit\n            if (i < j) {\n                var tmp = re[i]; re[i] = re[j]; re[j] = tmp\n                tmp = im[i]; im[i] = im[j]; im[j] = tmp\n            }\n        }\n\n        // Cooley-Tukey butterfly stages (11 stages for n=2048)\n        var len = 2\n        while (len <= n) {\n            val halfLen = len ushr 1\n            val ang = -PI / halfLen       // = -2π / len\n            val wBaseRe = cos(ang)\n            val wBaseIm = kotlin.math.sin(ang)\n            var i = 0\n            while (i < n) {\n                var wRe = 1.0\n                var wIm = 0.0\n                for (k in 0 until halfLen) {\n                    val u = i + k\n                    val v = u + halfLen\n                    val evenRe = re[u]\n                    val evenIm = im[u]\n                    val oddRe = re[v] * wRe - im[v] * wIm\n                    val oddIm = re[v] * wIm + im[v] * wRe\n                    re[u] = evenRe + oddRe\n                    im[u] = evenIm + oddIm\n                    re[v] = evenRe - oddRe\n                    im[v] = evenIm - oddIm\n                    val newWRe = wRe * wBaseRe - wIm * wBaseIm\n                    wIm = wRe * wBaseIm + wIm * wBaseRe\n                    wRe = newWRe\n                }\n                i += len\n            }\n            len = len shl 1\n        }\n\n        // Extract magnitudes for bins 0..n/2 (FFT_OUTPUT_SIZE = 1025)\n        val scaleFactor = 1.0 / (1 shl 17)\n        val minVal = 1e-10\n        return DoubleArray(FFT_OUTPUT_SIZE) { idx ->\n            val r = re[idx]\n            val img = im[idx]\n            val mag = (r * r + img * img) * scaleFactor\n            if (mag < minVal) minVal else mag\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/recognition/VibraSignature.kt",
    "content": "package com.metrolist.music.recognition\n\n/**\n * Audio fingerprint generator for Shazam-compatible signatures.\n *\n * Pure Kotlin implementation — no native C++ or FFTW3 dependency required.\n * Uses [ShazamSignatureGenerator] which ports the vibra algorithm to JVM.\n */\nobject VibraSignature {\n\n    const val REQUIRED_SAMPLE_RATE = 16_000\n\n    /**\n     * Generates a Shazam signature from raw PCM audio data.\n     *\n     * @param samples Raw PCM audio data (mono, 16-bit signed little-endian, 16kHz)\n     * @return The encoded signature URI string suitable for the Shazam API\n     * @throws IllegalArgumentException if samples is empty or has odd length\n     */\n    @JvmStatic\n    fun fromI16(samples: ByteArray): String = ShazamSignatureGenerator.fromI16(samples)\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/AppNavigation.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.PressInteraction\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.NavigationBar\nimport androidx.compose.material3.NavigationBarItem\nimport androidx.compose.material3.NavigationRail\nimport androidx.compose.material3.NavigationRailItem\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.platform.LocalViewConfiguration\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport com.metrolist.music.ui.screens.Screens\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.collectLatest\n\n@Immutable\nprivate data class NavItemState(\n    val isSelected: Boolean,\n    val iconRes: Int\n)\n\n@Stable\nprivate fun isRouteSelected(currentRoute: String?, screenRoute: String, navigationItems: List<Screens>): Boolean {\n    if (currentRoute == null) return false\n    if (currentRoute == screenRoute) return true\n    return navigationItems.any { it.route == screenRoute } && \n           currentRoute.startsWith(\"$screenRoute/\")\n}\n\n@Composable\nfun AppNavigationRail(\n    navigationItems: List<Screens>,\n    currentRoute: String?,\n    onItemClick: (Screens, Boolean) -> Unit,\n    modifier: Modifier = Modifier,\n    pureBlack: Boolean = false,\n    onSearchLongClick: (() -> Unit)? = null\n) {\n    val containerColor = if (pureBlack) Color.Black else MaterialTheme.colorScheme.surfaceContainer\n    val haptics = LocalHapticFeedback.current\n    val viewConfiguration = LocalViewConfiguration.current\n    \n    NavigationRail(\n        modifier = modifier,\n        containerColor = containerColor\n    ) {\n        Spacer(modifier = Modifier.weight(1f))\n        \n        navigationItems.forEach { screen ->\n            val isSelected = remember(currentRoute, screen.route) {\n                isRouteSelected(currentRoute, screen.route, navigationItems)\n            }\n            val iconRes = remember(isSelected, screen) {\n                if (isSelected) screen.iconIdActive else screen.iconIdInactive\n            }\n            \n            val isSearchItem = screen == Screens.Search && onSearchLongClick != null\n            val interactionSource = remember { MutableInteractionSource() }\n            \n            // Long press detection using InteractionSource\n            if (isSearchItem) {\n                LaunchedEffect(interactionSource) {\n                    var isLongClick = false\n                    interactionSource.interactions.collectLatest { interaction ->\n                        when (interaction) {\n                            is PressInteraction.Press -> {\n                                isLongClick = false\n                                delay(viewConfiguration.longPressTimeoutMillis)\n                                isLongClick = true\n                                haptics.performHapticFeedback(HapticFeedbackType.LongPress)\n                                onSearchLongClick.invoke()\n                            }\n                            is PressInteraction.Release -> {\n                                if (!isLongClick) {\n                                    onItemClick(screen, isSelected)\n                                }\n                            }\n                            is PressInteraction.Cancel -> {\n                                isLongClick = false\n                            }\n                        }\n                    }\n                }\n            }\n            \n            NavigationRailItem(\n                selected = isSelected,\n                onClick = { \n                    if (!isSearchItem) {\n                        onItemClick(screen, isSelected)\n                    }\n                    // For search item, click is handled via InteractionSource\n                },\n                interactionSource = interactionSource,\n                icon = {\n                    Icon(\n                        painter = painterResource(id = iconRes),\n                        contentDescription = stringResource(screen.titleId)\n                    )\n                }\n            )\n        }\n        \n        Spacer(modifier = Modifier.weight(1f))\n    }\n}\n\n@Composable\nfun AppNavigationBar(\n    navigationItems: List<Screens>,\n    currentRoute: String?,\n    onItemClick: (Screens, Boolean) -> Unit,\n    modifier: Modifier = Modifier,\n    pureBlack: Boolean = false,\n    slimNav: Boolean = false,\n    onSearchLongClick: (() -> Unit)? = null\n) {\n    val containerColor = if (pureBlack) Color.Black else MaterialTheme.colorScheme.surfaceContainer\n    val contentColor = if (pureBlack) Color.White else MaterialTheme.colorScheme.onSurfaceVariant\n    val haptics = LocalHapticFeedback.current\n    val viewConfiguration = LocalViewConfiguration.current\n    \n    NavigationBar(\n        modifier = modifier,\n        containerColor = containerColor,\n        contentColor = contentColor\n    ) {\n        navigationItems.forEach { screen ->\n            val isSelected = remember(currentRoute, screen.route) {\n                isRouteSelected(currentRoute, screen.route, navigationItems)\n            }\n            val iconRes = remember(isSelected, screen) {\n                if (isSelected) screen.iconIdActive else screen.iconIdInactive\n            }\n            \n            val isSearchItem = screen == Screens.Search && onSearchLongClick != null\n            val interactionSource = remember { MutableInteractionSource() }\n            \n            // Long press detection using InteractionSource\n            if (isSearchItem) {\n                LaunchedEffect(interactionSource) {\n                    var isLongClick = false\n                    interactionSource.interactions.collectLatest { interaction ->\n                        when (interaction) {\n                            is PressInteraction.Press -> {\n                                isLongClick = false\n                                delay(viewConfiguration.longPressTimeoutMillis)\n                                isLongClick = true\n                                haptics.performHapticFeedback(HapticFeedbackType.LongPress)\n                                onSearchLongClick.invoke()\n                            }\n                            is PressInteraction.Release -> {\n                                if (!isLongClick) {\n                                    onItemClick(screen, isSelected)\n                                }\n                            }\n                            is PressInteraction.Cancel -> {\n                                isLongClick = false\n                            }\n                        }\n                    }\n                }\n            }\n            \n            NavigationBarItem(\n                selected = isSelected,\n                onClick = { \n                    if (!isSearchItem) {\n                        onItemClick(screen, isSelected)\n                    }\n                    // For search item, click is handled via InteractionSource\n                },\n                interactionSource = interactionSource,\n                icon = {\n                    Icon(\n                        painter = painterResource(id = iconRes),\n                        contentDescription = stringResource(screen.titleId)\n                    )\n                },\n                label = if (!slimNav) {\n                    {\n                        Text(\n                            text = stringResource(screen.titleId),\n                            maxLines = 1,\n                            overflow = TextOverflow.Ellipsis\n                        )\n                    }\n                } else null\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/AutoResizeText.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.drawWithContent\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.sp\n\n/**\n * From https://stackoverflow.com/a/69780826\n */\n@Composable\nfun AutoResizeText(\n    text: String,\n    fontSizeRange: FontSizeRange,\n    modifier: Modifier = Modifier,\n    color: Color = Color.Unspecified,\n    fontStyle: FontStyle? = null,\n    fontWeight: FontWeight? = null,\n    fontFamily: FontFamily? = null,\n    letterSpacing: TextUnit = TextUnit.Unspecified,\n    textDecoration: TextDecoration? = null,\n    textAlign: TextAlign? = null,\n    lineHeight: TextUnit = TextUnit.Unspecified,\n    overflow: TextOverflow = TextOverflow.Clip,\n    softWrap: Boolean = true,\n    maxLines: Int = Int.MAX_VALUE,\n    style: TextStyle = LocalTextStyle.current,\n) {\n    var fontSizeValue by remember { mutableFloatStateOf(fontSizeRange.max.value) }\n    var readyToDraw by remember { mutableStateOf(false) }\n\n    Text(\n        text = text,\n        color = color,\n        maxLines = maxLines,\n        fontStyle = fontStyle,\n        fontWeight = fontWeight,\n        fontFamily = fontFamily,\n        letterSpacing = letterSpacing,\n        textDecoration = textDecoration,\n        textAlign = textAlign,\n        lineHeight = lineHeight,\n        overflow = overflow,\n        softWrap = softWrap,\n        style = style,\n        fontSize = fontSizeValue.sp,\n        onTextLayout = {\n            if (it.didOverflowHeight && !readyToDraw) {\n                // Did Overflow height, calculate next font size value\n                val nextFontSizeValue = fontSizeValue - fontSizeRange.step.value\n                if (nextFontSizeValue <= fontSizeRange.min.value) {\n                    // Reached minimum, set minimum font size and it's readToDraw\n                    fontSizeValue = fontSizeRange.min.value\n                    readyToDraw = true\n                } else {\n                    // Text doesn't fit yet and haven't reached minimum text range, keep decreasing\n                    fontSizeValue = nextFontSizeValue\n                }\n            } else {\n                // Text fits before reaching the minimum, it's readyToDraw\n                readyToDraw = true\n            }\n        },\n        modifier = modifier.drawWithContent { if (readyToDraw) drawContent() },\n    )\n}\n\ndata class FontSizeRange(\n    val min: TextUnit,\n    val max: TextUnit,\n    val step: TextUnit = DEFAULT_TEXT_STEP,\n) {\n    init {\n        require(min < max) { \"min should be less than max, $this\" }\n        require(step.value > 0) { \"step should be greater than 0, $this\" }\n    }\n\n    companion object {\n        private val DEFAULT_TEXT_STEP = 1.sp\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/BigSeekBar.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.gestures.detectHorizontalDragGestures\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.layout.onPlaced\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun BigSeekBar(\n    progressProvider: () -> Float,\n    onProgressChange: (Float) -> Unit,\n    modifier: Modifier = Modifier,\n    background: Color = MaterialTheme.colorScheme.surfaceTint.copy(alpha = 0.13f),\n    color: Color = MaterialTheme.colorScheme.primary,\n) {\n    var width by remember {\n        mutableFloatStateOf(0f)\n    }\n\n    Canvas(\n        modifier\n            .fillMaxWidth()\n            .height(48.dp)\n            .clip(RoundedCornerShape(16.dp))\n            .onPlaced {\n                width = it.size.width.toFloat()\n            }.pointerInput(progressProvider) {\n                detectHorizontalDragGestures { _, dragAmount ->\n                    onProgressChange(\n                        (progressProvider() + dragAmount * 1.2f / width).coerceIn(\n                            0f,\n                            1f\n                        )\n                    )\n                }\n            },\n    ) {\n        drawRect(color = background)\n\n        drawRect(\n            color = color,\n            size = size.copy(width = size.width * progressProvider()),\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/BottomSheet.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.AnimationSpec\nimport androidx.compose.animation.core.AnimationVector1D\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.animation.core.SpringSpec\nimport androidx.compose.animation.core.VectorConverter\nimport androidx.compose.animation.core.spring\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.gestures.DraggableState\nimport androidx.compose.foundation.gestures.detectVerticalDragGestures\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.input.nestedscroll.NestedScrollConnection\nimport androidx.compose.ui.input.nestedscroll.NestedScrollSource\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.input.pointer.util.VelocityTracker\nimport androidx.compose.ui.input.pointer.util.addPointerInputChange\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.Velocity\nimport androidx.compose.ui.unit.dp\nimport com.metrolist.music.constants.NavigationBarAnimationSpec\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.launch\nimport kotlin.math.pow\n\n/**\n * Bottom Sheet\n * Modified from [ViMusic](https://github.com/vfsfitvnm/ViMusic)\n */\n@Composable\nfun BottomSheet(\n    state: BottomSheetState,\n    modifier: Modifier = Modifier,\n    background: @Composable (BoxScope.() -> Unit) = { },\n    onDismiss: (() -> Unit)? = null,\n    collapsedContent: @Composable BoxScope.() -> Unit,\n    isExpandable: Boolean = true,\n    content: @Composable BoxScope.() -> Unit,\n) {\n    val density = LocalDensity.current\n    \n    Box(\n        modifier = modifier\n            .graphicsLayer {\n                // background fades during about 10%-61% progress\n                alpha = (1.4f * (state.progress.coerceAtLeast(0.1f) - 0.1f).pow(0.5f)).coerceIn(0f, 1f)\n            }\n            .fillMaxSize(),\n        content = background\n    )\n    Box(\n        modifier = modifier\n            .fillMaxSize()\n            // Use graphicsLayer for offset to ensure hardware acceleration and 120Hz support\n            .graphicsLayer {\n                val y = (state.expandedBound - state.value)\n                    .toPx()\n                    .coerceAtLeast(0f)\n                translationY = y\n            }\n            .pointerInput(state, isExpandable) {\n                if (!isExpandable) return@pointerInput\n                val velocityTracker = VelocityTracker()\n\n                detectVerticalDragGestures(\n                    onVerticalDrag = { change, dragAmount ->\n                        velocityTracker.addPointerInputChange(change)\n                        state.dispatchRawDelta(dragAmount)\n                    },\n                    onDragCancel = {\n                        velocityTracker.resetTracking()\n                        state.snapTo(state.collapsedBound)\n                    },\n                    onDragEnd = {\n                        val velocity = -velocityTracker.calculateVelocity().y\n                        velocityTracker.resetTracking()\n                        state.performFling(velocity, onDismiss)\n                    }\n                )\n            }\n            .graphicsLayer {\n                val cornerRadius = if (!state.isExpanded) 16.dp.toPx() else 0f\n                shape = RoundedCornerShape(topStart = cornerRadius, topEnd = cornerRadius)\n                clip = true\n            }\n    ) {\n        if (!state.isCollapsed && !state.isDismissed) {\n            BackHandler(onBack = state::collapseSoft)\n        }\n\n        // main content\n        if (!state.isCollapsed) {\n            BoxWithConstraints(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .graphicsLayer {\n                        alpha = ((state.progress - 0.15f) * 4).coerceIn(0f, 1f)\n                    },\n                content = content\n            )\n        }\n\n        if (!state.isExpanded && (onDismiss == null || !state.isDismissed)) {\n            Box(\n                modifier =\n                Modifier\n                    .graphicsLayer {\n                        alpha = 1f - (state.progress * 4).coerceAtMost(1f)\n                    }.clickable(\n                        interactionSource = remember { MutableInteractionSource() },\n                        indication = null,\n                        onClick = { if (isExpandable) state.expandSoft() },\n                    ).fillMaxWidth()\n                    .height(state.collapsedBound),\n                content = collapsedContent,\n            )\n        }\n    }\n}\n\n@Stable\nclass BottomSheetState(\n    draggableState: DraggableState,\n    private val coroutineScope: CoroutineScope,\n    private val animatable: Animatable<Dp, AnimationVector1D>,\n    private val onAnchorChanged: (Int) -> Unit,\n    val collapsedBound: Dp,\n) : DraggableState by draggableState {\n    val dismissedBound: Dp\n        get() = animatable.lowerBound!!\n\n    val expandedBound: Dp\n        get() = animatable.upperBound!!\n\n    val value by animatable.asState()\n\n    val isDismissed by derivedStateOf {\n        value == animatable.lowerBound!!\n    }\n\n    val isCollapsed by derivedStateOf {\n        value == collapsedBound\n    }\n\n    val isExpanded by derivedStateOf {\n        value == animatable.upperBound\n    }\n\n    val progress by derivedStateOf {\n        1f - (animatable.upperBound!! - animatable.value) / (animatable.upperBound!! - collapsedBound)\n    }\n\n    fun collapse(animationSpec: AnimationSpec<Dp>) {\n        onAnchorChanged(collapsedAnchor)\n        coroutineScope.launch {\n            animatable.animateTo(collapsedBound, animationSpec)\n        }\n    }\n\n    fun expand(animationSpec: AnimationSpec<Dp>) {\n        onAnchorChanged(expandedAnchor)\n        coroutineScope.launch {\n            animatable.animateTo(animatable.upperBound!!, animationSpec)\n        }\n    }\n\n    private fun collapse() {\n        collapse(SpringSpec())\n    }\n\n    private fun expand() {\n        expand(SpringSpec())\n    }\n\n    fun collapseSoft() {\n        collapse(spring(stiffness = Spring.StiffnessMediumLow))\n    }\n\n    fun expandSoft() {\n        expand(spring(stiffness = Spring.StiffnessMediumLow))\n    }\n\n    fun dismiss() {\n        onAnchorChanged(dismissedAnchor)\n        coroutineScope.launch {\n            animatable.animateTo(animatable.lowerBound!!)\n        }\n    }\n    \n    suspend fun dismissAndWait() {\n        onAnchorChanged(dismissedAnchor)\n        animatable.animateTo(animatable.lowerBound!!)\n    }\n\n    fun snapTo(value: Dp) {\n        coroutineScope.launch {\n            animatable.snapTo(value)\n        }\n    }\n\n    fun performFling(velocity: Float, onDismiss: (() -> Unit)?) {\n        if (velocity > 250) {\n            expand()\n        } else if (velocity < -250) {\n            if (value < collapsedBound && onDismiss != null) {\n                dismiss()\n                onDismiss.invoke()\n            } else {\n                collapse()\n            }\n        } else {\n            val l0 = dismissedBound\n            val l1 = (collapsedBound - dismissedBound) / 2\n            val l2 = (expandedBound - collapsedBound) / 2\n            val l3 = expandedBound\n\n            when (value) {\n                in l0..l1 -> {\n                    if (onDismiss != null) {\n                        dismiss()\n                        onDismiss.invoke()\n                    } else {\n                        collapse()\n                    }\n                }\n\n                in l1..l2 -> collapse()\n                in l2..l3 -> expand()\n                else -> Unit\n            }\n        }\n    }\n\n    val preUpPostDownNestedScrollConnection\n        get() = object : NestedScrollConnection {\n            var isTopReached = false\n\n            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {\n                if (isExpanded && available.y < 0) {\n                    isTopReached = false\n                }\n\n                return if (isTopReached && available.y < 0 && source == NestedScrollSource.UserInput) {\n                    dispatchRawDelta(available.y)\n                    available\n                } else {\n                    Offset.Zero\n                }\n            }\n\n            override fun onPostScroll(\n                consumed: Offset,\n                available: Offset,\n                source: NestedScrollSource,\n            ): Offset {\n                if (!isTopReached) {\n                    isTopReached = consumed.y == 0f && available.y > 0\n                }\n\n                return if (isTopReached && source == NestedScrollSource.UserInput) {\n                    dispatchRawDelta(available.y)\n                    available\n                } else {\n                    Offset.Zero\n                }\n            }\n\n            override suspend fun onPreFling(available: Velocity): Velocity {\n                return if (isTopReached) {\n                    val velocity = -available.y\n                    performFling(velocity, null)\n\n                    available\n                } else {\n                    Velocity.Zero\n                }\n            }\n\n            override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {\n                isTopReached = false\n                return Velocity.Zero\n            }\n        }\n}\n\nconst val expandedAnchor = 2\nconst val collapsedAnchor = 1\nconst val dismissedAnchor = 0\n\n@Composable\nfun rememberBottomSheetState(\n    dismissedBound: Dp,\n    expandedBound: Dp,\n    collapsedBound: Dp = dismissedBound,\n    initialAnchor: Int = dismissedAnchor,\n): BottomSheetState {\n    val density = LocalDensity.current\n    val coroutineScope = rememberCoroutineScope()\n\n    var previousAnchor by rememberSaveable {\n        mutableIntStateOf(initialAnchor)\n    }\n    val animatable = remember {\n        Animatable(0.dp, Dp.VectorConverter)\n    }\n\n    return remember(dismissedBound, expandedBound, collapsedBound, coroutineScope) {\n        val initialValue = when (previousAnchor) {\n            expandedAnchor -> expandedBound\n            collapsedAnchor -> collapsedBound\n            dismissedAnchor -> dismissedBound\n            else -> error(\"Unknown BottomSheet anchor\")\n        }\n\n        animatable.updateBounds(dismissedBound.coerceAtMost(expandedBound), expandedBound)\n        coroutineScope.launch {\n            animatable.animateTo(initialValue, NavigationBarAnimationSpec)\n        }\n\n        BottomSheetState(\n            draggableState = DraggableState { delta ->\n                coroutineScope.launch {\n                    animatable.snapTo(animatable.value - with(density) { delta.toDp() })\n                }\n            },\n            onAnchorChanged = { previousAnchor = it },\n            coroutineScope = coroutineScope,\n            animatable = animatable,\n            collapsedBound = collapsedBound\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/BottomSheetMenu.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.ColumnScope\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.BottomSheetDefaults\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.ModalBottomSheetDefaults\nimport androidx.compose.material3.ModalBottomSheetProperties\nimport androidx.compose.material3.SheetState\nimport androidx.compose.material3.contentColorFor\nimport androidx.compose.material3.rememberModalBottomSheetState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.platform.LocalFocusManager\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\nval LocalMenuState = compositionLocalOf { MenuState() }\n\n@Stable\nclass MenuState(\n    isVisible: Boolean = false,\n    content: @Composable ColumnScope.() -> Unit = {},\n) {\n    var isVisible by mutableStateOf(isVisible)\n    var content by mutableStateOf(content)\n\n    fun show(content: @Composable ColumnScope.() -> Unit) {\n        isVisible = true\n        this.content = content\n    }\n\n    fun dismiss() {\n        isVisible = false\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun AnimatedBottomSheet(\n    isVisible: Boolean,\n    onDismissRequest: () -> Unit,\n    modifier: Modifier = Modifier,\n    sheetState: SheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false),\n    sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth,\n    shape: Shape = BottomSheetDefaults.ExpandedShape,\n    containerColor: Color = BottomSheetDefaults.ContainerColor,\n    contentColor: Color = contentColorFor(containerColor),\n    tonalElevation: Dp = 0.dp,\n    scrimColor: Color = BottomSheetDefaults.ScrimColor,\n    dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },\n    contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.modalWindowInsets },\n    properties: ModalBottomSheetProperties = ModalBottomSheetDefaults.properties,\n    content: @Composable ColumnScope.() -> Unit,\n) {\n    var lastContent by remember { mutableStateOf(content) }\n\n    LaunchedEffect(content) {\n        if (isVisible) {\n            lastContent = content\n        }\n    }\n\n    LaunchedEffect(isVisible) {\n        if (isVisible) {\n            sheetState.show()\n        } else {\n            sheetState.hide()\n        }\n    }\n\n    if (!sheetState.isVisible && !isVisible) {\n        return\n    }\n\n    ModalBottomSheet(\n        onDismissRequest = onDismissRequest,\n        modifier = modifier,\n        sheetState = sheetState,\n        sheetMaxWidth = sheetMaxWidth,\n        shape = shape,\n        containerColor = containerColor,\n        contentColor = contentColor,\n        tonalElevation = tonalElevation,\n        scrimColor = scrimColor,\n        dragHandle = dragHandle,\n        contentWindowInsets = contentWindowInsets,\n        properties = properties,\n        content = lastContent,\n    )\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun BottomSheetMenu(\n    modifier: Modifier = Modifier,\n    state: MenuState,\n    background: Color = MaterialTheme.colorScheme.surface,\n) {\n    val focusManager = LocalFocusManager.current\n    val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)\n\n    AnimatedBottomSheet(\n        isVisible = state.isVisible,\n        onDismissRequest = {\n            focusManager.clearFocus()\n            state.isVisible = false\n        },\n        sheetState = sheetState,\n        containerColor = background,\n        contentColor = MaterialTheme.colorScheme.onSurface,\n        dragHandle = {\n            Box(\n                modifier = Modifier\n                    .padding(vertical = 12.dp)\n                    .size(width = 40.dp, height = 4.dp)\n                    .clip(RoundedCornerShape(2.dp))\n                    .background(MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f))\n            )\n        },\n        modifier = modifier.fillMaxHeight()\n    ) {\n        Column(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 20.dp)\n        ) {\n            state.content(this)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/BottomSheetPage.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.ColumnScope\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.NavigationBarDefaults\nimport androidx.compose.material3.rememberModalBottomSheetState\nimport androidx.compose.material3.surfaceColorAtElevation\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalFocusManager\nimport androidx.compose.ui.unit.dp\n\nval LocalBottomSheetPageState = compositionLocalOf { BottomSheetPageState() }\n\n@Stable\nclass BottomSheetPageState(\n    isVisible: Boolean = false,\n    content: @Composable ColumnScope.() -> Unit = {},\n) {\n    var isVisible by mutableStateOf(isVisible)\n    var content by mutableStateOf(content)\n\n    fun show(content: @Composable ColumnScope.() -> Unit) {\n        isVisible = true\n        this.content = content\n    }\n\n    fun dismiss() {\n        isVisible = false\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun BottomSheetPage(\n    modifier: Modifier = Modifier,\n    state: BottomSheetPageState,\n    background: Color = MaterialTheme.colorScheme.surfaceColorAtElevation(NavigationBarDefaults.Elevation),\n) {\n    val focusManager = LocalFocusManager.current\n    val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)\n\n    AnimatedBottomSheet(\n        isVisible = state.isVisible,\n        onDismissRequest = {\n            focusManager.clearFocus()\n            state.isVisible = false\n        },\n        sheetState = sheetState,\n        containerColor = background,\n        contentColor = MaterialTheme.colorScheme.onSurface,\n        dragHandle = {\n            Box(\n                modifier = Modifier\n                    .padding(vertical = 12.dp)\n                    .size(width = 32.dp, height = 4.dp)\n                    .clip(RoundedCornerShape(2.dp))\n                    .background(MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f))\n            )\n        },\n        modifier = modifier.fillMaxHeight()\n    ) {\n        Column(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 16.dp)\n                .padding(bottom = 16.dp)\n        ) {\n            state.content(this)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/ChipsRow.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport android.annotation.SuppressLint\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.expandIn\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.shrinkOut\nimport androidx.compose.animation.slideInHorizontally\nimport androidx.compose.animation.slideOutHorizontally\nimport androidx.compose.animation.togetherWith\nimport androidx.compose.foundation.horizontalScroll\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.AssistChip\nimport androidx.compose.material3.AssistChipDefaults\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.FilterChip\nimport androidx.compose.material3.FilterChipDefaults\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport com.metrolist.music.R\nimport com.metrolist.music.ui.screens.OptionStats\n\n@Composable\nfun <E> ChipsRow(\n    chips: List<Pair<E, String>>,\n    currentValue: E,\n    onValueUpdate: (E) -> Unit,\n    modifier: Modifier = Modifier,\n    containerColor: Color = MaterialTheme.colorScheme.surfaceContainer,\n) {\n    Row(\n        modifier =\n        modifier\n            .fillMaxWidth()\n            .horizontalScroll(rememberScrollState())\n            .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)),\n    ) {\n        Spacer(Modifier.width(12.dp))\n\n        chips.forEach { (value, label) ->\n            FilterChip(\n                label = { Text(label) },\n                selected = currentValue == value,\n                colors = FilterChipDefaults.filterChipColors(\n                    containerColor = containerColor,\n                ),\n                onClick = { onValueUpdate(value) },\n                shape = RoundedCornerShape(16.dp),\n                border = null\n            )\n\n            Spacer(Modifier.width(8.dp))\n        }\n    }\n}\n\n@SuppressLint(\"UnusedContentLambdaTargetStateParameter\")\n@Composable\nfun <Int> ChoiceChipsRow(\n    chips: List<Pair<Int, String>>,\n    options: List<Pair<OptionStats, String>>,\n    selectedOption: OptionStats,\n    onSelectionChange: (OptionStats) -> Unit,\n    currentValue: Int,\n    onValueUpdate: (Int) -> Unit,\n    modifier: Modifier = Modifier,\n    containerColor: Color = MaterialTheme.colorScheme.surfaceContainer,\n) {\n    var expandIconDegree by remember { mutableFloatStateOf(0f) }\n    val rotationAnimation by animateFloatAsState(\n        targetValue = expandIconDegree,\n        animationSpec = tween(durationMillis = 400),\n        label = \"\",\n    )\n\n    Row(\n        modifier =\n        modifier\n            .fillMaxWidth()\n            .padding(start = 12.dp)\n            .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)),\n    ) {\n        var expanded by remember { mutableStateOf(false) }\n\n        Column {\n            AssistChip(\n                onClick = {\n                    expanded = !expanded\n                    expandIconDegree -= 180\n                },\n                label = {\n                    Text(\n                        text =\n                        when (selectedOption) {\n                            OptionStats.WEEKS -> stringResource(id = R.string.weeks)\n                            OptionStats.MONTHS -> stringResource(id = R.string.months)\n                            OptionStats.YEARS -> stringResource(id = R.string.years)\n                            OptionStats.CONTINUOUS -> stringResource(id = R.string.continuous)\n                        },\n                    )\n                },\n                trailingIcon = {\n                    Icon(\n                        painter = painterResource(R.drawable.expand_more),\n                        contentDescription = null,\n                        modifier = Modifier.graphicsLayer(rotationZ = rotationAnimation),\n                    )\n                },\n                shape = RoundedCornerShape(16.dp),\n                border = null,\n                colors = AssistChipDefaults.assistChipColors(\n                    containerColor = containerColor,\n                    labelColor = MaterialTheme.colorScheme.onSurface\n                )\n            )\n\n            AnimatedVisibility(\n                visible = expanded,\n                enter = expandIn() + fadeIn(),\n                exit = shrinkOut() + fadeOut(),\n            ) {\n                DropdownMenu(\n                    modifier = Modifier.padding(start = 12.dp),\n                    expanded = expanded,\n                    onDismissRequest = {\n                        expanded = false\n                        expandIconDegree -= 180\n                    },\n                ) {\n                    options.forEach { option ->\n                        DropdownMenuItem(\n                            text = { Text(text = option.second) },\n                            onClick = {\n                                onSelectionChange(option.first)\n                                expandIconDegree -= 180\n                                expanded = false\n                            },\n                        )\n                    }\n                }\n            }\n        }\n\n        AnimatedContent(\n            targetState = selectedOption,\n            transitionSpec = { slideInHorizontally() + fadeIn() togetherWith slideOutHorizontally() + fadeOut() },\n            label = \"\",\n        ) {\n            Row(\n                modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .horizontalScroll(rememberScrollState())\n                    .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)),\n            ) {\n                chips.forEach { (value, label) ->\n                    Spacer(Modifier.width(8.dp))\n\n                    FilterChip(\n                        label = { Text(label) },\n                        selected = currentValue == value,\n                        colors = FilterChipDefaults.filterChipColors(\n                            containerColor = containerColor,\n                        ),\n                        onClick = { onValueUpdate(value) },\n                        shape = RoundedCornerShape(16.dp),\n                        border = null\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/CreatePlaylistDialog.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport android.widget.Toast\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.unit.dp\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.InnerTubeCookieKey\nimport com.metrolist.music.db.entities.PlaylistEntity\nimport com.metrolist.music.extensions.isSyncEnabled\nimport com.metrolist.music.utils.rememberPreference\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport java.time.LocalDateTime\nimport java.util.logging.Logger\n\n@Composable\nfun CreatePlaylistDialog(\n    onDismiss: () -> Unit,\n    initialTextFieldValue: String? = null,\n    allowSyncing: Boolean = true,\n    onPlaylistCreated: ((String) -> Unit)? = null,\n) {\n    val database = LocalDatabase.current\n    val coroutineScope = rememberCoroutineScope()\n    var syncedPlaylist by remember { mutableStateOf(false) }\n    val context = LocalContext.current\n\n    val innerTubeCookie by rememberPreference(InnerTubeCookieKey, \"\")\n    val isSignedIn = innerTubeCookie.isNotEmpty()\n\n    val notLoggedInYoutubeStr = stringResource(R.string.not_logged_in_youtube)\n    val syncDisabledStr = stringResource(R.string.sync_disabled)\n\n    TextFieldDialog(\n        icon = { Icon(painter = painterResource(R.drawable.add), contentDescription = null) },\n        title = { Text(text = stringResource(R.string.create_playlist)) },\n        initialTextFieldValue = TextFieldValue(initialTextFieldValue ?: \"\"),\n        onDismiss = onDismiss,\n        onDone = { playlistName ->\n            coroutineScope.launch(Dispatchers.IO) {\n                val browseId =\n                    if (syncedPlaylist && isSignedIn) {\n                        YouTube.createPlaylist(playlistName)\n                    } else if (syncedPlaylist) {\n                        Logger.getLogger(\"CreatePlaylistDialog\").warning(\"Not signed in\")\n                        return@launch\n                    } else {\n                        null\n                    }\n\n                val playlistEntity =\n                    PlaylistEntity(\n                        name = playlistName,\n                        browseId = browseId,\n                        bookmarkedAt = LocalDateTime.now(),\n                        isEditable = true,\n                    )\n\n                database.query {\n                    insert(playlistEntity)\n                }\n\n                withContext(Dispatchers.Main) {\n                    onPlaylistCreated?.invoke(playlistEntity.id)\n                }\n            }\n        },\n        extraContent = {\n            if (allowSyncing) {\n                Row(\n                    modifier = Modifier.padding(vertical = 16.dp, horizontal = 40.dp),\n                ) {\n                    Column {\n                        Text(\n                            text = stringResource(R.string.sync_playlist),\n                            style = MaterialTheme.typography.titleLarge,\n                        )\n                        Text(\n                            text = stringResource(R.string.allows_for_sync_witch_youtube),\n                            style = MaterialTheme.typography.bodySmall,\n                            modifier = Modifier.fillMaxWidth(0.7f),\n                        )\n                    }\n                    Row(\n                        modifier = Modifier.weight(1f),\n                        horizontalArrangement = Arrangement.End,\n                    ) {\n                        Switch(\n                            checked = syncedPlaylist,\n                            onCheckedChange = {\n                                val isYtmSyncEnabled = context.isSyncEnabled()\n                                if (!isSignedIn && !syncedPlaylist) {\n                                    Toast\n                                        .makeText(\n                                            context,\n                                            notLoggedInYoutubeStr,\n                                            Toast.LENGTH_SHORT,\n                                        ).show()\n                                } else if (!isYtmSyncEnabled) {\n                                    Toast\n                                        .makeText(\n                                            context,\n                                            syncDisabledStr,\n                                            Toast.LENGTH_SHORT,\n                                        ).show()\n                                } else {\n                                    syncedPlaylist = !syncedPlaylist\n                                }\n                            },\n                        )\n                    }\n                }\n            }\n        },\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/Dialog.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.ColumnScope\nimport androidx.compose.foundation.layout.FlowRow\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.imePadding\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyListScope\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material3.AlertDialogDefaults\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextFieldDefaults\nimport androidx.compose.material3.ProvideTextStyle\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TextField\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Dialog\nimport androidx.compose.ui.window.DialogProperties\nimport androidx.navigation.NavController\nimport com.metrolist.music.R\nimport com.metrolist.music.ui.screens.settings.AccountSettings\nimport kotlinx.coroutines.delay\n\n@Composable\nfun DefaultDialog(\n    onDismiss: () -> Unit,\n    modifier: Modifier = Modifier,\n    icon: (@Composable () -> Unit)? = null,\n    title: (@Composable () -> Unit)? = null,\n    buttons: (@Composable RowScope.() -> Unit)? = null,\n    horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,\n    content: @Composable ColumnScope.() -> Unit,\n) {\n    Dialog(\n        onDismissRequest = onDismiss,\n        properties = DialogProperties(usePlatformDefaultWidth = false)\n    ) {\n        Surface(\n            modifier = Modifier.padding(24.dp),\n            shape = AlertDialogDefaults.shape,\n            color = AlertDialogDefaults.containerColor,\n            tonalElevation = AlertDialogDefaults.TonalElevation\n        ) {\n            Column(\n                horizontalAlignment = horizontalAlignment,\n                modifier = modifier\n                    .padding(24.dp)\n            ) {\n                if (icon != null) {\n                    CompositionLocalProvider(LocalContentColor provides AlertDialogDefaults.iconContentColor) {\n                        Box(\n                            Modifier.align(Alignment.CenterHorizontally)\n                        ) {\n                            icon()\n                        }\n                    }\n\n                    Spacer(Modifier.height(16.dp))\n                }\n                if (title != null) {\n                    CompositionLocalProvider(LocalContentColor provides AlertDialogDefaults.titleContentColor) {\n                        ProvideTextStyle(MaterialTheme.typography.headlineSmall) {\n                            Box(\n                                // Align the title to the center when an icon is present.\n                                Modifier.align(if (icon == null) Alignment.Start else Alignment.CenterHorizontally)\n                            ) {\n                                title()\n                            }\n                        }\n                    }\n\n                    Spacer(Modifier.height(16.dp))\n                }\n\n                content()\n\n                if (buttons != null) {\n                    Spacer(Modifier.height(24.dp))\n\n                    FlowRow(\n                        modifier = Modifier.align(Alignment.End)\n                    ) {\n                        CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) {\n                            ProvideTextStyle(\n                                value = MaterialTheme.typography.labelLarge\n                            ) {\n                                buttons()\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun AccountSettingsDialog(\n    navController: NavController,\n    onDismiss: () -> Unit,\n    latestVersionName: String\n) {\n    Dialog(\n        onDismissRequest = onDismiss,\n        properties = DialogProperties(\n            usePlatformDefaultWidth = false,\n            dismissOnClickOutside = true\n        )\n    ) {\n        Box(\n            modifier = Modifier\n                .fillMaxSize()\n                .clickable(\n                    indication = null,\n                    interactionSource = remember { MutableInteractionSource() }\n                ) {\n                    onDismiss()\n                },\n            contentAlignment = Alignment.TopCenter\n        ) {\n            Surface(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(top = 72.dp, start = 16.dp, end = 16.dp)\n                    .clip(RoundedCornerShape(28.dp)),\n                shape = MaterialTheme.shapes.large,\n                color = MaterialTheme.colorScheme.surface,\n                tonalElevation = 8.dp\n            ) {\n                AccountSettings(\n                    navController = navController,\n                    onClose = onDismiss,\n                    latestVersionName = latestVersionName\n                )\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun ActionPromptDialog(\n    title: String? = null,\n    titleBar: @Composable (RowScope.() -> Unit)? = null,\n    onDismiss: () -> Unit,\n    onConfirm: () -> Unit,\n    onReset: (() -> Unit)? = null,\n    onCancel: (() -> Unit)? = null,\n    content: @Composable ColumnScope.() -> Unit = {}\n) {\n    DefaultDialog(\n        onDismiss = onDismiss,\n        title = if (titleBar != null) {\n            { Row { titleBar() } }\n        } else if (title != null) {\n            {\n                Text(\n                    text = title,\n                    overflow = TextOverflow.Ellipsis,\n                    maxLines = 1,\n                    style = MaterialTheme.typography.headlineSmall,\n                )\n            }\n        } else null,\n        buttons = {\n            if (onReset != null) {\n                Row(modifier = Modifier.weight(1f)) {\n                    TextButton(\n                        onClick = { onReset() },\n                    ) {\n                        Text(stringResource(R.string.reset))\n                    }\n                }\n            }\n\n            if (onCancel != null) {\n                TextButton(\n                    onClick = { onCancel() }\n                ) {\n                    Text(stringResource(android.R.string.cancel))\n                }\n            }\n\n            TextButton(\n                onClick = { onConfirm() }\n            ) {\n                Text(stringResource(android.R.string.ok))\n            }\n        }\n    ) {\n        content()\n    }\n}\n\n@Composable\nfun ListDialog(\n    onDismiss: () -> Unit,\n    modifier: Modifier = Modifier,\n    content: LazyListScope.() -> Unit,\n) {\n    Dialog(\n        onDismissRequest = onDismiss,\n        properties = DialogProperties(usePlatformDefaultWidth = false),\n    ) {\n        Surface(\n            modifier = Modifier.padding(24.dp),\n            shape = AlertDialogDefaults.shape,\n            color = AlertDialogDefaults.containerColor,\n            tonalElevation = AlertDialogDefaults.TonalElevation,\n        ) {\n            Column(\n                horizontalAlignment = Alignment.CenterHorizontally,\n                modifier = modifier\n                    .padding(vertical = 24.dp)\n                    .imePadding(),\n            ) {\n                LazyColumn(content = content)\n            }\n        }\n    }\n}\n\n@Composable\nfun InfoLabel(\n    text: String\n) = Row(\n    verticalAlignment = Alignment.CenterVertically,\n    modifier = Modifier.padding(horizontal = 8.dp)\n) {\n    Icon(\n        painter = painterResource(id = R.drawable.info),\n        contentDescription = null,\n        tint = MaterialTheme.colorScheme.secondary,\n        modifier = Modifier.padding(4.dp)\n    )\n    Text(\n        text = text,\n        style = MaterialTheme.typography.bodySmall,\n        modifier = Modifier.padding(horizontal = 4.dp)\n    )\n}\n\n@Composable\nfun TextFieldDialog(\n    modifier: Modifier = Modifier,\n    icon: (@Composable () -> Unit)? = null,\n    title: (@Composable () -> Unit)? = null,\n    initialTextFieldValue: TextFieldValue = TextFieldValue(),\n    placeholder: @Composable (() -> Unit)? = null,\n    singleLine: Boolean = true,\n    autoFocus: Boolean = true,\n    maxLines: Int = if (singleLine) 1 else 10,\n    isInputValid: (String) -> Boolean = { it.isNotEmpty() },\n    keyboardType: KeyboardType = KeyboardType.Text,\n    onDone: (String) -> Unit = {},\n\n    // new multi-field support\n    textFields: List<Pair<String, TextFieldValue>>? = null,\n    onTextFieldsChange: ((Int, TextFieldValue) -> Unit)? = null,\n    onDoneMultiple: ((List<String>) -> Unit)? = null,\n\n    onDismiss: () -> Unit,\n    autoDismiss: Boolean = true,\n    extraContent: (@Composable () -> Unit)? = null,\n) {\n    val legacyFieldState = remember { mutableStateOf(initialTextFieldValue) }\n\n    val focusRequester = remember { FocusRequester() }\n\n    LaunchedEffect(Unit) {\n        if (autoFocus) {\n            delay(300)\n            focusRequester.requestFocus()\n        }\n    }\n\n    DefaultDialog(\n        onDismiss = onDismiss,\n        modifier = modifier,\n        icon = icon,\n        title = title,\n        buttons = {\n            TextButton(onClick = onDismiss) {\n                Text(text = stringResource(android.R.string.cancel))\n            }\n\n            val isValid = textFields?.all { isInputValid(it.second.text) }\n                ?: isInputValid(legacyFieldState.value.text)\n\n            TextButton(\n                enabled = isValid,\n                onClick = {\n                    if (autoDismiss) onDismiss()\n                    if (textFields != null && onDoneMultiple != null) {\n                        onDoneMultiple(textFields.map { it.second.text })\n                    } else {\n                        onDone(legacyFieldState.value.text)\n                    }\n                }\n            ) {\n                Text(text = stringResource(android.R.string.ok))\n            }\n        }\n    ) {\n        Column(\n            modifier = Modifier.weight(weight = 1f, fill = false)\n        ) {\n            if (textFields != null) {\n                textFields.forEachIndexed { index, (label, value) ->\n                    TextField(\n                        value = value,\n                        onValueChange = { onTextFieldsChange?.invoke(index, it) },\n                        placeholder = { Text(label) },\n                        singleLine = singleLine,\n                        maxLines = maxLines,\n                        colors = OutlinedTextFieldDefaults.colors(),\n                        keyboardOptions = KeyboardOptions(\n                            imeAction = if (singleLine) ImeAction.Done else ImeAction.None,\n                            keyboardType = keyboardType\n                        ),\n                    keyboardActions = KeyboardActions(\n                        onDone = {\n                            if (onDoneMultiple != null) {\n                                onDoneMultiple(textFields.map { it.second.text })\n                                if (autoDismiss) onDismiss()\n                            }\n                        }\n                    ),\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(bottom = if (index < textFields.size - 1) 12.dp else 0.dp)\n                            .then(if (index == 0) Modifier.focusRequester(focusRequester) else Modifier)\n                    )\n                }\n            } else {\n                TextField(\n                    value = legacyFieldState.value,\n                    onValueChange = { legacyFieldState.value = it },\n                    placeholder = placeholder,\n                    singleLine = singleLine,\n                    maxLines = maxLines,\n                    colors = OutlinedTextFieldDefaults.colors(),\n                    keyboardOptions = KeyboardOptions(\n                        imeAction = if (singleLine) ImeAction.Done else ImeAction.None,\n                        keyboardType = keyboardType\n                    ),\n                    keyboardActions = KeyboardActions(\n                        onDone = {\n                            onDone(legacyFieldState.value.text)\n                            if (autoDismiss) onDismiss()\n                        }\n                    ),\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .focusRequester(focusRequester)\n                )\n            }\n\n            extraContent?.invoke()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/DraggableLyricsProviderList.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.res.painterResource\nimport com.metrolist.music.R\nimport sh.calvin.reorderable.ReorderableItem\nimport sh.calvin.reorderable.rememberReorderableLazyListState\n\ndata class DraggableLyricsProviderItem(\n    val id: String,\n    val name: String,\n    val icon: Painter,\n)\n\n@Composable\nfun DraggableLyricsProviderList(\n    items: MutableList<DraggableLyricsProviderItem>,\n    onItemsReordered: (List<DraggableLyricsProviderItem>) -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    val lazyListState = rememberLazyListState()\n    var hasDragged by remember { mutableStateOf(false) }\n    val reorderableState = rememberReorderableLazyListState(\n        lazyListState = lazyListState,\n    ) { from, to ->\n        val movedItem = items.removeAt(from.index)\n        items.add(to.index, movedItem)\n        hasDragged = true\n    }\n\n    LaunchedEffect(reorderableState.isAnyItemDragging) {\n        if (!reorderableState.isAnyItemDragging && hasDragged) {\n            onItemsReordered(items.toList())\n            hasDragged = false\n        }\n    }\n\n    LazyColumn(\n        state = lazyListState,\n        modifier = modifier,\n    ) {\n        itemsIndexed(\n            items,\n            key = { _, item -> item.id }\n        ) { _, item ->\n            ReorderableItem(\n                state = reorderableState,\n                key = item.id,\n            ) {\n                Surface(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(vertical = 4.dp),\n                    shape = RoundedCornerShape(8.dp),\n                    color = MaterialTheme.colorScheme.surfaceContainer,\n                ) {\n                    Row(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(12.dp),\n                        horizontalArrangement = Arrangement.spacedBy(12.dp),\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        IconButton(\n                            onClick = { },\n                            modifier = Modifier.draggableHandle(),\n                        ) {\n                            Icon(\n                                painter = painterResource(R.drawable.drag_handle),\n                                contentDescription = null,\n                                tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                            )\n                        }\n\n                        Text(\n                            text = item.name,\n                            style = MaterialTheme.typography.bodyLarge,\n                            overflow = TextOverflow.Ellipsis,\n                            modifier = Modifier.weight(1f),\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/DraggableScrollBarOverlay.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.spring\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.gestures.detectDragGestures\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.CornerRadius\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.launch\nimport kotlin.math.abs\nimport kotlin.math.max\n\n@Composable\nfun DraggableScrollbar(\n    scrollState: LazyListState,\n    modifier: Modifier = Modifier,\n    thumbColor: Color = LocalContentColor.current.copy(alpha = 0.8f),\n    thumbColorActive: Color = MaterialTheme.colorScheme.secondary,\n    thumbHeight: Dp = 72.dp,\n    thumbWidth: Dp = 8.dp,\n    thumbCornerRadius: Dp = 4.dp,\n    trackWidth: Dp = 24.dp,\n    minItemCountForScroll: Int = 15,\n    minScrollRangeForDrag: Int = 5,\n    headerItems: Int = 0\n) {\n    val density = LocalDensity.current\n    val coroutineScope = rememberCoroutineScope()\n    var isDragging by remember { mutableStateOf(false) }\n    var lastScrollTime by remember { mutableLongStateOf(0L) }\n    var smoothedY by remember { mutableFloatStateOf(0f) }\n    var smoothedThumbY by remember { mutableFloatStateOf(0f) }\n    var lastThumbPosition by remember { mutableFloatStateOf(0f) }\n    val animatedThumbY = remember { Animatable(0f) }\n\n    val isUserScrolling by remember(scrollState) {\n        derivedStateOf { scrollState.isScrollInProgress }\n    }\n\n    val isScrollable by remember {\n        derivedStateOf {\n            val layoutInfo = scrollState.layoutInfo\n            val total = layoutInfo.totalItemsCount\n            val visible = layoutInfo.visibleItemsInfo.size\n            val contentCount = total - headerItems\n            contentCount > minItemCountForScroll && contentCount > visible\n        }\n    }\n\n    if (!isScrollable) return\n\n    var lastTargetIndex by remember { mutableIntStateOf(-1) }\n\n    BoxWithConstraints(\n        modifier = modifier\n            .width(trackWidth)\n            .fillMaxHeight()\n            .pointerInput(scrollState) {\n                detectDragGestures(\n                    onDragStart = { offset ->\n                        isDragging = true\n                        lastTargetIndex = -1\n                        val viewportHeight = size.height.toFloat()\n                        val constThumbHeight = with(density) { thumbHeight.toPx() }\n                        val maxThumbY = viewportHeight - constThumbHeight\n                        smoothedThumbY = (offset.y - constThumbHeight / 2).coerceIn(0f, maxThumbY)\n                    },\n                    onDragEnd = { \n                        isDragging = false\n                        lastScrollTime = 0L\n                    },\n                    onDragCancel = { \n                        isDragging = false \n                        lastScrollTime = 0L\n                    }\n                ) { change, _ ->\n                    val currentTime = System.currentTimeMillis()\n                    val viewportHeight = size.height.toFloat()\n                    val constThumbHeight = with(density) { thumbHeight.toPx() }\n                    val maxThumbY = viewportHeight - constThumbHeight\n                    \n                    val targetThumbY = (change.position.y - constThumbHeight / 2).coerceIn(0f, maxThumbY)\n                    \n                    val layoutInfo = scrollState.layoutInfo\n                    val totalContentItems = layoutInfo.totalItemsCount - headerItems\n                    \n                    val thumbSmoothingFactor = when {\n                        totalContentItems < 20 -> 0.1f\n                        totalContentItems < 50 -> 0.3f\n                        else -> 0.7f\n                    }\n                    \n                    smoothedThumbY = smoothedThumbY * (1f - thumbSmoothingFactor) + targetThumbY * thumbSmoothingFactor\n                    \n                    if (currentTime - lastScrollTime < 40) return@detectDragGestures\n                    lastScrollTime = currentTime\n\n                    val visibleItems = layoutInfo.visibleItemsInfo\n                    if (visibleItems.isEmpty()) return@detectDragGestures\n\n                    val maxScrollIndex = max(1, totalContentItems - visibleItems.size)\n\n                    if (maxScrollIndex > minScrollRangeForDrag) {\n                        val touchProgress = (change.position.y / size.height).coerceIn(0f, 1f)\n                        \n                        val listSmoothingFactor = when {\n                            totalContentItems < 20 -> 0.15f\n                            totalContentItems < 50 -> 0.4f\n                            else -> 0.8f\n                        }\n                        \n                        smoothedY = smoothedY * (1f - listSmoothingFactor) + touchProgress * listSmoothingFactor\n                        \n                        val targetFractionalIndex = smoothedY * maxScrollIndex\n                        val targetIndex = (headerItems + targetFractionalIndex.toInt())\n                            .coerceIn(headerItems, layoutInfo.totalItemsCount - 1)\n\n                        if (abs(targetIndex - lastTargetIndex) >= 1) {\n                            lastTargetIndex = targetIndex\n                            coroutineScope.launch {\n                                try {\n                                    scrollState.animateScrollToItem(\n                                        index = targetIndex,\n                                        scrollOffset = 0\n                                    )\n                                } catch (e: Exception) {\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n    ) {\n        val viewportHeight = with(density) { this@BoxWithConstraints.maxHeight.toPx() }\n        val constThumbHeight = with(density) { thumbHeight.toPx() }\n\n        val targetThumbY by remember {\n            derivedStateOf {\n                val layoutInfo = scrollState.layoutInfo\n                val visibleItems = layoutInfo.visibleItemsInfo\n                if (visibleItems.isEmpty()) return@derivedStateOf lastThumbPosition\n\n                val totalContentItems = layoutInfo.totalItemsCount - headerItems\n                val maxScrollIndex = max(1, totalContentItems - visibleItems.size)\n                if (maxScrollIndex <= minScrollRangeForDrag) return@derivedStateOf lastThumbPosition\n\n                val rawIndex = (scrollState.firstVisibleItemIndex - headerItems).coerceAtLeast(0)\n\n                val scrollProgress = if (totalContentItems < 30) {\n\n                    val currentProgress = rawIndex.toFloat() / maxScrollIndex\n                    val smoothingFactor = 0.2f\n                    val previousProgress = lastThumbPosition / (viewportHeight - constThumbHeight)\n                    previousProgress * (1f - smoothingFactor) + currentProgress * smoothingFactor\n                } else {\n                    rawIndex.toFloat() / maxScrollIndex\n                }\n\n                val maxThumbY = viewportHeight - constThumbHeight\n                val newPosition = (scrollProgress * maxThumbY).coerceIn(0f, maxThumbY)\n\n                lastThumbPosition = newPosition\n                newPosition\n            }\n        }\n\n        LaunchedEffect(targetThumbY, isDragging, isUserScrolling, smoothedThumbY) {\n            val layoutInfo = scrollState.layoutInfo\n            val totalContentItems = layoutInfo.totalItemsCount - headerItems\n            \n            when {\n                isDragging -> {\n                    animatedThumbY.snapTo(smoothedThumbY)\n                }\n                isUserScrolling -> {\n                    if (totalContentItems < 30) {\n                        animatedThumbY.animateTo(\n                            targetValue = targetThumbY,\n                            animationSpec = spring(\n                                stiffness = 100f,\n                                dampingRatio = 1.2f\n                            )\n                        )\n                    } else {\n                        animatedThumbY.snapTo(targetThumbY)\n                    }\n                }\n                else -> {\n                    animatedThumbY.animateTo(\n                        targetValue = targetThumbY,\n                        animationSpec = spring(\n                            stiffness = if (totalContentItems < 30) 80f else 150f,\n                            dampingRatio = if (totalContentItems < 30) 1.5f else 0.9f\n                        )\n                    )\n                }\n            }\n        }\n\n        Canvas(\n            modifier = Modifier\n                .width(thumbWidth)\n                .fillMaxHeight()\n                .align(Alignment.CenterEnd)\n        ) {\n            val color = if (isDragging) thumbColorActive else thumbColor\n            val cornerRadiusPx = thumbCornerRadius.toPx()\n\n            drawRoundRect(\n                color = color,\n                topLeft = Offset(0f, animatedThumbY.value),\n                size = Size(this.size.width, constThumbHeight),\n                cornerRadius = CornerRadius(cornerRadiusPx)\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/EmptyPlaceholder.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.annotation.DrawableRes\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.ColorFilter\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun EmptyPlaceholder(\n    @DrawableRes icon: Int,\n    text: String,\n    modifier: Modifier = Modifier,\n) {\n    Column(\n        horizontalAlignment = Alignment.CenterHorizontally,\n        modifier =\n        modifier\n            .fillMaxSize()\n            .padding(12.dp),\n    ) {\n        Image(\n            painter = painterResource(icon),\n            contentDescription = null,\n            colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),\n            modifier = Modifier.size(64.dp),\n        )\n\n        Spacer(Modifier.height(12.dp))\n\n        Text(\n            text = text,\n            style = MaterialTheme.typography.bodyLarge,\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/EnumDialog.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.RadioButton\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun <T> EnumDialog(\n    onDismiss: () -> Unit,\n    onSelect: (T) -> Unit,\n    title: String,\n    current: T,\n    values: List<T>,\n    valueText: @Composable (T) -> String,\n    valueDescription: (@Composable (T) -> String)? = null,\n) {\n    ListDialog(\n        onDismiss = onDismiss,\n    ) {\n        items(values) { value ->\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .clickable {\n                        onSelect(value)\n                    }\n                    .padding(horizontal = 16.dp, vertical = 12.dp),\n            ) {\n                RadioButton(\n                    selected = value == current,\n                    onClick = null,\n                )\n\n                Column(\n                    modifier = Modifier.padding(start = 16.dp),\n                ) {\n                    Text(\n                        text = valueText(value),\n                    )\n                    if (valueDescription != null) {\n                        Text(\n                            text = valueDescription(value),\n                            style = MaterialTheme.typography.bodySmall,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/ExpandableText.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.animation.animateContentSize\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.text.ClickableText\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalUriHandler\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.text.withStyle\nimport androidx.compose.ui.unit.dp\nimport com.metrolist.music.R\n\ndata class LinkSegment(\n    val text: String,\n    val url: String? = null,\n)\n\n@Composable\nfun ExpandableText(\n    text: String = \"\",\n    runs: List<LinkSegment>? = null,\n    modifier: Modifier = Modifier,\n    collapsedMaxLines: Int = 3,\n) {\n    var isExpanded by rememberSaveable { mutableStateOf(false) }\n    var hasOverflow by rememberSaveable { mutableStateOf(false) }\n    val uriHandler = LocalUriHandler.current\n    val linkColor = MaterialTheme.colorScheme.primary\n    val bodyColor = MaterialTheme.colorScheme.onSurfaceVariant\n\n    val annotatedText: AnnotatedString = remember(text, runs, linkColor) {\n        if (runs.isNullOrEmpty()) {\n            AnnotatedString(text)\n        } else {\n            buildAnnotatedString {\n                runs.forEach { segment ->\n                    if (segment.url != null) {\n                        pushStringAnnotation(tag = \"URL\", annotation = segment.url)\n                        withStyle(SpanStyle(color = linkColor)) {\n                            append(segment.text)\n                        }\n                        pop()\n                    } else {\n                        append(segment.text)\n                    }\n                }\n            }\n        }\n    }\n\n    Column(\n        modifier = modifier.animateContentSize()\n    ) {\n        @Suppress(\"DEPRECATION\")\n        ClickableText(\n            text = annotatedText,\n            style = MaterialTheme.typography.bodyMedium.copy(color = bodyColor),\n            maxLines = if (isExpanded) Int.MAX_VALUE else collapsedMaxLines,\n            overflow = TextOverflow.Ellipsis,\n            onTextLayout = { textLayoutResult ->\n                hasOverflow = textLayoutResult.hasVisualOverflow || textLayoutResult.lineCount > collapsedMaxLines\n            },\n            onClick = { offset ->\n                annotatedText.getStringAnnotations(tag = \"URL\", start = offset, end = offset)\n                    .firstOrNull()?.let { annotation ->\n                        uriHandler.openUri(annotation.item)\n                        return@ClickableText\n                    }\n                if (hasOverflow) {\n                    isExpanded = !isExpanded\n                }\n            }\n        )\n        \n        if (hasOverflow) {\n            Text(\n                text = stringResource(if (isExpanded) R.string.show_less else R.string.show_more),\n                style = MaterialTheme.typography.labelMedium,\n                color = MaterialTheme.colorScheme.primary,\n                modifier = Modifier\n                    .padding(top = 4.dp)\n                    .clickable { isExpanded = !isExpanded }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/GridMenu.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.annotation.DrawableRes\nimport androidx.annotation.StringRes\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyGridScope\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ShapeDefaults\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.media3.exoplayer.offline.Download\nimport com.metrolist.music.R\nimport com.metrolist.music.utils.makeTimeString\n\nval GridMenuItemHeight = 108.dp\n\n@Composable\nfun GridMenu(\n    modifier: Modifier = Modifier,\n    contentPadding: PaddingValues = PaddingValues(0.dp),\n    content: LazyGridScope.() -> Unit,\n) {\n    LazyVerticalGrid(\n        columns = GridCells.Adaptive(minSize = 120.dp),\n        modifier = modifier,\n        contentPadding = contentPadding,\n        content = content\n    )\n}\n\nfun LazyGridScope.GridMenuItem(\n    modifier: Modifier = Modifier,\n    @DrawableRes icon: Int,\n    tint: @Composable () -> Color = { LocalContentColor.current },\n    @StringRes title: Int,\n    enabled: Boolean = true,\n    onClick: () -> Unit,\n) = GridMenuItem(\n    modifier = modifier,\n    icon = {\n        Icon(\n            painter = painterResource(icon),\n            tint = tint(),\n            contentDescription = null\n        )\n    },\n    title = title,\n    enabled = enabled,\n    onClick = onClick\n)\n\nfun LazyGridScope.GridMenuItem(\n    modifier: Modifier = Modifier,\n    icon: @Composable BoxScope.() -> Unit,\n    @StringRes title: Int,\n    enabled: Boolean = true,\n    onClick: () -> Unit,\n) {\n    item {\n        Column(\n            modifier = modifier\n                .clip(ShapeDefaults.Large)\n                .height(GridMenuItemHeight)\n                .clickable(\n                    enabled = enabled,\n                    onClick = onClick\n                )\n                .alpha(if (enabled) 1f else 0.5f)\n                .padding(12.dp)\n        ) {\n            Box(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .weight(1f),\n                contentAlignment = Alignment.Center,\n                content = icon\n            )\n            Text(\n                text = stringResource(title),\n                style = MaterialTheme.typography.labelLarge,\n                textAlign = TextAlign.Center,\n                maxLines = 2,\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .height(with(LocalDensity.current) {\n                        MaterialTheme.typography.labelLarge.lineHeight.toDp() * 2\n                    })\n            )\n        }\n    }\n}\n\n\nfun LazyGridScope.DownloadGridMenu(\n    @Download.State state: Int?,\n    onRemoveDownload: () -> Unit,\n    onDownload: () -> Unit,\n) {\n    when (state) {\n        Download.STATE_COMPLETED -> {\n            GridMenuItem(\n                icon = R.drawable.offline,\n                title = R.string.remove_download,\n                onClick = onRemoveDownload\n            )\n        }\n\n        Download.STATE_QUEUED, Download.STATE_DOWNLOADING -> {\n            GridMenuItem(\n                icon = {\n                    CircularProgressIndicator(\n                        modifier = Modifier.size(24.dp),\n                        strokeWidth = 2.dp\n                    )\n                },\n                title = R.string.downloading,\n                onClick = onRemoveDownload\n            )\n        }\n\n        else -> {\n            GridMenuItem(\n                icon = R.drawable.download,\n                title = R.string.action_download,\n                onClick = onDownload\n            )\n        }\n    }\n}\n\nfun LazyGridScope.SleepTimerGridMenu(\n    modifier: Modifier = Modifier,\n    sleepTimerTimeLeft: Long,\n    enabled: Boolean = true,\n    onClick: () -> Unit\n) {\n    item {\n        Column(\n            modifier = modifier\n                .clip(ShapeDefaults.Large)\n                .height(GridMenuItemHeight)\n                .clickable(\n                    onClick = onClick\n                )\n                .padding(12.dp)\n        ) {\n            Box(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .weight(1f),\n                contentAlignment = Alignment.Center,\n                content = {\n                    Icon(\n                        painterResource(R.drawable.bedtime),\n                        contentDescription = null,\n                        modifier = Modifier.alpha(if (enabled) 1f else 0.5f)\n                    )\n                }\n            )\n            Text(\n                text = if (enabled) makeTimeString(sleepTimerTimeLeft) else stringResource(\n                    id = R.string.sleep_timer\n                ),\n                style = MaterialTheme.typography.labelLarge,\n                textAlign = TextAlign.Center,\n                maxLines = 2,\n                modifier = Modifier.fillMaxWidth()\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/HideOnScrollFAB.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.annotation.DrawableRes\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.slideInVertically\nimport androidx.compose.animation.slideOutVertically\nimport androidx.compose.foundation.ScrollState\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.foundation.lazy.grid.LazyGridState\nimport androidx.compose.material3.FloatingActionButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.SmallFloatingActionButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.ui.utils.isScrollingUp\n\n@Composable\nfun BoxScope.HideOnScrollFAB(\n    visible: Boolean = true,\n    lazyListState: LazyListState,\n    @DrawableRes icon: Int,\n    onClick: () -> Unit,\n    onRecognitionClick: (() -> Unit)? = null,\n) {\n    AnimatedVisibility(\n        visible = visible && lazyListState.isScrollingUp(),\n        enter = slideInVertically { it },\n        exit = slideOutVertically { it },\n        modifier =\n        Modifier\n            .align(Alignment.BottomEnd)\n            .windowInsetsPadding(\n                LocalPlayerAwareWindowInsets.current\n                    .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal),\n            ),\n    ) {\n        Column(\n            horizontalAlignment = Alignment.CenterHorizontally,\n            modifier = Modifier.padding(16.dp)\n        ) {\n            if (onRecognitionClick != null) {\n                SmallFloatingActionButton(\n                    onClick = onRecognitionClick,\n                    containerColor = MaterialTheme.colorScheme.secondaryContainer,\n                    contentColor = MaterialTheme.colorScheme.onSecondaryContainer,\n                    modifier = Modifier.size(40.dp)\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.mic),\n                        contentDescription = stringResource(R.string.recognize_music),\n                        modifier = Modifier.size(20.dp)\n                    )\n                }\n                Spacer(modifier = Modifier.height(12.dp))\n            }\n            FloatingActionButton(\n                onClick = onClick,\n            ) {\n                Icon(\n                    painter = painterResource(icon),\n                    contentDescription = null,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun BoxScope.HideOnScrollFAB(\n    visible: Boolean = true,\n    lazyListState: LazyGridState,\n    @DrawableRes icon: Int,\n    onClick: () -> Unit,\n    onRecognitionClick: (() -> Unit)? = null,\n) {\n    AnimatedVisibility(\n        visible = visible && lazyListState.isScrollingUp(),\n        enter = slideInVertically { it },\n        exit = slideOutVertically { it },\n        modifier =\n        Modifier\n            .align(Alignment.BottomEnd)\n            .windowInsetsPadding(\n                LocalPlayerAwareWindowInsets.current\n                    .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal),\n            ),\n    ) {\n        Column(\n            horizontalAlignment = Alignment.CenterHorizontally,\n            modifier = Modifier.padding(16.dp)\n        ) {\n            if (onRecognitionClick != null) {\n                SmallFloatingActionButton(\n                    onClick = onRecognitionClick,\n                    containerColor = MaterialTheme.colorScheme.secondaryContainer,\n                    contentColor = MaterialTheme.colorScheme.onSecondaryContainer,\n                    modifier = Modifier.size(40.dp)\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.mic),\n                        contentDescription = stringResource(R.string.recognize_music),\n                        modifier = Modifier.size(20.dp)\n                    )\n                }\n                Spacer(modifier = Modifier.height(12.dp))\n            }\n            FloatingActionButton(\n                onClick = onClick,\n            ) {\n                Icon(\n                    painter = painterResource(icon),\n                    contentDescription = null,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun BoxScope.HideOnScrollFAB(\n    visible: Boolean = true,\n    scrollState: ScrollState,\n    @DrawableRes icon: Int,\n    onClick: () -> Unit,\n    onRecognitionClick: (() -> Unit)? = null,\n) {\n    AnimatedVisibility(\n        visible = visible && scrollState.isScrollingUp(),\n        enter = slideInVertically { it },\n        exit = slideOutVertically { it },\n        modifier =\n        Modifier\n            .align(Alignment.BottomEnd)\n            .windowInsetsPadding(\n                LocalPlayerAwareWindowInsets.current\n                    .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal),\n            ),\n    ) {\n        Column(\n            horizontalAlignment = Alignment.CenterHorizontally,\n            modifier = Modifier.padding(16.dp)\n        ) {\n            if (onRecognitionClick != null) {\n                SmallFloatingActionButton(\n                    onClick = onRecognitionClick,\n                    containerColor = MaterialTheme.colorScheme.secondaryContainer,\n                    contentColor = MaterialTheme.colorScheme.onSecondaryContainer,\n                    modifier = Modifier.size(40.dp)\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.mic),\n                        contentDescription = stringResource(R.string.recognize_music),\n                        modifier = Modifier.size(20.dp)\n                    )\n                }\n                Spacer(modifier = Modifier.height(12.dp))\n            }\n            FloatingActionButton(\n                onClick = onClick,\n            ) {\n                Icon(\n                    painter = painterResource(icon),\n                    contentDescription = null,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/IconButton.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.annotation.DrawableRes\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.Indication\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.sizeIn\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.IconButtonColors\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.minimumInteractiveComponentSize\nimport androidx.compose.material3.ripple\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.ColorFilter\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun ResizableIconButton(\n    @DrawableRes icon: Int,\n    modifier: Modifier = Modifier,\n    color: Color = MaterialTheme.colorScheme.onSurface,\n    enabled: Boolean = true,\n    indication: Indication? = null,\n    onClick: () -> Unit = {},\n) {\n    Image(\n        painter = painterResource(icon),\n        contentDescription = null,\n        colorFilter = ColorFilter.tint(color),\n        modifier = modifier\n            .clickable(\n                indication = indication ?: ripple(bounded = false),\n                interactionSource = remember { MutableInteractionSource() },\n                enabled = enabled,\n                onClick = onClick,\n            )\n            .alpha(if (enabled) 1f else 0.5f),\n    )\n}\n\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nfun IconButton(\n    onClick: () -> Unit,\n    onLongClick: () -> Unit,\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    colors: IconButtonColors = IconButtonDefaults.iconButtonColors(),\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    content: @Composable () -> Unit,\n) {\n    Box(\n        modifier = modifier\n            .minimumInteractiveComponentSize()\n            .sizeIn(minWidth = 48.dp, minHeight = 48.dp)\n            .clip(CircleShape)\n            .background(color = colors.containerColor)\n            .combinedClickable(\n                onClick = onClick,\n                onLongClick = onLongClick,\n                enabled = enabled,\n                role = Role.Button,\n                interactionSource = interactionSource,\n                indication = ripple(\n                    bounded = false,\n                    radius = 24.dp\n                ),\n            ),\n        contentAlignment = Alignment.Center,\n    ) {\n        val contentColor = colors.contentColor\n        CompositionLocalProvider(LocalContentColor provides contentColor, content = content)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/IntegrationCard.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.animation.animateContentSize\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Badge\nimport androidx.compose.material3.BadgedBox\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ProvideTextStyle\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.unit.dp\n\n/**\n * A Material 3 Expressive style settings group component\n * @param title The title of the settings group\n * @param items List of settings items to display\n */\n@Composable\nfun IntegrationCard(\n    title: String? = null,\n    items: List<IntegrationCardItem>\n) {\n    Column(\n        modifier = Modifier\n            .fillMaxWidth()\n    ) {\n        // Section title\n        title?.let {\n            Text(\n                text = it,\n                style = MaterialTheme.typography.labelLarge,\n                color = MaterialTheme.colorScheme.primary,\n                modifier = Modifier.padding(bottom = 8.dp, top = 8.dp)\n            )\n        }\n\n        // Settings items\n        Column(\n            modifier = Modifier.fillMaxWidth(),\n            verticalArrangement = Arrangement.spacedBy(4.dp)\n        ) {\n            items.forEachIndexed { index, item ->\n                val shape = when {\n                    items.size == 1 -> RoundedCornerShape(24.dp)\n                    index == 0 -> RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp, bottomStart = 6.dp, bottomEnd = 6.dp)\n                    index == items.size - 1 -> RoundedCornerShape(topStart = 6.dp, topEnd = 6.dp, bottomStart = 24.dp, bottomEnd = 24.dp)\n                    else -> RoundedCornerShape(6.dp)\n                }\n\n                Card(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .animateContentSize(),\n                    shape = shape,\n                    colors = CardDefaults.cardColors(\n                        containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)\n                    ),\n                    elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)\n                ) {\n                    IntegrationCardItemRow(item = item)\n                }\n            }\n        }\n    }\n}\n\n/**\n * Individual settings item row with Material 3 styling\n */\n@Composable\nprivate fun IntegrationCardItemRow(\n    item: IntegrationCardItem\n) {\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .clickable(\n                enabled = item.onClick != null,\n                onClick = { item.onClick?.invoke() }\n            )\n            .padding(horizontal = 20.dp, vertical = 16.dp),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        // Icon with background\n        item.icon?.let { icon ->\n            Box(\n                modifier = Modifier\n                    .size(40.dp)\n                    .clip(RoundedCornerShape(12.dp))\n                    .background(\n                        MaterialTheme.colorScheme.primary.copy(\n                            alpha = if (item.isHighlighted) 0.15f else 0.1f\n                        )\n                    ),\n                contentAlignment = Alignment.Center\n            ) {\n                if (item.showBadge) {\n                    BadgedBox(\n                        badge = {\n                            Badge(\n                                containerColor = MaterialTheme.colorScheme.error\n                            )\n                        }\n                    ) {\n                        Icon(\n                            painter = icon,\n                            contentDescription = null,\n                            tint = if (item.isHighlighted)\n                                MaterialTheme.colorScheme.primary\n                            else\n                                MaterialTheme.colorScheme.primary.copy(alpha = 0.9f),\n                            modifier = Modifier.size(24.dp)\n                        )\n                    }\n                } else {\n                    Icon(\n                        painter = icon,\n                        contentDescription = null,\n                        tint = if (item.isHighlighted)\n                            MaterialTheme.colorScheme.primary\n                        else\n                            MaterialTheme.colorScheme.primary.copy(alpha = 0.9f),\n                        modifier = Modifier.size(24.dp)\n                    )\n                }\n            }\n\n            Spacer(modifier = Modifier.width(16.dp))\n        }\n\n        // Title and description\n        Column(\n            modifier = Modifier.weight(1f)\n        ) {\n            // Title content\n            ProvideTextStyle(MaterialTheme.typography.titleMedium) {\n                item.title()\n            }\n\n            // Description if provided\n            item.description?.let { desc ->\n                Spacer(modifier = Modifier.height(2.dp))\n                ProvideTextStyle(\n                    MaterialTheme.typography.bodyMedium.copy(\n                        color = MaterialTheme.colorScheme.onSurfaceVariant\n                    )\n                ) {\n                    desc()\n                }\n            }\n        }\n\n        // Trailing content\n        item.trailingContent?.let { trailing ->\n            Spacer(modifier = Modifier.width(8.dp))\n            trailing()\n        }\n    }\n}\n\n/**\n * Data class for Material 3 settings item\n */\ndata class IntegrationCardItem(\n    val icon: Painter? = null,\n    val title: @Composable () -> Unit,\n    val description: (@Composable () -> Unit)? = null,\n    val trailingContent: (@Composable () -> Unit)? = null,\n    val showBadge: Boolean = false,\n    val isHighlighted: Boolean = false,\n    val onClick: (() -> Unit)? = null\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/Items.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n * \n * Optimized for minimal recomposition during navigation\n */\n\npackage com.metrolist.music.ui.component\n\nimport android.widget.Toast\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.animate\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.expandIn\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.shrinkOut\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.basicMarquee\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.gestures.Orientation\nimport androidx.compose.foundation.gestures.draggable\nimport androidx.compose.foundation.gestures.rememberDraggableState\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.BoxWithConstraintsScope\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.produceState\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.pluralStringResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.text.withStyle\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.util.fastForEachIndexed\nimport androidx.compose.ui.zIndex\nimport androidx.media3.common.MediaItem\nimport androidx.media3.exoplayer.offline.Download\nimport androidx.media3.exoplayer.offline.Download.STATE_COMPLETED\nimport androidx.media3.exoplayer.offline.Download.STATE_DOWNLOADING\nimport androidx.media3.exoplayer.offline.Download.STATE_QUEUED\nimport coil3.compose.AsyncImage\nimport coil3.request.ImageRequest\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.ArtistItem\nimport com.metrolist.innertube.models.EpisodeItem\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.PodcastItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.YTItem\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalDownloadUtil\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.CropAlbumArtKey\nimport com.metrolist.music.constants.GridItemSize\nimport com.metrolist.music.constants.GridItemsSizeKey\nimport com.metrolist.music.constants.GridThumbnailHeight\nimport com.metrolist.music.constants.ListItemHeight\nimport com.metrolist.music.constants.ListThumbnailSize\nimport com.metrolist.music.constants.SmallGridThumbnailHeight\nimport com.metrolist.music.constants.SwipeToSongKey\nimport com.metrolist.music.constants.ThumbnailCornerRadius\nimport com.metrolist.music.db.entities.Album\nimport com.metrolist.music.db.entities.Artist\nimport com.metrolist.music.db.entities.Playlist\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.models.MediaMetadata\nimport com.metrolist.music.playback.queues.LocalAlbumRadio\nimport com.metrolist.music.ui.utils.resize\nimport com.metrolist.music.utils.joinByBullet\nimport com.metrolist.music.utils.makeTimeString\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.utils.reportException\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.firstOrNull\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport kotlin.math.roundToInt\n\nconst val ActiveBoxAlpha = 0.6f\n\n@Composable\nfun currentGridThumbnailHeight(): Dp {\n    val gridItemSize by rememberEnumPreference(GridItemsSizeKey, GridItemSize.BIG)\n    return if (gridItemSize == GridItemSize.BIG) GridThumbnailHeight else SmallGridThumbnailHeight\n}\n\n// Basic list item - optimized with inline to reduce recomposition\n@Composable\ninline fun ListItem(\n    modifier: Modifier = Modifier,\n    title: String,\n    noinline subtitle: (@Composable RowScope.() -> Unit)? = null,\n    thumbnailContent: @Composable () -> Unit,\n    trailingContent: @Composable RowScope.() -> Unit = {},\n    isSelected: Boolean? = false,\n    isActive: Boolean = false,\n    isAvailable: Boolean = true,\n) {\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = if (isActive) {\n            modifier // playing highlight\n                .height(ListItemHeight)\n                .padding(horizontal = 8.dp)\n                .clip(RoundedCornerShape(8.dp))\n                .background(\n                    color = // selected active\n                        if (isSelected == true) MaterialTheme.colorScheme.primary.copy(alpha = 0.4f)\n                        else MaterialTheme.colorScheme.secondaryContainer\n                )\n        } else if (isSelected == true) {\n            modifier // inactive selected\n                .height(ListItemHeight)\n                .padding(horizontal = 8.dp)\n                .clip(RoundedCornerShape(8.dp))\n                .background(color = MaterialTheme.colorScheme.inversePrimary.copy(alpha = 0.4f))\n        } else {\n            modifier // default\n                .height(ListItemHeight)\n                .padding(horizontal = 8.dp)\n        }\n    ) {\n        Box(\n            modifier = Modifier.padding(6.dp),\n            contentAlignment = Alignment.Center\n        ) {\n            thumbnailContent()\n            if (!isAvailable) {\n                Box(\n                    modifier = Modifier\n                        .size(ListThumbnailSize)\n                        .align(Alignment.Center)\n                        .background(\n                            Color.Black.copy(alpha = 0.25f),\n                            RoundedCornerShape(ThumbnailCornerRadius)\n                        )\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.offline),\n                        contentDescription = null,\n                        tint = Color.White,\n                        modifier = Modifier\n                            .size(ListThumbnailSize / 2)\n                            .align(Alignment.Center)\n                            .graphicsLayer { alpha = 1f }\n                    )\n                }\n            }\n        }\n        Column(\n            modifier = Modifier\n                .weight(1f)\n                .padding(horizontal = 6.dp)\n        ) {\n            Text(\n                text = title,\n                style = MaterialTheme.typography.bodyMedium,\n                fontWeight = FontWeight.Bold,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis\n            )\n\n            if (subtitle != null) {\n                Row(verticalAlignment = Alignment.CenterVertically) {\n                    subtitle()\n                }\n            }\n        }\n\n        trailingContent()\n    }\n}\n\n@Composable\nfun ListItem(\n    modifier: Modifier = Modifier,\n    title: String,\n    subtitle: AnnotatedString?,\n    badges: @Composable RowScope.() -> Unit = {},\n    thumbnailContent: @Composable () -> Unit,\n    trailingContent: @Composable RowScope.() -> Unit = {},\n    isSelected: Boolean? = false,\n    isActive: Boolean = false,\n) = ListItem(\n    title = title,\n    subtitle = {\n        badges()\n        if (subtitle != null) {\n            Text(\n                text = subtitle,\n                style = MaterialTheme.typography.bodyMedium,\n                color = MaterialTheme.colorScheme.secondary,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis\n            )\n        }\n    },\n    thumbnailContent = thumbnailContent,\n    trailingContent = trailingContent,\n    modifier = modifier,\n    isSelected = isSelected,\n    isActive = isActive\n)\n\n// merge badges and subtitle text and pass to basic list item\n@Composable\nfun ListItem(\n    modifier: Modifier = Modifier,\n    title: String,\n    subtitle: String?,\n    badges: @Composable RowScope.() -> Unit = {},\n    thumbnailContent: @Composable () -> Unit,\n    trailingContent: @Composable RowScope.() -> Unit = {},\n    isSelected: Boolean? = false,\n    isActive: Boolean = false,\n) = ListItem(\n    title = title,\n    subtitle = {\n        badges()\n\n        if (!subtitle.isNullOrEmpty()) {\n            Text(\n                text = subtitle,\n                color = MaterialTheme.colorScheme.secondary,\n                style = MaterialTheme.typography.bodySmall,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis\n            )\n        }\n    },\n    thumbnailContent = thumbnailContent,\n    trailingContent = trailingContent,\n    modifier = modifier,\n    isSelected = isSelected,\n    isActive = isActive\n)\n\n@Composable\nfun GridItem(\n    modifier: Modifier = Modifier,\n    title: @Composable () -> Unit,\n    subtitle: @Composable () -> Unit,\n    badges: @Composable RowScope.() -> Unit = {},\n    thumbnailContent: @Composable BoxWithConstraintsScope.() -> Unit,\n    thumbnailRatio: Float = 1f,\n    fillMaxWidth: Boolean = false,\n) {\n    val gridHeight = currentGridThumbnailHeight()\n    Column(\n        modifier = if (fillMaxWidth) {\n            modifier\n                .padding(12.dp)\n                .fillMaxWidth()\n        } else {\n            modifier\n                .padding(12.dp)\n                .width(gridHeight * thumbnailRatio)\n        }\n    ) {\n        BoxWithConstraints(\n            contentAlignment = Alignment.Center,\n            modifier = if (fillMaxWidth) {\n                Modifier.fillMaxWidth()\n            } else {\n                Modifier.height(gridHeight)\n            }\n                .aspectRatio(thumbnailRatio)\n        ) {\n            thumbnailContent()\n        }\n\n        Spacer(modifier = Modifier.height(6.dp))\n\n        title()\n\n        Row(verticalAlignment = Alignment.CenterVertically) {\n            badges()\n\n            subtitle()\n        }\n    }\n}\n\n@Composable\nfun GridItem(\n    modifier: Modifier = Modifier,\n    title: String,\n    subtitle: String,\n    badges: @Composable RowScope.() -> Unit = {},\n    thumbnailContent: @Composable BoxWithConstraintsScope.() -> Unit,\n    thumbnailRatio: Float = 1f,\n    fillMaxWidth: Boolean = false,\n) = GridItem(\n    modifier = modifier,\n    title = {\n        Text(\n            text = title,\n            style = MaterialTheme.typography.bodyLarge,\n            fontWeight = FontWeight.Bold,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n            textAlign = TextAlign.Start,\n            modifier = Modifier.fillMaxWidth()\n        )\n    },\n    subtitle = {\n        Text(\n            text = subtitle,\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.secondary,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n        )\n    },\n    thumbnailContent = thumbnailContent,\n    thumbnailRatio = thumbnailRatio,\n    fillMaxWidth = fillMaxWidth\n)\n\n@Composable\nfun SongListItem(\n    song: Song,\n    modifier: Modifier = Modifier,\n    albumIndex: Int? = null,\n    showLikedIcon: Boolean = true,\n    showInLibraryIcon: Boolean = false,\n    showDownloadIcon: Boolean = true,\n    subtitleOverride: String? = null,\n    badges: @Composable RowScope.() -> Unit = {\n        if (showLikedIcon && song.song.liked) {\n            Icon.Favorite()\n        }\n        if (song.song.explicit) {\n            Icon.Explicit()\n        }\n        if (showInLibraryIcon && song.song.inLibrary != null) {\n            Icon.Library()\n        }\n        if (showDownloadIcon) {\n            val download by LocalDownloadUtil.current.getDownload(song.id)\n                .collectAsState(initial = null)\n            Icon.Download(download?.state)\n        }\n    },\n    isSelected: Boolean = false,\n    isActive: Boolean = false,\n    isPlaying: Boolean = false,\n    isSwipeable: Boolean = true,\n    trailingContent: @Composable RowScope.() -> Unit = {},\n) {\n    val swipeEnabled by rememberPreference(SwipeToSongKey, defaultValue = false)\n\n    val content: @Composable () -> Unit = {\n        ListItem(\n            title = song.song.title,\n            subtitle = subtitleOverride ?: joinByBullet(\n                song.orderedArtists.joinToString { it.name },\n                makeTimeString(song.song.duration * 1000L)\n            ),\n            badges = badges,\n            thumbnailContent = {\n                ItemThumbnail(\n                    thumbnailUrl = song.song.thumbnailUrl,\n                    albumIndex = albumIndex,\n                    isSelected = isSelected,\n                    isActive = isActive,\n                    isPlaying = isPlaying,\n                    shape = RoundedCornerShape(ThumbnailCornerRadius),\n                    modifier = Modifier.size(ListThumbnailSize)\n                )\n            },\n            trailingContent = trailingContent,\n            modifier = modifier,\n            isSelected = isSelected,\n            isActive = isActive\n        )\n    }\n\n    if (isSwipeable && swipeEnabled) {\n        SwipeToSongBox(\n            mediaItem = song.toMediaItem(),\n            modifier = Modifier.fillMaxWidth()\n        ) {\n            content()\n        }\n    } else {\n        content()\n    }\n}\n\n@Composable\nfun SongGridItem(\n    song: Song,\n    modifier: Modifier = Modifier,\n    showLikedIcon: Boolean = true,\n    showInLibraryIcon: Boolean = false,\n    showDownloadIcon: Boolean = true,\n    badges: @Composable RowScope.() -> Unit = {\n        if (showLikedIcon && song.song.liked) {\n            Icon.Favorite()\n        }\n        if (showInLibraryIcon && song.song.inLibrary != null) {\n            Icon.Library()\n        }\n        if (showDownloadIcon) {\n            val download by LocalDownloadUtil.current.getDownload(song.id).collectAsState(initial = null)\n            Icon.Download(download?.state)\n        }\n    },\n    isActive: Boolean = false,\n    isPlaying: Boolean = false,\n    fillMaxWidth: Boolean = false,\n) = GridItem(\n    title = {\n        Text(\n            text = song.song.title,\n            style = MaterialTheme.typography.bodyLarge,\n            fontWeight = FontWeight.Bold,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n            modifier = Modifier.basicMarquee().fillMaxWidth()\n        )\n    },\n    subtitle = {\n        Text(\n            text = joinByBullet(\n                song.orderedArtists.joinToString { it.name },\n                makeTimeString(song.song.duration * 1000L)\n            ),\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.secondary,\n            maxLines = 2,\n            overflow = TextOverflow.Ellipsis,\n        )\n    },\n    badges = badges,\n    thumbnailContent = {\n        val gridHeight = currentGridThumbnailHeight()\n        ItemThumbnail(\n            thumbnailUrl = song.song.thumbnailUrl,\n            isActive = isActive,\n            isPlaying = isPlaying,\n            shape = RoundedCornerShape(ThumbnailCornerRadius),\n            modifier = Modifier.size(gridHeight)\n        )\n        if (!isActive) {\n            OverlayPlayButton(\n                visible = true\n            )\n        }\n    },\n    fillMaxWidth = fillMaxWidth,\n    modifier = modifier\n)\n\n@Composable\nfun ArtistListItem(\n    artist: Artist,\n    modifier: Modifier = Modifier,\n    badges: @Composable RowScope.() -> Unit = {\n        if (artist.artist.bookmarkedAt != null) {\n            Icon(\n                painter = painterResource(R.drawable.favorite),\n                contentDescription = null,\n                tint = MaterialTheme.colorScheme.error,\n                modifier = Modifier\n                    .size(18.dp)\n                    .padding(end = 2.dp),\n            )\n        }\n    },\n    trailingContent: @Composable RowScope.() -> Unit = {},\n) = ListItem(\n    title = artist.artist.name,\n    subtitle = if (artist.songCount > 0) pluralStringResource(R.plurals.n_song, artist.songCount, artist.songCount) else null,\n    badges = badges,\n    thumbnailContent = {\n        AsyncImage(\n            model = ImageRequest.Builder(LocalContext.current)\n                .data(artist.artist.thumbnailUrl)\n                .memoryCachePolicy(coil3.request.CachePolicy.ENABLED)\n                .diskCachePolicy(coil3.request.CachePolicy.ENABLED)\n                .networkCachePolicy(coil3.request.CachePolicy.ENABLED)\n                .build(),\n            contentDescription = null,\n            modifier = Modifier\n                .size(ListThumbnailSize)\n                .clip(CircleShape),\n        )\n    },\n    trailingContent = trailingContent,\n    modifier = modifier,\n)\n\n@Composable\nfun ArtistGridItem(\n    artist: Artist,\n    modifier: Modifier = Modifier,\n    badges: @Composable RowScope.() -> Unit = {\n        if (artist.artist.bookmarkedAt != null) {\n            Icon.Favorite()\n        }\n    },\n    fillMaxWidth: Boolean = false,\n) = GridItem(\n    title = artist.artist.name,\n    subtitle = if (artist.songCount > 0) pluralStringResource(R.plurals.n_song, artist.songCount, artist.songCount) else \"\",\n    badges = badges,\n    thumbnailContent = {\n        AsyncImage(\n            model = ImageRequest.Builder(LocalContext.current)\n                .data(artist.artist.thumbnailUrl)\n                .memoryCachePolicy(coil3.request.CachePolicy.ENABLED)\n                .diskCachePolicy(coil3.request.CachePolicy.ENABLED)\n                .networkCachePolicy(coil3.request.CachePolicy.ENABLED)\n                .build(),\n            contentDescription = null,\n            contentScale = ContentScale.Crop,\n            modifier = Modifier\n                .fillMaxSize()\n                .clip(CircleShape)\n        )\n    },\n    fillMaxWidth = fillMaxWidth,\n    modifier = modifier\n)\n\n@Composable\nfun AlbumListItem(\n    album: Album,\n    modifier: Modifier = Modifier,\n    showLikedIcon: Boolean = true,\n    badges: @Composable RowScope.() -> Unit = {\n        val downloadUtil = LocalDownloadUtil.current\n        val database = LocalDatabase.current\n\n        val songs by produceState<List<Song>>(initialValue = emptyList(), album.id) {\n            withContext(Dispatchers.IO) {\n                value = database.albumSongs(album.id).first()\n            }\n        }\n\n        val allDownloads by downloadUtil.downloads.collectAsState()\n\n        val downloadState by remember(songs, allDownloads) {\n            androidx.compose.runtime.mutableIntStateOf(\n                if (songs.isEmpty()) {\n                    Download.STATE_STOPPED\n                } else {\n                    when {\n                        songs.all { allDownloads[it.id]?.state == STATE_COMPLETED } -> STATE_COMPLETED\n                        songs.any { allDownloads[it.id]?.state in listOf(STATE_QUEUED, STATE_DOWNLOADING) } -> STATE_DOWNLOADING\n                        else -> Download.STATE_STOPPED\n                    }\n                }\n            )\n        }\n\n        if (showLikedIcon && album.album.bookmarkedAt != null) {\n            Icon.Favorite()\n        }\n        if (album.album.explicit) {\n            Icon.Explicit()\n        }\n        Icon.Download(downloadState)\n    },\n    isActive: Boolean = false,\n    isPlaying: Boolean = false,\n    trailingContent: @Composable RowScope.() -> Unit = {},\n) = ListItem(\n    title = album.album.title,\n    subtitle = joinByBullet(\n        album.artists.joinToString { it.name },\n        pluralStringResource(R.plurals.n_song, album.album.songCount, album.album.songCount),\n        album.album.year?.toString()\n    ),\n    badges = badges,\n    thumbnailContent = {\n        ItemThumbnail(\n            thumbnailUrl = album.album.thumbnailUrl,\n            isActive = isActive,\n            isPlaying = isPlaying,\n            shape = RoundedCornerShape(ThumbnailCornerRadius),\n            modifier = Modifier.size(ListThumbnailSize)\n        )\n    },\n    trailingContent = trailingContent,\n    modifier = modifier\n)\n\n@Composable\nfun AlbumGridItem(\n    album: Album,\n    modifier: Modifier = Modifier,\n    coroutineScope: CoroutineScope,\n    badges: @Composable RowScope.() -> Unit = {\n        val downloadUtil = LocalDownloadUtil.current\n        val database = LocalDatabase.current\n\n        val songs by produceState<List<Song>>(initialValue = emptyList(), album.id) {\n            withContext(Dispatchers.IO) {\n                value = database.albumSongs(album.id).first()\n            }\n        }\n\n        val allDownloads by downloadUtil.downloads.collectAsState()\n\n        val downloadState by remember(songs, allDownloads) {\n            androidx.compose.runtime.mutableIntStateOf(\n                if (songs.isEmpty()) {\n                    Download.STATE_STOPPED\n                } else {\n                    when {\n                        songs.all { allDownloads[it.id]?.state == STATE_COMPLETED } -> STATE_COMPLETED\n                        songs.any { allDownloads[it.id]?.state in listOf(STATE_QUEUED, STATE_DOWNLOADING) } -> STATE_DOWNLOADING\n                        else -> Download.STATE_STOPPED\n                    }\n                }\n            )\n        }\n\n        if (album.album.bookmarkedAt != null) {\n            Icon.Favorite()\n        }\n        if (album.album.explicit) {\n            Icon.Explicit()\n        }\n        Icon.Download(downloadState)\n    },\n    isActive: Boolean = false,\n    isPlaying: Boolean = false,\n    fillMaxWidth: Boolean = false,\n) = GridItem(\n    title = {\n        Text(\n            text = album.album.title,\n            style = MaterialTheme.typography.bodyLarge,\n            fontWeight = FontWeight.Bold,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n            modifier = Modifier.basicMarquee().fillMaxWidth()\n        )\n    },\n    subtitle = {\n        Text(\n            text = album.artists.joinToString { it.name },\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.secondary,\n            maxLines = 2,\n            overflow = TextOverflow.Ellipsis\n        )\n    },\n    badges = badges,\n    thumbnailContent = {\n        val database = LocalDatabase.current\n        val playerConnection = LocalPlayerConnection.current ?: return@GridItem\n        val scope = rememberCoroutineScope()\n\n        ItemThumbnail(\n            thumbnailUrl = album.album.thumbnailUrl,\n            isActive = isActive,\n            isPlaying = isPlaying,\n            shape = RoundedCornerShape(ThumbnailCornerRadius),\n        )\n\n        AlbumPlayButton(\n            visible = !isActive,\n            onClick = {\n                scope.launch {\n                    val albumWithSongs = withContext(Dispatchers.IO) {\n                        database.albumWithSongs(album.id).firstOrNull()\n                    }\n                    albumWithSongs?.let {\n                        playerConnection.playQueue(LocalAlbumRadio(it))\n                    }\n                }\n            }\n        )\n    },\n    fillMaxWidth = fillMaxWidth,\n    modifier = modifier\n)\n\n@Composable\nfun PlaylistListItem(\n    playlist: Playlist,\n    modifier: Modifier = Modifier,\n    autoPlaylist: Boolean = false,\n    badges: @Composable RowScope.() -> Unit = {\n        val downloadUtil = LocalDownloadUtil.current\n        val database = LocalDatabase.current\n\n        val songs by produceState<List<Song>>(initialValue = emptyList(), playlist.id) {\n            withContext(Dispatchers.IO) {\n                value = database.playlistSongs(playlist.id).first().map { it.song }\n            }\n        }\n\n        val allDownloads by downloadUtil.downloads.collectAsState()\n\n        val downloadState by remember(songs, allDownloads) {\n            androidx.compose.runtime.mutableIntStateOf(\n                if (songs.isEmpty()) {\n                    Download.STATE_STOPPED\n                } else {\n                    when {\n                        songs.all { allDownloads[it.id]?.state == STATE_COMPLETED } -> STATE_COMPLETED\n                        songs.any { allDownloads[it.id]?.state in listOf(STATE_QUEUED, STATE_DOWNLOADING) } -> STATE_DOWNLOADING\n                        else -> Download.STATE_STOPPED\n                    }\n                }\n            )\n        }\n\n        Icon.Download(downloadState)\n    },\n    trailingContent: @Composable RowScope.() -> Unit = {}\n) = ListItem(\n    title = playlist.playlist.name,\n    subtitle = if (autoPlaylist) {\n        \"\"\n    } else {\n        if (playlist.songCount == 0 && playlist.playlist.remoteSongCount != null) {\n            pluralStringResource(\n                R.plurals.n_song,\n                playlist.playlist.remoteSongCount,\n                playlist.playlist.remoteSongCount\n            )\n        } else {\n            pluralStringResource(\n                R.plurals.n_song,\n                playlist.songCount,\n                playlist.songCount\n            )\n        }\n    },\n    badges = badges,\n    thumbnailContent = {\n        PlaylistThumbnail(\n            thumbnails = playlist.thumbnails,\n            size = ListThumbnailSize,\n            placeHolder = {\n                val painter = when (playlist.playlist.name) {\n                    stringResource(R.string.liked) -> R.drawable.favorite_border\n                    stringResource(R.string.offline) -> R.drawable.offline\n                    stringResource(R.string.cached_playlist) -> R.drawable.cached\n                    // R.drawable.backup as placeholder\n                    stringResource(R.string.uploaded_playlist) -> R.drawable.backup\n                    else -> if (autoPlaylist) R.drawable.trending_up else R.drawable.queue_music\n                }\n                Icon(\n                    painter = painterResource(painter),\n                    contentDescription = null,\n                    tint = LocalContentColor.current.copy(alpha = 0.8f),\n                    modifier = Modifier.size(ListThumbnailSize / 2)\n                )\n            },\n            shape = RoundedCornerShape(ThumbnailCornerRadius)\n        )\n    },\n    trailingContent = trailingContent,\n    modifier = modifier\n)\n\n@Composable\nfun PlaylistGridItem(\n    playlist: Playlist,\n    modifier: Modifier = Modifier,\n    autoPlaylist: Boolean = false,\n    badges: @Composable RowScope.() -> Unit = {\n        val downloadUtil = LocalDownloadUtil.current\n        val database = LocalDatabase.current\n\n        val songs by produceState<List<Song>>(initialValue = emptyList(), playlist.id) {\n            withContext(Dispatchers.IO) {\n                value = database.playlistSongs(playlist.id).first().map { it.song }\n            }\n        }\n\n        val allDownloads by downloadUtil.downloads.collectAsState()\n\n        val downloadState by remember(songs, allDownloads) {\n            mutableIntStateOf(\n                if (songs.isEmpty()) {\n                    Download.STATE_STOPPED\n                } else {\n                    when {\n                        songs.all { allDownloads[it.id]?.state == STATE_COMPLETED } -> STATE_COMPLETED\n                        songs.any { allDownloads[it.id]?.state in listOf(STATE_QUEUED, STATE_DOWNLOADING) } -> STATE_DOWNLOADING\n                        else -> Download.STATE_STOPPED\n                    }\n                }\n            )\n        }\n\n        Icon.Download(downloadState)\n    },\n    fillMaxWidth: Boolean = false,\n) = GridItem(\n    title = {\n        Text(\n            text = playlist.playlist.name,\n            style = MaterialTheme.typography.bodyLarge,\n            fontWeight = FontWeight.Bold,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n            modifier = Modifier.basicMarquee().fillMaxWidth()\n        )\n    },\n    subtitle = {\n        val subtitle = if (autoPlaylist) {\n            \"\"\n        } else {\n            if (playlist.songCount == 0 && playlist.playlist.remoteSongCount != null) {\n                pluralStringResource(\n                    R.plurals.n_song,\n                    playlist.playlist.remoteSongCount,\n                    playlist.playlist.remoteSongCount\n                )\n            } else {\n                pluralStringResource(\n                    R.plurals.n_song,\n                    playlist.songCount,\n                    playlist.songCount\n                )\n            }\n        }\n        Text(\n            text = subtitle,\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.secondary,\n            maxLines = 2,\n            overflow = TextOverflow.Ellipsis\n        )\n    },\n    badges = badges,\n    thumbnailContent = {\n        val width = maxWidth\n        PlaylistThumbnail(\n            thumbnails = playlist.thumbnails,\n            size = width,\n            placeHolder = {\n                val painter = when (playlist.playlist.name) {\n                    stringResource(R.string.liked) -> R.drawable.favorite_border\n                    stringResource(R.string.offline) -> R.drawable.offline\n                    stringResource(R.string.cached_playlist) -> R.drawable.cached\n                    // R.drawable.backup as placeholder\n                    stringResource(R.string.uploaded_playlist) -> R.drawable.backup\n                    else -> if (autoPlaylist) R.drawable.trending_up else R.drawable.queue_music\n                }\n                Box(\n                    contentAlignment = Alignment.Center,\n                    modifier = Modifier.fillMaxSize()\n                ) {\n                    Icon(\n                        painter = painterResource(painter),\n                        contentDescription = null,\n                        tint = LocalContentColor.current.copy(alpha = 0.8f),\n                        modifier = Modifier.size(width / 2)\n                    )\n                }\n            },\n            shape = RoundedCornerShape(ThumbnailCornerRadius)\n        )\n    },\n    fillMaxWidth = fillMaxWidth,\n    modifier = modifier\n)\n\n@Composable\nfun MediaMetadataListItem(\n    mediaMetadata: MediaMetadata,\n    modifier: Modifier = Modifier,\n    isSelected: Boolean = false,\n    isActive: Boolean = false,\n    isPlaying: Boolean = false,\n    trailingContent: @Composable RowScope.() -> Unit = {},\n) {\n    ListItem(\n        title = mediaMetadata.title,\n        subtitle = if (mediaMetadata.suggestedBy != null) {\n            buildAnnotatedString {\n                append(mediaMetadata.artists.joinToString { it.name })\n                append(\" • \")\n                append(makeTimeString(mediaMetadata.duration * 1000L))\n                append(\" • \")\n                withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {\n                    append(mediaMetadata.suggestedBy)\n                }\n            }\n        } else {\n            AnnotatedString(\n                joinByBullet(\n                    mediaMetadata.artists.joinToString { it.name },\n                    makeTimeString(mediaMetadata.duration * 1000L)\n                )\n            )\n        },\n        badges = { if (mediaMetadata.explicit) Icon.Explicit()},\n        thumbnailContent = {\n            ItemThumbnail(\n                thumbnailUrl = mediaMetadata.thumbnailUrl,\n                albumIndex = null,\n                isSelected = isSelected,\n                isActive = isActive,\n                isPlaying = isPlaying,\n                shape = RoundedCornerShape(ThumbnailCornerRadius),\n                modifier = Modifier.size(ListThumbnailSize)\n            )\n        },\n        trailingContent = trailingContent,\n        modifier = modifier,\n        isActive = isActive\n    )\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun YouTubeListItem(\n    item: YTItem,\n    modifier: Modifier = Modifier,\n    albumIndex: Int? = null,\n    isSelected: Boolean = false,\n    isActive: Boolean = false,\n    isPlaying: Boolean = false,\n    isSwipeable: Boolean = true,\n    trailingContent: @Composable RowScope.() -> Unit = {},\n    badges: @Composable RowScope.() -> Unit = {\n        val database = LocalDatabase.current\n        val song by produceState<Song?>(initialValue = null, item.id) {\n            if (item is SongItem) value = database.song(item.id).firstOrNull()\n        }\n        val album by produceState<Album?>(initialValue = null, item.id) {\n            if (item is AlbumItem) value = database.album(item.id).firstOrNull()\n        }\n\n        if ((item is SongItem && song?.song?.liked == true) ||\n            (item is AlbumItem && album?.album?.bookmarkedAt != null)\n        ) {\n            Icon.Favorite()\n        }\n        if (item.explicit) Icon.Explicit()\n        // if (item is SongItem && song?.song?.inLibrary != null) {\n        //     Icon.Library()\n        // }\n        if (item is SongItem) {\n            val download by LocalDownloadUtil.current.getDownload(item.id).collectAsState(null)\n            Icon.Download(download?.state)\n        }\n    },\n) {\n    val swipeEnabled by rememberPreference(SwipeToSongKey, defaultValue = false)\n\n    val content: @Composable () -> Unit = {\n        ListItem(\n            title = item.title,\n            subtitle = when (item) {\n                is SongItem -> joinByBullet(item.artists.joinToString { it.name }, makeTimeString(item.duration?.times(1000L)))\n                is AlbumItem -> joinByBullet(item.artists?.joinToString { it.name }, item.year?.toString())\n                is ArtistItem -> null\n                is PlaylistItem -> joinByBullet(item.author?.name, item.songCountText)\n                is PodcastItem -> joinByBullet(item.author?.name, item.episodeCountText)\n                is EpisodeItem -> joinByBullet(item.author?.name, item.publishDateText, makeTimeString(item.duration?.times(1000L)))\n            },\n            badges = badges,\n            thumbnailContent = {\n                ItemThumbnail(\n                    thumbnailUrl = item.thumbnail,\n                    albumIndex = albumIndex,\n                    isSelected = isSelected,\n                    isActive = isActive,\n                    isPlaying = isPlaying,\n                    shape = if (item is ArtistItem) CircleShape else RoundedCornerShape(ThumbnailCornerRadius),\n                    modifier = Modifier.size(ListThumbnailSize)\n                )\n            },\n            trailingContent = trailingContent,\n            modifier = modifier,\n            isActive = isActive\n        )\n    }\n\n    if (item is SongItem && isSwipeable && swipeEnabled) {\n        SwipeToSongBox(\n            mediaItem = item.copy(thumbnail = item.thumbnail.resize(544,544)).toMediaItem(),\n            modifier = Modifier.fillMaxWidth()\n        ) {\n            content()\n        }\n    } else {\n        content()\n    }\n}\n\n@Composable\nfun YouTubeGridItem(\n    item: YTItem,\n    modifier: Modifier = Modifier,\n    coroutineScope: CoroutineScope? = null,\n    badges: @Composable RowScope.() -> Unit = {\n        val database = LocalDatabase.current\n        val song by produceState<Song?>(initialValue = null, item.id) {\n            if (item is SongItem) value = database.song(item.id).firstOrNull()\n        }\n        val album by produceState<Album?>(initialValue = null, item.id) {\n            if (item is AlbumItem) value = database.album(item.id).firstOrNull()\n        }\n\n        if (item is SongItem && song?.song?.liked == true ||\n            item is AlbumItem && album?.album?.bookmarkedAt != null\n        ) {\n            Icon.Favorite()\n        }\n        if (item.explicit) Icon.Explicit()\n        // if (item is SongItem && song?.song?.inLibrary != null) Icon.Library()\n        if (item is SongItem) {\n            val download by LocalDownloadUtil.current.getDownload(item.id).collectAsState(null)\n            Icon.Download(download?.state)\n        }\n    },\n    thumbnailRatio: Float = if (item is SongItem) 16f / 9 else 1f,\n    isActive: Boolean = false,\n    isPlaying: Boolean = false,\n    fillMaxWidth: Boolean = false,\n) = GridItem(\n    title = {\n        Text(\n            text = item.title,\n            style = MaterialTheme.typography.bodyLarge,\n            fontWeight = FontWeight.Bold,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n            textAlign = if (item is ArtistItem) TextAlign.Center else TextAlign.Start,\n            modifier = Modifier.basicMarquee().fillMaxWidth()\n        )\n    },\n    subtitle = {\n        val subtitle = when (item) {\n            is SongItem -> joinByBullet(item.artists.joinToString { it.name }, makeTimeString(item.duration?.times(1000L)))\n            is AlbumItem -> joinByBullet(item.artists?.joinToString { it.name }, item.year?.toString())\n            is ArtistItem -> null\n            is PlaylistItem -> joinByBullet(item.author?.name, item.songCountText)\n            is PodcastItem -> joinByBullet(item.author?.name, item.episodeCountText)\n            is EpisodeItem -> joinByBullet(item.author?.name, makeTimeString(item.duration?.times(1000L)))\n        }\n        if (subtitle != null) {\n            Text(\n                text = subtitle,\n                style = MaterialTheme.typography.bodyMedium,\n                color = MaterialTheme.colorScheme.secondary,\n                maxLines = 2,\n                overflow = TextOverflow.Ellipsis,\n            )\n        }\n    },\n    badges = badges,\n    thumbnailContent = {\n        val database = LocalDatabase.current\n        val playerConnection = LocalPlayerConnection.current ?: return@GridItem\n        val scope = rememberCoroutineScope()\n\n        ItemThumbnail(\n            thumbnailUrl = item.thumbnail,\n            isActive = isActive,\n            isPlaying = isPlaying,\n            shape = if (item is ArtistItem) CircleShape else RoundedCornerShape(ThumbnailCornerRadius),\n        )\n\n        if (item is SongItem && !isActive) {\n            OverlayPlayButton(\n                visible = true\n            )\n        }\n\n        AlbumPlayButton(\n            visible = item is AlbumItem && !isActive,\n            onClick = {\n                scope.launch(Dispatchers.IO) {\n                    var albumWithSongs = database.albumWithSongs(item.id).first()\n                    if (albumWithSongs?.songs.isNullOrEmpty()) {\n                        YouTube.album(item.id).onSuccess { albumPage ->\n                            database.transaction { insert(albumPage) }\n                            albumWithSongs = database.albumWithSongs(item.id).first()\n                        }.onFailure { reportException(it) }\n                    }\n                    albumWithSongs?.let {\n                        withContext(Dispatchers.Main) {\n                            playerConnection.playQueue(LocalAlbumRadio(it))\n                        }\n                    }\n                }\n            }\n        )\n    },\n    thumbnailRatio = thumbnailRatio,\n    fillMaxWidth = fillMaxWidth,\n    modifier = modifier\n)\n\n@Composable\nfun LocalSongsGrid(\n    title: String,\n    subtitle: String,\n    badges: @Composable RowScope.() -> Unit = {},\n    thumbnailUrl: String?,\n    isActive: Boolean = false,\n    isPlaying: Boolean = false,\n    fillMaxWidth: Boolean = false,\n    modifier: Modifier = Modifier\n) = GridItem(\n    title = { Text(title, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis) },\n    subtitle = {\n        Text(\n            text = subtitle,\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.secondary,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n            modifier = Modifier.basicMarquee(\n                iterations = 3,\n                initialDelayMillis = 1000,\n                velocity = 30.dp\n            )\n        )\n    },\n    badges = badges,\n    thumbnailContent = {\n        LocalThumbnail(\n            thumbnailUrl = thumbnailUrl,\n            isActive = isActive,\n            isPlaying = isPlaying,\n            shape = RoundedCornerShape(ThumbnailCornerRadius),\n            modifier = if (fillMaxWidth) Modifier.fillMaxWidth() else Modifier,\n            showCenterPlay = true,\n            playButtonVisible = false\n        )\n    },\n    fillMaxWidth = fillMaxWidth,\n    modifier = modifier\n)\n\n@Composable\nfun LocalArtistsGrid(\n    title: String,\n    subtitle: String,\n    badges: @Composable RowScope.() -> Unit = {},\n    thumbnailUrl: String?,\n    isActive: Boolean = false,\n    isPlaying: Boolean = false,\n    fillMaxWidth: Boolean = false,\n    modifier: Modifier = Modifier\n) = GridItem(\n    title = { Text(title, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis) },\n    subtitle = {\n        Text(\n            text = subtitle,\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.secondary,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n            modifier = Modifier.basicMarquee(\n                iterations = 3,\n                initialDelayMillis = 1000,\n                velocity = 30.dp\n            )\n        )\n    },\n    badges = badges,\n    thumbnailContent = {\n        LocalThumbnail(\n            thumbnailUrl = thumbnailUrl,\n            isActive = false,\n            isPlaying = false,\n            shape = CircleShape,\n            modifier = if (fillMaxWidth) Modifier.fillMaxWidth() else Modifier,\n            showCenterPlay = false,\n            playButtonVisible = false\n        )\n    },\n    fillMaxWidth = fillMaxWidth,\n    modifier = modifier\n)\n\n@Composable\nfun LocalAlbumsGrid(\n    title: String,\n    subtitle: String,\n    badges: @Composable RowScope.() -> Unit = {},\n    thumbnailUrl: String?,\n    isActive: Boolean = false,\n    isPlaying: Boolean = false,\n    fillMaxWidth: Boolean = false,\n    modifier: Modifier = Modifier\n) = GridItem(\n    title = { Text(title, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis) },\n    subtitle = {\n        Text(\n            text = subtitle,\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.secondary,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n            modifier = Modifier.basicMarquee(\n                iterations = 3,\n                initialDelayMillis = 1000,\n                velocity = 30.dp\n            )\n        )\n    },\n    badges = badges,\n    thumbnailContent = {\n        LocalThumbnail(\n            thumbnailUrl = thumbnailUrl,\n            isActive = isActive,\n            isPlaying = isPlaying,\n            shape = RoundedCornerShape(ThumbnailCornerRadius),\n            modifier = if (fillMaxWidth) Modifier.fillMaxWidth() else Modifier,\n            showCenterPlay = false,\n            playButtonVisible = true\n        )\n    },\n    fillMaxWidth = fillMaxWidth,\n    modifier = modifier\n)\n\n@Composable\nfun ItemThumbnail(\n    thumbnailUrl: String?,\n    isActive: Boolean,\n    isPlaying: Boolean,\n    shape: Shape,\n    modifier: Modifier = Modifier,\n    albumIndex: Int? = null,\n    isSelected: Boolean = false,\n    thumbnailRatio: Float = 1f\n) {\n    val cropAlbumArt by rememberPreference(CropAlbumArtKey, false)\n    \n    Box(\n        contentAlignment = Alignment.Center,\n        modifier = modifier\n            .fillMaxSize()\n            .aspectRatio(thumbnailRatio)\n            .clip(shape)\n    ) {\n        if (albumIndex == null) {\n            AsyncImage(\n                model = ImageRequest.Builder(LocalContext.current)\n                    .data(thumbnailUrl)\n                    .memoryCachePolicy(coil3.request.CachePolicy.ENABLED)\n                    .diskCachePolicy(coil3.request.CachePolicy.ENABLED)\n                    .networkCachePolicy(coil3.request.CachePolicy.ENABLED)\n                    .build(),\n                contentDescription = null,\n                contentScale = if (cropAlbumArt) ContentScale.Crop else ContentScale.Fit,\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .clip(shape)\n            )\n        }\n\n        if (albumIndex != null) {\n            AnimatedVisibility(\n                visible = !isActive,\n                enter = fadeIn() + expandIn(expandFrom = Alignment.Center),\n                exit = shrinkOut(shrinkTowards = Alignment.Center) + fadeOut()\n            ) {\n                Text(\n                    text = albumIndex.toString(),\n                    style = MaterialTheme.typography.labelLarge\n                )\n            }\n        }\n\n        if (isSelected) {\n            Box(\n                contentAlignment = Alignment.Center,\n                modifier = Modifier\n                    .fillMaxSize()\n                    .zIndex(1f)\n                    .clip(shape)\n                    .background(Color.Black.copy(alpha = 0.5f))\n            ) {\n                Icon(\n                    painter = painterResource(R.drawable.done),\n                    contentDescription = null\n                )\n            }\n        }\n\n        PlayingIndicatorBox(\n            isActive = isActive,\n            playWhenReady = isPlaying,\n            color = if (albumIndex != null) MaterialTheme.colorScheme.onBackground else Color.White,\n            modifier = Modifier\n                .fillMaxSize()\n                .background(\n                    color = if (albumIndex != null)\n                        Color.Transparent\n                    else\n                        Color.Black.copy(alpha = ActiveBoxAlpha),\n                    shape = shape\n                )\n        )\n    }\n}\n\n@Composable\nfun LocalThumbnail(\n    thumbnailUrl: String?,\n    isActive: Boolean,\n    isPlaying: Boolean,\n    shape: Shape,\n    modifier: Modifier = Modifier,\n    showCenterPlay: Boolean = false,\n    playButtonVisible: Boolean = false,\n    thumbnailRatio: Float = 1f\n) {\n    val cropAlbumArt by rememberPreference(CropAlbumArtKey, false)\n    \n    Box(\n        contentAlignment = Alignment.Center,\n        modifier = modifier\n            .aspectRatio(thumbnailRatio)\n            .clip(shape)\n    ) {\n        AsyncImage(\n            model = ImageRequest.Builder(LocalContext.current)\n                .data(thumbnailUrl)\n                .memoryCachePolicy(coil3.request.CachePolicy.ENABLED)\n                .diskCachePolicy(coil3.request.CachePolicy.ENABLED)\n                .networkCachePolicy(coil3.request.CachePolicy.ENABLED)\n                .build(),\n            contentDescription = null,\n            contentScale = if (cropAlbumArt) ContentScale.Crop else ContentScale.Fit,\n            modifier = Modifier.fillMaxSize()\n        )\n\n        AnimatedVisibility(\n            visible = isActive,\n            enter = fadeIn(tween(500)),\n            exit = fadeOut(tween(500))\n        ) {\n            Box(\n                contentAlignment = Alignment.Center,\n                modifier = Modifier\n                    .fillMaxSize()\n                    .background(Color.Black.copy(alpha = 0.4f), shape)\n            ) {\n                if (isPlaying) {\n                    PlayingIndicator(\n                        color = Color.White,\n                        modifier = Modifier.height(24.dp)\n                    )\n                } else {\n                    Icon(\n                        painter = painterResource(R.drawable.play),\n                        contentDescription = null,\n                        tint = Color.White\n                    )\n                }\n            }\n        }\n\n        if (showCenterPlay) {\n            AnimatedVisibility(\n                visible = !(isActive && isPlaying),\n                enter = fadeIn(),\n                exit = fadeOut(),\n                modifier = Modifier\n                    .align(Alignment.Center)\n                    .padding(8.dp)\n            ) {\n                Box(\n                    contentAlignment = Alignment.Center,\n                    modifier = Modifier\n                        .size(36.dp)\n                        .clip(CircleShape)\n                        .background(Color.Black.copy(alpha = 0.6f))\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.play),\n                        contentDescription = null,\n                        tint = Color.White\n                    )\n                }\n            }\n        }\n\n        if (playButtonVisible) {\n            AnimatedVisibility(\n                visible = true,\n                enter = fadeIn(),\n                exit = fadeOut(),\n                modifier = Modifier\n                    .align(Alignment.BottomEnd)\n                    .padding(8.dp)\n            ) {\n                Box(\n                    contentAlignment = Alignment.Center,\n                    modifier = Modifier\n                        .size(36.dp)\n                        .clip(CircleShape)\n                        .background(Color.Black.copy(alpha = ActiveBoxAlpha))\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.play),\n                        contentDescription = null,\n                        tint = Color.White\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun PlaylistThumbnail(\n    thumbnails: List<String>,\n    size: Dp,\n    placeHolder: @Composable () -> Unit,\n    shape: Shape,\n    cacheKey: String? = null\n) {\n    val cropAlbumArt by rememberPreference(CropAlbumArtKey, false)\n    \n    when (thumbnails.size) {\n        0 -> Box(\n            contentAlignment = Alignment.Center,\n            modifier = Modifier\n                .size(size)\n                .clip(shape)\n                .background(MaterialTheme.colorScheme.surfaceContainer)\n        ) {\n            placeHolder()\n        }\n        1 -> AsyncImage(\n            model = ImageRequest.Builder(LocalContext.current)\n                .data(thumbnails[0])\n                .apply { /* Removed cache key extensions due to unresolved in env */ }\n                .memoryCachePolicy(coil3.request.CachePolicy.ENABLED)\n                .diskCachePolicy(coil3.request.CachePolicy.ENABLED)\n                .networkCachePolicy(coil3.request.CachePolicy.ENABLED)\n                .build(),\n            contentDescription = null,\n            contentScale = if (cropAlbumArt) ContentScale.Crop else ContentScale.Fit,\n            placeholder = painterResource(R.drawable.queue_music),\n            error = painterResource(R.drawable.queue_music),\n            modifier = Modifier\n                .size(size)\n                .clip(shape)\n        )\n        else -> Box(\n            modifier = Modifier\n                .size(size)\n                .clip(shape)\n        ) {\n            listOf(\n                Alignment.TopStart,\n                Alignment.TopEnd,\n                Alignment.BottomStart,\n                Alignment.BottomEnd\n            ).fastForEachIndexed { index, alignment ->\n                AsyncImage(\n                    model = ImageRequest.Builder(LocalContext.current)\n                        .data(thumbnails.getOrNull(index))\n                        .apply { /* Removed cache key extensions due to unresolved in env */ }\n                        .memoryCachePolicy(coil3.request.CachePolicy.ENABLED)\n                        .diskCachePolicy(coil3.request.CachePolicy.ENABLED)\n                        .networkCachePolicy(coil3.request.CachePolicy.ENABLED)\n                        .build(),\n                    contentDescription = null,\n                    contentScale = if (cropAlbumArt) ContentScale.Crop else ContentScale.Fit,\n                    placeholder = painterResource(R.drawable.queue_music),\n                    error = painterResource(R.drawable.queue_music),\n                    modifier = Modifier\n                        .align(alignment)\n                        .size(size / 2)\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun BoxScope.OverlayPlayButton(\n    visible: Boolean\n) {\n    AnimatedVisibility(\n        visible = visible,\n        enter = fadeIn(),\n        exit = fadeOut(),\n        modifier = Modifier\n            .align(Alignment.Center)\n    ) {\n        Box(\n            contentAlignment = Alignment.Center,\n            modifier = Modifier\n                .size(36.dp)\n                .clip(CircleShape)\n                .background(Color.Black.copy(alpha = ActiveBoxAlpha))\n        ) {\n            Icon(\n                painter = painterResource(R.drawable.play),\n                contentDescription = null,\n                tint = Color.White,\n                modifier = Modifier.size(20.dp)\n            )\n        }\n    }\n}\n\n@Composable\nfun BoxScope.OverlayEditButton(\n    visible: Boolean,\n    onClick: () -> Unit,\n    alignment: Alignment = Alignment.Center,\n) {\n    AnimatedVisibility(\n        visible = visible,\n        enter = fadeIn(),\n        exit = fadeOut(),\n        modifier = Modifier\n            .align(alignment)\n            .then(if (alignment == Alignment.BottomEnd) Modifier.padding(8.dp) else Modifier)\n    ) {\n        Box(\n            contentAlignment = Alignment.Center,\n            modifier = Modifier\n                .size(36.dp)\n                .clip(CircleShape)\n                .background(Color.Black.copy(alpha = ActiveBoxAlpha))\n                .padding(0.dp)\n                .clickable(onClick = onClick)\n        ) {\n            Icon(\n                painter = painterResource(R.drawable.edit),\n                contentDescription = null,\n                tint = Color.White,\n                modifier = Modifier.size(20.dp)\n            )\n        }\n    }\n}\n\n@Composable\nfun BoxScope.AlbumPlayButton(\n    visible: Boolean,\n    onClick: () -> Unit,\n) {\n    AnimatedVisibility(\n        visible = visible,\n        enter = fadeIn(),\n        exit = fadeOut(),\n        modifier = Modifier\n            .align(Alignment.BottomEnd)\n            .padding(8.dp)\n    ) {\n        Box(\n            contentAlignment = Alignment.Center,\n            modifier = Modifier\n                .size(36.dp)\n                .clip(CircleShape)\n                .background(Color.Black.copy(alpha = ActiveBoxAlpha))\n                .clickable(onClick = onClick)\n        ) {\n            Icon(\n                painter = painterResource(R.drawable.play),\n                contentDescription = null,\n                tint = Color.White\n            )\n        }\n    }\n}\n\n@Composable\nfun SwipeToSongBox(\n    modifier: Modifier = Modifier,\n    mediaItem: MediaItem,\n    content: @Composable BoxScope.() -> Unit\n) {\n    val ctx = LocalContext.current\n    val player = LocalPlayerConnection.current\n    val scope = rememberCoroutineScope()\n    val offset = remember { mutableFloatStateOf(0f) }\n    val threshold = 300f\n\n    val dragState = rememberDraggableState { delta ->\n        offset.floatValue = (offset.floatValue + delta).coerceIn(-threshold, threshold)\n    }\n\n    Box(\n        modifier = modifier\n            .fillMaxWidth()\n            .draggable(\n                orientation = Orientation.Horizontal,\n                state = dragState,\n                onDragStopped = {\n                    when {\n                        offset.floatValue >= threshold -> {\n                            player?.playNext(listOf(mediaItem))\n                            Toast.makeText(ctx, R.string.play_next, Toast.LENGTH_SHORT).show()\n                            reset(offset, scope)\n                        }\n\n                        offset.floatValue <= -threshold -> {\n                            player?.addToQueue(listOf(mediaItem))\n                            Toast.makeText(ctx, R.string.add_to_queue, Toast.LENGTH_SHORT).show()\n                            reset(offset, scope)\n                        }\n\n                        else -> reset(offset, scope)\n                    }\n                }\n            )\n    ) {\n        if (offset.floatValue != 0f) {\n            val (iconRes, bg, tint, align) = if (offset.floatValue > 0)\n                Quadruple(\n                    R.drawable.playlist_play,\n                    MaterialTheme.colorScheme.secondary,\n                    MaterialTheme.colorScheme.onSecondary,\n                    Alignment.CenterStart\n                ) else\n                Quadruple(\n                    R.drawable.queue_music,\n                    MaterialTheme.colorScheme.primary,\n                    MaterialTheme.colorScheme.onPrimary,\n                    Alignment.CenterEnd\n                )\n\n            Box(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .height(60.dp)\n                    .align(Alignment.Center)\n                    .background(bg),\n                contentAlignment = align\n            ) {\n                Icon(\n                    painter = painterResource(id = iconRes),\n                    contentDescription = null,\n                    modifier = Modifier\n                        .padding(horizontal = 24.dp)\n                        .size(30.dp)\n                        .alpha(0.9f),\n                    tint = tint\n                )\n            }\n        }\n\n        Box(\n            modifier = Modifier\n                .offset { IntOffset(offset.floatValue.roundToInt(), 0) }\n                .fillMaxWidth()\n                .background(MaterialTheme.colorScheme.surface),\n            content = content\n        )\n    }\n}\n\n// Helper to animate reset of swipe offset\nprivate fun reset(offset: MutableState<Float>, scope: CoroutineScope) {\n    scope.launch {\n        animate(\n            initialValue = offset.value,\n            targetValue = 0f,\n            animationSpec = tween(durationMillis = 300)\n        ) { value, _ -> offset.value = value }\n    }\n}\n\n// Data holder for swipe visuals\ndata class Quadruple<A, B, C, D>(\n    val first: A,\n    val second: B,\n    val third: C,\n    val fourth: D\n)\n\nobject Icon {\n    @Composable\n    fun Favorite() {\n        Icon(\n            painter = painterResource(R.drawable.favorite),\n            contentDescription = null,\n            tint = MaterialTheme.colorScheme.error,\n            modifier = Modifier\n                .size(18.dp)\n                .padding(end = 2.dp)\n        )\n    }\n\n    @Composable\n    fun Library() {\n        Icon(\n            painter = painterResource(R.drawable.library_add_check),\n            contentDescription = null,\n            modifier = Modifier\n                .size(18.dp)\n                .padding(end = 2.dp)\n        )\n    }\n\n    @Composable\n    fun Download(state: Int?) {\n        when (state) {\n            STATE_COMPLETED -> Icon(\n                painter = painterResource(R.drawable.offline),\n                contentDescription = null,\n                modifier = Modifier\n                    .size(18.dp)\n                    .padding(end = 2.dp)\n            )\n            STATE_QUEUED, STATE_DOWNLOADING -> CircularProgressIndicator(\n                strokeWidth = 2.dp,\n                modifier = Modifier\n                    .size(16.dp)\n                    .padding(end = 2.dp)\n            )\n            else -> { /* no icon */ }\n        }\n    }\n\n    @Composable\n    fun Explicit() {\n        Icon(\n            painter = painterResource(R.drawable.explicit),\n            contentDescription = null,\n            modifier = Modifier\n                .size(18.dp)\n                .padding(end = 2.dp)\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/Library.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.material3.Icon\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.navigation.NavController\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.WatchEndpoint\nimport com.metrolist.music.R\nimport com.metrolist.music.db.entities.Album\nimport com.metrolist.music.db.entities.Artist\nimport com.metrolist.music.db.entities.Playlist\nimport com.metrolist.music.ui.menu.AlbumMenu\nimport com.metrolist.music.ui.menu.ArtistMenu\nimport com.metrolist.music.ui.menu.PlaylistMenu\nimport com.metrolist.music.ui.menu.YouTubePlaylistMenu\nimport kotlinx.coroutines.CoroutineScope\n\n@Composable\nfun LibraryArtistListItem(\n    navController: NavController,\n    menuState: MenuState,\n    coroutineScope: CoroutineScope,\n    artist: Artist,\n    modifier: Modifier = Modifier\n) = ArtistListItem(\n    artist = artist,\n    trailingContent = {\n        androidx.compose.material3.IconButton(\n            onClick = {\n                menuState.show {\n                    ArtistMenu(\n                        originalArtist = artist,\n                        coroutineScope = coroutineScope,\n                        onDismiss = menuState::dismiss\n                    )\n                }\n            }\n        ) {\n            Icon(\n                painter = painterResource(R.drawable.more_vert),\n                contentDescription = null\n            )\n        }\n    },\n    modifier = modifier\n        .fillMaxWidth()\n        .clickable {\n            navController.navigate(\"artist/${artist.id}\")\n        }\n)\n\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nfun LibraryArtistGridItem(\n    navController: NavController,\n    menuState: MenuState,\n    coroutineScope: CoroutineScope,\n    artist: Artist,\n    modifier: Modifier = Modifier\n) = ArtistGridItem(\n    artist = artist,\n    fillMaxWidth = true,\n    modifier = modifier\n        .fillMaxWidth()\n        .combinedClickable(\n            onClick = {\n                navController.navigate(\"artist/${artist.id}\")\n            },\n            onLongClick = {\n                menuState.show {\n                    ArtistMenu(\n                        originalArtist = artist,\n                        coroutineScope = coroutineScope,\n                        onDismiss = menuState::dismiss\n                    )\n                }\n            }\n        )\n)\n\n@Composable\nfun LibraryAlbumListItem(\n    modifier: Modifier = Modifier,\n    navController: NavController,\n    menuState: MenuState,\n    album: Album,\n    isActive: Boolean = false,\n    isPlaying: Boolean = false\n) = AlbumListItem(\n    album = album,\n    isActive = isActive,\n    isPlaying = isPlaying,\n    trailingContent = {\n        androidx.compose.material3.IconButton(\n            onClick = {\n                menuState.show {\n                    AlbumMenu(\n                        originalAlbum = album,\n                        navController = navController,\n                        onDismiss = menuState::dismiss\n                    )\n                }\n            }\n        ) {\n            Icon(\n                painter = painterResource(R.drawable.more_vert),\n                contentDescription = null\n            )\n        }\n    },\n    modifier = modifier\n        .fillMaxWidth()\n        .clickable {\n            navController.navigate(\"album/${album.id}\")\n        }\n)\n\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nfun LibraryAlbumGridItem(\n    modifier: Modifier = Modifier,\n    navController: NavController,\n    menuState: MenuState,\n    coroutineScope: CoroutineScope,\n    album: Album,\n    isActive: Boolean = false,\n    isPlaying: Boolean = false\n) = AlbumGridItem(\n    album = album,\n    isActive = isActive,\n    isPlaying = isPlaying,\n    coroutineScope = coroutineScope,\n    fillMaxWidth = true,\n    modifier = modifier\n        .fillMaxWidth()\n        .combinedClickable(\n            onClick = {\n                navController.navigate(\"album/${album.id}\")\n            },\n            onLongClick = {\n                menuState.show {\n                    AlbumMenu(\n                        originalAlbum = album,\n                        navController = navController,\n                        onDismiss = menuState::dismiss\n                    )\n                }\n            }\n        )\n)\n\n@Composable\nfun LibraryPlaylistListItem(\n    navController: NavController,\n    menuState: MenuState,\n    coroutineScope: CoroutineScope,\n    playlist: Playlist,\n    modifier: Modifier = Modifier\n) = PlaylistListItem(\n    playlist = playlist,\n    trailingContent = {\n        androidx.compose.material3.IconButton(\n            onClick = {\n                menuState.show {\n                    if (playlist.playlist.isEditable || playlist.songCount != 0) {\n                        PlaylistMenu(\n                            playlist = playlist,\n                            coroutineScope = coroutineScope,\n                            onDismiss = menuState::dismiss\n                        )\n                    } else {\n                        playlist.playlist.browseId?.let { browseId ->\n                            YouTubePlaylistMenu(\n                                playlist = PlaylistItem(\n                                    id = browseId,\n                                    title = playlist.playlist.name,\n                                    author = null,\n                                    songCountText = null,\n                                    thumbnail = playlist.thumbnails.getOrNull(0) ?: \"\",\n                                    playEndpoint = WatchEndpoint(\n                                        playlistId = browseId,\n                                        params = playlist.playlist.playEndpointParams\n                                    ),\n                                    shuffleEndpoint = WatchEndpoint(\n                                        playlistId = browseId,\n                                        params = playlist.playlist.shuffleEndpointParams\n                                    ),\n                                    radioEndpoint = WatchEndpoint(\n                                        playlistId = \"RDAMPL$browseId\",\n                                        params = playlist.playlist.radioEndpointParams\n                                    ),\n                                    isEditable = false\n                                ),\n                                coroutineScope = coroutineScope,\n                                onDismiss = menuState::dismiss\n                            )\n                        }\n                    }\n                }\n            }\n        ) {\n            Icon(\n                painter = painterResource(R.drawable.more_vert),\n                contentDescription = null\n            )\n        }\n    },\n    modifier = modifier\n        .fillMaxWidth()\n        .clickable {\n            if (!playlist.playlist.isEditable && playlist.songCount == 0 && playlist.playlist.browseId != null)\n                navController.navigate(\"online_playlist/${playlist.playlist.browseId}\")\n            else\n                navController.navigate(\"local_playlist/${playlist.id}\")\n        }\n)\n\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nfun LibraryPlaylistGridItem(\n    navController: NavController,\n    menuState: MenuState,\n    coroutineScope: CoroutineScope,\n    playlist: Playlist,\n    modifier: Modifier = Modifier\n) = PlaylistGridItem(\n    playlist = playlist,\n    fillMaxWidth = true,\n    modifier = modifier\n        .fillMaxWidth()\n        .combinedClickable(\n            onClick = {\n                if (!playlist.playlist.isEditable && playlist.songCount == 0 && playlist.playlist.browseId != null)\n                    navController.navigate(\"online_playlist/${playlist.playlist.browseId}\")\n                else\n                    navController.navigate(\"local_playlist/${playlist.id}\")\n            },\n            onLongClick = {\n                menuState.show {\n                    if (playlist.playlist.isEditable || playlist.songCount != 0) {\n                        PlaylistMenu(\n                            playlist = playlist,\n                            coroutineScope = coroutineScope,\n                            onDismiss = menuState::dismiss\n                        )\n                    } else {\n                        playlist.playlist.browseId?.let { browseId ->\n                            YouTubePlaylistMenu(\n                                playlist = PlaylistItem(\n                                    id = browseId,\n                                    title = playlist.playlist.name,\n                                    author = null,\n                                    songCountText = null,\n                                    thumbnail = playlist.thumbnails.getOrNull(0) ?: \"\",\n                                    playEndpoint = WatchEndpoint(\n                                        playlistId = browseId,\n                                        params = playlist.playlist.playEndpointParams\n                                    ),\n                                    shuffleEndpoint = WatchEndpoint(\n                                        playlistId = browseId,\n                                        params = playlist.playlist.shuffleEndpointParams\n                                    ),\n                                    radioEndpoint = WatchEndpoint(\n                                        playlistId = \"RDAMPL$browseId\",\n                                        params = playlist.playlist.radioEndpointParams\n                                    ),\n                                    isEditable = false\n                                ),\n                                coroutineScope = coroutineScope,\n                                onDismiss = menuState::dismiss\n                            )\n                        }\n                    }\n                }\n            }\n        )\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/Lyrics.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.Intent\nimport android.text.Layout\nimport android.view.WindowManager\nimport android.widget.Toast\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.FastOutSlowInEasing\nimport androidx.compose.animation.core.LinearEasing\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.slideInVertically\nimport androidx.compose.animation.slideOutVertically\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.gestures.animateScrollBy\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.add\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.BasicAlertDialog\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.Shadow\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.compose.ui.input.nestedscroll.NestedScrollConnection\nimport androidx.compose.ui.input.nestedscroll.NestedScrollSource\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalWindowInfo\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.rememberTextMeasurer\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.withStyle\nimport androidx.compose.ui.unit.Velocity\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.compose.ui.zIndex\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.LifecycleEventObserver\nimport androidx.lifecycle.compose.LocalLifecycleOwner\nimport androidx.palette.graphics.Palette\nimport coil3.ImageLoader\nimport coil3.request.ImageRequest\nimport coil3.request.allowHardware\nimport coil3.toBitmap\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalListenTogetherManager\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.AiProviderKey\nimport com.metrolist.music.constants.AiSystemPromptKey\nimport com.metrolist.music.constants.DarkModeKey\nimport com.metrolist.music.constants.DeeplApiKey\nimport com.metrolist.music.constants.DeeplFormalityKey\nimport com.metrolist.music.constants.LyricsAnimationStyle\nimport com.metrolist.music.constants.LyricsAnimationStyleKey\nimport com.metrolist.music.constants.LyricsClickKey\nimport com.metrolist.music.constants.LyricsGlowEffectKey\nimport com.metrolist.music.constants.LyricsLineSpacingKey\nimport com.metrolist.music.constants.LyricsRomanizeAsMainKey\nimport com.metrolist.music.constants.LyricsRomanizeCyrillicByLineKey\nimport com.metrolist.music.constants.LyricsRomanizeList\nimport com.metrolist.music.constants.LyricsScrollKey\nimport com.metrolist.music.constants.LyricsTextPositionKey\nimport com.metrolist.music.constants.LyricsTextSizeKey\nimport com.metrolist.music.constants.OpenRouterApiKey\nimport com.metrolist.music.constants.OpenRouterBaseUrlKey\nimport com.metrolist.music.constants.OpenRouterModelKey\nimport com.metrolist.music.constants.PlayerBackgroundStyle\nimport com.metrolist.music.constants.PlayerBackgroundStyleKey\nimport com.metrolist.music.constants.TranslateLanguageKey\nimport com.metrolist.music.constants.TranslateModeKey\nimport com.metrolist.music.db.entities.LyricsEntity.Companion.LYRICS_NOT_FOUND\nimport com.metrolist.music.lyrics.LyricsEntry\nimport com.metrolist.music.lyrics.LyricsTranslationHelper\nimport com.metrolist.music.lyrics.LyricsUtils.findCurrentLineIndex\nimport com.metrolist.music.lyrics.LyricsUtils.isBelarusian\nimport com.metrolist.music.lyrics.LyricsUtils.isBulgarian\nimport com.metrolist.music.lyrics.LyricsUtils.isChinese\nimport com.metrolist.music.lyrics.LyricsUtils.isHindi\nimport com.metrolist.music.lyrics.LyricsUtils.isJapanese\nimport com.metrolist.music.lyrics.LyricsUtils.isKorean\nimport com.metrolist.music.lyrics.LyricsUtils.isKyrgyz\nimport com.metrolist.music.lyrics.LyricsUtils.isMacedonian\nimport com.metrolist.music.lyrics.LyricsUtils.isRussian\nimport com.metrolist.music.lyrics.LyricsUtils.isSerbian\nimport com.metrolist.music.lyrics.LyricsUtils.isUkrainian\nimport com.metrolist.music.lyrics.LyricsUtils.parseLyrics\nimport com.metrolist.music.lyrics.LyricsUtils.romanizeChinese\nimport com.metrolist.music.lyrics.LyricsUtils.romanizeCyrillic\nimport com.metrolist.music.lyrics.LyricsUtils.romanizeHindi\nimport com.metrolist.music.lyrics.LyricsUtils.romanizeJapanese\nimport com.metrolist.music.lyrics.LyricsUtils.romanizeKorean\nimport com.metrolist.music.ui.component.shimmer.ShimmerHost\nimport com.metrolist.music.ui.component.shimmer.TextPlaceholder\nimport com.metrolist.music.ui.screens.settings.DarkMode\nimport com.metrolist.music.ui.screens.settings.LyricsPosition\nimport com.metrolist.music.ui.screens.settings.defaultList\nimport com.metrolist.music.ui.utils.fadingEdge\nimport com.metrolist.music.utils.ComposeToImage\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport kotlin.time.Duration.Companion.seconds\n\n/**\n * A composable function that displays lyrics for the currently playing song.\n *\n * @param sliderPositionProvider Provides the current playback position in milliseconds.\n * @param modifier Modifier to be applied to the layout.\n * @param showLyrics Whether lyrics should be displayed.\n */\n@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)\n@SuppressLint(\"UnusedBoxWithConstraintsScope\", \"StringFormatInvalid\")\n@Composable\nfun Lyrics(\n    sliderPositionProvider: () -> Long?,\n    modifier: Modifier = Modifier,\n    showLyrics: Boolean,\n) {\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val database = LocalDatabase.current\n    val menuState = LocalMenuState.current\n    val density = LocalDensity.current\n    val context = LocalContext.current\n    val configuration = LocalWindowInfo.current\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val isGuest = listenTogetherManager?.isInRoom == true && !listenTogetherManager.isHost\n    val shareLyricsStr = stringResource(R.string.share_lyrics)\n    val failedToCreateImageTemplate = stringResource(R.string.failed_to_create_image)\n\n    val lyricsTextPosition by rememberEnumPreference(LyricsTextPositionKey, LyricsPosition.CENTER)\n    val changeLyrics by rememberPreference(LyricsClickKey, true)\n    val scrollLyrics by rememberPreference(LyricsScrollKey, true)\n    val romanizeLyricsList = rememberPreference(LyricsRomanizeList, \"\")\n    val romanizeAsMain by rememberPreference(LyricsRomanizeAsMainKey, false)\n    val romanizeCyrillicByLine by rememberPreference(LyricsRomanizeCyrillicByLineKey, false)\n    val lyricsGlowEffect by rememberPreference(LyricsGlowEffectKey, false)\n    val lyricsAnimationStyle by rememberEnumPreference(LyricsAnimationStyleKey, LyricsAnimationStyle.APPLE)\n    val lyricsTextSize by rememberPreference(LyricsTextSizeKey, 24f)\n    val lyricsLineSpacing by rememberPreference(LyricsLineSpacingKey, 1.3f)\n\n    val openRouterApiKey by rememberPreference(OpenRouterApiKey, \"\")\n    val deeplApiKey by rememberPreference(DeeplApiKey, \"\")\n    val aiProvider by rememberPreference(AiProviderKey, \"OpenRouter\")\n    val openRouterBaseUrl by rememberPreference(OpenRouterBaseUrlKey, \"https://openrouter.ai/api/v1/chat/completions\")\n    val openRouterModel by rememberPreference(OpenRouterModelKey, \"google/gemini-2.5-flash-lite\")\n    val translateLanguage by rememberPreference(TranslateLanguageKey, \"en\")\n    val translateMode by rememberPreference(TranslateModeKey, \"Literal\")\n    val deeplFormality by rememberPreference(DeeplFormalityKey, \"default\")\n    val aiSystemPrompt by rememberPreference(AiSystemPromptKey, \"\")\n\n    val scope = rememberCoroutineScope()\n\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n    val lyricsEntity by playerConnection.currentLyrics.collectAsState(initial = null)\n    val currentSong by playerConnection.currentSong.collectAsState(initial = null)\n    val lyrics = remember(lyricsEntity) { lyricsEntity?.lyrics?.trim() }\n\n    val playerBackground by rememberEnumPreference(\n        key = PlayerBackgroundStyleKey,\n        defaultValue = PlayerBackgroundStyle.DEFAULT,\n    )\n\n    val darkTheme by rememberEnumPreference(DarkModeKey, defaultValue = DarkMode.AUTO)\n    val isSystemInDarkTheme = isSystemInDarkTheme()\n    val useDarkTheme =\n        remember(darkTheme, isSystemInDarkTheme) {\n            if (darkTheme == DarkMode.AUTO) isSystemInDarkTheme else darkTheme == DarkMode.ON\n        }\n\n    val decodedList =\n        if (romanizeLyricsList.value.isEmpty()) {\n            defaultList\n        } else {\n            romanizeLyricsList.value.split(\",\").map { entry ->\n                val (lang, checked) = entry.split(\":\")\n                Pair(lang, checked.toBoolean())\n            }\n        }\n\n    val enabledLanguages = decodedList.filter { (_, checked) -> checked }.map { (lang, _) -> lang }\n\n    val lines =\n        remember(lyrics, scope) {\n            if (lyrics == null || lyrics == LYRICS_NOT_FOUND) {\n                emptyList()\n            } else if (lyrics.startsWith(\"[\")) {\n                val parsedLines = parseLyrics(lyrics)\n\n                parsedLines\n                    .map { entry ->\n                        val newEntry =\n                            LyricsEntry(entry.time, entry.text, entry.words, agent = entry.agent, isBackground = entry.isBackground)\n\n                        scope.launch {\n                            val text = if (romanizeCyrillicByLine) entry.text else lyrics\n                            var value: String? = \"\"\n\n                            when {\n                                \"Japanese\" in enabledLanguages && isJapanese(text) && !isChinese(text) -> {\n                                    value =\n                                        romanizeJapanese(entry.text)\n                                }\n\n                                \"Korean\" in enabledLanguages && isKorean(text) -> {\n                                    value = romanizeKorean(entry.text)\n                                }\n\n                                \"Chinese\" in enabledLanguages && isChinese(text) -> {\n                                    value = romanizeChinese(entry.text)\n                                }\n\n                                \"Hindi\" in enabledLanguages && isHindi(text) -> {\n                                    value = romanizeHindi(entry.text)\n                                }\n\n                                \"Ukrainian\" in enabledLanguages && isUkrainian(text) -> {\n                                    value = romanizeCyrillic(entry.text)\n                                }\n\n                                \"Russian\" in enabledLanguages && isRussian(text) -> {\n                                    value = romanizeCyrillic(entry.text)\n                                }\n\n                                \"Serbian\" in enabledLanguages && isSerbian(text) -> {\n                                    value = romanizeCyrillic(entry.text)\n                                }\n\n                                \"Bulgarian\" in enabledLanguages && isBulgarian(text) -> {\n                                    value = romanizeCyrillic(entry.text)\n                                }\n\n                                \"Belarusian\" in enabledLanguages && isBelarusian(text) -> {\n                                    value = romanizeCyrillic(entry.text)\n                                }\n\n                                \"Kyrgyz\" in enabledLanguages && isKyrgyz(text) -> {\n                                    value = romanizeCyrillic(entry.text)\n                                }\n\n                                \"Macedonian\" in enabledLanguages && isMacedonian(text) -> {\n                                    value = romanizeCyrillic(entry.text)\n                                }\n                            }\n\n                            newEntry.romanizedTextFlow.value = value\n                        }\n\n                        newEntry\n                    }.let {\n                        listOf(LyricsEntry.HEAD_LYRICS_ENTRY) + it\n                    }\n            } else {\n                lyrics.lines().mapIndexed { index, line ->\n                    val newEntry = LyricsEntry(index * 100L, line)\n\n                    scope.launch {\n                        val text = if (romanizeCyrillicByLine) line else lyrics\n                        var value = newEntry.romanizedTextFlow.value\n\n                        when {\n                            \"Japanese\" in enabledLanguages && isJapanese(text) && !isChinese(text) -> value = romanizeJapanese(line)\n                            \"Korean\" in enabledLanguages && isKorean(text) -> value = romanizeKorean(line)\n                            \"Chinese\" in enabledLanguages && isChinese(text) -> value = romanizeChinese(line)\n                            \"Hindi\" in enabledLanguages && isHindi(text) -> value = romanizeHindi(line)\n                            \"Ukrainian\" in enabledLanguages && isUkrainian(text) -> value = romanizeCyrillic(line)\n                            \"Russian\" in enabledLanguages && isRussian(text) -> value = romanizeCyrillic(line)\n                            \"Serbian\" in enabledLanguages && isSerbian(text) -> value = romanizeCyrillic(line)\n                            \"Bulgarian\" in enabledLanguages && isBulgarian(text) -> value = romanizeCyrillic(line)\n                            \"Belarusian\" in enabledLanguages && isBelarusian(text) -> value = romanizeCyrillic(line)\n                            \"Kyrgyz\" in enabledLanguages && isKyrgyz(text) -> value = romanizeCyrillic(line)\n                            \"Macedonian\" in enabledLanguages && isMacedonian(text) -> value = romanizeCyrillic(line)\n                        }\n\n                        newEntry.romanizedTextFlow.value = value\n                    }\n\n                    newEntry\n                }\n            }\n        }\n    val isSynced =\n        remember(lyrics) {\n            !lyrics.isNullOrEmpty() && lyrics.startsWith(\"[\")\n        }\n\n    // State for translation status\n    val translationStatus by LyricsTranslationHelper.status.collectAsState()\n\n    // Track composition lifecycle\n    DisposableEffect(Unit) {\n        LyricsTranslationHelper.setCompositionActive(true)\n        onDispose {\n            LyricsTranslationHelper.setCompositionActive(false)\n            LyricsTranslationHelper.cancelTranslation()\n        }\n    }\n\n    // Load translations from database on initial display\n    LaunchedEffect(lines, lyricsEntity, translateLanguage, translateMode) {\n        if (lines.isNotEmpty() && lyricsEntity != null) {\n            LyricsTranslationHelper.loadTranslationsFromDatabase(\n                lyrics = lines,\n                lyricsEntity = lyricsEntity,\n                targetLanguage = translateLanguage,\n                mode = translateMode,\n            )\n        }\n    }\n\n    val aiApiKeyRequiredStr = stringResource(R.string.ai_api_key_required)\n\n    // Listen for manual trigger\n    LaunchedEffect(showLyrics, lines.size) {\n        LyricsTranslationHelper.manualTrigger.collect {\n            val effectiveApiKey = if (aiProvider == \"DeepL\") deeplApiKey else openRouterApiKey\n            if (showLyrics && lines.isNotEmpty() && effectiveApiKey.isNotBlank()) {\n                LyricsTranslationHelper.translateLyrics(\n                    lyrics = lines,\n                    targetLanguage = translateLanguage,\n                    apiKey = openRouterApiKey,\n                    baseUrl = openRouterBaseUrl,\n                    model = openRouterModel,\n                    mode = translateMode,\n                    scope = scope,\n                    context = context,\n                    provider = aiProvider,\n                    deeplApiKey = deeplApiKey,\n                    deeplFormality = deeplFormality,\n                    useStreaming = true,\n                    songId = currentSong?.id ?: \"\",\n                    database = database,\n                    systemPrompt = aiSystemPrompt,\n                )\n            } else if (effectiveApiKey.isBlank()) {\n                Toast.makeText(context, aiApiKeyRequiredStr, Toast.LENGTH_SHORT).show()\n            }\n        }\n    }\n\n    // Listen for clear translations trigger\n    LaunchedEffect(Unit) {\n        LyricsTranslationHelper.clearTranslationsTrigger.collect {\n            lines.forEach { it.translatedTextFlow.value = null }\n        }\n    }\n\n    // Use Material 3 expressive accents and keep glow/text colors unified\n    val expressiveAccent =\n        when (playerBackground) {\n            PlayerBackgroundStyle.DEFAULT -> {\n                MaterialTheme.colorScheme.primary\n            }\n\n            PlayerBackgroundStyle.BLUR, PlayerBackgroundStyle.GRADIENT -> {\n                // For blur/gradient backgrounds, always use light colors regardless of theme\n                Color.White\n            }\n        }\n\n    var currentLineIndex by remember {\n        mutableIntStateOf(-1)\n    }\n    var currentPlaybackPosition by remember {\n        mutableLongStateOf(0L)\n    }\n    // Because LaunchedEffect has delay, which leads to inconsistent with current line color and scroll animation,\n    // we use deferredCurrentLineIndex when user is scrolling\n    var deferredCurrentLineIndex by rememberSaveable {\n        mutableIntStateOf(0)\n    }\n\n    var previousLineIndex by rememberSaveable {\n        mutableIntStateOf(0)\n    }\n\n    var lastPreviewTime by rememberSaveable {\n        mutableLongStateOf(0L)\n    }\n    var isSeeking by remember {\n        mutableStateOf(false)\n    }\n\n    var initialScrollDone by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    var shouldScrollToFirstLine by rememberSaveable {\n        mutableStateOf(true)\n    }\n\n    var isAppMinimized by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    var showProgressDialog by remember { mutableStateOf(false) }\n    var showShareDialog by remember { mutableStateOf(false) }\n    var shareDialogData by remember { mutableStateOf<Triple<String, String, String>?>(null) }\n\n    var showColorPickerDialog by remember { mutableStateOf(false) }\n    var previewBackgroundColor by remember { mutableStateOf(Color(0xFF242424)) }\n    var previewTextColor by remember { mutableStateOf(Color.White) }\n    var previewSecondaryTextColor by remember { mutableStateOf(Color.White.copy(alpha = 0.7f)) }\n\n    // State for multi-selection\n    var isSelectionModeActive by rememberSaveable { mutableStateOf(false) }\n    val selectedIndices = remember { mutableStateListOf<Int>() }\n    var showMaxSelectionToast by remember { mutableStateOf(false) } // State for showing max selection toast\n\n    val isLyricsProviderShown = lyricsEntity?.provider != null && lyricsEntity?.provider != \"Unknown\" && !isSelectionModeActive\n\n    val lazyListState = rememberLazyListState()\n\n    // Professional animation states for smooth Metrolist-style transitions\n    var isAnimating by remember { mutableStateOf(false) }\n    var isAutoScrollEnabled by rememberSaveable { mutableStateOf(true) }\n\n    // Handle back button press - close selection mode instead of exiting screen\n    BackHandler(enabled = isSelectionModeActive) {\n        isSelectionModeActive = false\n        selectedIndices.clear()\n    }\n\n    // Define max selection limit\n    val maxSelectionLimit = 5\n    val maxSelectionLimitMsg = stringResource(R.string.max_selection_limit, maxSelectionLimit)\n\n    // Show toast when max selection is reached\n    LaunchedEffect(showMaxSelectionToast) {\n        if (showMaxSelectionToast) {\n            Toast\n                .makeText(\n                    context,\n                    maxSelectionLimitMsg,\n                    Toast.LENGTH_SHORT,\n                ).show()\n            showMaxSelectionToast = false\n        }\n    }\n\n    val lifecycleOwner = LocalLifecycleOwner.current\n\n    // Keep screen on while lyrics are visible\n    DisposableEffect(showLyrics) {\n        val activity = context as? Activity\n        if (showLyrics) {\n            activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)\n        }\n        onDispose {\n            activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)\n        }\n    }\n\n    DisposableEffect(lifecycleOwner) {\n        val observer =\n            LifecycleEventObserver { _, event ->\n                if (event == Lifecycle.Event.ON_STOP) {\n                    val visibleItemsInfo = lazyListState.layoutInfo.visibleItemsInfo\n                    val isCurrentLineVisible = visibleItemsInfo.any { it.index == currentLineIndex }\n                    if (isCurrentLineVisible) {\n                        initialScrollDone = false\n                    }\n                    isAppMinimized = true\n                } else if (event == Lifecycle.Event.ON_START) {\n                    isAppMinimized = false\n                }\n            }\n        lifecycleOwner.lifecycle.addObserver(observer)\n        onDispose {\n            lifecycleOwner.lifecycle.removeObserver(observer)\n        }\n    }\n\n    // Reset selection mode if lyrics change\n    LaunchedEffect(lines) {\n        isSelectionModeActive = false\n        selectedIndices.clear()\n    }\n\n    LaunchedEffect(lyrics) {\n        if (lyrics.isNullOrEmpty() || !lyrics.startsWith(\"[\")) {\n            currentLineIndex = -1\n            return@LaunchedEffect\n        }\n        while (isActive) {\n            delay(8) // Faster update for word-by-word animation\n            val sliderPosition = sliderPositionProvider()\n            isSeeking = sliderPosition != null\n            val position = sliderPosition ?: playerConnection.player.currentPosition\n            currentPlaybackPosition = position\n            val lyricsOffset = currentSong?.song?.lyricsOffset ?: 0\n            currentLineIndex = findCurrentLineIndex(lines, position + lyricsOffset)\n        }\n    }\n\n    LaunchedEffect(isSeeking, lastPreviewTime) {\n        if (isSeeking) {\n            lastPreviewTime = 0L\n        } else if (lastPreviewTime != 0L) {\n            delay(LyricsPreviewTime)\n            lastPreviewTime = 0L\n        }\n    }\n\n    /**\n     * Smoothly scrolls the lyrics list to center the item at [targetIndex].\n     *\n     * @param targetIndex The index of the lyrics line to scroll to.\n     * @param duration The duration of the scroll animation in milliseconds.\n     */\n    suspend fun performSmoothPageScroll(\n        targetIndex: Int,\n        duration: Int = 1500,\n    ) {\n        if (isAnimating) return // Prevent multiple animations\n        isAnimating = true\n        try {\n            val lookUpIndex = if (isLyricsProviderShown) targetIndex + 1 else targetIndex\n            val itemInfo = lazyListState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == lookUpIndex }\n            if (itemInfo != null) {\n                // Item is visible, animate directly to center without sudden jumps\n                val viewportHeight = lazyListState.layoutInfo.viewportEndOffset - lazyListState.layoutInfo.viewportStartOffset\n                val center = lazyListState.layoutInfo.viewportStartOffset + (viewportHeight / 2)\n                val itemCenter = itemInfo.offset + itemInfo.size / 2\n                val offset = itemCenter - center\n                if (kotlin.math.abs(offset) > 10) {\n                    lazyListState.animateScrollBy(\n                        value = offset.toFloat(),\n                        animationSpec = tween(durationMillis = duration),\n                    )\n                }\n            } else {\n                // Item is not visible, scroll to it first without animation, then it will be handled in next cycle\n                lazyListState.scrollToItem(targetIndex)\n            }\n        } finally {\n            isAnimating = false\n        }\n    }\n    LaunchedEffect(currentLineIndex, lastPreviewTime, initialScrollDone, isAutoScrollEnabled) {\n        if (!isSynced) return@LaunchedEffect\n        if (isAutoScrollEnabled) {\n            if ((currentLineIndex == 0 && shouldScrollToFirstLine) || !initialScrollDone) {\n                shouldScrollToFirstLine = false\n                // Initial scroll to center the first line with medium animation (600ms)\n                val initialCenterIndex = kotlin.math.max(0, currentLineIndex)\n                performSmoothPageScroll(initialCenterIndex, 800) // Initial scroll duration\n                if (!isAppMinimized) {\n                    initialScrollDone = true\n                }\n            } else if (currentLineIndex != -1) {\n                deferredCurrentLineIndex = currentLineIndex\n                if (isSeeking) {\n                    // Fast scroll for seeking to center the target line (300ms)\n                    val seekCenterIndex = kotlin.math.max(0, currentLineIndex)\n                    performSmoothPageScroll(seekCenterIndex, 500) // Fast seek duration\n                } else if ((lastPreviewTime == 0L || currentLineIndex != previousLineIndex) && scrollLyrics) {\n                    // Auto-scroll when lyrics settings allow it\n                    if (currentLineIndex != previousLineIndex) {\n                        // Calculate which line should be at the top to center the active group\n                        val centerTargetIndex = currentLineIndex\n                        performSmoothPageScroll(centerTargetIndex, 1500) // Auto scroll duration\n                    }\n                }\n            }\n        }\n        if (currentLineIndex > 0) {\n            shouldScrollToFirstLine = true\n        }\n        previousLineIndex = currentLineIndex\n    }\n\n    BoxWithConstraints(\n        contentAlignment = Alignment.TopCenter,\n        modifier =\n            modifier\n                .fillMaxSize()\n                .padding(bottom = 12.dp),\n    ) {\n        // Status UI for translation\n        Box(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .zIndex(1f)\n                    .padding(top = 56.dp),\n            contentAlignment = Alignment.Center,\n        ) {\n            when (val status = translationStatus) {\n                is LyricsTranslationHelper.TranslationStatus.Translating -> {\n                    Card(\n                        colors =\n                            CardDefaults.cardColors(\n                                containerColor = MaterialTheme.colorScheme.primaryContainer,\n                            ),\n                        shape = RoundedCornerShape(16.dp),\n                        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),\n                    ) {\n                        Row(\n                            modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),\n                            verticalAlignment = Alignment.CenterVertically,\n                            horizontalArrangement = Arrangement.spacedBy(8.dp),\n                        ) {\n                            androidx.compose.material3.CircularProgressIndicator(\n                                modifier = Modifier.size(16.dp),\n                                strokeWidth = 2.dp,\n                                color = MaterialTheme.colorScheme.onPrimaryContainer,\n                            )\n                            Text(\n                                text = stringResource(R.string.ai_translating_lyrics),\n                                style = MaterialTheme.typography.labelMedium,\n                                color = MaterialTheme.colorScheme.onPrimaryContainer,\n                            )\n                        }\n                    }\n                }\n\n                is LyricsTranslationHelper.TranslationStatus.Error -> {\n                    Card(\n                        colors =\n                            CardDefaults.cardColors(\n                                containerColor = MaterialTheme.colorScheme.errorContainer,\n                            ),\n                        shape = RoundedCornerShape(16.dp),\n                        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),\n                    ) {\n                        Row(\n                            modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),\n                            verticalAlignment = Alignment.CenterVertically,\n                            horizontalArrangement = Arrangement.spacedBy(8.dp),\n                        ) {\n                            Icon(\n                                painter = painterResource(R.drawable.error),\n                                contentDescription = null,\n                                tint = MaterialTheme.colorScheme.onErrorContainer,\n                                modifier = Modifier.size(16.dp),\n                            )\n                            Text(\n                                text = status.message,\n                                style = MaterialTheme.typography.labelMedium,\n                                color = MaterialTheme.colorScheme.onErrorContainer,\n                            )\n                        }\n                    }\n                }\n\n                is LyricsTranslationHelper.TranslationStatus.Success -> {\n                    Card(\n                        colors =\n                            CardDefaults.cardColors(\n                                containerColor = MaterialTheme.colorScheme.tertiaryContainer,\n                            ),\n                        shape = RoundedCornerShape(16.dp),\n                        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),\n                    ) {\n                        Row(\n                            modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),\n                            verticalAlignment = Alignment.CenterVertically,\n                            horizontalArrangement = Arrangement.spacedBy(8.dp),\n                        ) {\n                            Icon(\n                                painter = painterResource(R.drawable.check),\n                                contentDescription = null,\n                                tint = MaterialTheme.colorScheme.onTertiaryContainer,\n                                modifier = Modifier.size(16.dp),\n                            )\n                            Text(\n                                text = stringResource(R.string.ai_lyrics_translated),\n                                style = MaterialTheme.typography.labelMedium,\n                                color = MaterialTheme.colorScheme.onTertiaryContainer,\n                            )\n                        }\n                    }\n                }\n\n                is LyricsTranslationHelper.TranslationStatus.Idle -> {\n                    // No status display\n                }\n            }\n        }\n\n        if (lyrics == LYRICS_NOT_FOUND) {\n            Box(\n                modifier = Modifier.fillMaxSize(),\n                contentAlignment = Alignment.Center,\n            ) {\n                Text(\n                    text = stringResource(R.string.lyrics_not_found),\n                    fontSize = 20.sp,\n                    color = MaterialTheme.colorScheme.secondary,\n                    textAlign = TextAlign.Center,\n                    fontWeight = FontWeight.Bold,\n                    modifier = Modifier.alpha(0.5f),\n                )\n            }\n        } else {\n            LazyColumn(\n                state = lazyListState,\n                contentPadding =\n                    WindowInsets.systemBars\n                        .only(WindowInsetsSides.Top)\n                        .add(WindowInsets(top = maxHeight / 3, bottom = maxHeight / 2))\n                        .asPaddingValues(),\n                modifier =\n                    Modifier\n                        .fadingEdge(vertical = 64.dp)\n                        .nestedScroll(\n                            remember {\n                                object : NestedScrollConnection {\n                                    override fun onPostScroll(\n                                        consumed: Offset,\n                                        available: Offset,\n                                        source: NestedScrollSource,\n                                    ): Offset {\n                                        if (source == NestedScrollSource.UserInput) {\n                                            isAutoScrollEnabled = false\n                                        }\n                                        if (!isSelectionModeActive) { // Only update preview time if not selecting\n                                            lastPreviewTime = System.currentTimeMillis()\n                                        }\n                                        return super.onPostScroll(consumed, available, source)\n                                    }\n\n                                    override suspend fun onPostFling(\n                                        consumed: Velocity,\n                                        available: Velocity,\n                                    ): Velocity {\n                                        isAutoScrollEnabled = false\n                                        if (!isSelectionModeActive) { // Only update preview time if not selecting\n                                            lastPreviewTime = System.currentTimeMillis()\n                                        }\n                                        return super.onPostFling(consumed, available)\n                                    }\n                                }\n                            },\n                        ),\n            ) {\n                val displayedCurrentLineIndex =\n                    if (!isAutoScrollEnabled) {\n                        currentLineIndex\n                    } else {\n                        if (isSeeking || isSelectionModeActive) deferredCurrentLineIndex else currentLineIndex\n                    }\n\n                // Show lyrics provider at the top, scrolling with content\n                if (isLyricsProviderShown) {\n                    item {\n                        Text(\n                            text = \"Lyrics from ${lyricsEntity?.provider}\",\n                            fontSize = 12.sp,\n                            color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),\n                            fontWeight = FontWeight.Medium,\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .padding(horizontal = 12.dp, vertical = 8.dp),\n                        )\n                    }\n                }\n\n                if (lyrics == null) {\n                    item {\n                        ShimmerHost {\n                            repeat(10) {\n                                Box(\n                                    contentAlignment =\n                                        when (lyricsTextPosition) {\n                                            LyricsPosition.LEFT -> Alignment.CenterStart\n                                            LyricsPosition.CENTER -> Alignment.Center\n                                            LyricsPosition.RIGHT -> Alignment.CenterEnd\n                                        },\n                                    modifier =\n                                        Modifier\n                                            .fillMaxWidth()\n                                            .padding(horizontal = 24.dp, vertical = 4.dp),\n                                ) {\n                                    TextPlaceholder()\n                                }\n                            }\n                        }\n                    }\n                } else {\n                    val lyricsOffset = currentSong?.song?.lyricsOffset?.toLong() ?: 0L\n                    val effectivePlaybackPosition = currentPlaybackPosition + lyricsOffset\n\n                    itemsIndexed(\n                        items = lines,\n                        key = { index, item -> \"$index-${item.time}\" }, // Add stable key\n                    ) { index, item ->\n                        val isSelected = selectedIndices.contains(index)\n                        val itemModifier =\n                            Modifier\n                                .fillMaxWidth()\n                                .clip(RoundedCornerShape(8.dp)) // Clip for background\n                                .combinedClickable(\n                                    enabled = true,\n                                    onClick = {\n                                        if (isSelectionModeActive) {\n                                            // Toggle selection\n                                            if (isSelected) {\n                                                selectedIndices.remove(index)\n                                                if (selectedIndices.isEmpty()) {\n                                                    isSelectionModeActive =\n                                                        false // Exit mode if last item deselected\n                                                }\n                                            } else {\n                                                if (selectedIndices.size < maxSelectionLimit) {\n                                                    selectedIndices.add(index)\n                                                } else {\n                                                    showMaxSelectionToast = true\n                                                }\n                                            }\n                                        } else if (isSynced && changeLyrics && !isGuest) {\n                                            // Professional seek action with smooth animation\n                                            val lyricsOffset = currentSong?.song?.lyricsOffset ?: 0\n                                            playerConnection.seekTo((item.time - lyricsOffset).coerceAtLeast(0))\n                                            // Smooth slow scroll when clicking on lyrics (3 seconds)\n                                            scope.launch {\n                                                // First scroll to the clicked item without animation\n                                                lazyListState.scrollToItem(index = index)\n\n                                                // Then animate it to center position slowly\n                                                val itemInfo =\n                                                    lazyListState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == index }\n                                                if (itemInfo != null) {\n                                                    val viewportHeight =\n                                                        lazyListState.layoutInfo.viewportEndOffset -\n                                                            lazyListState.layoutInfo.viewportStartOffset\n                                                    val center =\n                                                        lazyListState.layoutInfo.viewportStartOffset + (viewportHeight / 2)\n                                                    val itemCenter = itemInfo.offset + itemInfo.size / 2\n                                                    val offset = itemCenter - center\n\n                                                    if (kotlin.math.abs(offset) > 10) { // Only animate if not already centered\n                                                        lazyListState.animateScrollBy(\n                                                            value = offset.toFloat(),\n                                                            animationSpec = tween(durationMillis = 1500), // Reduced to half speed\n                                                        )\n                                                    }\n                                                }\n                                            }\n                                            lastPreviewTime = 0L\n                                        }\n                                    },\n                                    onLongClick = {\n                                        if (!isSelectionModeActive) {\n                                            isSelectionModeActive = true\n                                            selectedIndices.add(index)\n                                        } else if (!isSelected && selectedIndices.size < maxSelectionLimit) {\n                                            // If already in selection mode and item not selected, add it if below limit\n                                            selectedIndices.add(index)\n                                        } else if (!isSelected) {\n                                            // If already at limit, show toast\n                                            showMaxSelectionToast = true\n                                        }\n                                    },\n                                ).background(\n                                    if (isSelected && isSelectionModeActive) {\n                                        MaterialTheme.colorScheme.primary.copy(\n                                            alpha = 0.3f,\n                                        )\n                                    } else {\n                                        Color.Transparent\n                                    },\n                                ).padding(horizontal = 24.dp, vertical = 8.dp)\n\n                        // Check if this line shares the same time as the currently active line\n                        // This enables synchronized word-by-word animation for both main and background vocals\n                        val currentLineTime =\n                            if (displayedCurrentLineIndex >= 0 && displayedCurrentLineIndex < lines.size) {\n                                lines[displayedCurrentLineIndex].time\n                            } else {\n                                -1L\n                            }\n                        val isLineAtSameTime = item.time == currentLineTime\n                        val isActiveByIndex = index == displayedCurrentLineIndex\n                        val isActiveByTime = isLineAtSameTime && displayedCurrentLineIndex >= 0\n\n                        val alpha by animateFloatAsState(\n                            targetValue =\n                                when {\n                                    !isSynced || (isSelectionModeActive && isSelected) -> 1f\n                                    isActiveByIndex || isActiveByTime -> 1f\n                                    else -> 0.5f\n                                },\n                            animationSpec = tween(durationMillis = 400),\n                        )\n                        val scale by animateFloatAsState(\n                            targetValue = if (isActiveByIndex || isActiveByTime) 1.05f else 1f,\n                            animationSpec = tween(durationMillis = 400),\n                        )\n\n                        // Determine alignment based on agent for multi-singer support\n                        val agentAlignment =\n                            when {\n                                item.isBackground -> {\n                                    Alignment.CenterHorizontally\n                                }\n\n                                // Background always centered\n                                item.agent == \"v1\" -> {\n                                    Alignment.Start\n                                }\n\n                                // First vocalist - left\n                                item.agent == \"v2\" -> {\n                                    Alignment.End\n                                }\n\n                                // Second vocalist - right\n                                item.agent == \"v1000\" -> {\n                                    Alignment.CenterHorizontally\n                                }\n\n                                // Group/chorus - center\n                                else -> {\n                                    when (lyricsTextPosition) {\n                                        LyricsPosition.LEFT -> Alignment.Start\n                                        LyricsPosition.CENTER -> Alignment.CenterHorizontally\n                                        LyricsPosition.RIGHT -> Alignment.End\n                                    }\n                                }\n                            }\n\n                        val agentTextAlign =\n                            when {\n                                item.isBackground -> {\n                                    TextAlign.Center\n                                }\n\n                                item.agent == \"v1\" -> {\n                                    TextAlign.Left\n                                }\n\n                                item.agent == \"v2\" -> {\n                                    TextAlign.Right\n                                }\n\n                                item.agent == \"v1000\" -> {\n                                    TextAlign.Center\n                                }\n\n                                else -> {\n                                    when (lyricsTextPosition) {\n                                        LyricsPosition.LEFT -> TextAlign.Left\n                                        LyricsPosition.CENTER -> TextAlign.Center\n                                        LyricsPosition.RIGHT -> TextAlign.Right\n                                    }\n                                }\n                            }\n\n                        // Smaller scale for background vocals\n                        val bgScale = if (item.isBackground) 0.85f else 1f\n\n                        Column(\n                            modifier =\n                                itemModifier.graphicsLayer {\n                                    this.alpha = if (item.isBackground) alpha * 0.8f else alpha\n                                    this.scaleX = scale * bgScale\n                                    this.scaleY = scale * bgScale\n                                },\n                            horizontalAlignment = agentAlignment,\n                        ) {\n                            // Use time-based active check to sync both main and background lines with same timestamp\n                            val isActiveLine = (isActiveByIndex || isActiveByTime) && isSynced\n                            val lineColor =\n                                if (isActiveLine) {\n                                    if (item.isBackground) expressiveAccent.copy(alpha = 0.85f) else expressiveAccent\n                                } else {\n                                    expressiveAccent.copy(alpha = if (item.isBackground) 0.5f else 0.7f)\n                                }\n                            val alignment = agentTextAlign\n\n                            val romanizedTextState by item.romanizedTextFlow.collectAsState()\n                            val romanizedText = romanizedTextState\n                            val isRomanizedAvailable = romanizedText != null\n\n                            val mainText = if (romanizeAsMain && isRomanizedAvailable) romanizedText else item.text\n                            val subText = if (romanizeAsMain && isRomanizedAvailable) item.text else romanizedText\n\n                            val hasWordTimings = if (romanizeAsMain && isRomanizedAvailable) false else item.words?.isNotEmpty() == true\n\n                            // Word-by-word animation styles\n                            if (hasWordTimings && lyricsAnimationStyle == LyricsAnimationStyle.NONE) {\n                                val styledText =\n                                    buildAnnotatedString {\n                                        item.words?.forEachIndexed { wordIndex, word ->\n                                            val wordStartMs = (word.startTime * 1000).toLong()\n                                            val wordEndMs = (word.endTime * 1000).toLong()\n                                            val wordDuration = wordEndMs - wordStartMs\n\n                                            val isWordActive =\n                                                isActiveLine && effectivePlaybackPosition >= wordStartMs &&\n                                                    effectivePlaybackPosition <= wordEndMs\n                                            val hasWordPassed = isActiveLine && effectivePlaybackPosition > wordEndMs\n\n                                            val transitionProgress =\n                                                when {\n                                                    !isActiveLine -> {\n                                                        0f\n                                                    }\n\n                                                    hasWordPassed -> {\n                                                        1f\n                                                    }\n\n                                                    isWordActive && wordDuration > 0 -> {\n                                                        val elapsed = effectivePlaybackPosition - wordStartMs\n                                                        val linear = (elapsed.toFloat() / wordDuration).coerceIn(0f, 1f)\n                                                        linear * linear * (3f - 2f * linear)\n                                                    }\n\n                                                    else -> {\n                                                        0f\n                                                    }\n                                                }\n\n                                            val wordAlpha =\n                                                when {\n                                                    !isActiveLine -> 0.7f\n                                                    hasWordPassed -> 1f\n                                                    isWordActive -> 0.5f + (0.5f * transitionProgress)\n                                                    else -> 0.35f\n                                                }\n\n                                            val wordColor = expressiveAccent.copy(alpha = wordAlpha)\n                                            val wordWeight =\n                                                when {\n                                                    !isActiveLine -> FontWeight.Bold\n                                                    hasWordPassed -> FontWeight.Bold\n                                                    isWordActive -> FontWeight.ExtraBold\n                                                    else -> FontWeight.Medium\n                                                }\n\n                                            withStyle(style = SpanStyle(color = wordColor, fontWeight = wordWeight)) {\n                                                append(word.text)\n                                            }\n                                            if (wordIndex < item.words.size - 1) append(\" \")\n                                        }\n                                    }\n                                Text(\n                                    text = styledText,\n                                    fontSize = lyricsTextSize.sp,\n                                    textAlign = alignment,\n                                    lineHeight = (lyricsTextSize * lyricsLineSpacing).sp,\n                                )\n                            } else if (hasWordTimings && lyricsAnimationStyle == LyricsAnimationStyle.FADE) {\n                                val styledText =\n                                    buildAnnotatedString {\n                                        item.words?.forEachIndexed { wordIndex, word ->\n                                            val wordStartMs = (word.startTime * 1000).toLong()\n                                            val wordEndMs = (word.endTime * 1000).toLong()\n                                            val wordDuration = wordEndMs - wordStartMs\n\n                                            val isWordActive =\n                                                isActiveLine && effectivePlaybackPosition >= wordStartMs &&\n                                                    effectivePlaybackPosition <= wordEndMs\n                                            val hasWordPassed = isActiveLine && effectivePlaybackPosition > wordEndMs\n\n                                            val fadeProgress =\n                                                if (isWordActive && wordDuration > 0) {\n                                                    val timeElapsed = effectivePlaybackPosition - wordStartMs\n                                                    val linear = (timeElapsed.toFloat() / wordDuration.toFloat()).coerceIn(0f, 1f)\n                                                    // Smooth cubic easing\n                                                    linear * linear * (3f - 2f * linear)\n                                                } else if (hasWordPassed) {\n                                                    1f\n                                                } else {\n                                                    0f\n                                                }\n\n                                            val wordAlpha =\n                                                when {\n                                                    !isActiveLine -> 0.55f\n                                                    hasWordPassed -> 1f\n                                                    isWordActive -> 0.4f + (0.6f * fadeProgress)\n                                                    else -> 0.4f\n                                                }\n                                            val wordColor = expressiveAccent.copy(alpha = wordAlpha)\n                                            val wordWeight =\n                                                when {\n                                                    !isActiveLine -> FontWeight.Bold\n                                                    hasWordPassed -> FontWeight.Bold\n                                                    isWordActive -> FontWeight.ExtraBold\n                                                    else -> FontWeight.Medium\n                                                }\n                                            // Enhanced shadow for active words\n                                            val wordShadow =\n                                                when {\n                                                    isWordActive && fadeProgress > 0.2f -> {\n                                                        Shadow(\n                                                            color = expressiveAccent.copy(alpha = 0.35f * fadeProgress),\n                                                            offset = Offset.Zero,\n                                                            blurRadius = 10f * fadeProgress,\n                                                        )\n                                                    }\n\n                                                    hasWordPassed -> {\n                                                        Shadow(\n                                                            color = expressiveAccent.copy(alpha = 0.15f),\n                                                            offset = Offset.Zero,\n                                                            blurRadius = 6f,\n                                                        )\n                                                    }\n\n                                                    else -> {\n                                                        null\n                                                    }\n                                                }\n\n                                            withStyle(style = SpanStyle(color = wordColor, fontWeight = wordWeight, shadow = wordShadow)) {\n                                                append(word.text)\n                                            }\n                                            if (wordIndex < item.words.size - 1) append(\" \")\n                                        }\n                                    }\n                                Text(\n                                    text = styledText,\n                                    fontSize = lyricsTextSize.sp,\n                                    textAlign = alignment,\n                                    lineHeight = (lyricsTextSize * lyricsLineSpacing).sp,\n                                )\n                            } else if (hasWordTimings && lyricsAnimationStyle == LyricsAnimationStyle.GLOW) {\n                                val styledText =\n                                    buildAnnotatedString {\n                                        item.words?.forEachIndexed { wordIndex, word ->\n                                            val wordStartMs = (word.startTime * 1000).toLong()\n                                            val wordEndMs = (word.endTime * 1000).toLong()\n                                            val wordDuration = wordEndMs - wordStartMs\n\n                                            val isWordActive = isActiveLine && effectivePlaybackPosition in wordStartMs..wordEndMs\n                                            val hasWordPassed = isActiveLine && effectivePlaybackPosition > wordEndMs\n\n                                            val fillProgress =\n                                                if (isWordActive && wordDuration > 0) {\n                                                    val linear =\n                                                        ((effectivePlaybackPosition - wordStartMs).toFloat() / wordDuration)\n                                                            .coerceIn(\n                                                                0f,\n                                                                1f,\n                                                            )\n                                                    linear * linear * (3f - 2f * linear)\n                                                } else if (hasWordPassed) {\n                                                    1f\n                                                } else {\n                                                    0f\n                                                }\n\n                                            val glowIntensity = fillProgress * fillProgress\n                                            val brightness = 0.45f + (0.55f * fillProgress)\n\n                                            val wordColor =\n                                                when {\n                                                    !isActiveLine -> expressiveAccent.copy(alpha = 0.5f)\n                                                    isWordActive || hasWordPassed -> expressiveAccent.copy(alpha = brightness)\n                                                    else -> expressiveAccent.copy(alpha = 0.35f)\n                                                }\n                                            val wordWeight =\n                                                when {\n                                                    !isActiveLine -> FontWeight.Bold\n                                                    isWordActive -> FontWeight.ExtraBold\n                                                    hasWordPassed -> FontWeight.Bold\n                                                    else -> FontWeight.Medium\n                                                }\n                                            val wordShadow =\n                                                if (isWordActive && glowIntensity > 0.05f) {\n                                                    Shadow(\n                                                        color = expressiveAccent.copy(alpha = 0.5f + (0.3f * glowIntensity)),\n                                                        offset = Offset.Zero,\n                                                        blurRadius =\n                                                            16f + (12f * glowIntensity),\n                                                    )\n                                                } else if (hasWordPassed) {\n                                                    Shadow(\n                                                        color = expressiveAccent.copy(alpha = 0.25f),\n                                                        offset = Offset.Zero,\n                                                        blurRadius = 8f,\n                                                    )\n                                                } else {\n                                                    null\n                                                }\n\n                                            withStyle(style = SpanStyle(color = wordColor, fontWeight = wordWeight, shadow = wordShadow)) {\n                                                append(word.text)\n                                            }\n                                            if (wordIndex < item.words.size - 1) append(\" \")\n                                        }\n                                    }\n                                Text(\n                                    text = styledText,\n                                    fontSize = lyricsTextSize.sp,\n                                    textAlign = alignment,\n                                    lineHeight = (lyricsTextSize * lyricsLineSpacing).sp,\n                                )\n                            } else if (hasWordTimings && lyricsAnimationStyle == LyricsAnimationStyle.SLIDE) {\n                                val styledText =\n                                    buildAnnotatedString {\n                                        item.words?.forEachIndexed { wordIndex, word ->\n                                            val wordStartMs = (word.startTime * 1000).toLong()\n                                            val wordEndMs = (word.endTime * 1000).toLong()\n                                            val wordDuration = wordEndMs - wordStartMs\n\n                                            val isWordActive =\n                                                isActiveLine && effectivePlaybackPosition >= wordStartMs &&\n                                                    effectivePlaybackPosition < wordEndMs\n                                            val hasWordPassed =\n                                                (isActiveLine && effectivePlaybackPosition >= wordEndMs) ||\n                                                    (!isActiveLine && item.time < currentLineTime)\n\n                                            if (isWordActive && wordDuration > 0) {\n                                                val timeElapsed = effectivePlaybackPosition - wordStartMs\n                                                val fillProgress = (timeElapsed.toFloat() / wordDuration.toFloat()).coerceIn(0f, 1f)\n                                                val breatheValue = (timeElapsed % 3000) / 3000f\n                                                val breatheEffect =\n                                                    (\n                                                        kotlin.math.sin(\n                                                            breatheValue * Math.PI.toFloat() * 2f,\n                                                        ) * 0.03f\n                                                    ).coerceIn(0f, 0.03f)\n                                                val glowIntensity = (0.3f + fillProgress * 0.7f + breatheEffect).coerceIn(0f, 1.1f)\n\n                                                val slideBrush =\n                                                    Brush.horizontalGradient(\n                                                        0.0f to expressiveAccent,\n                                                        (fillProgress * 0.95f).coerceIn(0f, 1f) to expressiveAccent,\n                                                        fillProgress to expressiveAccent.copy(alpha = 0.9f),\n                                                        (fillProgress + 0.02f).coerceIn(0f, 1f) to expressiveAccent.copy(alpha = 0.5f),\n                                                        (fillProgress + 0.08f).coerceIn(0f, 1f) to expressiveAccent.copy(alpha = 0.35f),\n                                                        1.0f to expressiveAccent.copy(alpha = 0.35f),\n                                                    )\n\n                                                withStyle(\n                                                    style =\n                                                        SpanStyle(\n                                                            brush = slideBrush,\n                                                            fontWeight = FontWeight.ExtraBold,\n                                                            shadow =\n                                                                Shadow(\n                                                                    color = expressiveAccent.copy(alpha = 0.4f * glowIntensity),\n                                                                    offset = Offset(0f, 0f),\n                                                                    blurRadius =\n                                                                        14f + (4f * fillProgress),\n                                                                ),\n                                                        ),\n                                                ) {\n                                                    append(word.text)\n                                                }\n                                            } else if (hasWordPassed && isActiveLine) {\n                                                withStyle(\n                                                    style =\n                                                        SpanStyle(\n                                                            color = expressiveAccent,\n                                                            fontWeight = FontWeight.Bold,\n                                                            shadow =\n                                                                Shadow(\n                                                                    color = expressiveAccent.copy(alpha = 0.4f),\n                                                                    offset = Offset(0f, 0f),\n                                                                    blurRadius = 12f,\n                                                                ),\n                                                        ),\n                                                ) {\n                                                    append(word.text)\n                                                }\n                                            } else {\n                                                val wordColor = if (!isActiveLine) lineColor else expressiveAccent.copy(alpha = 0.35f)\n                                                withStyle(style = SpanStyle(color = wordColor, fontWeight = FontWeight.Medium)) {\n                                                    append(word.text)\n                                                }\n                                            }\n                                            if (wordIndex < item.words.size - 1) append(\" \")\n                                        }\n                                    }\n                                Text(\n                                    text = styledText,\n                                    fontSize = lyricsTextSize.sp,\n                                    textAlign = alignment,\n                                    lineHeight =\n                                        (\n                                            lyricsTextSize *\n                                                lyricsLineSpacing\n                                        ).sp,\n                                )\n                            } else if (hasWordTimings && lyricsAnimationStyle == LyricsAnimationStyle.KARAOKE) {\n                                val styledText =\n                                    buildAnnotatedString {\n                                        item.words?.forEachIndexed { wordIndex, word ->\n                                            val wordStartMs = (word.startTime * 1000).toLong()\n                                            val wordEndMs = (word.endTime * 1000).toLong()\n                                            val wordDuration = wordEndMs - wordStartMs\n\n                                            val isWordActive =\n                                                isActiveLine && effectivePlaybackPosition >= wordStartMs &&\n                                                    effectivePlaybackPosition < wordEndMs\n                                            val hasWordPassed =\n                                                (isActiveLine && effectivePlaybackPosition >= wordEndMs) ||\n                                                    (!isActiveLine && item.time < currentLineTime)\n\n                                            if (isWordActive && wordDuration > 0) {\n                                                val timeElapsed = effectivePlaybackPosition - wordStartMs\n                                                val linearProgress = (timeElapsed.toFloat() / wordDuration.toFloat()).coerceIn(0f, 1f)\n                                                // Smoother easing curve for more natural fill animation\n                                                val fillProgress = linearProgress * linearProgress * (3f - 2f * linearProgress)\n\n                                                // Enhanced glow intensity calculation\n                                                val glowIntensity = fillProgress * fillProgress\n\n                                                val wordBrush =\n                                                    Brush.horizontalGradient(\n                                                        0.0f to expressiveAccent.copy(alpha = 0.4f),\n                                                        (fillProgress * 0.6f).coerceIn(0f, 1f) to expressiveAccent.copy(alpha = 0.75f),\n                                                        (fillProgress * 0.85f).coerceIn(0f, 1f) to expressiveAccent.copy(alpha = 0.95f),\n                                                        fillProgress to expressiveAccent,\n                                                        (fillProgress + 0.03f).coerceIn(0f, 1f) to expressiveAccent.copy(alpha = 0.85f),\n                                                        (fillProgress + 0.1f).coerceIn(0f, 1f) to expressiveAccent.copy(alpha = 0.5f),\n                                                        1.0f to expressiveAccent.copy(alpha = if (fillProgress >= 0.9f) 0.95f else 0.4f),\n                                                    )\n\n                                                // Improved shadow with better glow effect\n                                                val wordShadow =\n                                                    Shadow(\n                                                        color = expressiveAccent.copy(alpha = 0.5f + (0.3f * glowIntensity)),\n                                                        offset = Offset.Zero,\n                                                        blurRadius = 16f + (12f * glowIntensity),\n                                                    )\n\n                                                withStyle(\n                                                    style =\n                                                        SpanStyle(\n                                                            brush = wordBrush,\n                                                            fontWeight = FontWeight.ExtraBold,\n                                                            shadow = wordShadow,\n                                                        ),\n                                                ) {\n                                                    append(word.text)\n                                                }\n                                            } else if (hasWordPassed && isActiveLine) {\n                                                // Completed words with subtle glow\n                                                withStyle(\n                                                    style =\n                                                        SpanStyle(\n                                                            color = expressiveAccent,\n                                                            fontWeight = FontWeight.Bold,\n                                                            shadow =\n                                                                Shadow(\n                                                                    color = expressiveAccent.copy(alpha = 0.25f),\n                                                                    offset = Offset.Zero,\n                                                                    blurRadius = 8f,\n                                                                ),\n                                                        ),\n                                                ) {\n                                                    append(word.text)\n                                                }\n                                            } else {\n                                                // Inactive words\n                                                val wordColor = if (!isActiveLine) lineColor else expressiveAccent.copy(alpha = 0.4f)\n                                                withStyle(style = SpanStyle(color = wordColor, fontWeight = FontWeight.Medium)) {\n                                                    append(word.text)\n                                                }\n                                            }\n                                            if (wordIndex < item.words.size - 1) append(\" \")\n                                        }\n                                    }\n                                Text(\n                                    text = styledText,\n                                    fontSize = lyricsTextSize.sp,\n                                    textAlign = alignment,\n                                    lineHeight =\n                                        (\n                                            lyricsTextSize *\n                                                lyricsLineSpacing\n                                        ).sp,\n                                )\n                            } else if (hasWordTimings && lyricsAnimationStyle == LyricsAnimationStyle.APPLE) {\n                                val styledText =\n                                    buildAnnotatedString {\n                                        item.words?.forEachIndexed { wordIndex, word ->\n                                            val wordStartMs = (word.startTime * 1000).toLong()\n                                            val wordEndMs = (word.endTime * 1000).toLong()\n                                            val wordDuration = wordEndMs - wordStartMs\n\n                                            val isWordActive =\n                                                isActiveLine && effectivePlaybackPosition >= wordStartMs &&\n                                                    effectivePlaybackPosition < wordEndMs\n                                            val hasWordPassed =\n                                                (isActiveLine && effectivePlaybackPosition >= wordEndMs) ||\n                                                    (!isActiveLine && item.time < currentLineTime)\n\n                                            val rawProgress =\n                                                if (isWordActive && wordDuration > 0) {\n                                                    val elapsed = effectivePlaybackPosition - wordStartMs\n                                                    (elapsed.toFloat() / wordDuration).coerceIn(0f, 1f)\n                                                } else if (hasWordPassed) {\n                                                    1f\n                                                } else {\n                                                    0f\n                                                }\n\n                                            // Smooth cubic easing for natural animation\n                                            val smoothProgress = rawProgress * rawProgress * (3f - 2f * rawProgress)\n\n                                            val wordAlpha =\n                                                when {\n                                                    !isActiveLine -> 0.55f\n                                                    hasWordPassed -> 1f\n                                                    isWordActive -> 0.55f + (0.45f * smoothProgress)\n                                                    else -> 0.4f\n                                                }\n                                            val wordColor = expressiveAccent.copy(alpha = wordAlpha)\n                                            val wordWeight =\n                                                when {\n                                                    !isActiveLine -> FontWeight.SemiBold\n                                                    hasWordPassed -> FontWeight.Bold\n                                                    isWordActive -> FontWeight.ExtraBold\n                                                    else -> FontWeight.Normal\n                                                }\n                                            // Enhanced shadow with better glow intensity\n                                            val glowIntensity = smoothProgress * smoothProgress\n                                            val wordShadow =\n                                                when {\n                                                    isWordActive -> {\n                                                        Shadow(\n                                                            color = expressiveAccent.copy(alpha = 0.2f + (0.4f * glowIntensity)),\n                                                            offset = Offset.Zero,\n                                                            blurRadius = 10f + (12f * glowIntensity),\n                                                        )\n                                                    }\n\n                                                    hasWordPassed && isActiveLine -> {\n                                                        Shadow(\n                                                            color = expressiveAccent.copy(alpha = 0.2f),\n                                                            offset = Offset.Zero,\n                                                            blurRadius = 8f,\n                                                        )\n                                                    }\n\n                                                    else -> {\n                                                        null\n                                                    }\n                                                }\n\n                                            withStyle(style = SpanStyle(color = wordColor, fontWeight = wordWeight, shadow = wordShadow)) {\n                                                append(word.text)\n                                            }\n                                            if (wordIndex < item.words.size - 1) append(\" \")\n                                        }\n                                    }\n                                Text(\n                                    text = styledText,\n                                    fontSize = lyricsTextSize.sp,\n                                    textAlign = alignment,\n                                    lineHeight =\n                                        (\n                                            lyricsTextSize *\n                                                lyricsLineSpacing\n                                        ).sp,\n                                )\n                            } else if (isActiveLine && lyricsGlowEffect) {\n                                // Initial animation for glow fill from left to right\n                                val fillProgress = remember { Animatable(0f) }\n                                // Continuous pulsing animation for the glow\n                                val pulseProgress = remember { Animatable(0f) }\n\n                                LaunchedEffect(index) {\n                                    fillProgress.snapTo(0f)\n                                    fillProgress.animateTo(\n                                        targetValue = 1f,\n                                        animationSpec =\n                                            tween(\n                                                durationMillis = 1200,\n                                                easing = FastOutSlowInEasing,\n                                            ),\n                                    )\n                                }\n\n                                // Continuous slow pulsing animation\n                                LaunchedEffect(Unit) {\n                                    while (true) {\n                                        pulseProgress.animateTo(\n                                            targetValue = 1f,\n                                            animationSpec =\n                                                tween(\n                                                    durationMillis = 3000,\n                                                    easing = LinearEasing,\n                                                ),\n                                        )\n                                        pulseProgress.snapTo(0f)\n                                    }\n                                }\n\n                                val fill = fillProgress.value\n                                val pulse = pulseProgress.value\n\n                                // Combine fill animation with subtle pulse\n                                val pulseEffect = (kotlin.math.sin(pulse * Math.PI.toFloat()) * 0.15f).coerceIn(0f, 0.15f)\n                                val glowIntensity = (fill + pulseEffect).coerceIn(0f, 1.2f)\n\n                                // Create left-to-right gradient fill with glow\n                                val glowBrush =\n                                    Brush.horizontalGradient(\n                                        0.0f to expressiveAccent.copy(alpha = 0.3f),\n                                        (fill * 0.7f).coerceIn(0f, 1f) to expressiveAccent.copy(alpha = 0.9f),\n                                        fill to expressiveAccent,\n                                        (fill + 0.1f).coerceIn(0f, 1f) to expressiveAccent.copy(alpha = 0.7f),\n                                        1.0f to expressiveAccent.copy(alpha = if (fill >= 1f) 1f else 0.3f),\n                                    )\n\n                                val styledText =\n                                    buildAnnotatedString {\n                                        withStyle(\n                                            style =\n                                                SpanStyle(\n                                                    shadow =\n                                                        Shadow(\n                                                            color = expressiveAccent.copy(alpha = 0.8f * glowIntensity),\n                                                            offset = Offset(0f, 0f),\n                                                            blurRadius = 28f * (1f + pulseEffect),\n                                                        ),\n                                                    brush = glowBrush,\n                                                ),\n                                        ) {\n                                            append(mainText)\n                                        }\n                                    }\n\n                                // Single smooth bounce animation\n                                val bounceScale =\n                                    if (fill < 0.3f) {\n                                        // Gentler rise during fill\n                                        1f + (kotlin.math.sin(fill * 3.33f * Math.PI.toFloat()) * 0.03f)\n                                    } else {\n                                        // Hold at normal scale\n                                        1f\n                                    }\n\n                                Text(\n                                    text = styledText,\n                                    fontSize = lyricsTextSize.sp,\n                                    textAlign = alignment,\n                                    fontWeight = FontWeight.ExtraBold,\n                                    lineHeight = (lyricsTextSize * lyricsLineSpacing).sp,\n                                    modifier =\n                                        Modifier\n                                            .graphicsLayer {\n                                                scaleX = bounceScale\n                                                scaleY = bounceScale\n                                            },\n                                )\n                            } else if (isActiveLine && !lyricsGlowEffect) {\n                                // Active line without glow effect - just bold text\n                                Text(\n                                    text = mainText,\n                                    fontSize = lyricsTextSize.sp,\n                                    color = expressiveAccent,\n                                    textAlign = alignment,\n                                    fontWeight = FontWeight.ExtraBold,\n                                    lineHeight = (lyricsTextSize * lyricsLineSpacing).sp,\n                                )\n                            } else {\n                                // Inactive line\n                                Text(\n                                    text = mainText,\n                                    fontSize = lyricsTextSize.sp,\n                                    color = lineColor,\n                                    textAlign = alignment,\n                                    fontWeight = FontWeight.Bold,\n                                    lineHeight = (lyricsTextSize * lyricsLineSpacing).sp,\n                                )\n                            }\n                            if (currentSong?.romanizeLyrics == true && enabledLanguages.isNotEmpty()) {\n                                // Show secondary text (romanized or original) if available\n                                subText?.let { text ->\n                                    Text(\n                                        text = text,\n                                        fontSize = 18.sp,\n                                        color = expressiveAccent.copy(alpha = 0.6f),\n                                        textAlign =\n                                            when (lyricsTextPosition) {\n                                                LyricsPosition.LEFT -> TextAlign.Left\n                                                LyricsPosition.CENTER -> TextAlign.Center\n                                                LyricsPosition.RIGHT -> TextAlign.Right\n                                            },\n                                        fontWeight = FontWeight.Normal,\n                                        modifier = Modifier.padding(top = 2.dp),\n                                    )\n                                }\n                            }\n\n                            // Show translated text if available\n                            val translatedText by item.translatedTextFlow.collectAsState()\n                            translatedText?.let { translated ->\n                                Text(\n                                    text = translated,\n                                    fontSize = 16.sp,\n                                    color = expressiveAccent.copy(alpha = 0.5f),\n                                    textAlign =\n                                        when (lyricsTextPosition) {\n                                            LyricsPosition.LEFT -> TextAlign.Left\n                                            LyricsPosition.CENTER -> TextAlign.Center\n                                            LyricsPosition.RIGHT -> TextAlign.Right\n                                        },\n                                    fontWeight = FontWeight.Normal,\n                                    modifier = Modifier.padding(top = 4.dp),\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n            // Action buttons are now in the bottom bar\n            // Removed the more button from bottom - it's now in the top header\n        }\n\n        AnimatedVisibility(\n            visible = isSelectionModeActive,\n            enter = slideInVertically { it } + fadeIn(),\n            exit = slideOutVertically { it } + fadeOut(),\n        ) {\n            AnimatedVisibility(\n                visible = !isAutoScrollEnabled && isSynced && !isSelectionModeActive,\n                enter = slideInVertically { it } + fadeIn(),\n                exit = slideOutVertically { it } + fadeOut(),\n            ) {\n                FilledTonalButton(onClick = {\n                    scope.launch {\n                        performSmoothPageScroll(currentLineIndex, 1500)\n                    }\n                    isAutoScrollEnabled = true\n                }) {\n                    Icon(\n                        painter = painterResource(id = R.drawable.sync),\n                        contentDescription = stringResource(R.string.auto_scroll),\n                        modifier = Modifier.size(20.dp),\n                    )\n                    Spacer(modifier = Modifier.width(8.dp))\n                    Text(text = stringResource(R.string.auto_scroll))\n                }\n            }\n\n            AnimatedVisibility(\n                visible = isSelectionModeActive,\n                enter = slideInVertically { it } + fadeIn(),\n                exit = slideOutVertically { it } + fadeOut(),\n            ) {\n                Row(\n                    horizontalArrangement = Arrangement.spacedBy(8.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    FilledTonalButton(\n                        onClick = {\n                            isSelectionModeActive = false\n                            selectedIndices.clear()\n                        },\n                    ) {\n                        Icon(\n                            painter = painterResource(id = R.drawable.close),\n                            contentDescription = stringResource(R.string.cancel),\n                            modifier = Modifier.size(20.dp),\n                        )\n                    }\n                    FilledTonalButton(\n                        onClick = {\n                            if (selectedIndices.isNotEmpty()) {\n                                val sortedIndices = selectedIndices.sorted()\n                                val selectedLyricsText =\n                                    sortedIndices\n                                        .mapNotNull { lines.getOrNull(it)?.text }\n                                        .joinToString(\"\\n\")\n\n                                if (selectedLyricsText.isNotBlank()) {\n                                    shareDialogData =\n                                        Triple(\n                                            selectedLyricsText,\n                                            mediaMetadata?.title ?: \"\",\n                                            mediaMetadata?.artists?.joinToString { it.name } ?: \"\",\n                                        )\n                                    showShareDialog = true\n                                }\n                                isSelectionModeActive = false\n                                selectedIndices.clear()\n                            }\n                        },\n                        enabled = selectedIndices.isNotEmpty(),\n                    ) {\n                        Icon(\n                            painter = painterResource(id = R.drawable.share),\n                            contentDescription = stringResource(R.string.share_selected),\n                            modifier = Modifier.size(20.dp),\n                        )\n                        Spacer(modifier = Modifier.width(8.dp))\n                        Text(text = stringResource(R.string.share))\n                    }\n                }\n            }\n        }\n\n        if (showProgressDialog) {\n            BasicAlertDialog(onDismissRequest = { /* Don't dismiss */ }) {\n                Card( // Use Card for better styling\n                    shape = MaterialTheme.shapes.medium,\n                    colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),\n                    elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),\n                ) {\n                    Box(modifier = Modifier.padding(32.dp)) {\n                        Text(\n                            text = stringResource(R.string.generating_image) + \"\\n\" + stringResource(R.string.please_wait),\n                            color = MaterialTheme.colorScheme.onSurface,\n                        )\n                    }\n                }\n            }\n        }\n\n        if (showShareDialog && shareDialogData != null) {\n            val (lyricsText, songTitle, artists) = shareDialogData!! // Renamed 'lyrics' to 'lyricsText' for clarity\n            BasicAlertDialog(onDismissRequest = { showShareDialog = false }) {\n                Card(\n                    shape = MaterialTheme.shapes.medium,\n                    elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),\n                    colors =\n                        CardDefaults.cardColors(\n                            containerColor = MaterialTheme.colorScheme.surface,\n                        ),\n                    modifier =\n                        Modifier\n                            .padding(16.dp)\n                            .fillMaxWidth(0.85f),\n                ) {\n                    Column(modifier = Modifier.padding(20.dp)) {\n                        Text(\n                            text = stringResource(R.string.share_lyrics),\n                            fontWeight = FontWeight.Bold,\n                            fontSize = 20.sp,\n                            color = MaterialTheme.colorScheme.onSurface,\n                        )\n                        Spacer(modifier = Modifier.height(16.dp))\n                        // Share as Text Row\n                        Row(\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .clickable {\n                                        val shareIntent =\n                                            Intent().apply {\n                                                action = Intent.ACTION_SEND\n                                                type = \"text/plain\"\n                                                val songLink =\n                                                    \"https://music.youtube.com/watch?v=${mediaMetadata?.id}\"\n                                                // Use the potentially multi-line lyricsText here\n                                                putExtra(\n                                                    Intent.EXTRA_TEXT,\n                                                    \"\\\"$lyricsText\\\"\\n\\n$songTitle - $artists\\n$songLink\",\n                                                )\n                                            }\n                                        context.startActivity(\n                                            Intent.createChooser(\n                                                shareIntent,\n                                                shareLyricsStr,\n                                            ),\n                                        )\n                                        showShareDialog = false\n                                    }.padding(vertical = 12.dp),\n                            verticalAlignment = Alignment.CenterVertically,\n                        ) {\n                            Icon(\n                                painter = painterResource(id = R.drawable.share), // Use new share icon\n                                contentDescription = null,\n                                tint = MaterialTheme.colorScheme.primary,\n                            )\n                            Spacer(modifier = Modifier.width(12.dp))\n                            Text(\n                                text = stringResource(R.string.share_as_text),\n                                fontSize = 16.sp,\n                                color = MaterialTheme.colorScheme.onSurface,\n                            )\n                        }\n                        // Share as Image Row\n                        Row(\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .clickable {\n                                        // Pass the potentially multi-line lyrics to the color picker\n                                        shareDialogData = Triple(lyricsText, songTitle, artists)\n                                        showColorPickerDialog = true\n                                        showShareDialog = false\n                                    }.padding(vertical = 12.dp),\n                            verticalAlignment = Alignment.CenterVertically,\n                        ) {\n                            Icon(\n                                painter = painterResource(id = R.drawable.share), // Use new share icon\n                                contentDescription = null,\n                                tint = MaterialTheme.colorScheme.primary,\n                            )\n                            Spacer(modifier = Modifier.width(12.dp))\n                            Text(\n                                text = stringResource(R.string.share_as_image),\n                                fontSize = 16.sp,\n                                color = MaterialTheme.colorScheme.onSurface,\n                            )\n                        }\n                        // Cancel Button Row\n                        Row(\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .padding(top = 8.dp, bottom = 4.dp),\n                            horizontalArrangement = Arrangement.End,\n                        ) {\n                            Text(\n                                text = stringResource(R.string.cancel),\n                                fontSize = 16.sp,\n                                color = MaterialTheme.colorScheme.error,\n                                fontWeight = FontWeight.Medium,\n                                modifier =\n                                    Modifier\n                                        .clickable { showShareDialog = false }\n                                        .padding(vertical = 8.dp, horizontal = 12.dp),\n                            )\n                        }\n                    }\n                }\n            }\n        }\n\n        if (showColorPickerDialog && shareDialogData != null) {\n            val (lyricsText, songTitle, artists) = shareDialogData!!\n            val coverUrl = mediaMetadata?.thumbnailUrl\n            val paletteColors = remember { mutableStateListOf<Color>() }\n\n            var previewBackgroundStyle by remember { mutableStateOf(LyricsBackgroundStyle.SOLID) }\n\n            val previewCardWidth = configuration.containerDpSize.width * 0.90f\n            val previewPadding = 20.dp * 2\n            val previewBoxPadding = 28.dp * 2\n            val previewAvailableWidth = previewCardWidth - previewPadding - previewBoxPadding\n            val previewBoxHeight = 340.dp\n            val headerFooterEstimate = (48.dp + 14.dp + 16.dp + 20.dp + 8.dp + 28.dp * 2)\n            val previewAvailableHeight = previewBoxHeight - headerFooterEstimate\n\n            val lyricsTextAlign =\n                when (lyricsTextPosition) {\n                    LyricsPosition.LEFT -> TextAlign.Left\n                    LyricsPosition.CENTER -> TextAlign.Center\n                    LyricsPosition.RIGHT -> TextAlign.Right\n                }\n\n            val textStyleForMeasurement =\n                TextStyle(\n                    color = previewTextColor,\n                    fontWeight = FontWeight.Bold,\n                    textAlign = lyricsTextAlign,\n                )\n            val textMeasurer = rememberTextMeasurer()\n\n            rememberAdjustedFontSize(\n                text = lyricsText,\n                maxWidth = previewAvailableWidth,\n                maxHeight = previewAvailableHeight,\n                density = density,\n                initialFontSize = 50.sp,\n                minFontSize = 22.sp,\n                style = textStyleForMeasurement,\n                textMeasurer = textMeasurer,\n            )\n\n            LaunchedEffect(coverUrl) {\n                if (coverUrl != null) {\n                    withContext(Dispatchers.IO) {\n                        try {\n                            val loader = ImageLoader(context)\n                            val req =\n                                ImageRequest\n                                    .Builder(context)\n                                    .data(coverUrl)\n                                    .allowHardware(false)\n                                    .build()\n                            val result = loader.execute(req)\n                            val bmp = result.image?.toBitmap()\n                            if (bmp != null) {\n                                val palette = Palette.from(bmp).generate()\n                                val swatches = palette.swatches.sortedByDescending { it.population }\n                                val colors =\n                                    swatches\n                                        .map { Color(it.rgb) }\n                                        .filter { color ->\n                                            val hsv = FloatArray(3)\n                                            android.graphics.Color.colorToHSV(color.toArgb(), hsv)\n                                            hsv[1] > 0.2f\n                                        }\n                                paletteColors.clear()\n                                paletteColors.addAll(colors.take(5))\n                            }\n                        } catch (_: Exception) {\n                        }\n                    }\n                }\n            }\n\n            BasicAlertDialog(onDismissRequest = { showColorPickerDialog = false }) {\n                Card(\n                    shape = RoundedCornerShape(20.dp),\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(20.dp),\n                ) {\n                    Column(\n                        horizontalAlignment = Alignment.CenterHorizontally,\n                        modifier =\n                            Modifier\n                                .verticalScroll(rememberScrollState())\n                                .padding(20.dp),\n                    ) {\n                        Text(\n                            text = stringResource(id = R.string.customize_colors),\n                            style = MaterialTheme.typography.headlineSmall,\n                            textAlign = TextAlign.Center,\n                            modifier = Modifier.fillMaxWidth(),\n                        )\n\n                        Spacer(modifier = Modifier.height(12.dp))\n\n                        Text(text = stringResource(id = R.string.player_background_style), style = MaterialTheme.typography.titleMedium)\n                        Row(\n                            horizontalArrangement = Arrangement.spacedBy(8.dp),\n                            modifier = Modifier.padding(vertical = 8.dp),\n                        ) {\n                            LyricsBackgroundStyle.entries.forEach { style ->\n                                val label =\n                                    when (style) {\n                                        LyricsBackgroundStyle.SOLID -> stringResource(R.string.player_background_solid)\n                                        LyricsBackgroundStyle.BLUR -> stringResource(R.string.player_background_blur)\n                                        LyricsBackgroundStyle.GRADIENT -> stringResource(R.string.gradient)\n                                    }\n                                val selected = previewBackgroundStyle == style\n\n                                androidx.compose.material3.FilterChip(\n                                    selected = selected,\n                                    onClick = { previewBackgroundStyle = style },\n                                    label = { Text(label) },\n                                )\n                            }\n                        }\n\n                        Box(\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .aspectRatio(1f)\n                                    .padding(8.dp)\n                                    .clip(RoundedCornerShape(12.dp)),\n                        ) {\n                            LyricsImageCard(\n                                lyricText = lyricsText,\n                                mediaMetadata = mediaMetadata ?: return@Box,\n                                backgroundColor = previewBackgroundColor,\n                                backgroundStyle = previewBackgroundStyle,\n                                textColor = previewTextColor,\n                                secondaryTextColor = previewSecondaryTextColor,\n                                textAlign = lyricsTextAlign,\n                            )\n                        }\n\n                        Spacer(modifier = Modifier.height(18.dp))\n\n                        Text(text = stringResource(id = R.string.background_color), style = MaterialTheme.typography.titleMedium)\n                        Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(vertical = 8.dp)) {\n                            (\n                                paletteColors +\n                                    listOf(\n                                        Color(0xFF242424),\n                                        Color(0xFF121212),\n                                        Color.White,\n                                        Color.Black,\n                                        Color(0xFFF5F5F5),\n                                    )\n                            ).distinct().take(8).forEach { color ->\n                                Box(\n                                    modifier =\n                                        Modifier\n                                            .size(32.dp)\n                                            .background(color, shape = RoundedCornerShape(8.dp))\n                                            .clickable { previewBackgroundColor = color }\n                                            .border(\n                                                2.dp,\n                                                if (previewBackgroundColor ==\n                                                    color\n                                                ) {\n                                                    MaterialTheme.colorScheme.primary\n                                                } else {\n                                                    Color.Transparent\n                                                },\n                                                RoundedCornerShape(8.dp),\n                                            ),\n                                )\n                            }\n                        }\n\n                        Text(text = stringResource(id = R.string.text_color), style = MaterialTheme.typography.titleMedium)\n                        Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(vertical = 8.dp)) {\n                            (paletteColors + listOf(Color.White, Color.Black, Color(0xFF1DB954))).distinct().take(8).forEach { color ->\n                                Box(\n                                    modifier =\n                                        Modifier\n                                            .size(32.dp)\n                                            .background(color, shape = RoundedCornerShape(8.dp))\n                                            .clickable { previewTextColor = color }\n                                            .border(\n                                                2.dp,\n                                                if (previewTextColor == color) MaterialTheme.colorScheme.primary else Color.Transparent,\n                                                RoundedCornerShape(8.dp),\n                                            ),\n                                )\n                            }\n                        }\n\n                        Text(text = stringResource(id = R.string.secondary_text_color), style = MaterialTheme.typography.titleMedium)\n                        Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(vertical = 8.dp)) {\n                            (\n                                paletteColors.map { it.copy(alpha = 0.7f) } +\n                                    listOf(Color.White.copy(alpha = 0.7f), Color.Black.copy(alpha = 0.7f), Color(0xFF1DB954))\n                            ).distinct().take(8).forEach { color ->\n                                Box(\n                                    modifier =\n                                        Modifier\n                                            .size(32.dp)\n                                            .background(color, shape = RoundedCornerShape(8.dp))\n                                            .clickable { previewSecondaryTextColor = color }\n                                            .border(\n                                                2.dp,\n                                                if (previewSecondaryTextColor ==\n                                                    color\n                                                ) {\n                                                    MaterialTheme.colorScheme.primary\n                                                } else {\n                                                    Color.Transparent\n                                                },\n                                                RoundedCornerShape(8.dp),\n                                            ),\n                                )\n                            }\n                        }\n\n                        Spacer(modifier = Modifier.height(12.dp))\n\n                        Button(\n                            onClick = {\n                                showColorPickerDialog = false\n                                showProgressDialog = true\n                                scope.launch {\n                                    try {\n                                        val screenWidth = configuration.containerSize.width\n                                        val screenHeight = configuration.containerSize.height\n\n                                        val image =\n                                            ComposeToImage.createLyricsImage(\n                                                context = context,\n                                                coverArtUrl = coverUrl,\n                                                songTitle = songTitle,\n                                                artistName = artists,\n                                                lyrics = lyricsText,\n                                                width = (screenWidth * density.density).toInt(),\n                                                height = (screenHeight * density.density).toInt(),\n                                                backgroundColor = previewBackgroundColor.toArgb(),\n                                                backgroundStyle = previewBackgroundStyle,\n                                                textColor = previewTextColor.toArgb(),\n                                                secondaryTextColor = previewSecondaryTextColor.toArgb(),\n                                                lyricsAlignment =\n                                                    when (lyricsTextPosition) {\n                                                        LyricsPosition.LEFT -> Layout.Alignment.ALIGN_NORMAL\n                                                        LyricsPosition.CENTER -> Layout.Alignment.ALIGN_CENTER\n                                                        LyricsPosition.RIGHT -> Layout.Alignment.ALIGN_OPPOSITE\n                                                    },\n                                            )\n                                        val timestamp = System.currentTimeMillis()\n                                        val filename = \"lyrics_$timestamp\"\n                                        val uri = ComposeToImage.saveBitmapAsFile(context, image, filename)\n                                        val shareIntent =\n                                            Intent(Intent.ACTION_SEND).apply {\n                                                type = \"image/png\"\n                                                putExtra(Intent.EXTRA_STREAM, uri)\n                                                addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n                                            }\n                                        context.startActivity(Intent.createChooser(shareIntent, shareLyricsStr))\n                                    } catch (e: Exception) {\n                                        Toast\n                                            .makeText(\n                                                context,\n                                                String.format(failedToCreateImageTemplate, e.message ?: \"\"),\n                                                Toast.LENGTH_SHORT,\n                                            ).show()\n                                    } finally {\n                                        showProgressDialog = false\n                                    }\n                                }\n                            },\n                            modifier = Modifier.fillMaxWidth(),\n                        ) {\n                            Text(stringResource(id = R.string.share))\n                        }\n                    }\n                }\n            }\n        } // إغلاق else block\n    }\n}\n\n// Professional page animation constants inspired by Metrolist design - slower for smoothness\nprivate const val METROLIST_AUTO_SCROLL_DURATION = 1500L // Much slower auto-scroll for smooth transitions\nprivate const val METROLIST_INITIAL_SCROLL_DURATION = 1000L // Slower initial positioning\nprivate const val METROLIST_SEEK_DURATION = 800L // Slower user interaction\nprivate const val METROLIST_FAST_SEEK_DURATION = 600L // Less aggressive seeking\n\n// Lyrics constants\nval LyricsPreviewTime = 2.seconds\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/LyricsImageCard.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport android.annotation.SuppressLint\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.AnnotatedString\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.rememberTextMeasurer\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.em\nimport androidx.compose.ui.unit.sp\nimport coil3.compose.rememberAsyncImagePainter\nimport coil3.request.ImageRequest\nimport coil3.request.crossfade\nimport com.metrolist.music.R\nimport com.metrolist.music.models.MediaMetadata\n\nimport androidx.compose.ui.draw.blur\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.TileMode\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.palette.graphics.Palette\nimport coil3.ImageLoader\nimport coil3.request.allowHardware\nimport coil3.toBitmap\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport androidx.compose.ui.res.stringResource\n\n@Composable\nfun rememberAdjustedFontSize(\n    text: String,\n    maxWidth: Dp,\n    maxHeight: Dp,\n    density: Density,\n    initialFontSize: TextUnit = 20.sp,\n    minFontSize: TextUnit = 14.sp,\n    style: TextStyle = TextStyle.Default,\n    textMeasurer: androidx.compose.ui.text.TextMeasurer? = null\n): TextUnit {\n    val measurer = textMeasurer ?: rememberTextMeasurer()\n\n    var calculatedFontSize by remember(text, maxWidth, maxHeight, style, density) {\n        val initialSize = when {\n            text.length < 50 -> initialFontSize\n            text.length < 100 -> (initialFontSize.value * 0.8f).sp\n            text.length < 200 -> (initialFontSize.value * 0.6f).sp\n            else -> (initialFontSize.value * 0.5f).sp\n        }\n        mutableStateOf(initialSize)\n    }\n\n    LaunchedEffect(key1 = text, key2 = maxWidth, key3 = maxHeight) {\n        val targetWidthPx = with(density) { maxWidth.toPx() * 0.92f }\n        val targetHeightPx = with(density) { maxHeight.toPx() * 0.92f }\n        if (text.isBlank()) {\n            calculatedFontSize = minFontSize\n            return@LaunchedEffect\n        }\n\n        if (text.length < 20) {\n            val largerSize = (initialFontSize.value * 1.1f).sp\n            val result = measurer.measure(\n                text = AnnotatedString(text),\n                style = style.copy(fontSize = largerSize)\n            )\n            if (result.size.width <= targetWidthPx && result.size.height <= targetHeightPx) {\n                calculatedFontSize = largerSize\n                return@LaunchedEffect\n            }\n        } else if (text.length < 30) {\n            val largerSize = (initialFontSize.value * 0.9f).sp\n            val result = measurer.measure(\n                text = AnnotatedString(text),\n                style = style.copy(fontSize = largerSize)\n            )\n            if (result.size.width <= targetWidthPx && result.size.height <= targetHeightPx) {\n                calculatedFontSize = largerSize\n                return@LaunchedEffect\n            }\n        }\n\n        var minSize = minFontSize.value\n        var maxSize = initialFontSize.value\n        var bestFit = minSize\n        var iterations = 0\n\n        while (minSize <= maxSize && iterations < 20) {\n            iterations++\n            val midSize = (minSize + maxSize) / 2\n            val midSizeSp = midSize.sp\n\n            val result = measurer.measure(\n                text = AnnotatedString(text),\n                style = style.copy(fontSize = midSizeSp)\n            )\n\n            if (result.size.width <= targetWidthPx && result.size.height <= targetHeightPx) {\n                bestFit = midSize\n                minSize = midSize + 0.5f\n            } else {\n                maxSize = midSize - 0.5f\n            }\n        }\n\n        calculatedFontSize = if (bestFit < minFontSize.value) minFontSize else bestFit.sp\n    }\n\n    return calculatedFontSize\n}\n\nenum class LyricsBackgroundStyle {\n    SOLID,\n    BLUR,\n    GRADIENT\n}\n\n@SuppressLint(\"UnusedBoxWithConstraintsScope\")\n@Composable\nfun LyricsImageCard(\n    lyricText: String,\n    mediaMetadata: MediaMetadata,\n    darkBackground: Boolean = true,\n    backgroundColor: Color? = null,\n    backgroundStyle: LyricsBackgroundStyle = LyricsBackgroundStyle.SOLID,\n    textColor: Color? = null,\n    secondaryTextColor: Color? = null,\n    textAlign: TextAlign = TextAlign.Center\n) {\n    val context = LocalContext.current\n    val density = LocalDensity.current\n\n    val cardCornerRadius = 20.dp\n    val padding = 28.dp\n    val coverArtSize = 64.dp\n\n    val defaultBgColor = if (darkBackground) Color(0xFF121212) else Color(0xFFF5F5F5)\n    val backgroundSolidColor = backgroundColor ?: defaultBgColor\n    \n    val mainTextColor = textColor ?: if (darkBackground) Color.White else Color.Black\n    val secondaryColor = secondaryTextColor ?: if (darkBackground) Color.White.copy(alpha = 0.7f) else Color.Black.copy(alpha = 0.7f)\n\n    val painter = rememberAsyncImagePainter(\n        ImageRequest.Builder(context)\n            .data(mediaMetadata.thumbnailUrl)\n            .crossfade(false)\n            .build()\n    )\n    \n    // Calculate gradient colors if needed\n    var gradientBrush by remember { mutableStateOf<Brush?>(null) }\n    \n    if (backgroundStyle == LyricsBackgroundStyle.GRADIENT) {\n        LaunchedEffect(mediaMetadata.thumbnailUrl) {\n            withContext(Dispatchers.IO) {\n                try {\n                    val loader = ImageLoader(context)\n                    val req = ImageRequest.Builder(context).data(mediaMetadata.thumbnailUrl).allowHardware(false).build()\n                    val result = loader.execute(req)\n                    val bmp = result.image?.toBitmap()\n                    if (bmp != null) {\n                        val palette = Palette.from(bmp).generate()\n                        val vibrant = palette.getVibrantColor(defaultBgColor.toArgb())\n                        val muted = palette.getMutedColor(defaultBgColor.toArgb())\n                        val darkVibrant = palette.getDarkVibrantColor(defaultBgColor.toArgb())\n                        \n                        val color1 = Color(vibrant)\n                        val color2 = Color(darkVibrant)\n                        \n                        gradientBrush = Brush.linearGradient(\n                            colors = listOf(color1, color2),\n                            tileMode = TileMode.Clamp\n                        )\n                    }\n                } catch (_: Exception) {}\n            }\n        }\n    }\n\n    Box(\n        modifier = Modifier\n            .background(Color.Black) // Base background\n            .fillMaxSize(),\n        contentAlignment = Alignment.Center\n    ) {\n        // Background Layer\n        Box(\n            modifier = Modifier.fillMaxSize()\n        ) {\n            when (backgroundStyle) {\n                LyricsBackgroundStyle.SOLID -> {\n                    Box(modifier = Modifier.fillMaxSize().background(backgroundSolidColor))\n                }\n                LyricsBackgroundStyle.BLUR -> {\n                    Image(\n                        painter = painter,\n                        contentDescription = null,\n                        contentScale = ContentScale.Crop,\n                        modifier = Modifier\n                            .fillMaxSize()\n                            .blur(50.dp) // High blur for background\n                            .background(Color.Black.copy(alpha = 0.3f)) // Overlay to ensure text readability\n                    )\n                }\n                LyricsBackgroundStyle.GRADIENT -> {\n                    Box(\n                        modifier = Modifier\n                            .fillMaxSize()\n                            .background(gradientBrush ?: androidx.compose.ui.graphics.Brush.linearGradient(listOf(backgroundSolidColor, backgroundSolidColor)))\n                    )\n                }\n            }\n        }\n    \n        Box(\n            modifier = Modifier\n                .fillMaxSize()\n                .clip(RoundedCornerShape(cardCornerRadius))\n                // For the card itself, we can make it slightly transparent or match the background style\n                // but usually the card IS the background cut out.\n                // Here we simulate the card being transparent so the background shows through,\n                // OR we redraw the background inside the card if we want the \"card on background\" look.\n                // Based on previous code, the card had its own background.\n                // Let's apply the same background logic to the card box.\n        ) {\n             when (backgroundStyle) {\n                LyricsBackgroundStyle.SOLID -> {\n                    Box(modifier = Modifier.fillMaxSize().background(backgroundSolidColor))\n                }\n                LyricsBackgroundStyle.BLUR -> {\n                    // For blur, we want the card to be a window to the blurred background?\n                    // Or have its own blurred background?\n                    // Typically \"Share Lyrics\" looks like a card on a background.\n                    // If we want the card to be seamless with the full image background, we can just use transparent.\n                    // But to ensure it looks like the generated image:\n                    Image(\n                        painter = painter,\n                        contentDescription = null,\n                        contentScale = ContentScale.Crop,\n                        modifier = Modifier\n                            .fillMaxSize()\n                            .blur(50.dp)\n                            .background(Color.Black.copy(alpha = 0.3f))\n                    )\n                }\n                LyricsBackgroundStyle.GRADIENT -> {\n                    Box(\n                        modifier = Modifier\n                            .fillMaxSize()\n                            .background(gradientBrush ?: androidx.compose.ui.graphics.Brush.linearGradient(listOf(backgroundSolidColor, backgroundSolidColor)))\n                    )\n                }\n            }\n            \n            // Border\n            Box(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .border(1.dp, mainTextColor.copy(alpha = 0.09f), RoundedCornerShape(cardCornerRadius))\n            )\n\n            Column(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .padding(padding),\n                verticalArrangement = Arrangement.SpaceBetween\n            ) {\n                // Header: Cover + Title/Artist aligned left\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(bottom = 12.dp)\n                ) {\n                    Image(\n                        painter = painter,\n                        contentDescription = null,\n                        contentScale = ContentScale.Crop,\n                        modifier = Modifier\n                            .size(coverArtSize)\n                            .clip(RoundedCornerShape(3.dp))\n                            .border(1.dp, mainTextColor.copy(alpha = 0.16f), RoundedCornerShape(3.dp))\n                    )\n                    Spacer(modifier = Modifier.width(16.dp))\n                    Column(\n                        verticalArrangement = Arrangement.Center,\n                        horizontalAlignment = Alignment.Start,\n                        modifier = Modifier.weight(1f)\n                    ) {\n                        Text(\n                            text = mediaMetadata.title,\n                            color = mainTextColor,\n                            fontSize = 20.sp,\n                            fontWeight = FontWeight.Bold,\n                            maxLines = 1,\n                            overflow = TextOverflow.Ellipsis,\n                            modifier = Modifier.padding(bottom = 2.dp)\n                        )\n                        Text(\n                            text = mediaMetadata.artists.joinToString { it.name },\n                            color = secondaryColor,\n                            fontSize = 16.sp,\n                            maxLines = 1,\n                            overflow = TextOverflow.Ellipsis\n                        )\n                    }\n                }\n                // Lyrics text (centered)\n                BoxWithConstraints(\n                    modifier = Modifier\n                        .weight(1f)\n                        .fillMaxWidth()\n                        .padding(vertical = 6.dp),\n                    contentAlignment = when (textAlign) {\n                        TextAlign.Left, TextAlign.Start -> Alignment.CenterStart\n                        TextAlign.Right, TextAlign.End -> Alignment.CenterEnd\n                        else -> Alignment.Center\n                    }\n                ) {\n                    val availableWidth = maxWidth\n                    val availableHeight = maxHeight\n                    val textStyle = TextStyle(\n                        color = mainTextColor,\n                        fontWeight = FontWeight.Bold,\n                        textAlign = textAlign,\n                        letterSpacing = 0.005.em,\n                    )\n\n                    val textMeasurer = rememberTextMeasurer()\n                    val initialSize = when {\n                        lyricText.length < 50 -> 24.sp\n                        lyricText.length < 100 -> 20.sp\n                        lyricText.length < 200 -> 17.sp\n                        lyricText.length < 300 -> 15.sp\n                        else -> 13.sp\n                    }\n\n                    val dynamicFontSize = rememberAdjustedFontSize(\n                        text = lyricText,\n                        maxWidth = availableWidth - 8.dp,\n                        maxHeight = availableHeight - 8.dp,\n                        density = density,\n                        initialFontSize = initialSize,\n                        minFontSize = 18.sp,\n                        style = textStyle,\n                        textMeasurer = textMeasurer\n                    )\n\n                    Text(\n                        text = lyricText,\n                        style = textStyle.copy(\n                            fontSize = dynamicFontSize,\n                            lineHeight = dynamicFontSize.value.sp * 1.2f\n                        ),\n                        overflow = TextOverflow.Ellipsis,\n                        textAlign = textAlign,\n                        modifier = Modifier.fillMaxWidth()\n                    )\n                }\n                // Footer\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    modifier = Modifier.fillMaxWidth()\n                ) {\n                    Box(\n                        modifier = Modifier\n                            .size(22.dp)\n                            .clip(RoundedCornerShape(50))\n                            .background(secondaryColor),\n                        contentAlignment = Alignment.Center\n                    ) {\n                        Image(\n                            painter = painterResource(id = R.drawable.small_icon),\n                            contentDescription = null,\n                            modifier = Modifier\n                                .size(16.dp),\n                            colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(backgroundSolidColor) // Try to use a contrasting color, fallback to solid bg color\n                        )\n                    }\n\n                    Spacer(modifier = Modifier.width(8.dp))\n\n                    Text(\n                        text = stringResource(R.string.app_name),\n                        color = secondaryColor,\n                        fontSize = 14.sp,\n                        fontWeight = FontWeight.Bold\n                    )\n                }\n            }\n        }\n    }\n}\n\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/Material3SettingsGroup.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.animation.animateContentSize\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Badge\nimport androidx.compose.material3.BadgedBox\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ProvideTextStyle\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.unit.dp\n\n/**\n * A Material 3 Expressive style settings group component\n * @param title The title of the settings group\n * @param items List of settings items to display\n */\n@Composable\nfun Material3SettingsGroup(\n    title: String? = null,\n    items: List<Material3SettingsItem>,\n    useLowContrast: Boolean = false\n) {\n    Column(\n        modifier = Modifier\n            .fillMaxWidth()\n    ) {\n        // Section title\n        title?.let {\n            Text(\n                text = it,\n                style = MaterialTheme.typography.labelLarge,\n                color = MaterialTheme.colorScheme.primary,\n                modifier = Modifier.padding(bottom = 8.dp, top = 8.dp)\n            )\n        }\n\n        // Settings items\n        Column(\n            modifier = Modifier.fillMaxWidth(),\n            verticalArrangement = Arrangement.spacedBy(4.dp)\n        ) {\n            items.forEachIndexed { index, item ->\n                val shape = when {\n                    items.size == 1 -> RoundedCornerShape(24.dp)\n                    index == 0 -> RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp, bottomStart = 6.dp, bottomEnd = 6.dp)\n                    index == items.size - 1 -> RoundedCornerShape(topStart = 6.dp, topEnd = 6.dp, bottomStart = 24.dp, bottomEnd = 24.dp)\n                    else -> RoundedCornerShape(6.dp)\n                }\n\n                Card(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .animateContentSize(),\n                    shape = shape,\n                    colors = CardDefaults.cardColors(\n                        containerColor = if (!useLowContrast) {\n                            MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)\n                        } else {\n                            MaterialTheme.colorScheme.surfaceContainerLow\n                        }\n                    ),\n                    elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)\n                ) {\n                    Material3SettingsItemRow(item = item)\n                }\n            }\n        }\n    }\n}\n\n/**\n * Individual settings item row with Material 3 styling\n */\n@Composable\nprivate fun Material3SettingsItemRow(\n    item: Material3SettingsItem\n) {\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .clickable(\n                enabled = item.enabled && item.onClick != null,\n                onClick = { item.onClick?.invoke() }\n            )\n            .padding(horizontal = 20.dp, vertical = 16.dp),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        // Icon with background\n        item.icon?.let { icon ->\n            Box(\n                modifier = Modifier\n                    .size(40.dp)\n                    .clip(RoundedCornerShape(12.dp))\n                    .background(\n                        MaterialTheme.colorScheme.primary.copy(\n                            alpha = if (item.isHighlighted) 0.15f else 0.1f\n                        )\n                    ),\n                contentAlignment = Alignment.Center\n            ) {\n                if (item.showBadge) {\n                    BadgedBox(\n                        badge = {\n                            Badge(\n                                containerColor = MaterialTheme.colorScheme.error\n                            )\n                        }\n                    ) {\n                        Icon(\n                            painter = icon,\n                            contentDescription = null,\n                            tint = if (!item.enabled)\n                                MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)\n                            else if (item.isHighlighted)\n                                MaterialTheme.colorScheme.primary\n                            else\n                                MaterialTheme.colorScheme.primary.copy(alpha = 0.9f),\n                            modifier = Modifier.size(24.dp)\n                        )\n                    }\n                } else {\n                    Icon(\n                        painter = icon,\n                        contentDescription = null,\n                        tint = if (!item.enabled)\n                            MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)\n                        else if (item.isHighlighted)\n                            MaterialTheme.colorScheme.primary\n                        else\n                            MaterialTheme.colorScheme.primary.copy(alpha = 0.9f),\n                        modifier = Modifier.size(24.dp)\n                    )\n                }\n            }\n\n            Spacer(modifier = Modifier.width(16.dp))\n        }\n\n        // Title and description\n        Column(\n            modifier = Modifier.weight(1f)\n        ) {\n            // Title content\n            ProvideTextStyle(\n                MaterialTheme.typography.titleMedium.copy(\n                    color = if (!item.enabled) \n                        MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)\n                    else\n                        MaterialTheme.colorScheme.onSurface\n                )\n            ) {\n                item.title()\n            }\n\n            // Description if provided\n            item.description?.let { desc ->\n                Spacer(modifier = Modifier.height(2.dp))\n                ProvideTextStyle(\n                    MaterialTheme.typography.bodyMedium.copy(\n                        color = if (!item.enabled)\n                            MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)\n                        else\n                            MaterialTheme.colorScheme.onSurfaceVariant\n                    )\n                ) {\n                    desc()\n                }\n            }\n        }\n\n        // Trailing content\n        item.trailingContent?.let { trailing ->\n            Spacer(modifier = Modifier.width(8.dp))\n            trailing()\n        }\n    }\n}\n\n/**\n * Data class for Material 3 settings item\n */\ndata class Material3SettingsItem(\n    val icon: Painter? = null,\n    val title: @Composable () -> Unit,\n    val description: (@Composable () -> Unit)? = null,\n    val trailingContent: (@Composable () -> Unit)? = null,\n    val showBadge: Boolean = false,\n    val isHighlighted: Boolean = false,\n    val enabled: Boolean = true,\n    val onClick: (() -> Unit)? = null\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/Menu.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.animation.animateContentSize\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardColors\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ProvideTextStyle\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun Material3MenuGroup(\n    items: List<Material3MenuItemData>\n) {\n    Column(\n        modifier = Modifier.fillMaxWidth(),\n        verticalArrangement = Arrangement.spacedBy(4.dp)\n    ) {\n        items.forEachIndexed { index, item ->\n            val shape = when {\n                items.size == 1 -> RoundedCornerShape(24.dp)\n                index == 0 -> RoundedCornerShape(topStart = 24.dp, topEnd = 24.dp, bottomStart = 6.dp, bottomEnd = 6.dp)\n                index == items.size - 1 -> RoundedCornerShape(topStart = 6.dp, topEnd = 6.dp, bottomStart = 24.dp, bottomEnd = 24.dp)\n                else -> RoundedCornerShape(6.dp)\n            }\n\n            Card(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .animateContentSize(),\n                shape = shape,\n                colors = item.cardColors ?: CardDefaults.cardColors(\n                    containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)\n                ),\n                elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)\n            ) {\n                Material3MenuItemRow(item = item)\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun Material3MenuItemRow(\n    item: Material3MenuItemData\n) {\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .clickable(\n                enabled = item.onClick != null,\n                onClick = { item.onClick?.invoke() }\n            )\n            .padding(horizontal = 20.dp, vertical = 16.dp),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        item.icon?.let { icon ->\n            icon()\n            Spacer(modifier = Modifier.width(16.dp))\n        }\n\n        Column(\n            modifier = Modifier.weight(1f)\n        ) {\n            ProvideTextStyle(MaterialTheme.typography.titleMedium) {\n                item.title()\n            }\n\n            item.description?.let { desc ->\n                Spacer(modifier = Modifier.height(2.dp))\n                ProvideTextStyle(\n                    MaterialTheme.typography.bodyMedium.copy(\n                        color = MaterialTheme.colorScheme.onSurfaceVariant\n                    )\n                ) {\n                    desc()\n                }\n            }\n        }\n        item.trailingContent?.let { trailing ->\n            Spacer(modifier = Modifier.width(8.dp))\n            trailing()\n        }\n    }\n}\n\ndata class Material3MenuItemData(\n    val icon: (@Composable () -> Unit)? = null,\n    val title: @Composable () -> Unit,\n    val description: (@Composable () -> Unit)? = null,\n    val onClick: (() -> Unit)? = null,\n    val cardColors: CardColors? = null,\n    val trailingContent: (@Composable () -> Unit)? = null\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/NavigationTile.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.annotation.DrawableRes\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun NavigationTile(\n    title: String,\n    @DrawableRes icon: Int,\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    Column(\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.spacedBy(4.dp),\n        modifier = modifier.padding(6.dp),\n    ) {\n        Box(\n            contentAlignment = Alignment.Center,\n            modifier =\n            Modifier\n                .size(56.dp)\n                .clip(CircleShape)\n                .background(MaterialTheme.colorScheme.surfaceContainer)\n                .clickable(onClick = onClick),\n        ) {\n            Icon(\n                painter = painterResource(icon),\n                contentDescription = null,\n            )\n        }\n\n        Text(\n            text = title,\n            style = MaterialTheme.typography.labelMedium,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/NavigationTitle.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedButton\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.metrolist.music.R\n\n@Composable\nfun NavigationTitle(\n    title: String,\n    modifier: Modifier = Modifier,\n    label: String? = null,\n    thumbnail: (@Composable () -> Unit)? = null,\n    onClick: (() -> Unit)? = null,\n    onPlayAllClick: (() -> Unit)? = null,\n) {\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        horizontalArrangement = Arrangement.spacedBy(12.dp),\n        modifier = modifier\n            .fillMaxWidth()\n            .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal))\n            .clickable(enabled = onClick != null) {\n                onClick?.invoke()\n            }\n            .padding(horizontal = 12.dp, vertical = 12.dp)\n    ) {\n        thumbnail?.invoke()\n\n        Column(\n            verticalArrangement = Arrangement.Center,\n            modifier = Modifier.weight(1f)\n        ) {\n            label?.let { label ->\n                Text(\n                    text = label,\n                    style = MaterialTheme.typography.labelLarge,\n                    overflow = TextOverflow.Ellipsis,\n                )\n            }\n\n            Text(\n                text = title,\n                style = MaterialTheme.typography.titleLarge,\n                fontWeight = FontWeight.Bold,\n                color = MaterialTheme.colorScheme.primary,\n                overflow = TextOverflow.Ellipsis,\n                maxLines = 1,\n            )\n        }\n\n        onPlayAllClick?.let { playAllClick ->\n            OutlinedButton(\n                onClick = playAllClick,\n                shape = RoundedCornerShape(12.dp),\n                border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary.copy(alpha = 0.5f)),\n                colors = ButtonDefaults.outlinedButtonColors(\n                    contentColor = MaterialTheme.colorScheme.primary\n                ),\n                contentPadding = PaddingValues(horizontal = 12.dp, vertical = 2.dp),\n                modifier = Modifier\n                    .height(24.dp)\n            ) {\n                Text(\n                    text = stringResource(R.string.play_all),\n                    style = MaterialTheme.typography.labelSmall\n                )\n            }\n        }\n\n        if (onClick != null) {\n            Icon(\n                painter = painterResource(R.drawable.arrow_forward),\n                contentDescription = null,\n                tint = MaterialTheme.colorScheme.primary\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/NewMenuComponents.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.animation.animateColorAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.basicMarquee\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\n\n// Enhanced Action Button - Material 3 Expressive Design\n@Composable\nfun NewActionButton(\n    icon: @Composable () -> Unit,\n    text: String,\n    onClick: @Composable () -> Unit,\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    backgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant,\n    contentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,\n) {\n    val animatedBackground by animateColorAsState(\n        targetValue = if (enabled) backgroundColor else backgroundColor.copy(alpha = 0.5f),\n        animationSpec = tween(200),\n        label = \"background\",\n    )\n\n    val animatedContent by animateColorAsState(\n        targetValue = if (enabled) contentColor else contentColor.copy(alpha = 0.5f),\n        animationSpec = tween(200),\n        label = \"content\",\n    )\n\n    var performAction by remember { mutableStateOf(false) }\n\n    if (performAction) {\n        onClick()\n        LaunchedEffect(Unit) {\n            performAction = false\n        }\n    }\n\n    Card(\n        modifier =\n            modifier\n                .clickable(enabled = enabled) { performAction = true },\n        colors =\n            CardDefaults.cardColors(\n                containerColor = animatedBackground,\n            ),\n        shape = RoundedCornerShape(16.dp),\n        elevation =\n            CardDefaults.cardElevation(),\n    ) {\n        Column(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(12.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.Center,\n        ) {\n            Box(\n                modifier = Modifier.size(28.dp),\n                contentAlignment = Alignment.Center,\n            ) {\n                icon()\n            }\n\n            Spacer(modifier = Modifier.height(6.dp))\n\n            Text(\n                text = text,\n                style = MaterialTheme.typography.labelMedium,\n                color = animatedContent,\n                textAlign = TextAlign.Center,\n                maxLines = 2,\n                overflow = TextOverflow.Ellipsis,\n                modifier = Modifier.basicMarquee(),\n            )\n        }\n    }\n}\n\n// Enhanced Menu Item - Material 3 Expressive Design\n@Composable\nfun NewMenuItem(\n    modifier: Modifier = Modifier,\n    headlineContent: @Composable () -> Unit,\n    leadingContent: @Composable (() -> Unit)? = null,\n    trailingContent: @Composable (() -> Unit)? = null,\n    supportingContent: @Composable (() -> Unit)? = null,\n    onClick: (() -> Unit)? = null,\n    enabled: Boolean = true,\n) {\n    androidx.compose.material3.ListItem(\n        headlineContent = headlineContent,\n        leadingContent = leadingContent,\n        trailingContent = trailingContent,\n        supportingContent = supportingContent,\n        modifier =\n            modifier\n                .clickable(enabled = enabled) { onClick?.invoke() }\n                .padding(horizontal = 4.dp),\n        tonalElevation = 0.dp,\n    )\n}\n\n// Enhanced Menu Section Header - Material 3 Expressive Design\n@Composable\nfun NewMenuSectionHeader(\n    text: String,\n    modifier: Modifier = Modifier,\n) {\n    Text(\n        text = text,\n        style =\n            MaterialTheme.typography.titleMedium.copy(\n                fontWeight = FontWeight.SemiBold,\n                fontSize = 16.sp,\n            ),\n        color = MaterialTheme.colorScheme.primary,\n        modifier = modifier.padding(horizontal = 20.dp, vertical = 12.dp),\n    )\n}\n\n// Enhanced Action Grid - Material 3 Expressive Design\n@Composable\nfun NewActionGrid(\n    actions: List<NewAction>,\n    modifier: Modifier = Modifier,\n    columns: Int = 3,\n) {\n    val rows = actions.chunked(columns)\n\n    Column(\n        modifier = modifier,\n        verticalArrangement = Arrangement.spacedBy(12.dp),\n    ) {\n        rows.forEach { row ->\n            Row(\n                horizontalArrangement = Arrangement.spacedBy(12.dp),\n            ) {\n                row.forEach { action ->\n                    NewActionButton(\n                        icon = action.icon,\n                        text = action.text,\n                        onClick = action.onClick,\n                        modifier = Modifier.weight(1f),\n                        enabled = action.enabled,\n                        backgroundColor =\n                            if (action.backgroundColor !=\n                                Color.Unspecified\n                            ) {\n                                action.backgroundColor\n                            } else {\n                                MaterialTheme.colorScheme.surfaceVariant\n                            },\n                        contentColor =\n                            if (action.contentColor !=\n                                Color.Unspecified\n                            ) {\n                                action.contentColor\n                            } else {\n                                MaterialTheme.colorScheme.onSurfaceVariant\n                            },\n                    )\n                }\n\n                // Fill remaining space if row is not full\n                repeat(columns - row.size) {\n                    Spacer(modifier = Modifier.weight(1f))\n                }\n            }\n        }\n    }\n}\n\n// Enhanced Action Data Class\ndata class NewAction(\n    val icon: @Composable () -> Unit,\n    val text: String,\n    val onClick: @Composable () -> Unit,\n    val enabled: Boolean = true,\n    val backgroundColor: Color = Color.Unspecified,\n    val contentColor: Color = Color.Unspecified,\n)\n\n// Enhanced Menu Content - Material 3 Expressive Design\n@Composable\nfun NewMenuContent(\n    headerContent: @Composable (() -> Unit)? = null,\n    actionGrid: @Composable (() -> Unit)? = null,\n    menuItems: @Composable (() -> Unit)? = null,\n    modifier: Modifier = Modifier,\n) {\n    Column(\n        modifier = modifier,\n        verticalArrangement = Arrangement.spacedBy(8.dp),\n    ) {\n        // Header\n        headerContent?.invoke()\n\n        // Action Grid\n        actionGrid?.invoke()\n\n        // Divider if both header and actions exist\n        if (headerContent != null && actionGrid != null) {\n            HorizontalDivider(\n                modifier = Modifier.padding(vertical = 16.dp),\n                color = MaterialTheme.colorScheme.outlineVariant,\n            )\n        }\n\n        // Menu Items\n        menuItems?.invoke()\n    }\n}\n\n// Enhanced Icon Button - Material 3 Expressive Design\n@Composable\nfun NewIconButton(\n    icon: @Composable () -> Unit,\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    backgroundColor: Color = MaterialTheme.colorScheme.surfaceVariant,\n    contentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant,\n) {\n    val animatedBackground by animateColorAsState(\n        targetValue = if (enabled) backgroundColor else backgroundColor.copy(alpha = 0.5f),\n        animationSpec = tween(200),\n        label = \"background\",\n    )\n\n    val animatedContent by animateColorAsState(\n        targetValue = if (enabled) contentColor else contentColor.copy(alpha = 0.5f),\n        animationSpec = tween(200),\n        label = \"content\",\n    )\n\n    Card(\n        modifier =\n            modifier\n                .clickable(enabled = enabled) { onClick() },\n        colors =\n            CardDefaults.cardColors(\n                containerColor = animatedBackground,\n            ),\n        shape = CircleShape,\n        elevation =\n            CardDefaults.cardElevation(\n                defaultElevation = 2.dp,\n            ),\n    ) {\n        Box(\n            modifier =\n                Modifier\n                    .size(48.dp)\n                    .padding(12.dp),\n            contentAlignment = Alignment.Center,\n        ) {\n            icon()\n        }\n    }\n}\n\n// Enhanced Menu Container - Material 3 Expressive Design\n@Composable\nfun NewMenuContainer(\n    content: @Composable () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    Column(\n        modifier =\n            modifier\n                .fillMaxWidth()\n                .padding(horizontal = 20.dp)\n                .padding(bottom = 32.dp),\n    ) {\n        content()\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/PlayerSlider.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.SliderColors\nimport androidx.compose.material3.SliderDefaults\nimport androidx.compose.material3.SliderState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.lerp\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.drawscope.DrawScope\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.unit.dp\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun PlayerSliderTrack(\n    sliderState: SliderState,\n    modifier: Modifier = Modifier,\n    colors: SliderColors = SliderDefaults.colors(),\n    trackHeight: Dp = 10.dp\n) {\n    val inactiveTrackColor = colors.inactiveTrackColor\n    val activeTrackColor = colors.activeTrackColor\n    val inactiveTickColor = colors.inactiveTickColor\n    val activeTickColor = colors.activeTickColor\n    val valueRange = sliderState.valueRange\n    Canvas(\n        modifier\n            .fillMaxWidth()\n            .height(trackHeight)\n    ) {\n        drawTrack(\n            stepsToTickFractions(sliderState.steps),\n            0f,\n            calcFraction(\n                valueRange.start,\n                valueRange.endInclusive,\n                sliderState.value.coerceIn(valueRange.start, valueRange.endInclusive)\n            ),\n            inactiveTrackColor,\n            activeTrackColor,\n            inactiveTickColor,\n            activeTickColor,\n            trackHeight\n        )\n    }\n}\n\nprivate fun DrawScope.drawTrack(\n    tickFractions: FloatArray,\n    activeRangeStart: Float,\n    activeRangeEnd: Float,\n    inactiveTrackColor: Color,\n    activeTrackColor: Color,\n    inactiveTickColor: Color,\n    activeTickColor: Color,\n    trackHeight: Dp = 2.dp\n) {\n    val isRtl = layoutDirection == LayoutDirection.Rtl\n    val sliderLeft = Offset(0f, center.y)\n    val sliderRight = Offset(size.width, center.y)\n    val sliderStart = if (isRtl) sliderRight else sliderLeft\n    val sliderEnd = if (isRtl) sliderLeft else sliderRight\n    val tickSize = 2.0.dp.toPx()\n    val trackStrokeWidth = trackHeight.toPx()\n    drawLine(\n        inactiveTrackColor,\n        sliderStart,\n        sliderEnd,\n        trackStrokeWidth,\n        StrokeCap.Round\n    )\n    val sliderValueEnd = Offset(\n        sliderStart.x +\n                (sliderEnd.x - sliderStart.x) * activeRangeEnd,\n        center.y\n    )\n    val sliderValueStart = Offset(\n        sliderStart.x +\n                (sliderEnd.x - sliderStart.x) * activeRangeStart,\n        center.y\n    )\n    drawLine(\n        activeTrackColor,\n        sliderValueStart,\n        sliderValueEnd,\n        trackStrokeWidth,\n        StrokeCap.Round\n    )\n    for (tick in tickFractions) {\n        val outsideFraction = tick > activeRangeEnd || tick < activeRangeStart\n        drawCircle(\n            color = if (outsideFraction) inactiveTickColor else activeTickColor,\n            center = Offset(lerp(sliderStart, sliderEnd, tick).x, center.y),\n            radius = tickSize / 2f\n        )\n    }\n}\n\nprivate fun stepsToTickFractions(steps: Int): FloatArray {\n    return if (steps == 0) floatArrayOf() else FloatArray(steps + 2) { it.toFloat() / (steps + 1) }\n}\n\nprivate fun calcFraction(a: Float, b: Float, pos: Float) =\n    (if (b - a == 0f) 0f else (pos - a) / (b - a)).coerceIn(0f, 1f)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/PlayingIndicator.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material3.Icon\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.CornerRadius\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.ThumbnailCornerRadius\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport kotlin.random.Random\n\n@Composable\nfun PlayingIndicator(\n    color: Color,\n    modifier: Modifier = Modifier,\n    bars: Int = 3,\n    barWidth: Dp = 4.dp,\n    cornerRadius: Dp = ThumbnailCornerRadius,\n) {\n    val animatables =\n        remember {\n            List(bars) {\n                Animatable(0.1f)\n            }\n        }\n\n    LaunchedEffect(Unit) {\n        delay(300)\n        animatables.forEach { animatable ->\n            launch {\n                while (true) {\n                    animatable.animateTo(Random.nextFloat() * 0.9f + 0.1f)\n                    delay(50)\n                }\n            }\n        }\n    }\n\n    Row(\n        horizontalArrangement = Arrangement.spacedBy(6.dp),\n        verticalAlignment = Alignment.Bottom,\n        modifier = modifier,\n    ) {\n        animatables.forEach { animatable ->\n            Canvas(\n                modifier =\n                Modifier\n                    .fillMaxHeight()\n                    .width(barWidth),\n            ) {\n                drawRoundRect(\n                    color = color,\n                    topLeft = Offset(x = 0f, y = size.height * (1 - animatable.value)),\n                    size = size.copy(height = animatable.value * size.height),\n                    cornerRadius = CornerRadius(cornerRadius.toPx()),\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun PlayingIndicatorBox(\n    modifier: Modifier = Modifier,\n    isActive: Boolean,\n    playWhenReady: Boolean,\n    color: Color = Color.White,\n) {\n    AnimatedVisibility(\n        visible = isActive,\n        enter = fadeIn(tween(500)),\n        exit = fadeOut(tween(500)),\n    ) {\n        Box(\n            contentAlignment = Alignment.Center,\n            modifier = modifier,\n        ) {\n            if (playWhenReady) {\n                PlayingIndicator(\n                    color = color,\n                    modifier = Modifier.height(24.dp),\n                )\n            } else {\n                Icon(\n                    painter = painterResource(R.drawable.play),\n                    contentDescription = null,\n                    tint = color,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/Preference.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ProvideTextStyle\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.SwitchDefaults\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport com.metrolist.music.R\n\n@Composable\n@Deprecated(\"Use Material3SettingsGroup instead :)\")\nfun PreferenceEntry(\n    modifier: Modifier = Modifier,\n    title: @Composable () -> Unit,\n    description: String? = null,\n    content: (@Composable () -> Unit)? = null,\n    icon: (@Composable () -> Unit)? = null,\n    trailingContent: (@Composable () -> Unit)? = null,\n    onClick: (() -> Unit)? = null,\n    isEnabled: Boolean = true,\n) {\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier =\n        modifier\n            .fillMaxWidth()\n            .clickable(\n                enabled = isEnabled && onClick != null,\n                onClick = onClick ?: {},\n            ).alpha(if (isEnabled) 1f else 0.5f)\n            .padding(horizontal = 16.dp, vertical = 16.dp),\n    ) {\n        if (icon != null) {\n            Box(\n                modifier = Modifier.padding(horizontal = 4.dp),\n            ) {\n                icon()\n            }\n\n            Spacer(Modifier.width(12.dp))\n        }\n\n        Column(\n            verticalArrangement = Arrangement.Center,\n            modifier = Modifier.weight(1f),\n        ) {\n            ProvideTextStyle(MaterialTheme.typography.titleMedium) {\n                title()\n            }\n\n            if (description != null) {\n                Text(\n                    text = description,\n                    style = MaterialTheme.typography.titleSmall,\n                    color = MaterialTheme.colorScheme.secondary,\n                )\n            }\n\n            content?.invoke()\n        }\n\n        if (trailingContent != null) {\n            Spacer(Modifier.width(12.dp))\n\n            trailingContent()\n        }\n    }\n}\n\n@Composable\n@Deprecated(\"Use the Switch component instead\")\nfun SwitchPreference(\n    modifier: Modifier = Modifier,\n    title: @Composable () -> Unit,\n    description: String? = null,\n    icon: (@Composable () -> Unit)? = null,\n    checked: Boolean,\n    onCheckedChange: (Boolean) -> Unit,\n    isEnabled: Boolean = true,\n) {\n    PreferenceEntry(\n        modifier = modifier,\n        title = title,\n        description = description,\n        icon = icon,\n        trailingContent = {\n            Switch(\n                checked = checked,\n                onCheckedChange = onCheckedChange,\n                enabled = isEnabled,\n                thumbContent = {\n                    Icon(\n                        painter = painterResource(\n                            id = if (checked) R.drawable.check else R.drawable.close\n                        ),\n                        contentDescription = null,\n                        modifier = Modifier.size(SwitchDefaults.IconSize),\n                    )\n                }\n            )\n        },\n        onClick = { onCheckedChange(!checked) },\n        isEnabled = isEnabled\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/RandomizeGridItem.kt",
    "content": "package com.metrolist.music.ui.component\n\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.LoadingIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.dp\nimport com.metrolist.music.constants.ThumbnailCornerRadius\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun RandomizeGridItem(\n    isLoading: Boolean,\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    // When isLoading is true, multiplier goes to 0 (moving dots to center)\n    // When isLoading is false, multiplier goes to 1 (moving dots to corners)\n    val dotOffsetMultiplier by animateFloatAsState(\n        targetValue = if (isLoading) 0f else 1f,\n        animationSpec = tween(durationMillis = 600),\n        label = \"dotOffset\",\n    )\n\n    val loadingAlpha by animateFloatAsState(\n        targetValue = if (isLoading) 1f else 0f,\n        animationSpec = tween(durationMillis = 400),\n        label = \"loadingAlpha\",\n    )\n\n    Box(\n        modifier =\n            modifier\n                .aspectRatio(1f)\n                .clip(RoundedCornerShape(ThumbnailCornerRadius))\n                .background(MaterialTheme.colorScheme.secondaryContainer)\n                .clickable(onClick = onClick),\n        contentAlignment = Alignment.Center,\n    ) {\n        // Die Dots (5-pattern)\n        val dotColor = MaterialTheme.colorScheme.onSecondaryContainer\n        val dotSize = 14.dp\n        val padding = 24.dp\n\n        // Using a single Center alignment and offsetting FROM center ensures they\n        // collapse TO center correctly.\n\n        // Top Left\n        Box(\n            modifier =\n                Modifier\n                    .align(Alignment.Center)\n                    .offset { IntOffset((-padding * dotOffsetMultiplier).roundToPx(), (-padding * dotOffsetMultiplier).roundToPx()) }\n                    .size(dotSize)\n                    .clip(CircleShape)\n                    .background(dotColor),\n        )\n        // Top Right\n        Box(\n            modifier =\n                Modifier\n                    .align(Alignment.Center)\n                    .offset { IntOffset((padding * dotOffsetMultiplier).roundToPx(), (-padding * dotOffsetMultiplier).roundToPx()) }\n                    .size(dotSize)\n                    .clip(CircleShape)\n                    .background(dotColor),\n        )\n        // Center\n        Box(\n            modifier =\n                Modifier\n                    .align(Alignment.Center)\n                    .size(dotSize)\n                    .clip(CircleShape)\n                    .background(dotColor),\n        )\n        // Bottom Left\n        Box(\n            modifier =\n                Modifier\n                    .align(Alignment.Center)\n                    .offset { IntOffset((-padding * dotOffsetMultiplier).roundToPx(), (padding * dotOffsetMultiplier).roundToPx()) }\n                    .size(dotSize)\n                    .clip(CircleShape)\n                    .background(dotColor),\n        )\n        // Bottom Right\n        Box(\n            modifier =\n                Modifier\n                    .align(Alignment.Center)\n                    .offset { IntOffset((padding * dotOffsetMultiplier).roundToPx(), (padding * dotOffsetMultiplier).roundToPx()) }\n                    .size(dotSize)\n                    .clip(CircleShape)\n                    .background(dotColor),\n        )\n\n        // Loading Indicator overlay\n        Box(modifier = Modifier.alpha(loadingAlpha)) {\n            LoadingIndicator(\n                modifier = Modifier.size(48.dp),\n                color = MaterialTheme.colorScheme.onSecondaryContainer,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/ReleaseNotesCard.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.surfaceColorAtElevation\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport com.metrolist.music.R\nimport com.metrolist.music.utils.Updater\n\n@Composable\nfun ReleaseNotesCard() {\n    val releaseInfo = Updater.getCachedLatestRelease() ?: return\n\n    Card(\n        modifier = Modifier\n            .padding(horizontal = 16.dp)\n            .fillMaxWidth(),\n        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),\n        colors = CardDefaults.cardColors(\n            containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(6.dp)\n        )\n    ) {\n        Column(\n            modifier = Modifier.padding(16.dp)\n        ) {\n            Text(\n                text = stringResource(R.string.release_notes),\n                style = MaterialTheme.typography.titleLarge\n            )\n            Spacer(modifier = Modifier.height(8.dp))\n            Text(\n                text = releaseInfo.description,\n                style = MaterialTheme.typography.bodyMedium,\n                modifier = Modifier.padding(vertical = 2.dp)\n            )\n        }\n    }\n    Spacer(modifier = Modifier.height(16.dp))\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/SearchBar.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.gestures.awaitEachGesture\nimport androidx.compose.foundation.gestures.awaitFirstDown\nimport androidx.compose.foundation.gestures.waitForUpOrCancellation\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsFocusedAsState\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.ColumnScope\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.BasicTextField\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.LocalTextStyle\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.TextFieldColors\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarColors\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.TopAppBarScrollBehavior\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.takeOrElse\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.input.key.onKeyEvent\nimport androidx.compose.ui.input.pointer.PointerEventPass\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.semantics.stateDescription\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.text.input.VisualTransformation\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport com.metrolist.music.constants.AppBarHeight\n\n@ExperimentalMaterial3Api\n@Composable\nfun TopSearch(\n    query: TextFieldValue,\n    onQueryChange: (TextFieldValue) -> Unit,\n    onSearch: (String) -> Unit,\n    active: Boolean,\n    onActiveChange: (Boolean) -> Unit,\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    placeholder: @Composable (() -> Unit)? = null,\n    leadingIcon: @Composable (() -> Unit)? = null,\n    trailingIcon: @Composable (() -> Unit)? = null,\n    shape: Shape? = null,\n    colors: TopAppBarColors = TopAppBarDefaults.topAppBarColors(\n        containerColor = MaterialTheme.colorScheme.surfaceContainerLow\n    ),\n    scrollBehavior: TopAppBarScrollBehavior? = null,\n    windowInsets: WindowInsets = WindowInsets.systemBars,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    focusRequester: FocusRequester = remember { FocusRequester() },\n    content: @Composable ColumnScope.() -> Unit = {},\n) {\n    Box(modifier = modifier) {\n        TopAppBar(\n            title = {\n                SearchBarInputField(\n                    query = query,\n                    onQueryChange = onQueryChange,\n                    onSearch = onSearch,\n                    active = active,\n                    onActiveChange = onActiveChange,\n                    enabled = enabled,\n                    placeholder = placeholder,\n                    // Icons are handled in navigationIcon and actions if preferred, or here for inline\n                    leadingIcon = null,\n                    trailingIcon = null,\n                    colors = TextFieldDefaults.colors(\n                        focusedTextColor = MaterialTheme.colorScheme.onSurface,\n                        unfocusedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,\n                        focusedContainerColor = Color.Transparent,\n                        unfocusedContainerColor = Color.Transparent,\n                        disabledContainerColor = Color.Transparent,\n                        cursorColor = MaterialTheme.colorScheme.primary,\n                        focusedIndicatorColor = Color.Transparent,\n                        unfocusedIndicatorColor = Color.Transparent\n                    ),\n                    interactionSource = interactionSource,\n                    focusRequester = focusRequester,\n                )\n            },\n            navigationIcon = {\n                if (leadingIcon != null) {\n                    leadingIcon()\n                }\n            },\n            actions = {\n                if (trailingIcon != null) {\n                    trailingIcon()\n                }\n            },\n            colors = colors,\n            scrollBehavior = scrollBehavior,\n            windowInsets = windowInsets\n        )\n        \n        if (active) {\n            Box(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(top = AppBarHeight + windowInsets.asPaddingValues().calculateTopPadding())\n                    .background(MaterialTheme.colorScheme.surface)\n            ) {\n                 Column {\n                    content()\n                 }\n            }\n            \n            BackHandler(enabled = active) {\n                onActiveChange(false)\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun SearchBarInputField(\n    query: TextFieldValue,\n    onQueryChange: (TextFieldValue) -> Unit,\n    onSearch: (String) -> Unit,\n    active: Boolean,\n    onActiveChange: (Boolean) -> Unit,\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    placeholder: @Composable (() -> Unit)? = null,\n    leadingIcon: @Composable (() -> Unit)? = null,\n    trailingIcon: @Composable (() -> Unit)? = null,\n    colors: TextFieldColors,\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    focusRequester: FocusRequester = remember { FocusRequester() },\n) {\n    val focused = interactionSource.collectIsFocusedAsState().value\n    val textColor = LocalTextStyle.current.color.takeOrElse {\n        if (focused) colors.focusedTextColor else colors.unfocusedTextColor\n    }\n\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = modifier\n            .fillMaxWidth()\n            .height(InputFieldHeight),\n    ) {\n        if (leadingIcon != null) {\n            Spacer(Modifier.width(SearchBarIconOffsetX))\n            leadingIcon()\n        }\n\n        BasicTextField(\n            value = query,\n            onValueChange = onQueryChange,\n            modifier = Modifier\n                .weight(1f)\n                .focusRequester(focusRequester)\n                .pointerInput(Unit) {\n                    awaitEachGesture {\n                        awaitFirstDown(pass = PointerEventPass.Initial)\n                        val upEvent = waitForUpOrCancellation(pass = PointerEventPass.Initial)\n                        if (upEvent != null) {\n                            onActiveChange(true)\n                        }\n                    }\n                }\n                .semantics {\n                    contentDescription = \"Search\"\n                    if (active) {\n                        stateDescription = \"Suggestions available\"\n                    }\n                }\n                .onKeyEvent {\n                    if (it.key == Key.Enter) {\n                        onSearch(query.text)\n                        return@onKeyEvent true\n                    }\n                    false\n                },\n            enabled = enabled,\n            singleLine = true,\n            textStyle = LocalTextStyle.current.merge(TextStyle(color = textColor)),\n            cursorBrush = SolidColor(colors.cursorColor),\n            keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),\n            keyboardActions = KeyboardActions(onSearch = { onSearch(query.text) }),\n            interactionSource = interactionSource,\n            decorationBox = @Composable { innerTextField ->\n                TextFieldDefaults.DecorationBox(\n                    value = query.text,\n                    innerTextField = innerTextField,\n                    enabled = enabled,\n                    singleLine = true,\n                    visualTransformation = VisualTransformation.None,\n                    interactionSource = interactionSource,\n                    placeholder = placeholder,\n                    shape = RoundedCornerShape(0.dp),\n                    colors = colors,\n                    contentPadding = PaddingValues(),\n                    container = {},\n                )\n            },\n        )\n\n        if (trailingIcon != null) {\n            trailingIcon()\n            Spacer(Modifier.width(SearchBarIconOffsetX))\n        }\n    }\n}\n\n// Measurement specs\nval InputFieldHeight = 48.dp\ninternal val TopAppBarVerticalPadding: Dp = 8.dp\ninternal val TopAppBarHorizontalPadding: Dp = 12.dp\nval SearchBarIconOffsetX: Dp = 4.dp\nprivate const val AnimationDurationMillis: Int = 300\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/SettingsSleepTimerDialog.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.SwitchDefaults\nimport androidx.compose.ui.draw.scale\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.RadioButton\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TimePicker\nimport androidx.compose.material3.rememberTimePickerState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport com.metrolist.music.R\nimport java.time.LocalTime\nimport java.time.format.DateTimeFormatter\nimport androidx.compose.material3.SingleChoiceSegmentedButtonRow\nimport androidx.compose.material3.SegmentedButton\nimport androidx.compose.material3.SegmentedButtonDefaults\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.ElevatedCard\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.Button\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.expandVertically\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.core.spring\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.material3.ButtonGroup\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\n\nfun decodeDayTimes(raw: String): MutableMap<Int, Pair<String, String>> {\n    if (raw.isBlank()) return mutableMapOf()\n    return raw\n        .split(\";\")\n        .mapNotNull { entry ->\n            val parts = entry.split(\"=\")\n            if (parts.size != 2) return@mapNotNull null\n            val dayIndex = parts[0].toIntOrNull() ?: return@mapNotNull null\n            val times = parts[1].split(\"-\")\n            if (times.size != 2) return@mapNotNull null\n            dayIndex to (times[0] to times[1])\n        }.toMap()\n        .toMutableMap()\n}\n\nfun encodeDayTimes(map: Map<Int, Pair<String, String>>): String =\n    map.entries.joinToString(\";\") { (day, times) -> \"$day=${times.first}-${times.second}\" }\n\nprivate const val DEFAULT_START = \"22:00\"\nprivate const val DEFAULT_END = \"06:00\"\n\nprivate val WEEKDAY_INDICES = 0..4 // Monday to Friday\nprivate val WEEKEND_INDICES = 5..6 // Saturday and Sunday\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun SleepTimerDialog(\n    isVisible: Boolean,\n    onDismiss: () -> Unit,\n    onConfirm: (\n        repeat: String,\n        startTime: String,\n        endTime: String,\n        customDays: List<Int>?,\n        dayTimes: Map<Int, Pair<String, String>>,\n    ) -> Unit,\n    initialRepeat: String = \"daily\",\n    initialStartTime: String = DEFAULT_START,\n    initialEndTime: String = DEFAULT_END,\n    initialCustomDays: List<Int> = listOf(0, 1, 2, 3, 4),\n    initialDayTimes: Map<Int, Pair<String, String>> = emptyMap(),\n) {\n    if (!isVisible) return\n\n    var selectedRepeat by remember {\n        mutableStateOf(\n            when (initialRepeat) {\n                \"weekdays\", \"weekends\", \"weekdays_weekends\" -> \"weekdays_weekends\"\n                else -> initialRepeat\n            },\n        )\n    }\n\n    var weekdaysEnabled by remember {\n        // Restore from the previously saved repeat value\n        mutableStateOf(initialRepeat in listOf(\"weekdays\", \"weekdays_weekends\"))\n    }\n    var weekendsEnabled by remember {\n        mutableStateOf(initialRepeat in listOf(\"weekends\", \"weekdays_weekends\"))\n    }\n\n    var weekdaysStart by remember {\n        mutableStateOf(initialDayTimes[WEEKDAY_INDICES.first]?.first ?: initialStartTime)\n    }\n    var weekdaysEnd by remember {\n        mutableStateOf(initialDayTimes[WEEKDAY_INDICES.first]?.second ?: initialEndTime)\n    }\n    var weekendsStart by remember {\n        mutableStateOf(initialDayTimes[WEEKEND_INDICES.first]?.first ?: initialStartTime)\n    }\n    var weekendsEnd by remember {\n        mutableStateOf(initialDayTimes[WEEKEND_INDICES.first]?.second ?: initialEndTime)\n    }\n\n    var selectedStartTime by remember { mutableStateOf(initialStartTime) }\n    var selectedEndTime by remember { mutableStateOf(initialEndTime) }\n    var selectedDays by remember { mutableStateOf(initialCustomDays) }\n    var dayTimesMap by remember { mutableStateOf(initialDayTimes) }\n\n    var activeTimePicker by remember { mutableStateOf<String?>(null) }\n\n    activeTimePicker?.let { pickerKey ->\n        val isStart = pickerKey.contains(\"start\")\n        val title =\n            if (isStart) {\n                stringResource(R.string.sleep_timer_start_time)\n            } else {\n                stringResource(R.string.sleep_timer_end_time)\n            }\n\n        val currentTime =\n            when (pickerKey) {\n                \"global_start\" -> {\n                    selectedStartTime\n                }\n\n                \"global_end\" -> {\n                    selectedEndTime\n                }\n\n                \"weekdays_start\" -> {\n                    weekdaysStart\n                }\n\n                \"weekdays_end\" -> {\n                    weekdaysEnd\n                }\n\n                \"weekends_start\" -> {\n                    weekendsStart\n                }\n\n                \"weekends_end\" -> {\n                    weekendsEnd\n                }\n\n                else -> {\n                    val dayIdx = pickerKey.substringAfterLast(\"_\").toIntOrNull() ?: 0\n                    if (isStart) {\n                        dayTimesMap[dayIdx]?.first ?: DEFAULT_START\n                    } else {\n                        dayTimesMap[dayIdx]?.second ?: DEFAULT_END\n                    }\n                }\n            }\n\n        SleepTimerTimePickerDialog(\n            title = title,\n            initialTime = currentTime,\n            onDismiss = { activeTimePicker = null },\n            onConfirm = { time ->\n                when (pickerKey) {\n                    \"global_start\" -> {\n                        selectedStartTime = time\n                    }\n\n                    \"global_end\" -> {\n                        selectedEndTime = time\n                    }\n\n                    \"weekdays_start\" -> {\n                        weekdaysStart = time\n                    }\n\n                    \"weekdays_end\" -> {\n                        weekdaysEnd = time\n                    }\n\n                    \"weekends_start\" -> {\n                        weekendsStart = time\n                    }\n\n                    \"weekends_end\" -> {\n                        weekendsEnd = time\n                    }\n\n                    else -> {\n                        val dayIdx = pickerKey.substringAfterLast(\"_\").toIntOrNull() ?: 0\n                        val existing = dayTimesMap[dayIdx] ?: (DEFAULT_START to DEFAULT_END)\n                        dayTimesMap =\n                            (\n                                dayTimesMap + (\n                                    dayIdx to\n                                        if (isStart) {\n                                            existing.copy(first = time)\n                                        } else {\n                                            existing.copy(second = time)\n                                        }\n                                )\n                            ).toMutableMap()\n                    }\n                }\n                activeTimePicker = null\n            },\n        )\n    }\n\n    val dayLabelRes =\n        listOf(\n            R.string.sleep_timer_monday,\n            R.string.sleep_timer_tuesday,\n            R.string.sleep_timer_wednesday,\n            R.string.sleep_timer_thursday,\n            R.string.sleep_timer_friday,\n            R.string.sleep_timer_saturday,\n            R.string.sleep_timer_sunday,\n        )\n\n    ListDialog(onDismiss = onDismiss) {\n        item {\n            Text(\n                text = stringResource(R.string.sleep_timer),\n                style = MaterialTheme.typography.headlineSmall,\n                modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),\n            )\n        }\n\n        item {\n            SingleChoiceSegmentedButtonRow(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = 16.dp, vertical = 8.dp),\n            ) {\n                SegmentedButton(\n                    selected = selectedRepeat == \"daily\",\n                    onClick = { selectedRepeat = \"daily\" },\n                    shape = SegmentedButtonDefaults.itemShape(index = 0, count = 3),\n                    label = { Text(stringResource(R.string.sleep_timer_daily)) },\n                )\n                SegmentedButton(\n                    selected = selectedRepeat == \"weekdays_weekends\",\n                    onClick = {\n                        selectedRepeat = \"weekdays_weekends\"\n                        if (!weekdaysEnabled && !weekendsEnabled) weekdaysEnabled = true\n                    },\n                    shape = SegmentedButtonDefaults.itemShape(index = 1, count = 3),\n                    label = { Text(stringResource(R.string.sleep_timer_weekdays_weekends)) },\n                )\n                SegmentedButton(\n                    selected = selectedRepeat == \"custom\",\n                    onClick = { selectedRepeat = \"custom\" },\n                    shape = SegmentedButtonDefaults.itemShape(index = 2, count = 3),\n                    label = { Text(stringResource(R.string.sleep_timer_custom)) },\n                )\n            }\n        }\n\n        item {\n            AnimatedVisibility(\n                visible = selectedRepeat == \"daily\",\n                enter = expandVertically(\n                    animationSpec = spring(\n                        dampingRatio = Spring.DampingRatioMediumBouncy,\n                        stiffness = Spring.StiffnessMedium,\n                    ),\n                ) + fadeIn(),\n                exit = shrinkVertically(\n                    animationSpec = spring(\n                        dampingRatio = Spring.DampingRatioMediumBouncy,\n                        stiffness = Spring.StiffnessMedium,\n                    ),\n                ) + fadeOut(),\n            ) {\n                ElevatedCard(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 16.dp, vertical = 4.dp),\n                ) {\n                    TimeRangeRow(\n                        startTime = selectedStartTime,\n                        endTime = selectedEndTime,\n                        onStartClick = { activeTimePicker = \"global_start\" },\n                        onEndClick = { activeTimePicker = \"global_end\" },\n                    )\n                }\n            }\n        }\n\n        item {\n            AnimatedVisibility(\n                visible = selectedRepeat == \"weekdays_weekends\",\n                enter = expandVertically(\n                    animationSpec = spring(\n                        dampingRatio = Spring.DampingRatioLowBouncy,\n                        stiffness = Spring.StiffnessMedium,\n                    ),\n                ) + fadeIn(),\n                exit = shrinkVertically(\n                    animationSpec = spring(\n                        dampingRatio = Spring.DampingRatioLowBouncy,\n                        stiffness = Spring.StiffnessMedium,\n                    ),\n                ) + fadeOut(),\n            ) {\n                ElevatedCard(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 16.dp, vertical = 4.dp),\n                ) {\n                    Column(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(horizontal = 16.dp),\n                    ) {\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .clickable { weekdaysEnabled = !weekdaysEnabled }\n                                .padding(vertical = 8.dp),\n                        ) {\n                            Text(\n                                text = stringResource(R.string.sleep_timer_weekdays),\n                                modifier = Modifier.weight(1f),\n                            )\n                            Switch(\n                                checked = weekdaysEnabled,\n                                onCheckedChange = { weekdaysEnabled = it },\n                                thumbContent = {\n                                    Icon(\n                                        painter = painterResource(\n                                            if (weekdaysEnabled) R.drawable.check else R.drawable.close,\n                                        ),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(SwitchDefaults.IconSize),\n                                    )\n                                },\n                                modifier = Modifier.scale(0.85f),\n                            )\n                        }\n                        AnimatedVisibility(\n                            visible = weekdaysEnabled,\n                            enter = expandVertically(\n                                animationSpec = spring(\n                                    dampingRatio = Spring.DampingRatioMediumBouncy,\n                                    stiffness = Spring.StiffnessMedium,\n                                ),\n                            ) + fadeIn(),\n                            exit = shrinkVertically(\n                                animationSpec = spring(\n                                    dampingRatio = Spring.DampingRatioMediumBouncy,\n                                    stiffness = Spring.StiffnessMedium,\n                                ),\n                            ) + fadeOut(),\n                        ) {\n                            TimeRangeRow(\n                                startTime = weekdaysStart,\n                                endTime = weekdaysEnd,\n                                onStartClick = { activeTimePicker = \"weekdays_start\" },\n                                onEndClick = { activeTimePicker = \"weekdays_end\" },\n                                modifier = Modifier.padding(bottom = 4.dp),\n                            )\n                        }\n\n                        HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))\n\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .clickable { weekendsEnabled = !weekendsEnabled }\n                                .padding(vertical = 8.dp),\n                        ) {\n                            Text(\n                                text = stringResource(R.string.sleep_timer_weekends),\n                                modifier = Modifier.weight(1f),\n                            )\n                            Switch(\n                                checked = weekendsEnabled,\n                                onCheckedChange = { weekendsEnabled = it },\n                                thumbContent = {\n                                    Icon(\n                                        painter = painterResource(\n                                            if (weekendsEnabled) R.drawable.check else R.drawable.close,\n                                        ),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(SwitchDefaults.IconSize),\n                                    )\n                                },\n                                modifier = Modifier.scale(0.85f),\n                            )\n                        }\n                        AnimatedVisibility(\n                            visible = weekendsEnabled,\n                            enter = expandVertically(\n                                animationSpec = spring(\n                                    dampingRatio = Spring.DampingRatioMediumBouncy,\n                                    stiffness = Spring.StiffnessMedium,\n                                ),\n                            ) + fadeIn(),\n                            exit = shrinkVertically(\n                                animationSpec = spring(\n                                    dampingRatio = Spring.DampingRatioMediumBouncy,\n                                    stiffness = Spring.StiffnessMedium,\n                                ),\n                            ) + fadeOut(),\n                        ) {\n                            TimeRangeRow(\n                                startTime = weekendsStart,\n                                endTime = weekendsEnd,\n                                onStartClick = { activeTimePicker = \"weekends_start\" },\n                                onEndClick = { activeTimePicker = \"weekends_end\" },\n                                modifier = Modifier.padding(bottom = 4.dp),\n                            )\n                        }\n                    }\n                }\n            }\n        }\n\n\n        item {\n            AnimatedVisibility(\n                visible = selectedRepeat == \"custom\",\n                enter = expandVertically(\n                    animationSpec = spring(\n                        dampingRatio = Spring.DampingRatioLowBouncy,\n                        stiffness = Spring.StiffnessMedium,\n                    ),\n                ) + fadeIn(),\n                exit = shrinkVertically(\n                    animationSpec = spring(\n                        dampingRatio = Spring.DampingRatioLowBouncy,\n                        stiffness = Spring.StiffnessMedium,\n                    ),\n                ) + fadeOut(),\n            ) {\n                ElevatedCard(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 16.dp, vertical = 4.dp),\n                ) {\n                    Column(modifier = Modifier.fillMaxWidth()) {\n                        dayLabelRes.indices.forEach { index ->\n                            val isDaySelected = index in selectedDays\n                            val dayTimes = dayTimesMap[index] ?: (DEFAULT_START to DEFAULT_END)\n\n                            if (index > 0) {\n                                HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))\n                            }\n\n                            Column(\n                                modifier = Modifier\n                                    .fillMaxWidth()\n                                    .padding(horizontal = 16.dp, vertical = 4.dp),\n                            ) {\n                                Row(\n                                    verticalAlignment = Alignment.CenterVertically,\n                                    modifier = Modifier\n                                        .fillMaxWidth()\n                                        .clickable {\n                                            selectedDays =\n                                                if (index in selectedDays) selectedDays - index else selectedDays + index\n                                        }.padding(vertical = 6.dp),\n                                ) {\n                                    Text(\n                                        text = stringResource(dayLabelRes[index]),\n                                        modifier = Modifier.weight(1f),\n                                        style = MaterialTheme.typography.bodyMedium,\n                                    )\n                                    Switch(\n                                        checked = isDaySelected,\n                                        onCheckedChange = {\n                                            selectedDays =\n                                                if (index in selectedDays) selectedDays - index else selectedDays + index\n                                        },\n                                        thumbContent = {\n                                            Icon(\n                                                painter = painterResource(\n                                                    if (isDaySelected) R.drawable.check else R.drawable.close,\n                                                ),\n                                                contentDescription = null,\n                                                modifier = Modifier.size(SwitchDefaults.IconSize),\n                                            )\n                                        },\n                                        modifier = Modifier.scale(0.85f),\n                                    )\n                                }\n                                AnimatedVisibility(\n                                    visible = isDaySelected,\n                                    enter = expandVertically(\n                                        animationSpec = spring(\n                                            dampingRatio = Spring.DampingRatioMediumBouncy,\n                                            stiffness = Spring.StiffnessMedium,\n                                        ),\n                                    ) + fadeIn(),\n                                    exit = shrinkVertically(\n                                        animationSpec = spring(\n                                            dampingRatio = Spring.DampingRatioMediumBouncy,\n                                            stiffness = Spring.StiffnessMedium,\n                                        ),\n                                    ) + fadeOut(),\n                                ) {\n                                    TimeRangeRow(\n                                        startTime = dayTimes.first,\n                                        endTime = dayTimes.second,\n                                        onStartClick = { activeTimePicker = \"day_start_$index\" },\n                                        onEndClick = { activeTimePicker = \"day_end_$index\" },\n                                        modifier = Modifier.padding(bottom = 4.dp),\n                                    )\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        item {\n            Row(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 16.dp, vertical = 12.dp),\n                horizontalArrangement = Arrangement.End,\n            ) {\n                ButtonGroup {\n                    TextButton(\n                        onClick = onDismiss,\n                        shapes = ButtonDefaults.shapes(),\n                    ) {\n                        Text(stringResource(android.R.string.cancel))\n                    }\n                    androidx.compose.material3.Button(\n                        shapes = ButtonDefaults.shapes(),\n                        onClick = {\n                            val (finalRepeat, finalDayTimes) =\n                                when (selectedRepeat) {\n                                    \"weekdays_weekends\" -> {\n                                        val repeat =\n                                            when {\n                                                weekdaysEnabled && weekendsEnabled -> \"weekdays_weekends\"\n                                                weekdaysEnabled -> \"weekdays\"\n                                                weekendsEnabled -> \"weekends\"\n                                                else -> \"daily\"\n                                            }\n                                        val times =\n                                            buildMap {\n                                                if (weekdaysEnabled) {\n                                                    for (d in WEEKDAY_INDICES) put(d, weekdaysStart to weekdaysEnd)\n                                                }\n                                                if (weekendsEnabled) {\n                                                    for (d in WEEKEND_INDICES) put(d, weekendsStart to weekendsEnd)\n                                                }\n                                            }\n                                        repeat to times\n                                    }\n                                    else -> {\n                                        selectedRepeat to dayTimesMap.toMap()\n                                    }\n                                }\n                            onConfirm(finalRepeat, selectedStartTime, selectedEndTime, selectedDays, finalDayTimes)\n                            onDismiss()\n                        },\n                    ) {\n                        Text(stringResource(android.R.string.ok))\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun TimeRangeRow(\n    startTime: String,\n    endTime: String,\n    onStartClick: () -> Unit,\n    onEndClick: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    Row(\n        modifier = modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp),\n        horizontalArrangement = Arrangement.spacedBy(8.dp),\n    ) {\n        FilledTonalButton(onClick = onStartClick, modifier = Modifier.weight(1f)) {\n            Column(horizontalAlignment = Alignment.CenterHorizontally) {\n                Text(stringResource(R.string.sleep_timer_start_time), style = MaterialTheme.typography.labelSmall)\n                Text(startTime, style = MaterialTheme.typography.bodyLarge)\n            }\n        }\n        FilledTonalButton(onClick = onEndClick, modifier = Modifier.weight(1f)) {\n            Column(horizontalAlignment = Alignment.CenterHorizontally) {\n                Text(stringResource(R.string.sleep_timer_end_time), style = MaterialTheme.typography.labelSmall)\n                Text(endTime, style = MaterialTheme.typography.bodyLarge)\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun SleepTimerTimePickerDialog(\n    title: String,\n    onDismiss: () -> Unit,\n    onConfirm: (String) -> Unit,\n    initialTime: String = DEFAULT_START,\n) {\n    val timeFormatter = DateTimeFormatter.ofPattern(\"HH:mm\")\n    val initialLocalTime =\n        try {\n            LocalTime.parse(initialTime, timeFormatter)\n        } catch (e: Exception) {\n            LocalTime.of(9, 0)\n        }\n    val timePickerState =\n        rememberTimePickerState(\n            initialHour = initialLocalTime.hour,\n            initialMinute = initialLocalTime.minute,\n            is24Hour = true,\n        )\n    DefaultDialog(\n        title = { Text(title) },\n        onDismiss = onDismiss,\n        buttons = {\n            TextButton(onClick = onDismiss) { Text(stringResource(android.R.string.cancel)) }\n            Button(onClick = {\n                val hour = timePickerState.hour.toString().padStart(2, '0')\n                val minute = timePickerState.minute.toString().padStart(2, '0')\n                onConfirm(\"$hour:$minute\")\n            }) { Text(stringResource(android.R.string.ok)) }\n        },\n    ) { TimePicker(state = timePickerState) }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/SongDropdownSelect.kt",
    "content": "package com.metrolist.music.ui.component\n\nimport androidx.compose.foundation.horizontalScroll\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextField\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.PopupProperties\nimport com.metrolist.music.R\nimport com.metrolist.music.db.entities.SongWithStats\n\n\n@Composable\nfun SongSelectDropdown(\n    titleT: String,\n    songs: List<SongWithStats>,\n    selectedSong: MutableState<SongWithStats?>,\n) {\n    var expanded by remember { mutableStateOf(false) }\n    var searchText by remember { mutableStateOf(\"\") }\n    var textFieldWidthPx by remember { mutableIntStateOf(0) }\n    val density = LocalDensity.current\n\n\n    val filteredSongs = songs.filter { song ->\n        song.title.contains(searchText, ignoreCase = true)\n    }\n\n    val maxItemsShown = 75\n    val visibleSongs = filteredSongs.take(maxItemsShown)\n    val remainingCount = filteredSongs.size - visibleSongs.size\n\n    Box(modifier = Modifier.fillMaxWidth()) {\n        Row(verticalAlignment = Alignment.CenterVertically) {\n            TextField(\n                value = searchText,\n                onValueChange = {\n                    searchText = it\n                    expanded = true\n                    selectedSong.value = null\n                },\n                label = { Text(titleT) },\n                modifier = Modifier\n                    .weight(1f)\n                    .onGloballyPositioned { coordinates ->\n                        textFieldWidthPx = coordinates.size.width\n                    }\n            )\n\n            Spacer(modifier = Modifier.width(8.dp))\n        }\n\n        DropdownMenu(\n            expanded = expanded,\n            onDismissRequest = { expanded = false },\n            modifier = Modifier.width(with(density) { textFieldWidthPx.toDp() }),\n            properties = PopupProperties(focusable = false)\n        ) {\n            Column(\n                modifier = Modifier\n                    .heightIn(max = 160.dp) // the scroll \"box\"\n                    .verticalScroll(rememberScrollState())\n            ) {\n                visibleSongs.forEach { song ->\n                    val scrollState = rememberScrollState()\n                    DropdownMenuItem(\n                        onClick = {\n                            searchText = song.title\n                            selectedSong.value = song\n                            expanded = false\n                        },\n                        text = {\n                            Row (modifier = Modifier.horizontalScroll(scrollState)) {\n                                Text(\n                                    text = song.title,\n                                    maxLines = 1,\n                                )\n                                Spacer(modifier = Modifier.width(8.dp))\n                                val displayArtists = song.artists.joinToString(\", \") { it.name }.ifBlank { song.artistName }\n                                displayArtists?.let {\n                                    Text(\n                                        text = it,\n                                        maxLines = 1,\n                                        color = androidx.compose.ui.graphics.Color.Gray,\n                                        overflow = TextOverflow.Ellipsis // Highly recommended for multi-artist names\n                                    )\n                                }\n                            }\n                        },\n                        modifier = Modifier.fillMaxWidth()\n                    )\n                }\n\n                if (remainingCount > 0) {\n                    DropdownMenuItem(\n                        onClick = { /* no-op */ },\n                        enabled = false,\n                        text = {\n                            Text(\n                                text = stringResource(\n                                    R.string.song_dropdown_more_results,\n                                    remainingCount,\n                                ),\n                            )\n                        },\n                        modifier = Modifier.fillMaxWidth()\n                    )\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/SortHeader.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.ripple\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.PlaylistSongSortType\n\n@Composable\ninline fun <reified T : Enum<T>> SortHeader(\n    sortType: T,\n    sortDescending: Boolean,\n    crossinline onSortTypeChange: (T) -> Unit,\n    crossinline onSortDescendingChange: (Boolean) -> Unit,\n    crossinline sortTypeText: (T) -> Int,\n    modifier: Modifier = Modifier,\n    showDescending: Boolean? = true,\n) {\n    var menuExpanded by remember { mutableStateOf(false) }\n\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = modifier.padding(vertical = 8.dp),\n    ) {\n        Text(\n            text = stringResource(sortTypeText(sortType)),\n            color = MaterialTheme.colorScheme.primary,\n            style = MaterialTheme.typography.labelLarge,\n            modifier =\n            Modifier\n                .clickable(\n                    interactionSource = remember { MutableInteractionSource() },\n                    indication = ripple(bounded = false),\n                ) {\n                    menuExpanded = !menuExpanded\n                }.padding(horizontal = 4.dp, vertical = 8.dp),\n        )\n\n        DropdownMenu(\n            expanded = menuExpanded,\n            onDismissRequest = { menuExpanded = false },\n            modifier = Modifier.widthIn(min = 172.dp),\n        ) {\n            enumValues<T>().forEach { type ->\n                DropdownMenuItem(\n                    text = {\n                        Text(\n                            text = stringResource(sortTypeText(type)),\n                            fontSize = 16.sp,\n                            fontWeight = FontWeight.Normal,\n                        )\n                    },\n                    trailingIcon = {\n                        Icon(\n                            painter =\n                            painterResource(\n                                if (sortType ==\n                                    type\n                                ) {\n                                    R.drawable.radio_button_checked\n                                } else {\n                                    R.drawable.radio_button_unchecked\n                                },\n                            ),\n                            contentDescription = null,\n                        )\n                    },\n                    onClick = {\n                        onSortTypeChange(type)\n                        menuExpanded = false\n                    },\n                )\n            }\n        }\n\n        if (sortType != PlaylistSongSortType.CUSTOM && showDescending == true) {\n            ResizableIconButton(\n                icon = if (sortDescending) R.drawable.arrow_downward else R.drawable.arrow_upward,\n                color = MaterialTheme.colorScheme.primary,\n                modifier =\n                Modifier\n                    .size(32.dp)\n                    .padding(8.dp),\n                onClick = { onSortDescendingChange(!sortDescending) },\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/SpeedDialGridItem.kt",
    "content": "package com.metrolist.music.ui.component\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport com.metrolist.innertube.models.ArtistItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.YTItem\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.ThumbnailCornerRadius\n\n@Composable\nfun SpeedDialGridItem(\n    item: YTItem,\n    isPinned: Boolean,\n    modifier: Modifier = Modifier,\n    isActive: Boolean = false,\n    isPlaying: Boolean = false,\n) {\n    Box(\n        modifier = modifier\n            .fillMaxWidth()\n            .aspectRatio(1f) // Square aspect ratio\n            .clip(RoundedCornerShape(ThumbnailCornerRadius))\n    ) {\n        // Thumbnail\n        ItemThumbnail(\n            thumbnailUrl = item.thumbnail,\n            isActive = isActive,\n            isPlaying = isPlaying,\n            shape = if (item is ArtistItem) CircleShape else RoundedCornerShape(ThumbnailCornerRadius),\n            modifier = Modifier.fillMaxSize()\n        )\n\n        // Gradient Overlay for Text Readability and Icon Contrast\n        Box(\n            modifier = Modifier\n                .fillMaxSize()\n                .background(\n                    Brush.verticalGradient(\n                        colors = listOf(\n                            Color.Black.copy(alpha = 0.4f), // Top scrim for icon visibility on bright covers\n                            Color.Transparent,\n                            Color.Black.copy(alpha = 0.6f),\n                            Color.Black.copy(alpha = 0.9f)\n                        )\n                    )\n                )\n        )\n\n        // Title and Chevron\n        Row(\n            modifier = Modifier\n                .align(Alignment.BottomStart)\n                .padding(8.dp) // Reduced padding for tighter layout\n                .fillMaxWidth(),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Text(\n                text = item.title,\n                style = MaterialTheme.typography.titleSmall, // Smaller, punchier font\n                fontWeight = FontWeight.Bold,\n                color = Color.White,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                modifier = Modifier.weight(1f)\n            )\n            \n            // Navigation Chevron for browsable items (Album, Playlist, Artist)\n            if (item !is SongItem) {\n                Icon(\n                    painter = painterResource(R.drawable.navigate_next),\n                    contentDescription = null,\n                    tint = Color.White,\n                    modifier = Modifier.size(20.dp)\n                )\n        }\n    }\n        // Pinned Icon\n        if (isPinned) {\n            Icon(\n                painter = painterResource(R.drawable.ic_push_pin),\n                contentDescription = null,\n                tint = Color.White,\n                modifier = Modifier\n                    .align(Alignment.TopEnd)\n                    .padding(8.dp)\n                    .size(16.dp)\n            )\n        }\n\n\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/SquigglySlider.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n * \n * Squiggly Slider - ported from mpvEx project\n * https://github.com/marlboro-advance/mpvEx\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.LinearEasing\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.gestures.detectDragGestures\nimport androidx.compose.foundation.gestures.detectTapGestures\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.material3.SliderColors\nimport androidx.compose.material3.SliderDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.withFrameMillis\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.Path\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.graphics.drawscope.clipRect\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\n\n@Composable\nfun SquigglySlider(\n    value: Float,\n    onValueChange: (Float) -> Unit,\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    valueRange: ClosedFloatingPointRange<Float> = 0f..1f,\n    onValueChangeFinished: (() -> Unit)? = null,\n    colors: SliderColors = SliderDefaults.colors(),\n    isPlaying: Boolean = true,\n) {\n    val primaryColor = colors.activeTrackColor\n    val inactiveColor = colors.inactiveTrackColor\n\n    var isDragging by remember { mutableStateOf(false) }\n    var dragPosition by remember { mutableFloatStateOf(value) }\n    \n    val currentValue = if (isDragging) dragPosition else value\n    val duration = valueRange.endInclusive - valueRange.start\n    val position = currentValue - valueRange.start\n\n    // Animation state\n    var phaseOffset by remember { mutableFloatStateOf(0f) }\n    var heightFraction by remember { mutableFloatStateOf(if (isPlaying) 1f else 0f) }\n\n    val scope = rememberCoroutineScope()\n\n    // Wave parameters\n    val waveLength = 80f\n    val lineAmplitude = 6f\n    val phaseSpeed = 24f // Faster wave movement to match old squiggly\n    val transitionPeriods = 1.5f\n    val minWaveEndpoint = 0f\n    val matchedWaveEndpoint = 1f\n    val transitionEnabled = true\n\n    // Animate height fraction based on playing state and dragging state\n    LaunchedEffect(isPlaying, isDragging) {\n        scope.launch {\n            val shouldFlatten = !isPlaying || isDragging\n            val targetHeight = if (shouldFlatten) 0f else 1f\n            val animDuration = if (shouldFlatten) 150 else 200 // Faster appear/disappear\n            val startDelay = if (shouldFlatten) 0L else 30L\n\n            delay(startDelay)\n\n            val animator = Animatable(heightFraction)\n            animator.animateTo(\n                targetValue = targetHeight,\n                animationSpec = tween(\n                    durationMillis = animDuration,\n                    easing = LinearEasing,\n                ),\n            ) {\n                heightFraction = this.value\n            }\n        }\n    }\n\n    // Animate wave movement only when playing\n    LaunchedEffect(isPlaying) {\n        if (!isPlaying) return@LaunchedEffect\n\n        var lastFrameTime = withFrameMillis { it }\n        while (isActive) {\n            withFrameMillis { frameTimeMillis ->\n                val deltaTime = (frameTimeMillis - lastFrameTime) / 1000f\n                phaseOffset += deltaTime * phaseSpeed\n                phaseOffset %= waveLength\n                lastFrameTime = frameTimeMillis\n            }\n        }\n    }\n\n    Box(\n        modifier = modifier\n            .fillMaxWidth()\n            .height(48.dp)\n            .then(\n                if (enabled) {\n                    Modifier\n                        .pointerInput(valueRange) {\n                            detectTapGestures { offset ->\n                                val newPosition = (offset.x / size.width) * duration\n                                val mappedValue = valueRange.start + newPosition.coerceIn(0f, duration)\n                                onValueChange(mappedValue)\n                                onValueChangeFinished?.invoke()\n                            }\n                        }\n                        .pointerInput(valueRange) {\n                            detectDragGestures(\n                                onDragStart = { offset ->\n                                    isDragging = true\n                                    val newPosition = (offset.x / size.width) * duration\n                                    dragPosition = valueRange.start + newPosition.coerceIn(0f, duration)\n                                    onValueChange(dragPosition)\n                                },\n                                onDragEnd = {\n                                    isDragging = false\n                                    onValueChangeFinished?.invoke()\n                                },\n                                onDragCancel = {\n                                    isDragging = false\n                                },\n                                onDrag = { change, _ ->\n                                    change.consume()\n                                    val newPosition = (change.position.x / size.width) * duration\n                                    dragPosition = valueRange.start + newPosition.coerceIn(0f, duration)\n                                    onValueChange(dragPosition)\n                                }\n                            )\n                        }\n                } else {\n                    Modifier\n                }\n            ),\n        contentAlignment = Alignment.Center\n    ) {\n        Canvas(\n            modifier = Modifier\n                .fillMaxWidth()\n                .height(48.dp)\n        ) {\n            val strokeWidth = 5.dp.toPx()\n            val progress = if (duration > 0f) (position / duration).coerceIn(0f, 1f) else 0f\n            val totalWidth = size.width\n            val totalProgressPx = totalWidth * progress\n            val centerY = size.height / 2f\n\n            // Calculate wave progress\n            val waveProgressPx = if (!transitionEnabled || progress > matchedWaveEndpoint) {\n                totalWidth * progress\n            } else {\n                val t = (progress / matchedWaveEndpoint).coerceIn(0f, 1f)\n                totalWidth * (minWaveEndpoint + (matchedWaveEndpoint - minWaveEndpoint) * t)\n            }\n\n            // Helper function to compute amplitude\n            fun computeAmplitude(x: Float, sign: Float): Float {\n                return if (transitionEnabled) {\n                    val length = transitionPeriods * waveLength\n                    val coeff = ((waveProgressPx + length / 2f - x) / length).coerceIn(0f, 1f)\n                    sign * heightFraction * lineAmplitude * coeff\n                } else {\n                    sign * heightFraction * lineAmplitude\n                }\n            }\n\n            // Build wavy path for played portion\n            val path = Path()\n            val waveStart = -phaseOffset - waveLength / 2f\n            val waveEnd = if (transitionEnabled) totalWidth else waveProgressPx\n\n            path.moveTo(waveStart, centerY)\n\n            var currentX = waveStart\n            var waveSign = 1f\n            var currentAmp = computeAmplitude(currentX, waveSign)\n            val dist = waveLength / 2f\n\n            while (currentX < waveEnd) {\n                waveSign = -waveSign\n                val nextX = currentX + dist\n                val midX = currentX + dist / 2f\n                val nextAmp = computeAmplitude(nextX, waveSign)\n\n                path.cubicTo(\n                    midX,\n                    centerY + currentAmp,\n                    midX,\n                    centerY + nextAmp,\n                    nextX,\n                    centerY + nextAmp,\n                )\n\n                currentAmp = nextAmp\n                currentX = nextX\n            }\n\n            // Draw path up to progress position using clipping\n            val clipTop = lineAmplitude + strokeWidth\n\n            val disabledAlpha = 77f / 255f\n            val inactiveTrackColor = primaryColor.copy(alpha = disabledAlpha)\n            val capRadius = strokeWidth / 2f\n\n            fun drawPathSegment(startX: Float, endX: Float, color: Color) {\n                if (endX <= startX) return\n                clipRect(\n                    left = startX,\n                    top = centerY - clipTop,\n                    right = endX,\n                    bottom = centerY + clipTop,\n                ) {\n                    drawPath(\n                        path = path,\n                        color = color,\n                        style = Stroke(width = strokeWidth, cap = StrokeCap.Round),\n                    )\n                }\n            }\n\n            // Played segment\n            drawPathSegment(0f, totalProgressPx, primaryColor)\n\n            // Unplayed segment\n            drawPathSegment(totalProgressPx, totalWidth, inactiveTrackColor)\n\n            // Helper function to get wave Y position at any X\n            fun getWaveY(x: Float): Float {\n                val phase = (x - waveStart) / waveLength\n                val waveCycle = phase - kotlin.math.floor(phase)\n                val waveValue = kotlin.math.cos(waveCycle * 2f * kotlin.math.PI.toFloat())\n                \n                // Calculate amplitude coefficient at this x position\n                val ampCoeff = if (transitionEnabled) {\n                    val length = transitionPeriods * waveLength\n                    ((waveProgressPx + length / 2f - x) / length).coerceIn(0f, 1f)\n                } else {\n                    1f\n                }\n                \n                return centerY + waveValue * lineAmplitude * heightFraction * ampCoeff\n            }\n\n            // Draw round cap at start (synced with wave)\n            drawCircle(\n                color = primaryColor,\n                radius = capRadius,\n                center = Offset(0f, getWaveY(0f)),\n            )\n\n            // Draw round cap at end (only right half, synced with wave movement)\n            val endWaveY = getWaveY(totalWidth)\n            clipRect(\n                left = totalWidth,\n                top = centerY - clipTop,\n                right = totalWidth + capRadius,\n                bottom = centerY + clipTop,\n            ) {\n                drawCircle(\n                    color = inactiveTrackColor,\n                    radius = capRadius,\n                    center = Offset(totalWidth, endWaveY),\n                )\n            }\n\n            // Vertical Bar Thumb\n            val barHalfHeight = (lineAmplitude + strokeWidth)\n            val barWidth = 5.dp.toPx()\n\n            if (barHalfHeight > 0.5f) {\n                drawLine(\n                    color = primaryColor,\n                    start = Offset(totalProgressPx, centerY - barHalfHeight),\n                    end = Offset(totalProgressPx, centerY + barHalfHeight),\n                    strokeWidth = barWidth,\n                    cap = StrokeCap.Round,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/TimeTransfer.kt",
    "content": "package com.metrolist.music.ui.component\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport com.metrolist.music.R\nimport com.metrolist.music.db.entities.SongWithStats\nimport com.metrolist.music.viewmodels.StatsViewModel\nimport java.util.Locale\nimport java.util.concurrent.TimeUnit\n\n@Composable\nfun TimeTransfer(\n    onDismiss: () -> Unit,\n    viewModel: StatsViewModel = hiltViewModel()\n) {\n    val sourceSong = remember { mutableStateOf<SongWithStats?>(null) }\n    val targetSong = remember { mutableStateOf<SongWithStats?>(null) }\n\n    val mostPlayedSongsStats by viewModel.mostPlayedSongsStats.collectAsState()\n\n    DefaultDialog(\n        onDismiss = onDismiss,\n        title = {\n            Text(\n                text = stringResource(R.string.time_transfer_title),\n                modifier = Modifier.fillMaxWidth(),\n                textAlign = TextAlign.Center,\n            )\n        },\n        content = {\n            Text(\n                text = stringResource(R.string.time_transfer_warning),\n                modifier = Modifier.fillMaxWidth(),\n                textAlign = TextAlign.Center,\n                color = androidx.compose.ui.graphics.Color.Red,\n            )\n\n            Spacer(modifier = Modifier.height(12.dp))\n\n            Column {\n                SongSelectDropdown(\n                    titleT = stringResource(R.string.time_transfer_source_song),\n                    songs = mostPlayedSongsStats,\n                    selectedSong = sourceSong\n                )\n\n                Spacer(modifier = Modifier.height(12.dp))\n\n                Row {\n                    Text(stringResource(R.string.time_transfer_listen_time_label))\n                    if (sourceSong.value != null) {\n                        Text(\n                            text = formatMillis(sourceSong.value!!.timeListened),\n                            fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,\n                        )\n                    }\n                }\n\n                Spacer(modifier = Modifier.height(12.dp))\n\n\n                SongSelectDropdown(\n                    titleT = stringResource(R.string.time_transfer_target_song),\n                    songs = mostPlayedSongsStats,\n                    selectedSong = targetSong,\n                )\n\n                Spacer(modifier = Modifier.height(12.dp))\n\n                Row {\n                    Text(stringResource(R.string.time_transfer_listen_time_label))\n                    if (targetSong.value != null) {\n                        Text(\n                            text = formatMillis(targetSong.value!!.timeListened),\n                            fontWeight = androidx.compose.ui.text.font.FontWeight.Bold,\n                        )\n                    }\n                }\n\n                Spacer(modifier = Modifier.height(12.dp))\n\n                Button(\n                    onClick = {\n                        val from = sourceSong.value?.id\n                        val to = targetSong.value?.id\n                        if (from != null && to != null && from != to) {\n                            viewModel.transferSongStats(from, to) {\n                                sourceSong.value = null\n                                targetSong.value = null\n                                onDismiss()\n                            }\n                        }\n                    },\n                    modifier = Modifier.fillMaxWidth(),\n                    enabled = sourceSong.value != null &&\n                            targetSong.value != null &&\n                            sourceSong.value!!.id != targetSong.value!!.id,\n                ) {\n                    Text(\n                        text = stringResource(R.string.time_transfer_convert),\n                        color = MaterialTheme.colorScheme.onPrimary,\n                    )\n                }\n            }\n        }\n\n    )\n}\n\nfun formatMillis(ms: Long?): String {\n    if (ms == null) {\n        return \"00:00:00\"\n    }\n    val hours = TimeUnit.MILLISECONDS.toHours(ms)\n    val minutes = TimeUnit.MILLISECONDS.toMinutes(ms) % 60\n    val seconds = TimeUnit.MILLISECONDS.toSeconds(ms) % 60\n\n    return String.format(Locale.US,\"%02d:%02d:%02d\", hours, minutes, seconds)\n}"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/VolumeSlider.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n * \n * Material 3 Expressive Volume Slider\n * Based on M3 Expressive Slider specifications (Size M):\n * - Track height: 40dp\n * - Handle height: 52dp\n * - Handle width: 4dp\n * - Track corner radius: 12dp\n * - Inset icon size: 24dp\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Slider\nimport androidx.compose.material3.SliderDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.drawWithContent\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.ColorFilter\nimport androidx.compose.ui.graphics.drawscope.DrawScope\nimport androidx.compose.ui.graphics.drawscope.translate\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport com.metrolist.music.R\n\n/**\n * Material 3 Expressive Volume Slider dimensions (Size M)\n */\nprivate object VolumeSliderDefaults {\n    val TrackHeight: Dp = 40.dp\n    val HandleHeight: Dp = 52.dp\n    val HandleWidth: Dp = 4.dp\n    val TrackCornerRadius: Dp = 12.dp\n    val InsetIconSize: Dp = 24.dp\n    val IconPadding: Dp = 10.dp\n    val ThumbTrackGapSize: Dp = 6.dp\n    val StopIndicatorRadius: Dp = 4.dp\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun VolumeSlider(\n    value: Float,\n    onValueChange: (Float) -> Unit,\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    onValueChangeFinished: (() -> Unit)? = null,\n    accentColor: Color = MaterialTheme.colorScheme.primary\n) {\n    val interactionSource = remember { MutableInteractionSource() }\n\n    val volumeOffIcon = painterResource(R.drawable.volume_off)\n    val volumeMuteIcon = painterResource(R.drawable.volume_mute)\n    val volumeDownIcon = painterResource(R.drawable.volume_down)\n    val volumeUpIcon = painterResource(R.drawable.volume_up)\n\n    val currentIcon = when {\n        value <= 0f -> volumeOffIcon\n        value < 0.33f -> volumeMuteIcon\n        value < 0.66f -> volumeDownIcon\n        else -> volumeUpIcon\n    }\n\n    val colors = SliderDefaults.colors(\n        thumbColor = accentColor,\n        activeTrackColor = accentColor,\n        activeTickColor = MaterialTheme.colorScheme.onPrimary,\n        inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant,\n        inactiveTickColor = MaterialTheme.colorScheme.onSurfaceVariant\n    )\n    \n    val stopIndicatorColor = MaterialTheme.colorScheme.onSurfaceVariant\n\n    Slider(\n        value = value,\n        onValueChange = onValueChange,\n        modifier = modifier,\n        enabled = enabled,\n        valueRange = 0f..1f,\n        onValueChangeFinished = onValueChangeFinished,\n        colors = colors,\n        interactionSource = interactionSource,\n        track = { sliderState ->\n            val iconSize = DpSize(VolumeSliderDefaults.InsetIconSize, VolumeSliderDefaults.InsetIconSize)\n            val activeIconColor = colors.activeTickColor\n            val inactiveIconColor = colors.inactiveTickColor\n\n            SliderDefaults.Track(\n                sliderState = sliderState,\n                modifier = Modifier\n                    .height(VolumeSliderDefaults.TrackHeight)\n                    .drawWithContent {\n                        drawContent()\n                        val yOffset = size.height / 2 - iconSize.toSize().height / 2\n                        val fraction = value.coerceIn(0f, 1f)\n                        val thumbGapPx = VolumeSliderDefaults.ThumbTrackGapSize.toPx()\n                        val activeTrackEnd = size.width * fraction - thumbGapPx\n                        val inactiveTrackStart = activeTrackEnd + thumbGapPx * 2\n                        val activeTrackWidth = activeTrackEnd\n                        val inactiveTrackWidth = size.width - inactiveTrackStart\n\n                        drawVolumeIcon(\n                            icon = currentIcon,\n                            iconSize = iconSize,\n                            iconPadding = VolumeSliderDefaults.IconPadding,\n                            yOffset = yOffset,\n                            activeTrackWidth = activeTrackWidth,\n                            inactiveTrackStart = inactiveTrackStart,\n                            inactiveTrackWidth = inactiveTrackWidth,\n                            activeIconColor = activeIconColor,\n                            inactiveIconColor = inactiveIconColor,\n                            volumeOffIcon = volumeOffIcon\n                        )\n                    },\n                colors = colors,\n                enabled = enabled,\n                thumbTrackGapSize = VolumeSliderDefaults.ThumbTrackGapSize,\n                trackCornerSize = VolumeSliderDefaults.TrackCornerRadius,\n                drawStopIndicator = if (value < 0.90f) { offset ->\n                    drawCircle(\n                        color = stopIndicatorColor,\n                        radius = VolumeSliderDefaults.StopIndicatorRadius.toPx(),\n                        center = offset\n                    )\n                } else null\n            )\n        }\n    )\n}\n\nprivate fun DrawScope.drawVolumeIcon(\n    icon: Painter,\n    iconSize: DpSize,\n    iconPadding: Dp,\n    yOffset: Float,\n    activeTrackWidth: Float,\n    inactiveTrackStart: Float,\n    inactiveTrackWidth: Float,\n    activeIconColor: Color,\n    inactiveIconColor: Color,\n    volumeOffIcon: Painter\n) {\n    val iconSizePx = iconSize.toSize()\n    val iconPaddingPx = iconPadding.toPx()\n    val minSpaceForIcon = iconSizePx.width + iconPaddingPx * 2\n\n    if (activeTrackWidth >= minSpaceForIcon) {\n        translate(iconPaddingPx, yOffset) {\n            with(icon) {\n                draw(iconSizePx, colorFilter = ColorFilter.tint(activeIconColor))\n            }\n        }\n    } else if (inactiveTrackWidth >= minSpaceForIcon) {\n        translate(inactiveTrackStart + iconPaddingPx, yOffset) {\n            with(volumeOffIcon) {\n                draw(iconSizePx, colorFilter = ColorFilter.tint(inactiveIconColor))\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/WavySlider.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component\n\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.gestures.detectHorizontalDragGestures\nimport androidx.compose.foundation.gestures.detectTapGestures\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.LinearWavyProgressIndicator\nimport androidx.compose.material3.ProgressIndicatorDefaults\nimport androidx.compose.material3.SliderColors\nimport androidx.compose.material3.SliderDefaults\nimport androidx.compose.material3.WavyProgressIndicatorDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun WavySlider(\n    value: Float,\n    onValueChange: (Float) -> Unit,\n    modifier: Modifier = Modifier,\n    valueRange: ClosedFloatingPointRange<Float> = 0f..1f,\n    onValueChangeFinished: (() -> Unit)? = null,\n    colors: SliderColors = SliderDefaults.colors(),\n    isPlaying: Boolean = true,\n    enabled: Boolean = true,\n    strokeWidth: Dp = 4.dp,\n    thumbRadius: Dp = 8.dp,\n    wavelength: Dp = WavyProgressIndicatorDefaults.LinearDeterminateWavelength,\n    waveSpeed: Dp = wavelength\n) {\n    val density = LocalDensity.current\n    val strokeWidthPx = with(density) { strokeWidth.toPx() }\n    val thumbRadiusPx = with(density) { thumbRadius.toPx() }\n    val stroke = remember(strokeWidthPx) { \n        Stroke(width = strokeWidthPx, cap = StrokeCap.Round) \n    }\n    \n    val normalizedValue = ((value - valueRange.start) / (valueRange.endInclusive - valueRange.start))\n        .coerceIn(0f, 1f)\n    \n    var isDragging by remember { mutableStateOf(false) }\n    var dragValue by remember { mutableFloatStateOf(normalizedValue) }\n    \n    val displayValue = if (isDragging) dragValue else normalizedValue\n    \n    val animatedAmplitude by animateFloatAsState(\n        targetValue = if (isPlaying) 1f else 0f,\n        animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,\n        label = \"amplitude\"\n    )\n    \n    val activeColor = colors.activeTrackColor\n    val inactiveColor = colors.inactiveTrackColor\n    val thumbColor = colors.thumbColor\n    \n    // Calculate container height to accommodate thumb\n    val containerHeight = maxOf(WavyProgressIndicatorDefaults.LinearContainerHeight, thumbRadius * 2)\n    \n    val baseModifier = modifier\n        .fillMaxWidth()\n        .height(containerHeight)\n\n    val interactiveModifier = if (enabled) {\n        baseModifier\n            .pointerInput(valueRange) {\n                detectTapGestures { offset ->\n                    val newValue = (offset.x / size.width).coerceIn(0f, 1f)\n                    val mappedValue = valueRange.start + newValue * (valueRange.endInclusive - valueRange.start)\n                    onValueChange(mappedValue)\n                    onValueChangeFinished?.invoke()\n                }\n            }\n            .pointerInput(valueRange) {\n                detectHorizontalDragGestures(\n                    onDragStart = { offset ->\n                        isDragging = true\n                        dragValue = (offset.x / size.width).coerceIn(0f, 1f)\n                        val mappedValue = valueRange.start + dragValue * (valueRange.endInclusive - valueRange.start)\n                        onValueChange(mappedValue)\n                    },\n                    onDragEnd = {\n                        isDragging = false\n                        onValueChangeFinished?.invoke()\n                    },\n                    onDragCancel = {\n                        isDragging = false\n                    },\n                    onHorizontalDrag = { _, dragAmount ->\n                        dragValue = (dragValue + dragAmount / size.width).coerceIn(0f, 1f)\n                        val mappedValue = valueRange.start + dragValue * (valueRange.endInclusive - valueRange.start)\n                        onValueChange(mappedValue)\n                    }\n                )\n            }\n    } else {\n        baseModifier\n    }\n\n    Box(\n        modifier = interactiveModifier,\n        contentAlignment = Alignment.Center\n    ) {\n        LinearWavyProgressIndicator(\n            progress = { displayValue },\n            modifier = Modifier.fillMaxWidth(),\n            color = activeColor,\n            trackColor = inactiveColor,\n            stroke = stroke,\n            trackStroke = stroke,\n            gapSize = thumbRadius + 4.dp,\n            stopSize = WavyProgressIndicatorDefaults.LinearTrackStopIndicatorSize,\n            amplitude = { progress -> if (progress > 0f) animatedAmplitude else 0f },\n            wavelength = wavelength,\n            waveSpeed = waveSpeed\n        )\n        \n        // Draw circular thumb - synced with progress indicator position\n        Canvas(modifier = Modifier.fillMaxSize()) {\n            val thumbX = size.width * displayValue\n            val thumbY = size.height / 2\n            \n            drawCircle(\n                color = thumbColor,\n                radius = thumbRadiusPx,\n                center = Offset(thumbX, thumbY)\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/shimmer/ButtonPlaceholder.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component.shimmer\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\n\n@Composable\nfun ButtonPlaceholder(modifier: Modifier = Modifier) {\n    Spacer(\n        modifier\n            .height(ButtonDefaults.MinHeight)\n            .clip(RoundedCornerShape(50))\n            .background(MaterialTheme.colorScheme.onSurface),\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/shimmer/GridItemPlaceholder.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component.shimmer\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.unit.dp\nimport com.metrolist.music.constants.GridItemSize\nimport com.metrolist.music.constants.GridItemsSizeKey\nimport com.metrolist.music.constants.GridThumbnailHeight\nimport com.metrolist.music.constants.SmallGridThumbnailHeight\nimport com.metrolist.music.constants.ThumbnailCornerRadius\nimport com.metrolist.music.utils.rememberEnumPreference\n\n@Composable\nfun GridItemPlaceHolder(\n    modifier: Modifier = Modifier,\n    thumbnailShape: Shape = RoundedCornerShape(ThumbnailCornerRadius),\n    fillMaxWidth: Boolean = false,\n) {\n    val gridItemSize by rememberEnumPreference(GridItemsSizeKey, GridItemSize.BIG)\n    val gridHeight = if (gridItemSize == GridItemSize.BIG) GridThumbnailHeight else SmallGridThumbnailHeight\n    \n    Column(\n        modifier =\n        if (fillMaxWidth) {\n            modifier\n                .padding(12.dp)\n                .fillMaxWidth()\n        } else {\n            modifier\n                .padding(12.dp)\n                .width(gridHeight)\n        },\n    ) {\n        Spacer(\n            modifier =\n            if (fillMaxWidth) {\n                Modifier.fillMaxWidth()\n            } else {\n                Modifier.height(gridHeight)\n            }.aspectRatio(1f)\n                .clip(thumbnailShape)\n                .background(MaterialTheme.colorScheme.onSurface),\n        )\n\n        Spacer(modifier = Modifier.height(6.dp))\n\n        TextPlaceholder()\n\n        TextPlaceholder()\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/shimmer/ListItemPlaceholder.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component.shimmer\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.unit.dp\nimport com.metrolist.music.constants.ListItemHeight\nimport com.metrolist.music.constants.ListThumbnailSize\nimport com.metrolist.music.constants.ThumbnailCornerRadius\n\n@Composable\nfun ListItemPlaceHolder(\n    modifier: Modifier = Modifier,\n    thumbnailShape: Shape = RoundedCornerShape(ThumbnailCornerRadius),\n) {\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier =\n        modifier\n            .height(ListItemHeight)\n            .padding(horizontal = 6.dp),\n    ) {\n        Spacer(\n            modifier =\n            Modifier\n                .padding(6.dp)\n                .size(ListThumbnailSize)\n                .clip(thumbnailShape)\n                .background(MaterialTheme.colorScheme.onSurface),\n        )\n\n        Column(\n            modifier =\n            Modifier\n                .weight(1f)\n                .padding(horizontal = 6.dp),\n        ) {\n            TextPlaceholder()\n            TextPlaceholder()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/shimmer/ShimmerHost.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component.shimmer\n\nimport androidx.compose.animation.core.LinearEasing\nimport androidx.compose.animation.core.RepeatMode\nimport androidx.compose.animation.core.infiniteRepeatable\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.ColumnScope\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.drawWithContent\nimport androidx.compose.ui.graphics.BlendMode\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.graphicsLayer\nimport com.valentinilk.shimmer.defaultShimmerTheme\nimport com.valentinilk.shimmer.shimmer\n\n@Composable\nfun ShimmerHost(\n    modifier: Modifier = Modifier,\n    horizontalAlignment: Alignment.Horizontal = Alignment.Start,\n    verticalArrangement: Arrangement.Vertical = Arrangement.Top,\n    showGradient: Boolean = true,\n    content: @Composable ColumnScope.() -> Unit,\n) {\n    val baseModifier = modifier\n        .shimmer()\n        .graphicsLayer(alpha = 0.99f)\n\n    Column(\n        horizontalAlignment = horizontalAlignment,\n        verticalArrangement = verticalArrangement,\n        modifier = if (showGradient) {\n            baseModifier.drawWithContent {\n                drawContent()\n                drawRect(\n                    brush = Brush.verticalGradient(listOf(Color.Black, Color.Transparent)),\n                    blendMode = BlendMode.DstIn,\n                )\n            }\n        } else {\n            baseModifier\n        },\n        content = content,\n    )\n}\n\nval ShimmerTheme =\n    defaultShimmerTheme.copy(\n        animationSpec =\n        infiniteRepeatable(\n            animation =\n            tween(\n                durationMillis = 800,\n                easing = LinearEasing,\n                delayMillis = 250,\n            ),\n            repeatMode = RepeatMode.Restart,\n        ),\n        shaderColors =\n        listOf(\n            Color.Unspecified.copy(alpha = 0.25f),\n            Color.Unspecified.copy(alpha = 0.50f),\n            Color.Unspecified.copy(alpha = 0.25f),\n        ),\n    )\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/component/shimmer/TextPlaceholder.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.component.shimmer\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.CornerBasedShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport kotlin.random.Random\n\n@Composable\nfun TextPlaceholder(\n    modifier: Modifier = Modifier,\n    height: Dp = 16.dp,\n    shape: CornerBasedShape = RoundedCornerShape(0.dp)\n) {\n    Box(\n        modifier = modifier\n            .padding(vertical = 4.dp)\n            .height(height)\n            .fillMaxWidth(remember { 0.25f + Random.nextFloat() * 0.5f })\n            .clip(shape)\n            .background(MaterialTheme.colorScheme.onSurface)\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/menu/AddToPlaylistDialog.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.menu\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.ColorFilter\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.utils.parseCookieString\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.AddToPlaylistSortDescendingKey\nimport com.metrolist.music.constants.AddToPlaylistSortTypeKey\nimport com.metrolist.music.constants.InnerTubeCookieKey\nimport com.metrolist.music.constants.ListThumbnailSize\nimport com.metrolist.music.constants.PlaylistSortType\nimport com.metrolist.music.db.entities.Playlist\nimport com.metrolist.music.ui.component.CreatePlaylistDialog\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.ListDialog\nimport com.metrolist.music.ui.component.ListItem\nimport com.metrolist.music.ui.component.PlaylistListItem\nimport com.metrolist.music.ui.component.SortHeader\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.viewmodels.PlaylistsViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport androidx.compose.foundation.background\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.ui.graphics.Color\nimport kotlinx.coroutines.withContext\nimport androidx.compose.animation.animateColorAsState\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.spring\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconToggleButton\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.foundation.layout.FlowRow\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsPressedAsState\nimport androidx.compose.material3.FilterChip\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.material3.FilterChipDefaults\n\n@Composable\nfun AddToPlaylistDialog(\n    isVisible: Boolean,\n    allowSyncing: Boolean = true,\n    initialTextFieldValue: String? = null,\n    onGetSong: suspend (Playlist) -> List<String>, // list of song ids. Songs should be inserted to database in this function.\n    onDismiss: () -> Unit,\n    viewModel: PlaylistsViewModel = hiltViewModel()\n) {\n    val database = LocalDatabase.current\n    val coroutineScope = rememberCoroutineScope()\n    val (sortType, onSortTypeChange) = rememberEnumPreference(\n        AddToPlaylistSortTypeKey,\n        PlaylistSortType.NAME\n    )\n    val (sortDescending, onSortDescendingChange) = rememberPreference(\n        AddToPlaylistSortDescendingKey,\n        false\n    )\n    val playlists by viewModel.allPlaylists.collectAsState()\n    val (innerTubeCookie) = rememberPreference(InnerTubeCookieKey, \"\")\n    val isLoggedIn = remember(innerTubeCookie) {\n        \"SAPISID\" in parseCookieString(innerTubeCookie)\n    }\n    var showCreatePlaylistDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    var showDuplicateDialog by remember {\n        mutableStateOf(false)\n    }\n    var selectedPlaylist by remember {\n        mutableStateOf<Playlist?>(null)\n    }\n    var songIds by remember {\n        mutableStateOf<List<String>?>(null) // list is not saveable\n    }\n    var duplicates by remember {\n        mutableStateOf(emptyList<String>())\n    }\n    var playlistsContainingSong by remember {\n        mutableStateOf<Set<String>>(emptySet())\n    }\n\n    LaunchedEffect(isVisible) {\n        if (!isVisible) {\n            songIds = null\n            playlistsContainingSong = emptySet()\n            return@LaunchedEffect\n        }\n        if (playlists.isNotEmpty() && songIds == null) {\n            withContext(Dispatchers.IO) {\n                val ids = onGetSong(playlists.first())\n                songIds = ids\n            }\n        }\n    }\n    LaunchedEffect(songIds, playlists) {\n        val ids = songIds ?: return@LaunchedEffect\n        withContext(Dispatchers.IO) {\n            playlistsContainingSong = playlists\n                .filter { playlist ->\n                    database.playlistDuplicates(playlist.id, ids).isNotEmpty()\n                }\n                .map { it.id }\n                .toSet()\n        }\n    }\n\n    if (isVisible) {\n        ListDialog(\n            onDismiss = onDismiss,\n        ) {\n            item {\n                val interactionSource = remember { MutableInteractionSource() }\n                val isPressed by interactionSource.collectIsPressedAsState()\n                val scale by animateFloatAsState(\n                    targetValue = if (isPressed) 0.7f else 1f,\n                    animationSpec = spring(\n                        dampingRatio = Spring.DampingRatioMediumBouncy,\n                        stiffness = Spring.StiffnessMedium\n                    ),\n                    label = \"buttonScale\"\n                )\n                FilledTonalButton(\n                    onClick = { showCreatePlaylistDialog = true},\n                    shape = RoundedCornerShape(50),\n                    interactionSource = interactionSource,\n                    colors = ButtonDefaults.filledTonalButtonColors(\n                        containerColor = MaterialTheme.colorScheme.primaryContainer,\n                        contentColor = MaterialTheme.colorScheme.onPrimaryContainer,\n                    ),\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 16.dp, vertical = 8.dp)\n                        .graphicsLayer { scaleX = scale; scaleY = scale }\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.add),\n                        contentDescription = null,\n                        modifier = Modifier\n                            .padding(end = 8.dp)\n                            .size(20.dp)\n                    )\n                    Text(\n                        text = stringResource(R.string.create_playlist),\n                        style = MaterialTheme.typography.titleSmall,\n                        fontWeight = FontWeight.SemiBold,\n                    )\n                }\n            }\n\n            if (playlists.isNotEmpty()) {\n                item {\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.SpaceBetween,\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(horizontal = 12.dp, vertical = 4.dp),\n                    ) {\n                        FlowRow(\n                            horizontalArrangement = Arrangement.spacedBy(6.dp),\n                            verticalArrangement = Arrangement.spacedBy(6.dp),\n                            modifier = Modifier.weight(1f)\n                        ) {\n                            PlaylistSortType.entries.forEach { type ->\n                                val selected = sortType == type\n                                FilterChip(\n                                    selected = selected,\n                                    onClick = { onSortTypeChange(type) },\n                                    shape = RoundedCornerShape(50),\n                                    border = FilterChipDefaults.filterChipBorder(\n                                        enabled = true,\n                                        selected = selected,\n                                        borderWidth = 0.dp,\n                                        selectedBorderWidth = 0.dp,\n                                    ),\n                                    colors = FilterChipDefaults.filterChipColors(\n                                        containerColor = MaterialTheme.colorScheme.surfaceVariant,\n                                        selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,\n                                        labelColor = MaterialTheme.colorScheme.onSurfaceVariant,\n                                        selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,\n                                    ),\n                                    label = {\n                                        Text(\n                                            text = stringResource(when (type) {\n                                                PlaylistSortType.CREATE_DATE  -> R.string.sort_by_create_date\n                                                PlaylistSortType.NAME         -> R.string.sort_by_name\n                                                PlaylistSortType.SONG_COUNT   -> R.string.sort_by_song_count\n                                                PlaylistSortType.LAST_UPDATED -> R.string.sort_by_last_updated\n                                            }),\n                                            fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal,\n                                        )\n                                    }\n                                )\n                            }\n                        }\n\n                        val arrowBg by animateColorAsState(\n                            targetValue = if (sortDescending) MaterialTheme.colorScheme.tertiaryContainer\n                            else MaterialTheme.colorScheme.surfaceVariant,\n                            animationSpec = spring(stiffness = Spring.StiffnessMediumLow),\n                            label = \"arrowBg\"\n                        )\n                        val arrowFg by animateColorAsState(\n                            targetValue = if (sortDescending) MaterialTheme.colorScheme.onTertiaryContainer\n                            else MaterialTheme.colorScheme.onSurfaceVariant,\n                            animationSpec = spring(stiffness = Spring.StiffnessMediumLow),\n                            label = \"arrowFg\"\n                        )\n                        IconToggleButton(\n                            checked = sortDescending,\n                            onCheckedChange = { onSortDescendingChange(it) },\n                            modifier = Modifier\n                                .clip(RoundedCornerShape(50))\n                                .background(arrowBg)\n                                .size(36.dp)\n                        ) {\n                            Icon(\n                                painter = painterResource(\n                                    if (sortDescending) R.drawable.arrow_downward else R.drawable.arrow_upward\n                                ),\n                                contentDescription = stringResource(\n                                    if (sortDescending) R.string.sort_descending else R.string.sort_ascending\n                                ),\n                                tint = arrowFg,\n                                modifier = Modifier.size(18.dp)\n                            )\n                        }\n                    }\n                }\n            }\n\n            items(playlists) { playlist ->\n                val containsSong = playlist.id in playlistsContainingSong\n                val rowBg by animateColorAsState(\n                    targetValue = if (containsSong)\n                        MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.45f)\n                    else Color.Transparent,\n                    animationSpec = spring(stiffness = Spring.StiffnessMediumLow),\n                    label = \"playlistBg\"\n                )\n                PlaylistListItem(\n                    playlist = playlist,\n                    modifier = Modifier\n                    .padding(horizontal = 8.dp, vertical = 2.dp)\n                    .clip(RoundedCornerShape(16.dp))\n                    .background(rowBg)\n                    .clickable {\n                        selectedPlaylist = playlist\n                        coroutineScope.launch(Dispatchers.IO) {\n                            if (songIds == null) {\n                                songIds = onGetSong(playlist)\n                            }\n                            duplicates = database.playlistDuplicates(playlist.id, songIds!!)\n                            if (duplicates.isNotEmpty()) {\n                                showDuplicateDialog = true\n                            } else {\n                                onDismiss()\n                                database.addSongToPlaylist(playlist, songIds!!)\n\n                                playlist.playlist.browseId?.let { plist ->\n                                    songIds?.forEach {\n                                        YouTube.addToPlaylist(plist, it)\n                                    }\n                                }\n                            }\n                        }\n                    }\n                )\n            }\n        }\n    }\n\n    if (showCreatePlaylistDialog) {\n        CreatePlaylistDialog(\n            onDismiss = { showCreatePlaylistDialog = false },\n            initialTextFieldValue = initialTextFieldValue,\n            allowSyncing = allowSyncing\n        )\n    }\n\n    // duplicate songs warning\n        if (showDuplicateDialog) {\n            DefaultDialog(\n                title = { Text(stringResource(R.string.duplicates)) },\n                buttons = {\n                    TextButton(\n                        onClick = {\n                            showDuplicateDialog = false\n                            onDismiss()\n                            database.transaction {\n                                addSongToPlaylist(\n                                    selectedPlaylist!!,\n                                    songIds!!.filter {\n                                        !duplicates.contains(it)\n                                    }\n                                )\n                            }\n                        }\n                    ) {\n                        Text(stringResource(R.string.skip_duplicates))\n                    }\n\n                    TextButton(\n                        onClick = {\n                            showDuplicateDialog = false\n                            onDismiss()\n                            database.transaction {\n                                addSongToPlaylist(selectedPlaylist!!, songIds!!)\n                            }\n                        }\n                    ) {\n                        Text(stringResource(R.string.add_anyway))\n                    }\n\n                    TextButton(\n                        onClick = {\n                            showDuplicateDialog = false\n                        }\n                    ) {\n                        Text(stringResource(android.R.string.cancel))\n                    }\n                },\n                onDismiss = {\n                    showDuplicateDialog = false\n                }\n            ) {\n                Text(\n                    text = if (duplicates.size == 1) {\n                        stringResource(R.string.duplicates_description_single)\n                    } else {\n                        stringResource(R.string.duplicates_description_multiple, duplicates.size)\n                    },\n                    textAlign = TextAlign.Start,\n                    modifier = Modifier.align(Alignment.Start)\n                )\n            }\n        }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/menu/AddToPlaylistDialogOnline.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.menu\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateMapOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.snapshots.SnapshotStateList\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.ColorFilter\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.TextUnit\nimport androidx.compose.ui.unit.TextUnitType\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.AddToPlaylistSortDescendingKey\nimport com.metrolist.music.constants.AddToPlaylistSortTypeKey\nimport com.metrolist.music.constants.ListThumbnailSize\nimport com.metrolist.music.constants.PlaylistSortType\nimport com.metrolist.music.db.entities.Playlist\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.models.ItemsPage\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.ui.component.CreatePlaylistDialog\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.ListDialog\nimport com.metrolist.music.ui.component.ListItem\nimport com.metrolist.music.ui.component.PlaylistListItem\nimport com.metrolist.music.ui.component.SortHeader\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.utils.reportException\nimport com.metrolist.music.viewmodels.PlaylistsViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport kotlinx.coroutines.sync.Semaphore\nimport kotlinx.coroutines.sync.withPermit\nimport timber.log.Timber\nimport java.net.URLDecoder\nimport java.nio.charset.StandardCharsets\nimport java.util.concurrent.atomic.AtomicInteger\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.animation.animateColorAsState\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.spring\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.FlowRow\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconToggleButton\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsPressedAsState\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.material3.FilterChip\nimport androidx.compose.material3.FilterChipDefaults\n\n@Composable\nfun AddToPlaylistDialogOnline(\n    isVisible: Boolean,\n    allowSyncing: Boolean = true,\n    initialTextFieldValue: String? = null,\n    songs: SnapshotStateList<Song>,\n    onDismiss: () -> Unit,\n    onProgressStart: (Boolean) -> Unit,\n    onPercentageChange: (Int) -> Unit,\n    onSongChange: (String) -> Unit = {},\n    viewModel: PlaylistsViewModel = hiltViewModel()\n) {\n    val database = LocalDatabase.current\n    val coroutineScope = rememberCoroutineScope()\n    val viewStateMap = remember { mutableStateMapOf<String, ItemsPage?>() }\n    val (sortType, onSortTypeChange) = rememberEnumPreference(\n        AddToPlaylistSortTypeKey,\n        PlaylistSortType.NAME\n    )\n    val (sortDescending, onSortDescendingChange) = rememberPreference(\n        AddToPlaylistSortDescendingKey,\n        false\n    )\n    val playlists by viewModel.allPlaylists.collectAsState()\n\n    var showCreatePlaylistDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    var showDuplicateDialog by remember {\n        mutableStateOf(false)\n    }\n    var selectedPlaylist by remember {\n        mutableStateOf<Playlist?>(null)\n    }\n    val songIds by remember {\n        mutableStateOf<List<String>?>(null)\n    }\n    val duplicates by remember {\n        mutableStateOf(emptyList<String>())\n    }\n    var playlistsContainingSong by remember {\n        mutableStateOf<Set<String>>(emptySet())\n    }\n\n    LaunchedEffect(isVisible, playlists) {\n        if (!isVisible) {\n            playlistsContainingSong = emptySet()\n            return@LaunchedEffect\n            }\n                playlistsContainingSong = emptySet()\n                if (playlists.isNotEmpty()) {\n            withContext(Dispatchers.IO) {\n                val ids = songs.map { it.id }\n                playlistsContainingSong = playlists\n                    .filter { playlist ->\n                        database.playlistDuplicates(playlist.id, ids).isNotEmpty()\n                    }\n                    .map { it.id }\n                    .toSet()\n            }\n        }\n    }\n\n    if (isVisible) {\n        ListDialog(\n            onDismiss = onDismiss\n        ) {\n            item {\n                val interactionSource = remember { MutableInteractionSource() }\n                val isPressed by interactionSource.collectIsPressedAsState()\n                val scale by animateFloatAsState(\n                    targetValue = if (isPressed) 0.94f else 1f,\n                    animationSpec = spring(\n                        dampingRatio = Spring.DampingRatioMediumBouncy,\n                        stiffness = Spring.StiffnessMedium\n                    ),\n                    label = \"buttonScale\"\n                )\n                FilledTonalButton(\n                    onClick = { showCreatePlaylistDialog = true },\n                    shape = RoundedCornerShape(50),\n                    interactionSource = interactionSource,\n                    colors = ButtonDefaults.filledTonalButtonColors(\n                        containerColor = MaterialTheme.colorScheme.primaryContainer,\n                        contentColor = MaterialTheme.colorScheme.onPrimaryContainer,\n                    ),\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 16.dp, vertical = 8.dp)\n                        .graphicsLayer { scaleX = scale; scaleY = scale }\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.add),\n                        contentDescription = null,\n                        modifier = Modifier.padding(end = 8.dp).size(20.dp)\n                    )\n                    Text(\n                        text = stringResource(R.string.create_playlist),\n                        style = MaterialTheme.typography.titleSmall,\n                        fontWeight = FontWeight.SemiBold,\n                    )\n                }\n            }\n\n            if (playlists.isNotEmpty()) {\n                item {\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.SpaceBetween,\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(horizontal = 12.dp, vertical = 4.dp),\n                    ) {\n                        FlowRow(\n                            horizontalArrangement = Arrangement.spacedBy(6.dp),\n                            verticalArrangement = Arrangement.spacedBy(6.dp),\n                            modifier = Modifier.weight(1f)\n                        ) {\n                            PlaylistSortType.entries.forEach { type ->\n                                val selected = sortType == type\n                                FilterChip(\n                                    selected = selected,\n                                    onClick = { onSortTypeChange(type) },\n                                    shape = RoundedCornerShape(50),\n                                    border = FilterChipDefaults.filterChipBorder(\n                                        enabled = true,\n                                        selected = selected,\n                                        borderWidth = 0.dp,\n                                        selectedBorderWidth = 0.dp,\n                                    ),\n                                    colors = FilterChipDefaults.filterChipColors(\n                                        containerColor = MaterialTheme.colorScheme.surfaceVariant,\n                                        selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,\n                                        labelColor = MaterialTheme.colorScheme.onSurfaceVariant,\n                                        selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,\n                                    ),\n                                    label = {\n                                        Text(\n                                            text = stringResource(when (type) {\n                                                PlaylistSortType.CREATE_DATE  -> R.string.sort_by_create_date\n                                                PlaylistSortType.NAME         -> R.string.sort_by_name\n                                                PlaylistSortType.SONG_COUNT   -> R.string.sort_by_song_count\n                                                PlaylistSortType.LAST_UPDATED -> R.string.sort_by_last_updated\n                                            }),\n                                            fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal,\n                                        )\n                                    }\n                                )\n                            }\n                        }\n                        val arrowBg by animateColorAsState(\n                            targetValue = if (sortDescending) MaterialTheme.colorScheme.tertiaryContainer\n                            else MaterialTheme.colorScheme.surfaceVariant,\n                            animationSpec = spring(stiffness = Spring.StiffnessMediumLow),\n                            label = \"arrowBg\"\n                        )\n                        val arrowFg by animateColorAsState(\n                            targetValue = if (sortDescending) MaterialTheme.colorScheme.onTertiaryContainer\n                            else MaterialTheme.colorScheme.onSurfaceVariant,\n                            animationSpec = spring(stiffness = Spring.StiffnessMediumLow),\n                            label = \"arrowFg\"\n                        )\n                        IconToggleButton(\n                            checked = sortDescending,\n                            onCheckedChange = { onSortDescendingChange(it) },\n                            modifier = Modifier\n                                .clip(RoundedCornerShape(50))\n                                .background(arrowBg)\n                                .size(36.dp)\n                        ) {\n                            Icon(\n                                painter = painterResource(\n                                    if (sortDescending) R.drawable.arrow_downward else R.drawable.arrow_upward\n                                ),\n                                contentDescription = stringResource(\n                                    if (sortDescending) R.string.sort_descending else R.string.sort_ascending\n                                ),\n                                tint = arrowFg,\n                                modifier = Modifier.size(18.dp)\n                            )\n                        }\n                    }\n                }\n            }\n\n            items(playlists) { playlist ->\n                val containsSong = playlist.id in playlistsContainingSong\n                val rowBg by animateColorAsState(\n                    targetValue = if (containsSong)\n                        MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.45f)\n                    else Color.Transparent,\n                    animationSpec = spring(stiffness = Spring.StiffnessMediumLow),\n                    label = \"playlistBg\"\n                )\n                PlaylistListItem(\n                    playlist = playlist,\n                    modifier = Modifier\n                    .padding(horizontal = 8.dp, vertical = 2.dp)\n                    .clip(RoundedCornerShape(16.dp))\n                    .background(rowBg)\n                    .clickable {\n                        selectedPlaylist = playlist\n                        coroutineScope.launch(Dispatchers.IO) {\n                            onDismiss()\n                            val songsTot = songs.count()\n                            if (songsTot == 0) return@launch\n                            \n                            val songsIdx = AtomicInteger(0)\n                            val semaphore = kotlinx.coroutines.sync.Semaphore(15)\n                            onProgressStart(true)\n                            try {\n                                val jobs = songs.reversed().map { song ->\n                                    coroutineScope.launch {\n                                        semaphore.withPermit {\n                                            try {\n                                                var allArtists = \"\"\n                                                song.artists.forEach { artist ->\n                                                    allArtists += \" ${URLDecoder.decode(artist.name, StandardCharsets.UTF_8.toString())}\"\n                                                }\n                                                val query = \"${song.title} - $allArtists\"\n\n                                                YouTube.search(query, YouTube.SearchFilter.FILTER_SONG)\n                                                    .onSuccess { result ->\n                                                        val items = result.items.distinctBy { it.id }\n                                                        if (items.isNotEmpty()) {\n                                                            val firstSong = items.firstOrNull() as? SongItem\n                                                            if (firstSong != null) {\n                                                                val firstSongMedia = firstSong.toMediaMetadata()\n                                                                val ids = listOf(firstSong.id)\n                                                                withContext(Dispatchers.IO) {\n                                                                    try {\n                                                                        database.insert(firstSongMedia)\n                                                                    } catch (e: Exception) {\n                                                                        Timber.tag(\"Exception\").e(e.toString())\n                                                                    }\n                                                                    database.addSongToPlaylist(playlist, ids)\n                                                                }\n                                                            }\n                                                        }\n                                                    }\n                                                    .onFailure { reportException(it) }\n                                            } catch (e: Exception) {\n                                                Timber.tag(\"ERROR\").v(e.toString())\n                                            } finally {\n                                                val completed = songsIdx.incrementAndGet()\n                                                onSongChange(song.title)\n                                                onPercentageChange(((completed.toDouble() / songsTot) * 100).toInt())\n                                            }\n                                        }\n                                    }\n                                }\n                                jobs.forEach { it.join() }\n                            } finally {\n                                withContext(Dispatchers.Main) {\n                                    onProgressStart(false)\n                                }\n                            }\n                        }\n                    }\n                )\n            }\n\n            item {\n                ListItem(\n                    modifier = Modifier.clickable {\n                        coroutineScope.launch(Dispatchers.IO) {\n                            onDismiss()\n                            val songsTot = songs.count()\n                            if (songsTot == 0) return@launch\n\n                            val songsIdx = AtomicInteger(0)\n                            val semaphore = kotlinx.coroutines.sync.Semaphore(15)\n                            onProgressStart(true)\n                            try {\n                                val jobs = songs.reversed().map { song ->\n                                    coroutineScope.launch {\n                                        semaphore.withPermit {\n                                            try {\n                                                var allArtists = \"\"\n                                                song.artists.forEach { artist ->\n                                                    allArtists += \" ${URLDecoder.decode(artist.name, StandardCharsets.UTF_8.toString())}\"\n                                                }\n                                                val query = \"${song.title} - $allArtists\"\n\n                                                YouTube.search(query, YouTube.SearchFilter.FILTER_SONG)\n                                                    .onSuccess { result ->\n                                                        val items = result.items.distinctBy { it.id }\n                                                        if (items.isNotEmpty()) {\n                                                            val firstSong = items.firstOrNull() as? SongItem\n                                                            if (firstSong != null) {\n                                                                val firstSongMedia = firstSong.toMediaMetadata()\n                                                                val firstSongEnt = firstSong.toMediaMetadata().toSongEntity()\n                                                                withContext(Dispatchers.IO) {\n                                                                    try {\n                                                                        database.insert(firstSongMedia)\n                                                                        database.query {\n                                                                            update(firstSongEnt.toggleLike())\n                                                                        }\n                                                                    } catch (e: Exception) {\n                                                                        Timber.tag(\"Exception\").e(e.toString())\n                                                                    }\n                                                                }\n                                                            }\n                                                        }\n                                                    }\n                                                    .onFailure { reportException(it) }\n                                            } catch (e: Exception) {\n                                                Timber.tag(\"ERROR\").v(e.toString())\n                                            } finally {\n                                                val completed = songsIdx.incrementAndGet()\n                                                onSongChange(song.title)\n                                                onPercentageChange(((completed.toDouble() / songsTot) * 100).toInt())\n                                            }\n                                        }\n                                    }\n                                }\n                                jobs.forEach { it.join() }\n                            } finally {\n                                withContext(Dispatchers.Main) {\n                                    onProgressStart(false)\n                                }\n                            }\n                        }\n                    },\n                    title = stringResource(R.string.liked_songs),\n                    thumbnailContent = {\n                        Image(\n                            painter = painterResource(id = R.drawable.favorite),\n                            contentDescription = null,\n                            modifier = Modifier.size(40.dp),\n                            colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground)\n                        )\n                    },\n                    trailingContent = {}\n                )\n            }\n\n            item {\n                Text(\n                    text = stringResource(R.string.playlist_add_local_to_synced_note),\n                    fontSize = TextUnit(12F, TextUnitType.Sp),\n                    modifier = Modifier.padding(horizontal = 20.dp)\n                )\n            }\n        }\n    }\n\n    if (showCreatePlaylistDialog) {\n        CreatePlaylistDialog(\n            onDismiss = { showCreatePlaylistDialog = false },\n            initialTextFieldValue = initialTextFieldValue,\n            allowSyncing = allowSyncing\n        )\n    }\n\n    // duplicate songs warning\n    if (showDuplicateDialog) {\n        DefaultDialog(\n            title = { Text(stringResource(R.string.duplicates)) },\n            buttons = {\n                TextButton(\n                    onClick = {\n                        showDuplicateDialog = false\n                        onDismiss()\n                        database.transaction {\n                            addSongToPlaylist(\n                                selectedPlaylist!!,\n                                songIds!!.filter {\n                                    !duplicates.contains(it)\n                                }\n                            )\n                        }\n                    }\n                ) {\n                    Text(stringResource(R.string.skip_duplicates))\n                }\n\n                TextButton(\n                    onClick = {\n                        showDuplicateDialog = false\n                        onDismiss()\n                        database.transaction {\n                            addSongToPlaylist(selectedPlaylist!!, songIds!!)\n                        }\n                    }\n                ) {\n                    Text(stringResource(R.string.add_anyway))\n                }\n\n                TextButton(\n                    onClick = {\n                        showDuplicateDialog = false\n                    }\n                ) {\n                    Text(stringResource(android.R.string.cancel))\n                }\n            },\n            onDismiss = {\n                showDuplicateDialog = false\n            }\n        ) {\n            Text(\n                text = if (duplicates.size == 1) {\n                    stringResource(R.string.duplicates_description_single)\n                } else {\n                    stringResource(R.string.duplicates_description_multiple, duplicates.size)\n                },\n                textAlign = TextAlign.Start,\n                modifier = Modifier.align(Alignment.Start)\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/menu/AlbumMenu.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.menu\n\nimport android.annotation.SuppressLint\nimport android.content.Intent\nimport android.content.res.Configuration\nimport android.widget.Toast\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.ColorFilter\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.core.net.toUri\nimport androidx.media3.exoplayer.offline.Download.STATE_COMPLETED\nimport androidx.media3.exoplayer.offline.Download.STATE_DOWNLOADING\nimport androidx.media3.exoplayer.offline.Download.STATE_QUEUED\nimport androidx.media3.exoplayer.offline.Download.STATE_STOPPED\nimport androidx.media3.exoplayer.offline.DownloadRequest\nimport androidx.media3.exoplayer.offline.DownloadService\nimport androidx.navigation.NavController\nimport coil3.compose.AsyncImage\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalDownloadUtil\nimport com.metrolist.music.LocalListenTogetherManager\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.ListItemHeight\nimport com.metrolist.music.constants.ListThumbnailSize\nimport com.metrolist.music.db.entities.Album\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.db.entities.SpeedDialItem\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.playback.ExoDownloadService\nimport com.metrolist.music.playback.queues.ListQueue\nimport com.metrolist.music.ui.component.AlbumListItem\nimport com.metrolist.music.ui.component.ListDialog\nimport com.metrolist.music.ui.component.ListItem\nimport com.metrolist.music.ui.component.Material3MenuGroup\nimport com.metrolist.music.ui.component.Material3MenuItemData\nimport com.metrolist.music.ui.component.NewAction\nimport com.metrolist.music.ui.component.NewActionGrid\nimport com.metrolist.music.ui.component.SongListItem\nimport com.metrolist.music.ui.menu.ExportDialog\nimport com.metrolist.music.utils.PlaylistExporter\nimport com.metrolist.music.utils.getExportFileUri\nimport com.metrolist.music.utils.saveToPublicDocuments\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\n\n@SuppressLint(\"MutableCollectionMutableState\")\n@Composable\nfun AlbumMenu(\n    originalAlbum: Album,\n    navController: NavController,\n    onDismiss: () -> Unit,\n) {\n    val context = LocalContext.current\n    val database = LocalDatabase.current\n    val downloadUtil = LocalDownloadUtil.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val isGuest = listenTogetherManager?.isInRoom == true && !listenTogetherManager.isHost\n    val scope = rememberCoroutineScope()\n    val libraryAlbum by database.album(originalAlbum.id).collectAsState(initial = originalAlbum)\n    val album = libraryAlbum ?: originalAlbum\n    var songs by remember {\n        mutableStateOf(emptyList<Song>())\n    }\n\n    val coroutineScope = rememberCoroutineScope()\n\n    LaunchedEffect(Unit) {\n        database.albumSongs(album.id).collect {\n            songs = it\n        }\n    }\n\n    var downloadState by remember {\n        mutableIntStateOf(STATE_STOPPED)\n    }\n\n    LaunchedEffect(songs) {\n        if (songs.isEmpty()) return@LaunchedEffect\n        downloadUtil.downloads.collect { downloads ->\n            downloadState =\n                if (songs.all { downloads[it.id]?.state == STATE_COMPLETED }) {\n                    STATE_COMPLETED\n                } else if (songs.all {\n                        downloads[it.id]?.state == STATE_QUEUED ||\n                            downloads[it.id]?.state == STATE_DOWNLOADING ||\n                            downloads[it.id]?.state == STATE_COMPLETED\n                    }\n                ) {\n                    STATE_DOWNLOADING\n                } else {\n                    STATE_STOPPED\n                }\n        }\n    }\n\n    var refetchIconDegree by remember { mutableFloatStateOf(0f) }\n\n    val rotationAnimation by animateFloatAsState(\n        targetValue = refetchIconDegree,\n        animationSpec = tween(durationMillis = 800),\n        label = \"\",\n    )\n\n    val isPinned by database.speedDialDao.isPinned(album.id).collectAsState(initial = false)\n\n    var showChoosePlaylistDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    var showSelectArtistDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    var showErrorPlaylistAddDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    val notAddedList by remember {\n        mutableStateOf(mutableListOf<Song>())\n    }\n\n    AddToPlaylistDialog(\n        isVisible = showChoosePlaylistDialog,\n        onGetSong = { playlist ->\n            coroutineScope.launch(Dispatchers.IO) {\n                playlist.playlist.browseId?.let { playlistId ->\n                    album.album.playlistId?.let { addPlaylistId ->\n                        YouTube.addPlaylistToPlaylist(playlistId, addPlaylistId)\n                    }\n                }\n            }\n            songs.map { it.id }\n        },\n        onDismiss = {\n            showChoosePlaylistDialog = false\n        },\n    )\n\n    if (showErrorPlaylistAddDialog) {\n        ListDialog(\n            onDismiss = {\n                showErrorPlaylistAddDialog = false\n                onDismiss()\n            },\n        ) {\n            item {\n                ListItem(\n                    title = stringResource(R.string.already_in_playlist),\n                    thumbnailContent = {\n                        Image(\n                            painter = painterResource(R.drawable.close),\n                            contentDescription = null,\n                            colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),\n                            modifier = Modifier.size(ListThumbnailSize),\n                        )\n                    },\n                    modifier =\n                        Modifier\n                            .clickable { showErrorPlaylistAddDialog = false },\n                )\n            }\n\n            items(notAddedList) { song ->\n                SongListItem(song = song)\n            }\n        }\n    }\n\n    if (showSelectArtistDialog) {\n        ListDialog(\n            onDismiss = { showSelectArtistDialog = false },\n        ) {\n            items(\n                items = album.artists.distinctBy { it.id },\n                key = { it.id },\n            ) { artist ->\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    modifier =\n                        Modifier\n                            .height(ListItemHeight)\n                            .clickable {\n                                navController.navigate(\"artist/${artist.id}\")\n                                showSelectArtistDialog = false\n                                onDismiss()\n                            }.padding(horizontal = 12.dp),\n                ) {\n                    Box(\n                        modifier = Modifier.padding(8.dp),\n                        contentAlignment = Alignment.Center,\n                    ) {\n                        AsyncImage(\n                            model = artist.thumbnailUrl,\n                            contentDescription = null,\n                            modifier =\n                                Modifier\n                                    .size(ListThumbnailSize)\n                                    .clip(CircleShape),\n                        )\n                    }\n                    Text(\n                        text = artist.name,\n                        fontSize = 18.sp,\n                        fontWeight = FontWeight.Bold,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis,\n                        modifier =\n                            Modifier\n                                .weight(1f)\n                                .padding(horizontal = 8.dp),\n                    )\n                }\n            }\n        }\n    }\n\n    AlbumListItem(\n        album = album,\n        showLikedIcon = false,\n        badges = {},\n        trailingContent = {\n            IconButton(\n                onClick = {\n                    database.query {\n                        update(album.album.toggleLike())\n                    }\n                },\n            ) {\n                Icon(\n                    painter = painterResource(if (album.album.bookmarkedAt != null) R.drawable.favorite else R.drawable.favorite_border),\n                    tint = if (album.album.bookmarkedAt != null) MaterialTheme.colorScheme.error else LocalContentColor.current,\n                    contentDescription = null,\n                )\n            }\n        },\n    )\n\n    HorizontalDivider()\n\n    Spacer(modifier = Modifier.height(12.dp))\n\n    val configuration = LocalConfiguration.current\n    val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT\n\n    LazyColumn(\n        contentPadding =\n            PaddingValues(\n                start = 0.dp,\n                top = 0.dp,\n                end = 0.dp,\n                bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(),\n            ),\n    ) {\n        item {\n            NewActionGrid(\n                actions =\n                    listOfNotNull(\n                        if (!isGuest) {\n                            NewAction(\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.play),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(28.dp),\n                                        tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                                    )\n                                },\n                                text = stringResource(R.string.play),\n                                onClick = {\n                                    onDismiss()\n                                    if (songs.isNotEmpty()) {\n                                        playerConnection.playQueue(\n                                            ListQueue(\n                                                title = album.album.title,\n                                                items = songs.map(Song::toMediaItem),\n                                            ),\n                                        )\n                                    }\n                                },\n                            )\n\n                            NewAction(\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.shuffle),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(28.dp),\n                                        tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                                    )\n                                },\n                                text = stringResource(R.string.shuffle),\n                                onClick = {\n                                    onDismiss()\n                                    if (songs.isNotEmpty()) {\n                                        album.album.playlistId?.let { playlistId ->\n                                            playerConnection.service.getAutomix(playlistId)\n                                        }\n                                        playerConnection.playQueue(\n                                            ListQueue(\n                                                title = album.album.title,\n                                                items = songs.shuffled().map(Song::toMediaItem),\n                                            ),\n                                        )\n                                    }\n                                },\n                            )\n                        } else {\n                            null\n                        },\n                        NewAction(\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.share),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(28.dp),\n                                    tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                                )\n                            },\n                            text = stringResource(R.string.share),\n                            onClick = {\n                                onDismiss()\n                                val intent =\n                                    Intent().apply {\n                                        action = Intent.ACTION_SEND\n                                        type = \"text/plain\"\n                                        putExtra(Intent.EXTRA_TEXT, \"https://music.youtube.com/playlist?list=${album.album.playlistId}\")\n                                    }\n                                context.startActivity(Intent.createChooser(intent, null))\n                            },\n                        ),\n                    ),\n                modifier = Modifier.padding(horizontal = 4.dp, vertical = 16.dp),\n                columns = if (isGuest) 1 else 3,\n            )\n        }\n        item {\n            Material3MenuGroup(\n                items =\n                    listOfNotNull(\n                        if (!isGuest) {\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.play_next)) },\n                                description = { Text(text = stringResource(R.string.play_next_desc)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.playlist_play),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    onDismiss()\n                                    playerConnection.playNext(songs.map { it.toMediaItem() })\n                                },\n                            )\n                        } else {\n                            null\n                        },\n                        if (!isGuest) {\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.add_to_queue)) },\n                                description = { Text(text = stringResource(R.string.add_to_queue_desc)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.queue_music),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    onDismiss()\n                                    playerConnection.addToQueue(songs.map { it.toMediaItem() })\n                                },\n                            )\n                        } else {\n                            null\n                        },\n                        Material3MenuItemData(\n                            title = { Text(text = stringResource(R.string.add_to_playlist)) },\n                            description = { Text(text = stringResource(R.string.add_to_playlist_desc)) },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.playlist_add),\n                                    contentDescription = null,\n                                )\n                            },\n                            onClick = {\n                                showChoosePlaylistDialog = true\n                            },\n                        ),\n                        Material3MenuItemData(\n                            title = {\n                                Text(\n                                    text = if (isPinned) \"Unpin from Speed dial\" else \"Pin to Speed dial\",\n                                )\n                            },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(if (isPinned) R.drawable.remove else R.drawable.add),\n                                    contentDescription = null,\n                                )\n                            },\n                            onClick = {\n                                coroutineScope.launch(Dispatchers.IO) {\n                                    if (isPinned) {\n                                        database.speedDialDao.delete(album.id)\n                                    } else {\n                                        database.speedDialDao.insert(\n                                            SpeedDialItem(\n                                                id = album.id,\n                                                secondaryId = album.album.playlistId,\n                                                title = album.album.title,\n                                                subtitle = album.artists.joinToString(\", \") { it.name },\n                                                thumbnailUrl = album.album.thumbnailUrl,\n                                                type = \"ALBUM\",\n                                                explicit = album.album.explicit,\n                                            ),\n                                        )\n                                    }\n                                }\n                                onDismiss()\n                            },\n                        ),\n                    ),\n            )\n        }\n\n        item { Spacer(modifier = Modifier.height(12.dp)) }\n\n        item {\n            Material3MenuGroup(\n                items =\n                    listOf(\n                        when (downloadState) {\n                            STATE_COMPLETED -> {\n                                Material3MenuItemData(\n                                    title = {\n                                        Text(\n                                            text = stringResource(R.string.remove_download),\n                                        )\n                                    },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.offline),\n                                            contentDescription = null,\n                                        )\n                                    },\n                                    onClick = {\n                                        songs.forEach { song ->\n                                            DownloadService.sendRemoveDownload(\n                                                context,\n                                                ExoDownloadService::class.java,\n                                                song.id,\n                                                false,\n                                            )\n                                        }\n                                    },\n                                )\n                            }\n\n                            STATE_QUEUED, STATE_DOWNLOADING -> {\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.downloading)) },\n                                    icon = {\n                                        CircularProgressIndicator(\n                                            modifier = Modifier.size(24.dp),\n                                            strokeWidth = 2.dp,\n                                        )\n                                    },\n                                    onClick = {\n                                        songs.forEach { song ->\n                                            DownloadService.sendRemoveDownload(\n                                                context,\n                                                ExoDownloadService::class.java,\n                                                song.id,\n                                                false,\n                                            )\n                                        }\n                                    },\n                                )\n                            }\n\n                            else -> {\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.action_download)) },\n                                    description = { Text(text = stringResource(R.string.download_desc)) },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.download),\n                                            contentDescription = null,\n                                        )\n                                    },\n                                    onClick = {\n                                        songs.forEach { song ->\n                                            val downloadRequest =\n                                                DownloadRequest\n                                                    .Builder(song.id, song.id.toUri())\n                                                    .setCustomCacheKey(song.id)\n                                                    .setData(song.song.title.toByteArray())\n                                                    .build()\n                                            DownloadService.sendAddDownload(\n                                                context,\n                                                ExoDownloadService::class.java,\n                                                downloadRequest,\n                                                false,\n                                            )\n                                        }\n                                    },\n                                )\n                            }\n                        },\n                    ),\n            )\n        }\n\n        item { Spacer(modifier = Modifier.height(12.dp)) }\n\n        item {\n            // Export album as a playlist (CSV/M3U)\n            var showExportDialog by remember { mutableStateOf(false) }\n            Material3MenuGroup(\n                items =\n                    listOf(\n                        Material3MenuItemData(\n                            title = { Text(text = stringResource(R.string.export_playlist)) },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.share),\n                                    contentDescription = null,\n                                )\n                            },\n                            onClick = { showExportDialog = true },\n                        ),\n                    ),\n            )\n\n            val exportPlaylistStr = stringResource(R.string.export_playlist)\n\n            if (showExportDialog) {\n                ExportDialog(\n                    onDismiss = { showExportDialog = false },\n                    onShare = { format ->\n                        val playlistSongs =\n                            songs.map { s ->\n                                com.metrolist.music.db.entities.PlaylistSong(\n                                    map =\n                                        com.metrolist.music.db.entities.PlaylistSongMap(\n                                            songId = s.id,\n                                            playlistId = album.id,\n                                            position = 0,\n                                        ),\n                                    song = s,\n                                )\n                            }\n                        val result =\n                            when (format) {\n                                \"csv\" -> PlaylistExporter.exportPlaylistAsCSV(context, album.album.title, playlistSongs)\n                                \"m3u\" -> PlaylistExporter.exportPlaylistAsM3U(context, album.album.title, playlistSongs)\n                                else -> Result.failure(IllegalArgumentException(\"Unknown format\"))\n                            }\n                        result\n                            .onSuccess { file ->\n                                val uri = getExportFileUri(context, file)\n                                val mimeType = if (format == \"csv\") \"text/csv\" else \"audio/x-mpegurl\"\n                                val shareIntent =\n                                    Intent(Intent.ACTION_SEND).apply {\n                                        type = mimeType\n                                        putExtra(Intent.EXTRA_STREAM, uri)\n                                        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n                                    }\n                                context.startActivity(Intent.createChooser(shareIntent, exportPlaylistStr))\n                            }.onFailure {\n                                Toast.makeText(context, R.string.export_failed, Toast.LENGTH_SHORT).show()\n                            }\n                        showExportDialog = false\n                    },\n                    onSave = { format ->\n                        val playlistSongs =\n                            songs.map { s ->\n                                com.metrolist.music.db.entities.PlaylistSong(\n                                    map =\n                                        com.metrolist.music.db.entities.PlaylistSongMap(\n                                            songId = s.id,\n                                            playlistId = album.id,\n                                            position = 0,\n                                        ),\n                                    song = s,\n                                )\n                            }\n                        val export =\n                            when (format) {\n                                \"csv\" -> PlaylistExporter.exportPlaylistAsCSV(context, album.album.title, playlistSongs)\n                                \"m3u\" -> PlaylistExporter.exportPlaylistAsM3U(context, album.album.title, playlistSongs)\n                                else -> Result.failure(IllegalArgumentException(\"Unknown format\"))\n                            }\n                        export\n                            .onSuccess { file ->\n                                val mimeType = if (format == \"csv\") \"text/csv\" else \"audio/x-mpegurl\"\n                                val save = saveToPublicDocuments(context, file, mimeType)\n                                save\n                                    .onSuccess { Toast.makeText(context, R.string.export_success, Toast.LENGTH_SHORT).show() }\n                                    .onFailure { Toast.makeText(context, R.string.export_failed, Toast.LENGTH_SHORT).show() }\n                            }.onFailure {\n                                Toast.makeText(context, R.string.export_failed, Toast.LENGTH_SHORT).show()\n                            }\n                        showExportDialog = false\n                    },\n                )\n            }\n        }\n\n        item { Spacer(modifier = Modifier.height(12.dp)) }\n\n        item {\n            Material3MenuGroup(\n                items =\n                    listOf(\n                        Material3MenuItemData(\n                            title = { Text(text = stringResource(R.string.view_artist)) },\n                            description = { Text(text = album.artists.joinToString { it.name }) },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.artist),\n                                    contentDescription = null,\n                                )\n                            },\n                            onClick = {\n                                if (album.artists.size == 1) {\n                                    navController.navigate(\"artist/${album.artists[0].id}\")\n                                    onDismiss()\n                                } else {\n                                    showSelectArtistDialog = true\n                                }\n                            },\n                        ),\n                        Material3MenuItemData(\n                            title = { Text(text = stringResource(R.string.refetch)) },\n                            description = { Text(text = stringResource(R.string.refetch_desc)) },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.sync),\n                                    contentDescription = null,\n                                    modifier = Modifier.graphicsLayer(rotationZ = rotationAnimation),\n                                )\n                            },\n                            onClick = {\n                                refetchIconDegree -= 360\n                                scope.launch(Dispatchers.IO) {\n                                    YouTube.album(album.id).onSuccess {\n                                        database.transaction {\n                                            update(album.album, it, album.artists)\n                                        }\n                                    }\n                                }\n                            },\n                        ),\n                    ),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/menu/ArtistMenu.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.menu\n\nimport android.content.Intent\nimport android.content.res.Configuration\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalListenTogetherManager\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.ArtistSongSortType\nimport com.metrolist.music.db.entities.SpeedDialItem\nimport com.metrolist.music.db.entities.Artist\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.playback.queues.ListQueue\nimport com.metrolist.music.ui.component.ArtistListItem\nimport com.metrolist.music.ui.component.Material3MenuGroup\nimport com.metrolist.music.ui.component.Material3MenuItemData\nimport com.metrolist.music.ui.component.NewAction\nimport com.metrolist.music.ui.component.NewActionGrid\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\n\n@Composable\nfun ArtistMenu(\n    originalArtist: Artist,\n    coroutineScope: CoroutineScope,\n    onDismiss: () -> Unit,\n) {\n    val context = LocalContext.current\n    val database = LocalDatabase.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val isGuest = listenTogetherManager?.isInRoom == true && !listenTogetherManager.isHost\n    val artistState = database.artist(originalArtist.id).collectAsState(initial = originalArtist)\n    val artist = artistState.value ?: originalArtist\n    val isPinned by database.speedDialDao.isPinned(artist.id).collectAsState(initial = false)\n\n    ArtistListItem(\n        artist = artist,\n        badges = {},\n        trailingContent = {},\n    )\n\n    HorizontalDivider()\n\n    Spacer(modifier = Modifier.height(12.dp))\n\n    val configuration = LocalConfiguration.current\n    val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT\n\n    LazyColumn(\n        contentPadding = PaddingValues(\n            start = 0.dp,\n            top = 0.dp,\n            end = 0.dp,\n            bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(),\n        ),\n    ) {\n        item {\n            NewActionGrid(\n                actions = buildList {\n                    if (!isGuest) {\n                        if (artist.songCount > 0) {\n                            add(\n                                NewAction(\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.play),\n                                            contentDescription = null,\n                                            modifier = Modifier.size(28.dp),\n                                            tint = MaterialTheme.colorScheme.onSurfaceVariant\n                                        )\n                                    },\n                                    text = stringResource(R.string.play),\n                                    onClick = {\n                                        coroutineScope.launch {\n                                            val songs = withContext(Dispatchers.IO) {\n                                                database\n                                                    .artistSongs(artist.id, ArtistSongSortType.CREATE_DATE, true)\n                                                    .first()\n                                                    .map { it.toMediaItem() }\n                                            }\n                                            playerConnection.playQueue(\n                                                ListQueue(\n                                                    title = artist.artist.name,\n                                                    items = songs,\n                                                ),\n                                            )\n                                        }\n                                        onDismiss()\n                                    }\n                                )\n                            )\n\n                            add(\n                                NewAction(\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.shuffle),\n                                            contentDescription = null,\n                                            modifier = Modifier.size(28.dp),\n                                            tint = MaterialTheme.colorScheme.onSurfaceVariant\n                                        )\n                                    },\n                                    text = stringResource(R.string.shuffle),\n                                    onClick = {\n                                        coroutineScope.launch {\n                                            val songs = withContext(Dispatchers.IO) {\n                                                database\n                                                    .artistSongs(artist.id, ArtistSongSortType.CREATE_DATE, true)\n                                                    .first()\n                                                    .map { it.toMediaItem() }\n                                                    .shuffled()\n                                            }\n                                            playerConnection.playQueue(\n                                                ListQueue(\n                                                    title = artist.artist.name,\n                                                    items = songs,\n                                                ),\n                                            )\n                                        }\n                                        onDismiss()\n                                    }\n                                )\n                            )\n                        }\n                    }\n\n                    add(\n                        NewAction(\n                            icon = {\n                                Icon(\n                                    painter = painterResource(if (isPinned) R.drawable.remove else R.drawable.add),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(28.dp),\n                                    tint = MaterialTheme.colorScheme.onSurfaceVariant\n                                )\n                            },\n                            text = if (isPinned) \"Unpin\" else \"Pin\",\n                            onClick = {\n                                coroutineScope.launch(Dispatchers.IO) {\n                                    if (isPinned) {\n                                        database.speedDialDao.delete(artist.id)\n                                    } else {\n                                        database.speedDialDao.insert(\n                                            SpeedDialItem(\n                                                id = artist.id,\n                                                title = artist.artist.name,\n                                                subtitle = null,\n                                                thumbnailUrl = artist.artist.thumbnailUrl,\n                                                type = \"ARTIST\"\n                                            )\n                                        )\n                                    }\n                                }\n                                onDismiss()\n                            }\n                        )\n                    )\n\n                    if (artist.artist.isYouTubeArtist) {\n                        add(\n                            NewAction(\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.share),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(28.dp),\n                                        tint = MaterialTheme.colorScheme.onSurfaceVariant\n                                    )\n                                },\n                                text = stringResource(R.string.share),\n                                onClick = {\n                                    onDismiss()\n                                val intent = Intent().apply {\n                                        action = Intent.ACTION_SEND\n                                        type = \"text/plain\"\n                                        putExtra(\n                                            Intent.EXTRA_TEXT,\n                                            \"https://music.youtube.com/channel/${artist.id}\"\n                                        )\n                                    }\n                                    context.startActivity(Intent.createChooser(intent, null))\n                                }\n                            )\n                        )\n                    }\n                },\n                modifier = Modifier.padding(horizontal = 4.dp, vertical = 16.dp),\n                columns = if (isGuest) 1 else 3\n            )\n        }\n\n        item {\n            Material3MenuGroup(\n                items = listOf(\n                    Material3MenuItemData(\n                        title = {\n                            Text(text = if (artist.artist.bookmarkedAt != null) stringResource(R.string.subscribed) else stringResource(R.string.subscribe))\n                        },\n                        icon = {\n                            Icon(\n                                painter = painterResource(if (artist.artist.bookmarkedAt != null) R.drawable.subscribed else R.drawable.subscribe),\n                                contentDescription = null,\n                            )\n                        },\n                        onClick = {\n                            database.transaction {\n                                update(artist.artist.toggleLike())\n                            }\n                        }\n                    )\n                )\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/menu/CsvColumnMappingDialog.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.menu\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.horizontalScroll\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.LinearProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedButton\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Dialog\nimport androidx.compose.ui.window.DialogProperties\nimport com.metrolist.music.R\nimport com.metrolist.music.viewmodels.ConvertedSongLog\nimport com.metrolist.music.viewmodels.CsvImportState\n\n@Composable\nfun CsvColumnMappingDialog(\n    isVisible: Boolean,\n    csvState: CsvImportState,\n    onDismiss: () -> Unit,\n    onConfirm: (CsvImportState) -> Unit,\n) {\n    if (!isVisible) return\n\n    var artistColumnIndex by remember { mutableIntStateOf(csvState.artistColumnIndex) }\n    var titleColumnIndex by remember { mutableIntStateOf(csvState.titleColumnIndex) }\n    var urlColumnIndex by remember { mutableIntStateOf(csvState.urlColumnIndex) }\n    var hasHeader by remember { mutableStateOf(csvState.hasHeader) }\n\n    Dialog(\n        onDismissRequest = onDismiss,\n        properties = DialogProperties(usePlatformDefaultWidth = false),\n    ) {\n        Column(\n            modifier =\n                Modifier\n                    .fillMaxWidth(0.95f)\n                    .clip(RoundedCornerShape(16.dp))\n                    .background(MaterialTheme.colorScheme.surface)\n                    .padding(24.dp)\n                    .verticalScroll(rememberScrollState()),\n            verticalArrangement = Arrangement.spacedBy(16.dp),\n        ) {\n            Text(\n                text = stringResource(R.string.map_csv_columns),\n                style = MaterialTheme.typography.headlineSmall,\n                color = MaterialTheme.colorScheme.onSurface,\n            )\n\n            // Preview rows\n            if (csvState.previewRows.isNotEmpty()) {\n                Column(\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .clip(RoundedCornerShape(8.dp))\n                            .background(MaterialTheme.colorScheme.surfaceVariant)\n                            .padding(12.dp),\n                    verticalArrangement = Arrangement.spacedBy(8.dp),\n                ) {\n                    Text(\n                        text = \"Preview\",\n                        style = MaterialTheme.typography.labelMedium,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n\n                    Row(\n                        modifier =\n                            Modifier\n                                .fillMaxWidth()\n                                .horizontalScroll(rememberScrollState()),\n                        horizontalArrangement = Arrangement.spacedBy(4.dp),\n                    ) {\n                        csvState.previewRows.take(5).forEachIndexed { rowIndex, row ->\n                            Column(\n                                modifier = Modifier,\n                                verticalArrangement = Arrangement.spacedBy(4.dp),\n                            ) {\n                                row.forEachIndexed { colIndex, cell ->\n                                    Box(\n                                        modifier =\n                                            Modifier\n                                                .width(120.dp)\n                                                .clip(RoundedCornerShape(4.dp))\n                                                .background(\n                                                    when {\n                                                        rowIndex == 0 && hasHeader -> MaterialTheme.colorScheme.primaryContainer\n                                                        colIndex == artistColumnIndex -> MaterialTheme.colorScheme.tertiaryContainer\n                                                        colIndex == titleColumnIndex -> MaterialTheme.colorScheme.secondaryContainer\n                                                        colIndex == urlColumnIndex && urlColumnIndex >= 0 -> MaterialTheme.colorScheme.tertiaryContainer\n                                                        else -> MaterialTheme.colorScheme.background\n                                                    },\n                                                ).padding(6.dp),\n                                    ) {\n                                        Text(\n                                            text = cell.take(18),\n                                            style = MaterialTheme.typography.labelSmall,\n                                            color = MaterialTheme.colorScheme.onSurface,\n                                            maxLines = 2,\n                                            overflow = TextOverflow.Ellipsis,\n                                            fontFamily = FontFamily.Monospace,\n                                        )\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n\n            // Header checkbox\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.spacedBy(8.dp),\n            ) {\n                Checkbox(\n                    checked = hasHeader,\n                    onCheckedChange = { hasHeader = it },\n                )\n                Text(\n                    text = stringResource(R.string.first_row_is_header),\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.onSurface,\n                )\n            }\n\n            // Column selectors\n            ColumnSelector(\n                label = stringResource(R.string.artist_name_column),\n                selectedIndex = artistColumnIndex,\n                maxColumns = csvState.previewRows.firstOrNull()?.size ?: 0,\n                onSelected = { artistColumnIndex = it },\n            )\n\n            ColumnSelector(\n                label = stringResource(R.string.song_title_column),\n                selectedIndex = titleColumnIndex,\n                maxColumns = csvState.previewRows.firstOrNull()?.size ?: 0,\n                onSelected = { titleColumnIndex = it },\n            )\n\n            ColumnSelector(\n                label = stringResource(R.string.youtube_url_column),\n                selectedIndex = urlColumnIndex,\n                maxColumns = csvState.previewRows.firstOrNull()?.size ?: 0,\n                allowNone = true,\n                onSelected = { urlColumnIndex = it },\n            )\n\n            // Buttons\n            Row(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .padding(top = 8.dp),\n                horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),\n            ) {\n                OutlinedButton(onClick = onDismiss) {\n                    Text(stringResource(R.string.cancel))\n                }\n                Button(\n                    onClick = {\n                        onConfirm(\n                            CsvImportState(\n                                previewRows = csvState.previewRows,\n                                artistColumnIndex = artistColumnIndex,\n                                titleColumnIndex = titleColumnIndex,\n                                urlColumnIndex = urlColumnIndex,\n                                hasHeader = hasHeader,\n                            ),\n                        )\n                    },\n                ) {\n                    Text(stringResource(R.string.continue_action))\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ColumnSelector(\n    label: String,\n    selectedIndex: Int,\n    maxColumns: Int,\n    allowNone: Boolean = false,\n    onSelected: (Int) -> Unit,\n) {\n    Column(\n        modifier = Modifier.fillMaxWidth(),\n        verticalArrangement = Arrangement.spacedBy(8.dp),\n    ) {\n        Text(\n            text = label,\n            style = MaterialTheme.typography.labelMedium,\n            color = MaterialTheme.colorScheme.onSurface,\n        )\n\n        Row(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .horizontalScroll(rememberScrollState()),\n            horizontalArrangement = Arrangement.spacedBy(6.dp),\n        ) {\n            if (allowNone) {\n                if (selectedIndex == -1) {\n                    Button(\n                        onClick = { onSelected(-1) },\n                        modifier = Modifier.height(36.dp),\n                    ) {\n                        Text(stringResource(R.string.none), style = MaterialTheme.typography.labelSmall)\n                    }\n                } else {\n                    OutlinedButton(\n                        onClick = { onSelected(-1) },\n                        modifier = Modifier.height(36.dp),\n                    ) {\n                        Text(stringResource(R.string.none), style = MaterialTheme.typography.labelSmall)\n                    }\n                }\n            }\n\n            repeat(maxColumns) { index ->\n                if (selectedIndex == index) {\n                    Button(\n                        onClick = { onSelected(index) },\n                        modifier = Modifier.height(36.dp),\n                    ) {\n                        Text(\n                            stringResource(R.string.column_label, index + 1),\n                            style = MaterialTheme.typography.labelSmall,\n                        )\n                    }\n                } else {\n                    OutlinedButton(\n                        onClick = { onSelected(index) },\n                        modifier = Modifier.height(36.dp),\n                    ) {\n                        Text(\n                            stringResource(R.string.column_label, index + 1),\n                            style = MaterialTheme.typography.labelSmall,\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun CsvImportProgressDialog(\n    isVisible: Boolean,\n    progress: Int,\n    recentLogs: List<ConvertedSongLog>,\n    onDismiss: () -> Unit,\n) {\n    if (!isVisible) return\n\n    Dialog(\n        onDismissRequest = onDismiss,\n        properties = DialogProperties(usePlatformDefaultWidth = false, dismissOnBackPress = false, dismissOnClickOutside = false),\n    ) {\n        Column(\n            modifier =\n                Modifier\n                    .fillMaxWidth(0.85f)\n                    .clip(RoundedCornerShape(16.dp))\n                    .background(MaterialTheme.colorScheme.surface)\n                    .padding(24.dp),\n            verticalArrangement = Arrangement.spacedBy(16.dp),\n        ) {\n            Text(\n                text = stringResource(R.string.importing_csv),\n                style = MaterialTheme.typography.headlineSmall,\n                color = MaterialTheme.colorScheme.onSurface,\n            )\n\n            LinearProgressIndicator(\n                progress = { progress / 100f },\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .height(8.dp)\n                        .clip(RoundedCornerShape(4.dp)),\n            )\n\n            Text(\n                text = stringResource(R.string.percentage_format, progress),\n                style = MaterialTheme.typography.bodySmall,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n            )\n\n            if (recentLogs.isNotEmpty()) {\n                Column(\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .clip(RoundedCornerShape(8.dp))\n                            .background(MaterialTheme.colorScheme.surfaceVariant)\n                            .padding(12.dp),\n                    verticalArrangement = Arrangement.spacedBy(8.dp),\n                ) {\n                    Text(\n                        text = stringResource(R.string.recently_converted),\n                        style = MaterialTheme.typography.labelMedium,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n\n                    recentLogs.forEach { log ->\n                        Column(\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .clip(RoundedCornerShape(4.dp))\n                                    .background(MaterialTheme.colorScheme.background)\n                                    .padding(8.dp),\n                            verticalArrangement = Arrangement.spacedBy(4.dp),\n                        ) {\n                            Text(\n                                text = log.title,\n                                style = MaterialTheme.typography.labelSmall,\n                                color = MaterialTheme.colorScheme.onSurface,\n                                maxLines = 1,\n                                overflow = TextOverflow.Ellipsis,\n                            )\n                            Text(\n                                text = log.artists,\n                                style = MaterialTheme.typography.labelSmall,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                                maxLines = 1,\n                                overflow = TextOverflow.Ellipsis,\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/menu/CustomThumbnailMenu.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.menu\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.ListItem\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport com.metrolist.music.R\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun CustomThumbnailMenu(\n    onEdit: () -> Unit,\n    onRemove: () -> Unit,\n    onDismiss: () -> Unit,\n) {\n    LazyColumn(\n        contentPadding = PaddingValues(\n            start = 8.dp,\n            top = 8.dp,\n            end = 8.dp,\n            bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(),\n        ),\n    ) {\n        item {\n            ListItem(\n                headlineContent = { \n                    Text(text = stringResource(R.string.choose_from_library)) \n                },\n                leadingContent = {\n                    Icon(\n                        painter = painterResource(R.drawable.insert_photo),\n                        contentDescription = null,\n                    )\n                },\n                modifier = Modifier.clickable {\n                    onEdit()\n                    onDismiss()\n                }\n            )\n        }\n        item {\n            ListItem(\n                headlineContent = { \n                    Text(text = stringResource(R.string.remove_custom_image)) \n                },\n                leadingContent = {\n                    Icon(\n                        painter = painterResource(R.drawable.delete),\n                        contentDescription = null,\n                    )\n                },\n                modifier = Modifier.clickable {\n                    onRemove()\n                    onDismiss()\n                }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/menu/ImportPlaylistDialog.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.menu\n\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.input.TextFieldValue\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.R\nimport com.metrolist.music.db.entities.PlaylistEntity\nimport com.metrolist.music.ui.component.TextFieldDialog\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.firstOrNull\nimport kotlinx.coroutines.launch\n\n@Composable\nfun ImportPlaylistDialog(\n    isVisible: Boolean,\n    onGetSong: suspend () -> List<String>, // list of song ids. Songs should be inserted to database in this function.\n    playlistTitle: String,\n    onDismiss: () -> Unit,\n) {\n    val database = LocalDatabase.current\n    val coroutineScope = rememberCoroutineScope()\n\n    val textFieldValue by remember { mutableStateOf(TextFieldValue(text = playlistTitle)) }\n    var songIds by remember {\n        mutableStateOf<List<String>?>(null) // list is not saveable\n    }\n\n    if (isVisible) {\n        TextFieldDialog(\n            icon = { Icon(painter = painterResource(R.drawable.add), contentDescription = null) },\n            title = { Text(text = stringResource(R.string.import_playlist)) },\n            initialTextFieldValue = textFieldValue,\n            autoFocus = false,\n            onDismiss = onDismiss,\n            onDone = { finalName ->\n                val newPlaylist = PlaylistEntity(\n                    name = finalName\n                )\n                database.query { insert(newPlaylist) }\n\n                coroutineScope.launch(Dispatchers.IO) {\n                    val playlist = database.playlist(newPlaylist.id).firstOrNull()\n\n                    if (playlist != null) {\n                        songIds = onGetSong()\n                        database.addSongToPlaylist(playlist, songIds!!)\n                    }\n\n                    onDismiss()\n                }\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/menu/LoadingScreen.kt",
    "content": "@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.menu\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LinearWavyProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Dialog\nimport androidx.compose.ui.window.DialogProperties\nimport com.metrolist.music.R\n\n@Composable\nfun LoadingScreen(\n    isVisible: Boolean,\n    value: Int,\n    songTitle: String? = null,\n    onCancel: (() -> Unit)? = null\n) {\n    if (!isVisible) return\n\n    Dialog(\n        onDismissRequest = { },\n        properties = DialogProperties(\n            usePlatformDefaultWidth = false,\n            dismissOnBackPress = false,\n            dismissOnClickOutside = false\n        ),\n    ) {\n        Column(\n            modifier = Modifier\n                .fillMaxWidth(0.9f)\n                .clip(RoundedCornerShape(28.dp))\n                .background(MaterialTheme.colorScheme.surfaceContainerHigh)\n                .padding(24.dp),\n            verticalArrangement = Arrangement.spacedBy(16.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n        ) {\n            Icon(\n                painter = painterResource(R.drawable.playlist_add),\n                contentDescription = null,\n                tint = MaterialTheme.colorScheme.secondary,\n                modifier = Modifier.size(32.dp)\n            )\n\n            Text(\n                text = stringResource(R.string.importing_playlist),\n                style = MaterialTheme.typography.headlineSmall,\n                color = MaterialTheme.colorScheme.onSurface,\n            )\n\n            if (songTitle != null && songTitle.isNotBlank()) {\n                Text(\n                    text = songTitle,\n                    style = MaterialTheme.typography.bodyLarge,\n                    color = MaterialTheme.colorScheme.onSurface,\n                    textAlign = TextAlign.Center,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis\n                )\n            } else {\n                Spacer(modifier = Modifier.height(24.dp))\n            }\n\n            Spacer(modifier = Modifier.height(8.dp))\n\n            LinearWavyProgressIndicator(\n                progress = { value / 100f },\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .height(8.dp),\n            )\n\n            Text(\n                text = stringResource(R.string.progress_percent, value.toString()),\n                style = MaterialTheme.typography.labelLarge,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n            )\n\n            if (onCancel != null) {\n                Spacer(modifier = Modifier.height(8.dp))\n                TextButton(\n                    onClick = onCancel,\n                    modifier = Modifier.align(Alignment.End)\n                ) {\n                    Text(stringResource(R.string.cancel))\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/menu/LyricsMenu.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.menu\n\nimport android.app.SearchManager\nimport android.content.ClipData\nimport android.content.ClipboardManager\nimport android.content.Context\nimport android.content.Intent\nimport android.content.res.Configuration\nimport android.widget.Toast\nimport androidx.compose.animation.animateContentSize\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.AiProviderKey\nimport com.metrolist.music.constants.DeeplApiKey\nimport com.metrolist.music.constants.DeeplFormalityKey\nimport com.metrolist.music.constants.OpenRouterApiKey\nimport com.metrolist.music.constants.OpenRouterBaseUrlKey\nimport com.metrolist.music.constants.OpenRouterModelKey\nimport com.metrolist.music.constants.TranslateLanguageKey\nimport com.metrolist.music.constants.TranslateModeKey\nimport com.metrolist.music.db.entities.LyricsEntity\nimport com.metrolist.music.db.entities.SongEntity\nimport com.metrolist.music.lyrics.LyricsTranslationHelper\nimport com.metrolist.music.lyrics.LyricsUtils\nimport com.metrolist.music.models.MediaMetadata\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.ListDialog\nimport com.metrolist.music.ui.component.Material3MenuGroup\nimport com.metrolist.music.ui.component.Material3MenuItemData\nimport com.metrolist.music.ui.component.NewAction\nimport com.metrolist.music.ui.component.NewActionGrid\nimport com.metrolist.music.ui.component.TextFieldDialog\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.viewmodels.LyricsMenuViewModel\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun LyricsMenu(\n    lyricsProvider: () -> LyricsEntity?,\n    songProvider: () -> SongEntity?,\n    mediaMetadataProvider: () -> MediaMetadata,\n    onDismiss: () -> Unit,\n    onShowOffsetDialog: () -> Unit = {},\n    viewModel: LyricsMenuViewModel = hiltViewModel(),\n) {\n    val context = LocalContext.current\n    val database = LocalDatabase.current\n\n    val openRouterApiKey by rememberPreference(OpenRouterApiKey, \"\")\n    val deeplApiKey by rememberPreference(DeeplApiKey, \"\")\n    val aiProvider by rememberPreference(AiProviderKey, \"OpenRouter\")\n    val translateLanguage by rememberPreference(TranslateLanguageKey, \"en\")\n    val translateMode by rememberPreference(TranslateModeKey, \"Literal\")\n    val openRouterBaseUrl by rememberPreference(OpenRouterBaseUrlKey, \"https://openrouter.ai/api/v1/chat/completions\")\n    val openRouterModel by rememberPreference(OpenRouterModelKey, \"google/gemini-2.5-flash-lite\")\n    val deeplFormality by rememberPreference(DeeplFormalityKey, \"default\")\n\n    val hasApiKey = if (aiProvider == \"DeepL\") deeplApiKey.isNotBlank() else openRouterApiKey.isNotBlank()\n\n    // Observe the authoritative translation-active state from the singleton; this persists\n    // correctly across menu open/close cycles and avoids the lyricsProvider() race condition.\n    val hasTranslations by LyricsTranslationHelper.hasActiveTranslations.collectAsState()\n\n    var showEditDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    if (showEditDialog) {\n        TextFieldDialog(\n            onDismiss = { showEditDialog = false },\n            icon = { Icon(painter = painterResource(R.drawable.edit), contentDescription = null) },\n            title = { Text(text = mediaMetadataProvider().title) },\n            initialTextFieldValue = TextFieldValue(lyricsProvider()?.lyrics.orEmpty()),\n            singleLine = false,\n            onDone = {\n                database.query {\n                    upsert(\n                        LyricsEntity(\n                            id = mediaMetadataProvider().id,\n                            lyrics = it,\n                            provider = lyricsProvider()?.provider ?: \"Manual\",\n                        ),\n                    )\n                }\n            },\n        )\n    }\n\n    var showSearchDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n    var showSearchResultDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    val searchMediaMetadata =\n        remember(showSearchDialog) {\n            mediaMetadataProvider()\n        }\n    val (titleField, onTitleFieldChange) =\n        rememberSaveable(showSearchDialog, stateSaver = TextFieldValue.Saver) {\n            mutableStateOf(\n                TextFieldValue(\n                    text = mediaMetadataProvider().title,\n                ),\n            )\n        }\n    val (artistField, onArtistFieldChange) =\n        rememberSaveable(showSearchDialog, stateSaver = TextFieldValue.Saver) {\n            mutableStateOf(\n                TextFieldValue(\n                    text = mediaMetadataProvider().artists.joinToString { it.name },\n                ),\n            )\n        }\n\n    val isNetworkAvailable by viewModel.isNetworkAvailable.collectAsState()\n    val errorNoInternetStr = stringResource(R.string.error_no_internet)\n\n    if (showSearchDialog) {\n        DefaultDialog(\n            modifier = Modifier.verticalScroll(rememberScrollState()),\n            onDismiss = { showSearchDialog = false },\n            icon = {\n                Icon(\n                    painter = painterResource(R.drawable.search),\n                    contentDescription = null,\n                )\n            },\n            title = { Text(stringResource(R.string.search_lyrics)) },\n            buttons = {\n                TextButton(\n                    onClick = { showSearchDialog = false },\n                ) {\n                    Text(stringResource(android.R.string.cancel))\n                }\n\n                Spacer(Modifier.width(8.dp))\n\n                TextButton(\n                    onClick = {\n                        showSearchDialog = false\n                        onDismiss()\n                        try {\n                            context.startActivity(\n                                Intent(Intent.ACTION_WEB_SEARCH).apply {\n                                    putExtra(\n                                        SearchManager.QUERY,\n                                        \"${artistField.text} ${titleField.text} lyrics\",\n                                    )\n                                },\n                            )\n                        } catch (_: Exception) {\n                        }\n                    },\n                ) {\n                    Text(stringResource(R.string.search_online))\n                }\n\n                Spacer(Modifier.width(8.dp))\n\n                TextButton(\n                    onClick = {\n                        // Try search regardless of network status indicator\n                        // as it might be a false negative\n                        viewModel.search(\n                            searchMediaMetadata.id,\n                            titleField.text,\n                            artistField.text,\n                            searchMediaMetadata.duration,\n                            searchMediaMetadata.album?.title,\n                        )\n                        showSearchResultDialog = true\n\n                        // Show warning only if network is definitely unavailable\n                        if (!isNetworkAvailable) {\n                            Toast.makeText(context, errorNoInternetStr, Toast.LENGTH_SHORT).show()\n                        }\n                    },\n                ) {\n                    Text(stringResource(android.R.string.ok))\n                }\n            },\n        ) {\n            OutlinedTextField(\n                value = titleField,\n                onValueChange = onTitleFieldChange,\n                singleLine = true,\n                label = { Text(stringResource(R.string.song_title)) },\n            )\n\n            Spacer(Modifier.height(12.dp))\n\n            OutlinedTextField(\n                value = artistField,\n                onValueChange = onArtistFieldChange,\n                singleLine = true,\n                label = { Text(stringResource(R.string.song_artists)) },\n            )\n        }\n    }\n\n    if (showSearchResultDialog) {\n        val results by viewModel.results.collectAsState()\n        val isLoading by viewModel.isLoading.collectAsState()\n\n        var expandedItemIndex by rememberSaveable {\n            mutableIntStateOf(-1)\n        }\n\n        ListDialog(\n            onDismiss = { showSearchResultDialog = false },\n        ) {\n            itemsIndexed(results) { index, result ->\n                Row(\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .clickable {\n                                onDismiss()\n                                viewModel.cancelSearch()\n                                database.query {\n                                    upsert(\n                                        LyricsEntity(\n                                            id = searchMediaMetadata.id,\n                                            lyrics = result.lyrics,\n                                            provider = result.providerName,\n                                        ),\n                                    )\n                                }\n                            }.padding(12.dp)\n                            .animateContentSize(),\n                ) {\n                    Column(\n                        modifier = Modifier.weight(1f),\n                    ) {\n                        Text(\n                            text = result.lyrics,\n                            style = MaterialTheme.typography.bodyMedium,\n                            maxLines = if (index == expandedItemIndex) Int.MAX_VALUE else 2,\n                            overflow = TextOverflow.Ellipsis,\n                            modifier = Modifier.padding(bottom = 4.dp),\n                        )\n\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                        ) {\n                            Text(\n                                text = result.providerName,\n                                style = MaterialTheme.typography.labelMedium,\n                                color = MaterialTheme.colorScheme.secondary,\n                                maxLines = 1,\n                            )\n                            if (result.lyrics.startsWith(\"[\")) {\n                                Icon(\n                                    painter = painterResource(R.drawable.sync),\n                                    contentDescription = null,\n                                    tint = MaterialTheme.colorScheme.secondary,\n                                    modifier =\n                                        Modifier\n                                            .padding(start = 4.dp)\n                                            .size(18.dp),\n                                )\n                            }\n                        }\n                    }\n\n                    IconButton(\n                        onClick = {\n                            expandedItemIndex = if (expandedItemIndex == index) -1 else index\n                        },\n                    ) {\n                        Icon(\n                            painter = painterResource(if (index == expandedItemIndex) R.drawable.expand_less else R.drawable.expand_more),\n                            contentDescription = null,\n                        )\n                    }\n                }\n            }\n\n            if (isLoading) {\n                item {\n                    Box(\n                        contentAlignment = Alignment.Center,\n                        modifier = Modifier.fillMaxWidth(),\n                    ) {\n                        CircularProgressIndicator()\n                    }\n                }\n            }\n\n            if (!isLoading && results.isEmpty()) {\n                item {\n                    Text(\n                        text = stringResource(R.string.lyrics_not_found),\n                        textAlign = TextAlign.Center,\n                        modifier =\n                            Modifier\n                                .fillMaxWidth(),\n                    )\n                }\n            }\n        }\n    }\n\n    var showRomanizationDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    var showRomanization by rememberSaveable { mutableStateOf(false) }\n    var isChecked by remember { mutableStateOf(songProvider()?.romanizeLyrics ?: true) }\n\n    var lyricsOffset by rememberSaveable { mutableIntStateOf(songProvider()?.lyricsOffset ?: 0) }\n\n    // Sync isChecked with song changes\n    LaunchedEffect(songProvider()) {\n        isChecked = songProvider()?.romanizeLyrics ?: true\n    }\n\n    LaunchedEffect(songProvider()) {\n        lyricsOffset = songProvider()?.lyricsOffset ?: 0\n    }\n\n    val configuration = LocalConfiguration.current\n    val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT\n\n    LazyColumn(\n        contentPadding =\n            PaddingValues(\n                start = 0.dp,\n                top = 0.dp,\n                end = 0.dp,\n                bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(),\n            ),\n    ) {\n        item {\n            NewActionGrid(\n                actions =\n                    listOf(\n                        NewAction(\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.edit),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(28.dp),\n                                    tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                                )\n                            },\n                            text = stringResource(R.string.edit),\n                            onClick = {\n                                showEditDialog = true\n                            },\n                        ),\n                        NewAction(\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.cached),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(28.dp),\n                                    tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                                )\n                            },\n                            text = stringResource(R.string.refetch),\n                            onClick = {\n                                onDismiss()\n                                viewModel.refetchLyrics(mediaMetadataProvider(), lyricsProvider())\n                            },\n                        ),\n                        NewAction(\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.search),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(28.dp),\n                                    tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                                )\n                            },\n                            text = stringResource(R.string.search),\n                            onClick = {\n                                showSearchDialog = true\n                            },\n                        ),\n                        NewAction(\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.content_copy),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(28.dp),\n                                    tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                                )\n                            },\n                            text = stringResource(R.string.copy),\n                            onClick = {\n                                lyricsProvider()?.lyrics?.let { lyrics ->\n                                    val plainLyrics = if (lyrics.startsWith(\"[\")) {\n                                        LyricsUtils.parseLyrics(lyrics)\n                                            .joinToString(\"\\n\") { it.text }\n                                    } else {\n                                        lyrics\n                                    }\n\n                                    val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager\n                                    val clip = ClipData.newPlainText(\"Lyrics\", plainLyrics)\n                                    clipboard.setPrimaryClip(clip)\n                                    Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()\n                                }\n                            },\n                        ),\n                    ),\n                modifier = Modifier.padding(horizontal = 4.dp, vertical = 16.dp),\n                columns = 4,\n            )\n        }\n\n        item {\n            Material3MenuGroup(\n                items =\n                    buildList {\n                        // Add translation toggle option if API key is configured\n                        if (hasApiKey) {\n                            add(\n                                Material3MenuItemData(\n                                    title = { Text(stringResource(R.string.ai_lyrics_translation)) },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.translate),\n                                            contentDescription = null,\n                                        )\n                                    },\n                                    onClick = {\n                                        if (hasTranslations) {\n                                            // Remove translations\n                                            lyricsProvider()?.let { lyrics ->\n                                                val clearedLyrics = LyricsTranslationHelper.clearTranslations(lyrics)\n                                                database.query {\n                                                    upsert(clearedLyrics)\n                                                }\n                                                // Resets hasActiveTranslations and clears in-memory translations\n                                                LyricsTranslationHelper.triggerClearTranslations()\n                                            }\n                                        } else {\n                                            // Trigger translation\n                                            LyricsTranslationHelper.triggerManualTranslation()\n                                        }\n                                    },\n                                    trailingContent = {\n                                        Switch(\n                                            checked = hasTranslations,\n                                            onCheckedChange = { newCheckedState ->\n                                                if (newCheckedState) {\n                                                    // Enable translations – hasActiveTranslations updates when done\n                                                    LyricsTranslationHelper.triggerManualTranslation()\n                                                } else {\n                                                    // Disable translations – triggerClearTranslations resets hasActiveTranslations\n                                                    lyricsProvider()?.let { lyrics ->\n                                                        val clearedLyrics = LyricsTranslationHelper.clearTranslations(lyrics)\n                                                        database.query {\n                                                            upsert(clearedLyrics)\n                                                        }\n                                                        LyricsTranslationHelper.triggerClearTranslations()\n                                                    }\n                                                }\n                                            },\n                                        )\n                                    },\n                                ),\n                            )\n                        }\n\n                        add(\n                            Material3MenuItemData(\n                                title = { Text(stringResource(R.string.lyrics_offset)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.fast_forward),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    onDismiss()\n                                    onShowOffsetDialog()\n                                },\n                                trailingContent = {\n                                    Text(\n                                        text = \"${if (lyricsOffset >= 0) \"+\" else \"\"}${lyricsOffset}ms\",\n                                        style = MaterialTheme.typography.bodyMedium,\n                                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                                    )\n                                },\n                            ),\n                        )\n\n                        add(\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.romanize_current_track)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.language_korean_latin),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    isChecked = !isChecked\n                                    songProvider()?.let { song ->\n                                        database.query {\n                                            upsert(song.copy(romanizeLyrics = isChecked))\n                                        }\n                                    }\n                                },\n                                trailingContent = {\n                                    Switch(\n                                        checked = isChecked,\n                                        onCheckedChange = { newCheckedState ->\n                                            isChecked = newCheckedState\n                                            songProvider()?.let { song ->\n                                                database.query {\n                                                    upsert(song.copy(romanizeLyrics = newCheckedState))\n                                                }\n                                            }\n                                        },\n                                    )\n                                },\n                            ),\n                        )\n                    },\n            )\n        }\n    }\n\n    /* if (showRomanizationDialog) {\n        var isChecked by remember { mutableStateOf(songProvider()?.romanizeLyrics ?: true) }\n\n        // Sync with song changes\n        LaunchedEffect(songProvider()) {\n            isChecked = songProvider()?.romanizeLyrics ?: true\n        }\n\n        DefaultDialog(\n            onDismiss = { showRomanizationDialog = false },\n            title = { Text(stringResource(R.string.romanization)) }\n        ) {\n            Row(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .clickable {\n                        // Toggle isChecked when the row is clicked\n                        isChecked = !isChecked\n                        songProvider()?.let { song ->\n                            database.query {\n                                upsert(song.copy(romanizeLyrics = isChecked))\n                            }\n                        }\n                    }\n                    .padding(vertical = 8.dp, horizontal = 16.dp),\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                Text(\n                    text = stringResource(R.string.romanize_current_track),\n                    modifier = Modifier.weight(1f)\n                )\n                Switch(\n                    checked = isChecked,\n                    onCheckedChange = { newCheckedState ->\n                        isChecked = newCheckedState\n                        songProvider()?.let { song ->\n                            database.query {\n                                upsert(song.copy(romanizeLyrics = newCheckedState))\n                            }\n                        }\n                    }\n                )\n            }\n        }\n    } */\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/menu/PlayerMenu.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.menu\n\nimport android.content.Context\nimport android.content.res.Configuration\nimport android.widget.Toast\nimport androidx.annotation.DrawableRes\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LinearProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.OutlinedTextFieldDefaults\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.compose.ui.window.DialogProperties\nimport androidx.core.net.toUri\nimport androidx.media3.common.PlaybackParameters\nimport androidx.media3.exoplayer.offline.Download\nimport androidx.media3.exoplayer.offline.DownloadRequest\nimport androidx.media3.exoplayer.offline.DownloadService\nimport androidx.navigation.NavController\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalDownloadUtil\nimport com.metrolist.music.LocalListenTogetherManager\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.ListItemHeight\nimport com.metrolist.music.listentogether.ConnectionState\nimport com.metrolist.music.listentogether.ListenTogetherEvent\nimport com.metrolist.music.models.MediaMetadata\nimport com.metrolist.music.playback.ExoDownloadService\nimport com.metrolist.music.ui.component.BottomSheetState\nimport com.metrolist.music.ui.component.ListDialog\nimport com.metrolist.music.ui.component.Material3MenuGroup\nimport com.metrolist.music.ui.component.Material3MenuItemData\nimport com.metrolist.music.ui.component.NewAction\nimport com.metrolist.music.ui.component.NewActionGrid\nimport com.metrolist.music.ui.component.VolumeSlider\nimport com.metrolist.music.utils.rememberPreference\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlin.math.log2\nimport kotlin.math.pow\nimport kotlin.math.round\n\n@Composable\nfun PlayerMenu(\n    mediaMetadata: MediaMetadata?,\n    navController: NavController,\n    playerBottomSheetState: BottomSheetState,\n    isQueueTrigger: Boolean? = false,\n    onShowDetailsDialog: () -> Unit,\n    onDismiss: () -> Unit,\n) {\n    mediaMetadata ?: return\n    val context = LocalContext.current\n    val database = LocalDatabase.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val playerVolume = playerConnection.service.playerVolume.collectAsState()\n\n    // Cast state for volume control - safely access castConnectionHandler to prevent crashes\n    val castHandler =\n        remember(playerConnection) {\n            try {\n                playerConnection.service.castConnectionHandler\n            } catch (e: Exception) {\n                null\n            }\n        }\n    val isCasting by castHandler?.isCasting?.collectAsState() ?: remember { mutableStateOf(false) }\n    val castVolume by castHandler?.castVolume?.collectAsState() ?: remember { mutableFloatStateOf(1f) }\n    val castDeviceName by castHandler?.castDeviceName?.collectAsState() ?: remember { mutableStateOf<String?>(null) }\n\n    val librarySong by database.song(mediaMetadata.id).collectAsState(initial = null)\n    val coroutineScope = rememberCoroutineScope()\n\n    val download by LocalDownloadUtil.current\n        .getDownload(mediaMetadata.id)\n        .collectAsState(initial = null)\n\n    val artists =\n        remember(mediaMetadata.artists) {\n            mediaMetadata.artists.filter { it.id != null }\n        }\n\n    var showChoosePlaylistDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    var showListenTogetherDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val listenTogetherRoleState = listenTogetherManager?.role?.collectAsState(initial = com.metrolist.music.listentogether.RoomRole.NONE)\n    val isListenTogetherGuest = listenTogetherRoleState?.value == com.metrolist.music.listentogether.RoomRole.GUEST\n    val pendingSuggestions by listenTogetherManager?.pendingSuggestions?.collectAsState(initial = emptyList())\n        ?: remember { mutableStateOf(emptyList()) }\n\n    AddToPlaylistDialog(\n        isVisible = showChoosePlaylistDialog,\n        onGetSong = { playlist ->\n            database.withTransaction {\n                insert(mediaMetadata)\n            }\n            coroutineScope.launch(Dispatchers.IO) {\n                playlist.playlist.browseId?.let { YouTube.addToPlaylist(it, mediaMetadata.id) }\n            }\n            listOf(mediaMetadata.id)\n        },\n        onDismiss = {\n            showChoosePlaylistDialog = false\n        },\n    )\n\n    ListenTogetherDialog(\n        visible = showListenTogetherDialog,\n        mediaMetadata = mediaMetadata,\n        onDismiss = { showListenTogetherDialog = false },\n    )\n\n    var showSelectArtistDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    if (showSelectArtistDialog) {\n        ListDialog(\n            onDismiss = { showSelectArtistDialog = false },\n        ) {\n            items(artists) { artist ->\n                Box(\n                    contentAlignment = Alignment.CenterStart,\n                    modifier =\n                        Modifier\n                            .fillParentMaxWidth()\n                            .height(ListItemHeight)\n                            .clickable {\n                                navController.navigate(\"artist/${artist.id}\")\n                                showSelectArtistDialog = false\n                                playerBottomSheetState.collapseSoft()\n                                onDismiss()\n                            }.padding(horizontal = 24.dp),\n                ) {\n                    Text(\n                        text = artist.name,\n                        fontSize = 18.sp,\n                        fontWeight = FontWeight.Bold,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis,\n                    )\n                }\n            }\n        }\n    }\n\n    var showPitchTempoDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    if (showPitchTempoDialog) {\n        TempoPitchDialog(\n            onDismiss = { showPitchTempoDialog = false },\n        )\n    }\n\n    if (isQueueTrigger != true) {\n        Column(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = 24.dp)\n                    .padding(top = 24.dp, bottom = 6.dp),\n        ) {\n            // Show Cast indicator when casting\n            if (isCasting && castDeviceName != null) {\n                Row(\n                    horizontalArrangement = Arrangement.Center,\n                    verticalAlignment = Alignment.CenterVertically,\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(bottom = 16.dp),\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.cast),\n                        contentDescription = null,\n                        modifier = Modifier.size(24.dp),\n                        tint = MaterialTheme.colorScheme.primary,\n                    )\n                    Spacer(modifier = Modifier.width(10.dp))\n                    Text(\n                        text = stringResource(R.string.casting_to, castDeviceName ?: \"\"),\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.primary,\n                    )\n                }\n            }\n\n            VolumeSlider(\n                value = if (isCasting) castVolume else playerVolume.value,\n                onValueChange = { volume ->\n                    if (isCasting) {\n                        castHandler?.setVolume(volume)\n                    } else {\n                        playerConnection.service.playerVolume.value = volume\n                    }\n                },\n                modifier = Modifier.fillMaxWidth(),\n                accentColor = MaterialTheme.colorScheme.primary,\n            )\n        }\n    }\n\n    Spacer(modifier = Modifier.height(20.dp))\n\n    HorizontalDivider()\n\n    Spacer(modifier = Modifier.height(12.dp))\n\n    val configuration = LocalConfiguration.current\n    val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT\n\n    LazyColumn(\n        contentPadding =\n            PaddingValues(\n                start = 0.dp,\n                top = 0.dp,\n                end = 0.dp,\n                bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(),\n            ),\n    ) {\n        item {\n            val startingRadioText = stringResource(R.string.starting_radio)\n            NewActionGrid(\n                actions =\n                    listOfNotNull(\n                        if (!isListenTogetherGuest) {\n                            NewAction(\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.radio),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(32.dp),\n                                        tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                                    )\n                                },\n                                text = stringResource(R.string.start_radio),\n                                onClick = {\n                                    Toast.makeText(context, startingRadioText, Toast.LENGTH_SHORT).show()\n                                    playerConnection.startRadioSeamlessly()\n                                    onDismiss()\n                                },\n                            )\n                        } else {\n                            null\n                        },\n                        NewAction(\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.playlist_add),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(32.dp),\n                                    tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                                )\n                            },\n                            text = stringResource(R.string.add_to_playlist),\n                            onClick = { showChoosePlaylistDialog = true },\n                        ),\n                        NewAction(\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.link),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(32.dp),\n                                    tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                                )\n                            },\n                            text = stringResource(R.string.copy_link),\n                            onClick = {\n                                val clipboard =\n                                    context.getSystemService(\n                                        android.content.Context.CLIPBOARD_SERVICE,\n                                    ) as android.content.ClipboardManager\n                                val clip =\n                                    android.content.ClipData.newPlainText(\n                                        \"Song Link\",\n                                        \"https://music.youtube.com/watch?v=${mediaMetadata.id}\",\n                                    )\n                                clipboard.setPrimaryClip(clip)\n                                android.widget.Toast\n                                    .makeText(context, R.string.link_copied, android.widget.Toast.LENGTH_SHORT)\n                                    .show()\n                                onDismiss()\n                            },\n                        ),\n                    ),\n                columns = if (isListenTogetherGuest) 2 else 3,\n                modifier = Modifier.padding(horizontal = 4.dp, vertical = 16.dp),\n            )\n        }\n\n        item {\n            // Check if this is a podcast episode (album ID doesn't start with MPREb_)\n            val isPodcast = mediaMetadata.album?.let { !it.id.startsWith(\"MPREb_\") } ?: false\n\n            Material3MenuGroup(\n                items =\n                    buildList {\n                        // Don't show \"View Artist\" for podcasts - only show \"View Podcast\"\n                        if (artists.isNotEmpty() && !isPodcast) {\n                            add(\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.view_artist)) },\n                                    description = {\n                                        Text(\n                                            text = mediaMetadata.artists.joinToString { it.name },\n                                            maxLines = 1,\n                                            overflow = TextOverflow.Ellipsis,\n                                        )\n                                    },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.artist),\n                                            contentDescription = null,\n                                            modifier = Modifier.size(24.dp),\n                                        )\n                                    },\n                                    onClick = {\n                                        if (mediaMetadata.artists.size == 1) {\n                                            navController.navigate(\"artist/${mediaMetadata.artists[0].id}\")\n                                            playerBottomSheetState.collapseSoft()\n                                            onDismiss()\n                                        } else {\n                                            showSelectArtistDialog = true\n                                        }\n                                    },\n                                ),\n                            )\n                        }\n                        if (mediaMetadata.album != null) {\n                            add(\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(if (isPodcast) R.string.view_podcast else R.string.view_album)) },\n                                    description = {\n                                        Text(\n                                            text = mediaMetadata.album.title,\n                                            maxLines = 1,\n                                            overflow = TextOverflow.Ellipsis,\n                                        )\n                                    },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(if (isPodcast) R.drawable.mic else R.drawable.album),\n                                            contentDescription = null,\n                                            modifier = Modifier.size(24.dp),\n                                        )\n                                    },\n                                    onClick = {\n                                        if (isPodcast) {\n                                            navController.navigate(\"online_podcast/${mediaMetadata.album.id}\")\n                                        } else {\n                                            navController.navigate(\"album/${mediaMetadata.album.id}\")\n                                        }\n                                        playerBottomSheetState.collapseSoft()\n                                        onDismiss()\n                                    },\n                                ),\n                            )\n                        }\n                        // Add to Library option\n                        val isInLibrary = librarySong?.song?.inLibrary != null\n                        add(\n                            Material3MenuItemData(\n                                title = {\n                                    Text(\n                                        text =\n                                            stringResource(\n                                                if (isInLibrary) {\n                                                    R.string.remove_from_library\n                                                } else {\n                                                    R.string.add_to_library\n                                                },\n                                            ),\n                                    )\n                                },\n                                icon = {\n                                    Icon(\n                                        painter =\n                                            painterResource(\n                                                if (isInLibrary) {\n                                                    R.drawable.library_add_check\n                                                } else {\n                                                    R.drawable.library_add\n                                                },\n                                            ),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(24.dp),\n                                    )\n                                },\n                                onClick = {\n                                    playerConnection.toggleLibrary()\n                                    onDismiss()\n                                },\n                            ),\n                        )\n                    },\n            )\n        }\n\n        item { Spacer(modifier = Modifier.height(12.dp)) }\n\n        item {\n            Material3MenuGroup(\n                items =\n                    listOf(\n                        when (download?.state) {\n                            Download.STATE_COMPLETED -> {\n                                Material3MenuItemData(\n                                    title = {\n                                        Text(\n                                            text = stringResource(R.string.remove_download),\n                                        )\n                                    },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.offline),\n                                            contentDescription = null,\n                                            modifier = Modifier.size(24.dp),\n                                        )\n                                    },\n                                    onClick = {\n                                        DownloadService.sendRemoveDownload(\n                                            context,\n                                            ExoDownloadService::class.java,\n                                            mediaMetadata.id,\n                                            false,\n                                        )\n                                    },\n                                )\n                            }\n\n                            Download.STATE_QUEUED, Download.STATE_DOWNLOADING -> {\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.downloading)) },\n                                    icon = {\n                                        CircularProgressIndicator(\n                                            modifier = Modifier.size(24.dp),\n                                            strokeWidth = 2.dp,\n                                        )\n                                    },\n                                    onClick = {\n                                        DownloadService.sendRemoveDownload(\n                                            context,\n                                            ExoDownloadService::class.java,\n                                            mediaMetadata.id,\n                                            false,\n                                        )\n                                    },\n                                )\n                            }\n\n                            else -> {\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.action_download)) },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.download),\n                                            contentDescription = null,\n                                            modifier = Modifier.size(24.dp),\n                                        )\n                                    },\n                                    onClick = {\n                                        database.transaction {\n                                            insert(mediaMetadata)\n                                        }\n                                        val downloadRequest =\n                                            DownloadRequest\n                                                .Builder(mediaMetadata.id, mediaMetadata.id.toUri())\n                                                .setCustomCacheKey(mediaMetadata.id)\n                                                .setData(mediaMetadata.title.toByteArray())\n                                                .build()\n                                        DownloadService.sendAddDownload(\n                                            context,\n                                            ExoDownloadService::class.java,\n                                            downloadRequest,\n                                            false,\n                                        )\n                                    },\n                                )\n                            }\n                        },\n                    ),\n            )\n        }\n\n        item { Spacer(modifier = Modifier.height(12.dp)) }\n\n        item {\n            Material3MenuGroup(\n                items =\n                    buildList {\n                        add(\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.listen_together)) },\n                                icon = {\n                                    // Show a small badge when there are pending suggestions\n                                    Box {\n                                        Icon(\n                                            painter = painterResource(R.drawable.group),\n                                            contentDescription = null,\n                                            modifier = Modifier.size(24.dp),\n                                        )\n                                        if (pendingSuggestions.isNotEmpty()) {\n                                            Surface(\n                                                shape = RoundedCornerShape(12.dp),\n                                                color = MaterialTheme.colorScheme.primary,\n                                                modifier =\n                                                    Modifier\n                                                        .offset(x = 8.dp, y = (-6).dp)\n                                                        .align(Alignment.TopEnd),\n                                            ) {\n                                                Text(\n                                                    text = pendingSuggestions.size.toString(),\n                                                    color = MaterialTheme.colorScheme.onPrimary,\n                                                    modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),\n                                                    style = MaterialTheme.typography.labelSmall,\n                                                )\n                                            }\n                                        }\n                                    }\n                                },\n                                onClick = { showListenTogetherDialog = true },\n                            ),\n                        )\n                        if (isListenTogetherGuest) {\n                            add(\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.resync)) },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.replay),\n                                            contentDescription = null,\n                                            modifier = Modifier.size(24.dp),\n                                        )\n                                    },\n                                    onClick = {\n                                        listenTogetherManager.requestSync()\n                                        onDismiss()\n                                    },\n                                ),\n                            )\n                        }\n                    },\n            )\n        }\n\n        item { Spacer(modifier = Modifier.height(12.dp)) }\n\n        item {\n            Material3MenuGroup(\n                items =\n                    buildList {\n                        add(\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.details)) },\n                                description = { Text(text = stringResource(R.string.details_desc)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.info),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(24.dp),\n                                    )\n                                },\n                                onClick = {\n                                    onShowDetailsDialog()\n                                    onDismiss()\n                                },\n                            ),\n                        )\n\n                        if (isQueueTrigger != true) {\n                            add(\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.equalizer)) },\n                                    description = { Text(text = stringResource(R.string.equalizer_desc)) },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.equalizer),\n                                            contentDescription = null,\n                                            modifier = Modifier.size(24.dp),\n                                        )\n                                    },\n                                    onClick = {\n                                        navController.navigate(\"equalizer\")\n                                        onDismiss()\n                                    },\n                                ),\n                            )\n                            add(\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.advanced)) },\n                                    description = { Text(text = stringResource(R.string.advanced_desc)) },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.tune),\n                                            contentDescription = null,\n                                            modifier = Modifier.size(24.dp),\n                                        )\n                                    },\n                                    onClick = {\n                                        showPitchTempoDialog = true\n                                    },\n                                ),\n                            )\n                        }\n                    },\n            )\n        }\n    }\n}\n\n@Composable\nfun TempoPitchDialog(onDismiss: () -> Unit) {\n    val playerConnection = LocalPlayerConnection.current ?: return\n    var tempo by remember {\n        mutableFloatStateOf(playerConnection.player.playbackParameters.speed)\n    }\n    var transposeValue by remember {\n        mutableIntStateOf(round(12 * log2(playerConnection.player.playbackParameters.pitch)).toInt())\n    }\n    val updatePlaybackParameters = {\n        playerConnection.player.playbackParameters =\n            PlaybackParameters(tempo, 2f.pow(transposeValue.toFloat() / 12))\n    }\n    val listenTogetherManager = com.metrolist.music.LocalListenTogetherManager.current\n    val isInRoom = listenTogetherManager?.isInRoom ?: false\n\n    AlertDialog(\n        properties = DialogProperties(usePlatformDefaultWidth = false),\n        onDismissRequest = onDismiss,\n        title = {\n            Text(stringResource(R.string.tempo_and_pitch))\n        },\n        dismissButton = {\n            TextButton(\n                onClick = {\n                    tempo = 1f\n                    transposeValue = 0\n                    updatePlaybackParameters()\n                },\n            ) {\n                Text(stringResource(R.string.reset))\n            }\n        },\n        confirmButton = {\n            TextButton(\n                onClick = onDismiss,\n            ) {\n                Text(stringResource(android.R.string.ok))\n            }\n        },\n        text = {\n            Column {\n                if (!isInRoom) {\n                    ValueAdjuster(\n                        icon = R.drawable.speed,\n                        currentValue = tempo,\n                        values = (0..35).map { round((0.25f + it * 0.05f) * 100) / 100 },\n                        onValueUpdate = {\n                            tempo = it\n                            updatePlaybackParameters()\n                        },\n                        valueText = { \"x$it\" },\n                        modifier = Modifier.padding(bottom = 12.dp),\n                    )\n                }\n                ValueAdjuster(\n                    icon = R.drawable.discover_tune,\n                    currentValue = transposeValue,\n                    values = (-12..12).toList(),\n                    onValueUpdate = {\n                        transposeValue = it\n                        updatePlaybackParameters()\n                    },\n                    valueText = { \"${if (it > 0) \"+\" else \"\"}$it\" },\n                )\n            }\n        },\n    )\n}\n\n@Composable\nfun <T> ValueAdjuster(\n    @DrawableRes icon: Int,\n    currentValue: T,\n    values: List<T>,\n    onValueUpdate: (T) -> Unit,\n    valueText: (T) -> String,\n    modifier: Modifier = Modifier,\n) {\n    Row(\n        horizontalArrangement = Arrangement.spacedBy(24.dp),\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = modifier,\n    ) {\n        Icon(\n            painter = painterResource(icon),\n            contentDescription = null,\n            modifier = Modifier.size(28.dp),\n        )\n\n        IconButton(\n            enabled = currentValue != values.first(),\n            onClick = {\n                onValueUpdate(values[values.indexOf(currentValue) - 1])\n            },\n        ) {\n            Icon(\n                painter = painterResource(R.drawable.remove),\n                contentDescription = null,\n            )\n        }\n\n        Text(\n            text = valueText(currentValue),\n            style = MaterialTheme.typography.titleMedium,\n            textAlign = TextAlign.Center,\n            modifier = Modifier.width(80.dp),\n        )\n\n        IconButton(\n            enabled = currentValue != values.last(),\n            onClick = {\n                onValueUpdate(values[values.indexOf(currentValue) + 1])\n            },\n        ) {\n            Icon(\n                painter = painterResource(R.drawable.add),\n                contentDescription = null,\n            )\n        }\n    }\n}\n\n@Composable\nfun ListenTogetherDialog(\n    visible: Boolean,\n    mediaMetadata: MediaMetadata?,\n    onDismiss: () -> Unit,\n) {\n    if (!visible) return\n\n    val context = LocalContext.current\n    val listenTogetherManager = com.metrolist.music.LocalListenTogetherManager.current\n    val joiningRoomTemplate = stringResource(R.string.joining_room)\n\n    // Handle case where manager is not available\n    if (listenTogetherManager == null) {\n        ListDialog(onDismiss = onDismiss) {\n            item {\n                Column(\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(24.dp),\n                    horizontalAlignment = Alignment.CenterHorizontally,\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.group),\n                        contentDescription = null,\n                        tint = MaterialTheme.colorScheme.primary,\n                        modifier = Modifier.size(48.dp),\n                    )\n                    Spacer(modifier = Modifier.height(16.dp))\n                    Text(\n                        text = stringResource(R.string.listen_together),\n                        style = MaterialTheme.typography.headlineSmall,\n                        fontWeight = FontWeight.Bold,\n                        color = MaterialTheme.colorScheme.primary,\n                    )\n                    Spacer(modifier = Modifier.height(8.dp))\n                    Text(\n                        text = stringResource(R.string.listen_together_not_configured),\n                        style = MaterialTheme.typography.bodyMedium,\n                        textAlign = TextAlign.Center,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n                    Spacer(modifier = Modifier.height(24.dp))\n                    Button(\n                        onClick = onDismiss,\n                        colors =\n                            ButtonDefaults.buttonColors(\n                                containerColor = MaterialTheme.colorScheme.primary,\n                            ),\n                    ) {\n                        Text(stringResource(android.R.string.ok))\n                    }\n                }\n            }\n        }\n        return\n    }\n\n    val connectionState by listenTogetherManager.connectionState.collectAsState()\n    val roomState by listenTogetherManager.roomState.collectAsState()\n    val userId by listenTogetherManager.userId.collectAsState()\n    val pendingJoinRequests by listenTogetherManager.pendingJoinRequests.collectAsState()\n    val pendingSuggestions by listenTogetherManager.pendingSuggestions.collectAsState()\n\n    // Load saved username\n    var savedUsername by rememberPreference(com.metrolist.music.constants.ListenTogetherUsernameKey, \"\")\n    var roomCodeInput by rememberSaveable { mutableStateOf(\"\") }\n    var usernameInput by rememberSaveable { mutableStateOf(savedUsername) }\n\n    // Local UI state for join/create actions\n    var isCreatingRoom by rememberSaveable { mutableStateOf(false) }\n    var isJoiningRoom by rememberSaveable { mutableStateOf(false) }\n    var joinErrorMessage by rememberSaveable { mutableStateOf<String?>(null) }\n\n    // User action menu state\n    var selectedUserForMenu by rememberSaveable { mutableStateOf<String?>(null) }\n    var selectedUsername by rememberSaveable { mutableStateOf<String?>(null) }\n\n    // Localized helper strings\n    val waitingForApprovalText = stringResource(R.string.waiting_for_approval)\n    val invalidRoomCodeText = stringResource(R.string.invalid_room_code)\n    val joinRequestDeniedText = stringResource(R.string.join_request_denied)\n\n    // User action menu dialog\n    if (selectedUserForMenu != null && selectedUsername != null) {\n        ListDialog(\n            onDismiss = {\n                selectedUserForMenu = null\n                selectedUsername = null\n            },\n        ) {\n            item {\n                Row(\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(horizontal = 24.dp, vertical = 16.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.Start,\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.group),\n                        contentDescription = null,\n                        tint = MaterialTheme.colorScheme.primary,\n                        modifier = Modifier.size(40.dp),\n                    )\n                    Spacer(modifier = Modifier.width(16.dp))\n                    Column {\n                        Text(\n                            text = stringResource(R.string.manage_user),\n                            style = MaterialTheme.typography.headlineSmall,\n                            fontWeight = FontWeight.Bold,\n                            color = MaterialTheme.colorScheme.primary,\n                        )\n                        Text(\n                            text = selectedUsername ?: \"\",\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        )\n                    }\n                }\n            }\n\n            item { Spacer(modifier = Modifier.height(12.dp)) }\n\n            // Kick button\n            item {\n                Surface(\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(horizontal = 12.dp)\n                            .clickable {\n                                selectedUserForMenu?.let {\n                                    listenTogetherManager.kickUser(it, \"Removed by host\")\n                                }\n                                selectedUserForMenu = null\n                                selectedUsername = null\n                            },\n                    shape = RoundedCornerShape(12.dp),\n                    color = MaterialTheme.colorScheme.errorContainer,\n                ) {\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        modifier = Modifier.padding(16.dp),\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.close),\n                            contentDescription = null,\n                            tint = MaterialTheme.colorScheme.error,\n                            modifier = Modifier.size(24.dp),\n                        )\n                        Spacer(modifier = Modifier.width(16.dp))\n                        Column(modifier = Modifier.weight(1f)) {\n                            Text(\n                                text = stringResource(R.string.kick_user),\n                                style = MaterialTheme.typography.titleMedium,\n                                fontWeight = FontWeight.SemiBold,\n                                color = MaterialTheme.colorScheme.error,\n                            )\n                            Text(\n                                text = stringResource(R.string.kick_user_desc),\n                                style = MaterialTheme.typography.bodySmall,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                            )\n                        }\n                    }\n                }\n            }\n\n            item { Spacer(modifier = Modifier.height(8.dp)) }\n\n            // Permanently kick button\n            item {\n                Surface(\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(horizontal = 12.dp)\n                            .clickable {\n                                selectedUserForMenu?.let { userId ->\n                                    selectedUsername?.let { username ->\n                                        listenTogetherManager.blockUser(username)\n                                        listenTogetherManager.kickUser(userId, R.string.user_blocked_by_host.toString())\n                                    }\n                                }\n                                selectedUserForMenu = null\n                                selectedUsername = null\n                            },\n                    shape = RoundedCornerShape(12.dp),\n                    color = MaterialTheme.colorScheme.surfaceVariant,\n                ) {\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        modifier = Modifier.padding(16.dp),\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.close),\n                            contentDescription = null,\n                            tint = MaterialTheme.colorScheme.error,\n                            modifier = Modifier.size(24.dp),\n                        )\n                        Spacer(modifier = Modifier.width(16.dp))\n                        Column(modifier = Modifier.weight(1f)) {\n                            Text(\n                                text = stringResource(R.string.permanently_kick_user),\n                                style = MaterialTheme.typography.titleMedium,\n                                fontWeight = FontWeight.SemiBold,\n                                color = MaterialTheme.colorScheme.onSurface,\n                            )\n                            Text(\n                                text = stringResource(R.string.permanently_kick_user_desc),\n                                style = MaterialTheme.typography.bodySmall,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                            )\n                        }\n                    }\n                }\n            }\n\n            item { Spacer(modifier = Modifier.height(8.dp)) }\n\n            // Transfer ownership button\n            item {\n                Surface(\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(horizontal = 12.dp)\n                            .clickable {\n                                selectedUserForMenu?.let {\n                                    listenTogetherManager.transferHost(it)\n                                }\n                                selectedUserForMenu = null\n                                selectedUsername = null\n                            },\n                    shape = RoundedCornerShape(12.dp),\n                    color = MaterialTheme.colorScheme.primaryContainer,\n                ) {\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        modifier = Modifier.padding(16.dp),\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.crown),\n                            contentDescription = null,\n                            tint = MaterialTheme.colorScheme.primary,\n                            modifier = Modifier.size(24.dp),\n                        )\n                        Spacer(modifier = Modifier.width(16.dp))\n                        Column(modifier = Modifier.weight(1f)) {\n                            Text(\n                                text = stringResource(R.string.transfer_ownership),\n                                style = MaterialTheme.typography.titleMedium,\n                                fontWeight = FontWeight.SemiBold,\n                                color = MaterialTheme.colorScheme.primary,\n                            )\n                            Text(\n                                text = stringResource(R.string.transfer_ownership_desc),\n                                style = MaterialTheme.typography.bodySmall,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                            )\n                        }\n                    }\n                }\n            }\n\n            item { Spacer(modifier = Modifier.height(16.dp)) }\n        }\n        return\n    }\n\n    // Sync usernameInput when savedUsername changes\n    LaunchedEffect(savedUsername) {\n        if (usernameInput.isBlank() && savedUsername.isNotBlank()) {\n            usernameInput = savedUsername\n        }\n    }\n\n    // Listen to low level events to update UI state (join rejected, approved, room created)\n    LaunchedEffect(listenTogetherManager) {\n        listenTogetherManager.events.collect { event ->\n            when (event) {\n                is ListenTogetherEvent.JoinRejected -> {\n                    val reason = event.reason\n                    joinErrorMessage =\n                        when {\n                            reason.isNullOrBlank() -> joinRequestDeniedText\n                            reason.contains(\"invalid\", ignoreCase = true) == true -> invalidRoomCodeText\n                            else -> \"$joinRequestDeniedText: $reason\"\n                        }\n                    isJoiningRoom = false\n                    isCreatingRoom = false\n                }\n\n                is ListenTogetherEvent.JoinApproved -> {\n                    isJoiningRoom = false\n                    joinErrorMessage = null\n                }\n\n                is ListenTogetherEvent.RoomCreated -> {\n                    isCreatingRoom = false\n                    val clipboard =\n                        context.getSystemService(android.content.Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager\n                    val clip = android.content.ClipData.newPlainText(\"ListenTogetherRoom\", event.roomCode)\n                    clipboard.setPrimaryClip(clip)\n                }\n\n                else -> { /* ignore other events here */ }\n            }\n        }\n    }\n\n    // Check if already in a room\n    val isInRoom = listenTogetherManager.isInRoom\n    val isHost = roomState?.hostId == userId\n\n    ListDialog(onDismiss = onDismiss) {\n        // Header - Icon on left, text left-aligned\n        item {\n            Row(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 24.dp, vertical = 16.dp),\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.Start,\n            ) {\n                Icon(\n                    painter = painterResource(R.drawable.group),\n                    contentDescription = null,\n                    tint = MaterialTheme.colorScheme.primary,\n                    modifier = Modifier.size(40.dp),\n                )\n                Spacer(modifier = Modifier.width(16.dp))\n                Text(\n                    text =\n                        if (isInRoom) {\n                            if (isHost) stringResource(R.string.hosting_room) else stringResource(R.string.in_room)\n                        } else {\n                            stringResource(R.string.listen_together)\n                        },\n                    style = MaterialTheme.typography.headlineSmall,\n                    fontWeight = FontWeight.Bold,\n                    color = MaterialTheme.colorScheme.primary,\n                )\n            }\n        }\n\n        // Connection status\n        item {\n            Surface(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 16.dp),\n                shape = RoundedCornerShape(16.dp),\n                color =\n                    when (connectionState) {\n                        ConnectionState.CONNECTED -> MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)\n                        ConnectionState.CONNECTING, ConnectionState.RECONNECTING -> MaterialTheme.colorScheme.secondary.copy(alpha = 0.15f)\n                        ConnectionState.ERROR -> MaterialTheme.colorScheme.error.copy(alpha = 0.15f)\n                        ConnectionState.DISCONNECTED -> MaterialTheme.colorScheme.surfaceVariant\n                    },\n            ) {\n                Column(\n                    modifier = Modifier.padding(16.dp),\n                    horizontalAlignment = Alignment.CenterHorizontally,\n                ) {\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.Center,\n                    ) {\n                        Box(\n                            modifier =\n                                Modifier\n                                    .size(10.dp)\n                                    .background(\n                                        color =\n                                            when (connectionState) {\n                                                ConnectionState.CONNECTED -> MaterialTheme.colorScheme.primary\n                                                ConnectionState.CONNECTING, ConnectionState.RECONNECTING -> MaterialTheme.colorScheme.secondary\n                                                ConnectionState.ERROR -> MaterialTheme.colorScheme.error\n                                                ConnectionState.DISCONNECTED -> MaterialTheme.colorScheme.outline\n                                            },\n                                        shape = RoundedCornerShape(50),\n                                    ),\n                        )\n                        Spacer(modifier = Modifier.width(8.dp))\n                        Text(\n                            text =\n                                when (connectionState) {\n                                    ConnectionState.CONNECTED -> stringResource(R.string.listen_together_connected)\n                                    ConnectionState.CONNECTING -> stringResource(R.string.listen_together_connecting)\n                                    ConnectionState.RECONNECTING -> stringResource(R.string.listen_together_reconnecting)\n                                    ConnectionState.ERROR -> stringResource(R.string.listen_together_error)\n                                    ConnectionState.DISCONNECTED -> stringResource(R.string.listen_together_disconnected)\n                                },\n                            style = MaterialTheme.typography.titleMedium,\n                            fontWeight = FontWeight.SemiBold,\n                            color =\n                                when (connectionState) {\n                                    ConnectionState.CONNECTED -> MaterialTheme.colorScheme.primary\n                                    ConnectionState.CONNECTING, ConnectionState.RECONNECTING -> MaterialTheme.colorScheme.secondary\n                                    ConnectionState.ERROR -> MaterialTheme.colorScheme.error\n                                    ConnectionState.DISCONNECTED -> MaterialTheme.colorScheme.onSurfaceVariant\n                                },\n                        )\n                    }\n\n                    if (connectionState == ConnectionState.CONNECTING || connectionState == ConnectionState.RECONNECTING) {\n                        Spacer(modifier = Modifier.height(12.dp))\n                        LinearProgressIndicator(\n                            modifier = Modifier.fillMaxWidth(),\n                            color = MaterialTheme.colorScheme.primary,\n                        )\n                    }\n\n                    Spacer(modifier = Modifier.height(12.dp))\n\n                    Row(\n                        horizontalArrangement = Arrangement.spacedBy(8.dp),\n                        modifier = Modifier.fillMaxWidth(),\n                    ) {\n                        if (connectionState == ConnectionState.DISCONNECTED || connectionState == ConnectionState.ERROR) {\n                            Button(\n                                onClick = { listenTogetherManager.connect() },\n                                modifier = Modifier.weight(1f),\n                                colors =\n                                    ButtonDefaults.buttonColors(\n                                        containerColor = MaterialTheme.colorScheme.primary,\n                                    ),\n                            ) {\n                                Text(stringResource(R.string.connect), fontWeight = FontWeight.SemiBold)\n                            }\n                        } else {\n                            Button(\n                                onClick = { listenTogetherManager.disconnect() },\n                                modifier = Modifier.weight(1f),\n                                colors =\n                                    ButtonDefaults.buttonColors(\n                                        containerColor = MaterialTheme.colorScheme.primary,\n                                    ),\n                            ) {\n                                Text(stringResource(R.string.disconnect), fontWeight = FontWeight.SemiBold)\n                            }\n                            FilledTonalButton(\n                                onClick = { listenTogetherManager.forceReconnect() },\n                                modifier = Modifier.weight(1f),\n                            ) {\n                                Text(\"Reconnect\", fontWeight = FontWeight.SemiBold)\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        item { Spacer(modifier = Modifier.height(12.dp)) }\n\n        if (connectionState == ConnectionState.CONNECTED && !isInRoom) {\n            item {\n                Text(\n                    text = stringResource(R.string.listen_together_background_disconnect_note),\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    modifier = Modifier.padding(horizontal = 24.dp),\n                    textAlign = TextAlign.Center,\n                )\n                Spacer(modifier = Modifier.height(12.dp))\n            }\n        }\n\n        if (isInRoom) {\n            // Room status card\n            roomState?.let { room ->\n                item {\n                    Surface(\n                        modifier =\n                            Modifier\n                                .fillMaxWidth()\n                                .padding(horizontal = 16.dp),\n                        shape = RoundedCornerShape(16.dp),\n                        color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),\n                    ) {\n                        Column(\n                            modifier = Modifier.padding(16.dp),\n                            horizontalAlignment = Alignment.CenterHorizontally,\n                        ) {\n                            Text(\n                                text = stringResource(R.string.room_code),\n                                style = MaterialTheme.typography.labelMedium,\n                                color = MaterialTheme.colorScheme.primary,\n                            )\n                            Spacer(modifier = Modifier.height(4.dp))\n                            Row(\n                                verticalAlignment = Alignment.CenterVertically,\n                                horizontalArrangement = Arrangement.Center,\n                            ) {\n                                Text(\n                                    text = room.roomCode,\n                                    style = MaterialTheme.typography.headlineLarge,\n                                    color = MaterialTheme.colorScheme.primary,\n                                    fontWeight = FontWeight.Bold,\n                                    letterSpacing = 6.sp,\n                                )\n                            }\n                            if (isHost) {\n                                Spacer(modifier = Modifier.height(12.dp))\n                                val inviteLink =\n                                    remember(room.roomCode) {\n                                        \"https://metrolist.meowery.eu/listen?code=${room.roomCode}\"\n                                    }\n                                Row(\n                                    verticalAlignment = Alignment.CenterVertically,\n                                    horizontalArrangement = Arrangement.Center,\n                                ) {\n                                    FilledTonalButton(\n                                        onClick = {\n                                            val clipboard =\n                                                context.getSystemService(\n                                                    Context.CLIPBOARD_SERVICE,\n                                                ) as android.content.ClipboardManager\n                                            val clip = android.content.ClipData.newPlainText(\"Listen Together Link\", inviteLink)\n                                            clipboard.setPrimaryClip(clip)\n                                            Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()\n                                        },\n                                    ) {\n                                        Icon(\n                                            painter = painterResource(R.drawable.link),\n                                            contentDescription = stringResource(R.string.copy_link),\n                                            modifier = Modifier.size(18.dp),\n                                        )\n                                        Spacer(modifier = Modifier.width(8.dp))\n                                        Text(stringResource(R.string.copy_link))\n                                    }\n\n                                    Spacer(modifier = Modifier.width(8.dp))\n\n                                    FilledTonalButton(\n                                        onClick = {\n                                            val clipboard =\n                                                context.getSystemService(\n                                                    Context.CLIPBOARD_SERVICE,\n                                                ) as android.content.ClipboardManager\n                                            val clip = android.content.ClipData.newPlainText(\"Room Code\", room.roomCode)\n                                            clipboard.setPrimaryClip(clip)\n                                            Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()\n                                        },\n                                    ) {\n                                        Icon(\n                                            painter = painterResource(R.drawable.content_copy),\n                                            contentDescription = stringResource(R.string.copy_code),\n                                            modifier = Modifier.size(18.dp),\n                                        )\n                                        Spacer(modifier = Modifier.width(8.dp))\n                                        Text(stringResource(R.string.copy_code))\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n\n                item { Spacer(modifier = Modifier.height(16.dp)) }\n\n                // Connected users - horizontal layout\n                val connectedUsers = room.users.filter { it.isConnected }\n\n                item {\n                    Column(\n                        modifier =\n                            Modifier\n                                .fillMaxWidth()\n                                .padding(horizontal = 16.dp),\n                    ) {\n                        Text(\n                            text = stringResource(R.string.connected_users, connectedUsers.size),\n                            style = MaterialTheme.typography.titleSmall,\n                            fontWeight = FontWeight.Bold,\n                            color = MaterialTheme.colorScheme.primary,\n                            modifier = Modifier.padding(bottom = 12.dp),\n                        )\n\n                        // Horizontal scrollable row for users\n                        Row(\n                            modifier = Modifier.fillMaxWidth(),\n                            horizontalArrangement = Arrangement.spacedBy(12.dp),\n                        ) {\n                            connectedUsers.forEach { user ->\n                                // User avatar card\n                                Column(\n                                    horizontalAlignment = Alignment.CenterHorizontally,\n                                    modifier =\n                                        Modifier\n                                            .width(72.dp)\n                                            .clickable(\n                                                enabled = isHost && user.userId != userId,\n                                                onClick = {\n                                                    selectedUserForMenu = user.userId\n                                                    selectedUsername = user.username\n                                                },\n                                            ),\n                                ) {\n                                    // Circular avatar\n                                    Box(\n                                        contentAlignment = Alignment.Center,\n                                    ) {\n                                        Surface(\n                                            modifier = Modifier.size(52.dp),\n                                            shape = RoundedCornerShape(50),\n                                            color =\n                                                if (user.isHost) {\n                                                    MaterialTheme.colorScheme.primary\n                                                } else if (user.userId == userId) {\n                                                    MaterialTheme.colorScheme.secondary\n                                                } else {\n                                                    MaterialTheme.colorScheme.surfaceVariant\n                                                },\n                                        ) {\n                                            Box(\n                                                contentAlignment = Alignment.Center,\n                                                modifier = Modifier.fillMaxSize(),\n                                            ) {\n                                                Text(\n                                                    text = user.username.take(1).uppercase(),\n                                                    style = MaterialTheme.typography.titleLarge,\n                                                    fontWeight = FontWeight.Bold,\n                                                    color =\n                                                        if (user.isHost) {\n                                                            MaterialTheme.colorScheme.onPrimary\n                                                        } else if (user.userId == userId) {\n                                                            MaterialTheme.colorScheme.onSecondary\n                                                        } else {\n                                                            MaterialTheme.colorScheme.onSurfaceVariant\n                                                        },\n                                                )\n                                            }\n                                        }\n\n                                        // Host/You badge\n                                        if (user.isHost || user.userId == userId) {\n                                            Surface(\n                                                modifier =\n                                                    Modifier\n                                                        .align(Alignment.BottomEnd)\n                                                        .offset(x = 4.dp, y = 4.dp)\n                                                        .size(18.dp),\n                                                shape = RoundedCornerShape(50),\n                                                color = if (user.isHost) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary,\n                                            ) {\n                                                Box(\n                                                    contentAlignment = Alignment.Center,\n                                                    modifier = Modifier.fillMaxSize(),\n                                                ) {\n                                                    Icon(\n                                                        painter =\n                                                            painterResource(\n                                                                if (user.isHost) R.drawable.crown else R.drawable.person,\n                                                            ),\n                                                        contentDescription = null,\n                                                        tint = MaterialTheme.colorScheme.onPrimary,\n                                                        modifier = Modifier.size(12.dp),\n                                                    )\n                                                }\n                                            }\n                                        }\n                                    }\n\n                                    Spacer(modifier = Modifier.height(6.dp))\n\n                                    // Username\n                                    Text(\n                                        text = user.username,\n                                        style = MaterialTheme.typography.labelMedium,\n                                        fontWeight = if (user.userId == userId) FontWeight.Bold else FontWeight.Medium,\n                                        color =\n                                            if (user.isHost) {\n                                                MaterialTheme.colorScheme.primary\n                                            } else {\n                                                MaterialTheme.colorScheme.onSurface\n                                            },\n                                        maxLines = 1,\n                                        overflow = TextOverflow.Ellipsis,\n                                        textAlign = TextAlign.Center,\n                                    )\n\n                                    // Role label\n                                    if (user.isHost) {\n                                        Text(\n                                            text = stringResource(R.string.host_label),\n                                            style = MaterialTheme.typography.labelSmall,\n                                            color = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f),\n                                        )\n                                    } else if (user.userId == userId) {\n                                        Text(\n                                            text = stringResource(R.string.you_label),\n                                            style = MaterialTheme.typography.labelSmall,\n                                            color = MaterialTheme.colorScheme.secondary.copy(alpha = 0.8f),\n                                        )\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n\n                // Pending join requests (host only)\n                if (isHost && pendingJoinRequests.isNotEmpty()) {\n                    item {\n                        Spacer(modifier = Modifier.height(16.dp))\n                        HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))\n                        Spacer(modifier = Modifier.height(12.dp))\n                        Text(\n                            text = stringResource(R.string.pending_requests),\n                            style = MaterialTheme.typography.titleSmall,\n                            fontWeight = FontWeight.Bold,\n                            color = MaterialTheme.colorScheme.primary,\n                            modifier = Modifier.padding(horizontal = 16.dp),\n                        )\n                        Spacer(modifier = Modifier.height(8.dp))\n                    }\n\n                    items(pendingJoinRequests) { request ->\n                        Surface(\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .padding(horizontal = 16.dp, vertical = 4.dp),\n                            shape = RoundedCornerShape(12.dp),\n                            color = MaterialTheme.colorScheme.secondary.copy(alpha = 0.15f),\n                        ) {\n                            Row(\n                                verticalAlignment = Alignment.CenterVertically,\n                                horizontalArrangement = Arrangement.SpaceBetween,\n                                modifier = Modifier.padding(12.dp),\n                            ) {\n                                Row(\n                                    verticalAlignment = Alignment.CenterVertically,\n                                    horizontalArrangement = Arrangement.spacedBy(12.dp),\n                                    modifier = Modifier.weight(1f),\n                                ) {\n                                    Surface(\n                                        modifier = Modifier.size(36.dp),\n                                        shape = RoundedCornerShape(50),\n                                        color = MaterialTheme.colorScheme.secondary,\n                                    ) {\n                                        Box(\n                                            contentAlignment = Alignment.Center,\n                                            modifier = Modifier.fillMaxSize(),\n                                        ) {\n                                            Text(\n                                                text = request.username.take(1).uppercase(),\n                                                style = MaterialTheme.typography.titleMedium,\n                                                fontWeight = FontWeight.Bold,\n                                                color = MaterialTheme.colorScheme.onSecondary,\n                                            )\n                                        }\n                                    }\n                                    Text(\n                                        text = request.username,\n                                        style = MaterialTheme.typography.bodyLarge,\n                                        fontWeight = FontWeight.Medium,\n                                        color = MaterialTheme.colorScheme.onSurface,\n                                    )\n                                }\n\n                                Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {\n                                    IconButton(\n                                        onClick = { listenTogetherManager.approveJoin(request.userId) },\n                                    ) {\n                                        Icon(\n                                            painter = painterResource(R.drawable.check),\n                                            contentDescription = stringResource(R.string.approve),\n                                            tint = MaterialTheme.colorScheme.primary,\n                                            modifier = Modifier.size(24.dp),\n                                        )\n                                    }\n                                    IconButton(\n                                        onClick = { listenTogetherManager.rejectJoin(request.userId, \"Rejected by host\") },\n                                    ) {\n                                        Icon(\n                                            painter = painterResource(R.drawable.close),\n                                            contentDescription = stringResource(R.string.reject),\n                                            tint = MaterialTheme.colorScheme.error,\n                                            modifier = Modifier.size(24.dp),\n                                        )\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n\n                // Pending suggestions (host only)\n                if (isHost && pendingSuggestions.isNotEmpty()) {\n                    item {\n                        Spacer(modifier = Modifier.height(16.dp))\n                        HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))\n                        Spacer(modifier = Modifier.height(12.dp))\n                        Text(\n                            text = stringResource(R.string.pending_suggestions),\n                            style = MaterialTheme.typography.titleSmall,\n                            fontWeight = FontWeight.Bold,\n                            color = MaterialTheme.colorScheme.primary,\n                            modifier = Modifier.padding(horizontal = 16.dp),\n                        )\n                        Spacer(modifier = Modifier.height(8.dp))\n                    }\n\n                    items(pendingSuggestions) { suggestion ->\n                        Surface(\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .padding(horizontal = 16.dp, vertical = 4.dp),\n                            shape = RoundedCornerShape(12.dp),\n                            color = MaterialTheme.colorScheme.secondary.copy(alpha = 0.15f),\n                        ) {\n                            Row(\n                                verticalAlignment = Alignment.CenterVertically,\n                                horizontalArrangement = Arrangement.SpaceBetween,\n                                modifier = Modifier.padding(12.dp),\n                            ) {\n                                Row(\n                                    verticalAlignment = Alignment.CenterVertically,\n                                    horizontalArrangement = Arrangement.spacedBy(12.dp),\n                                    modifier = Modifier.weight(1f),\n                                ) {\n                                    Icon(\n                                        painter = painterResource(R.drawable.queue_music),\n                                        contentDescription = null,\n                                        tint = MaterialTheme.colorScheme.primary,\n                                        modifier = Modifier.size(24.dp),\n                                    )\n                                    Column(modifier = Modifier.weight(1f)) {\n                                        Text(\n                                            text = suggestion.trackInfo.title,\n                                            style = MaterialTheme.typography.bodyMedium,\n                                            fontWeight = FontWeight.Medium,\n                                            color = MaterialTheme.colorScheme.onSurface,\n                                            maxLines = 1,\n                                            overflow = TextOverflow.Ellipsis,\n                                        )\n                                        Text(\n                                            text = suggestion.fromUsername,\n                                            style = MaterialTheme.typography.bodySmall,\n                                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                                            maxLines = 1,\n                                            overflow = TextOverflow.Ellipsis,\n                                        )\n                                    }\n                                }\n\n                                Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) {\n                                    IconButton(\n                                        onClick = { listenTogetherManager.approveSuggestion(suggestion.suggestionId) },\n                                    ) {\n                                        Icon(\n                                            painter = painterResource(R.drawable.check),\n                                            contentDescription = stringResource(R.string.approve),\n                                            tint = MaterialTheme.colorScheme.primary,\n                                            modifier = Modifier.size(24.dp),\n                                        )\n                                    }\n                                    IconButton(\n                                        onClick = { listenTogetherManager.rejectSuggestion(suggestion.suggestionId, \"Rejected by host\") },\n                                    ) {\n                                        Icon(\n                                            painter = painterResource(R.drawable.close),\n                                            contentDescription = stringResource(R.string.reject),\n                                            tint = MaterialTheme.colorScheme.error,\n                                            modifier = Modifier.size(24.dp),\n                                        )\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n\n                // Leave room button\n                item {\n                    Spacer(modifier = Modifier.height(20.dp))\n                    Row(\n                        modifier =\n                            Modifier\n                                .fillMaxWidth()\n                                .padding(horizontal = 16.dp),\n                        horizontalArrangement = Arrangement.spacedBy(12.dp),\n                    ) {\n                        TextButton(\n                            onClick = onDismiss,\n                            modifier = Modifier.weight(1f),\n                        ) {\n                            Text(\n                                stringResource(R.string.cancel),\n                                fontWeight = FontWeight.Medium,\n                            )\n                        }\n                        Button(\n                            onClick = {\n                                listenTogetherManager.leaveRoom()\n                                onDismiss()\n                            },\n                            modifier = Modifier.weight(1f),\n                            colors =\n                                ButtonDefaults.buttonColors(\n                                    containerColor = MaterialTheme.colorScheme.error,\n                                ),\n                        ) {\n                            Icon(\n                                painter = painterResource(R.drawable.logout),\n                                contentDescription = null,\n                                modifier = Modifier.size(18.dp),\n                            )\n                            Spacer(Modifier.width(8.dp))\n                            Text(stringResource(R.string.leave_room), fontWeight = FontWeight.SemiBold)\n                        }\n                    }\n                    Spacer(modifier = Modifier.height(16.dp))\n                }\n            }\n        } else {\n            // Join/Create room section\n            item {\n                Surface(\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(horizontal = 16.dp),\n                    shape = RoundedCornerShape(16.dp),\n                    color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),\n                ) {\n                    Column(\n                        modifier = Modifier.padding(20.dp),\n                        verticalArrangement = Arrangement.spacedBy(16.dp),\n                    ) {\n                        Text(\n                            text = stringResource(R.string.listen_together_description),\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                            textAlign = TextAlign.Center,\n                        )\n\n                        OutlinedTextField(\n                            value = usernameInput,\n                            onValueChange = { usernameInput = it },\n                            label = { Text(stringResource(R.string.username)) },\n                            placeholder = { Text(stringResource(R.string.enter_username)) },\n                            leadingIcon = {\n                                Icon(\n                                    painterResource(R.drawable.person),\n                                    null,\n                                    tint = MaterialTheme.colorScheme.primary,\n                                )\n                            },\n                            trailingIcon = {\n                                if (usernameInput.isNotBlank()) {\n                                    IconButton(onClick = { usernameInput = \"\" }) {\n                                        Icon(painterResource(R.drawable.close), null)\n                                    }\n                                }\n                            },\n                            singleLine = true,\n                            shape = RoundedCornerShape(12.dp),\n                            colors =\n                                OutlinedTextFieldDefaults.colors(\n                                    focusedBorderColor = MaterialTheme.colorScheme.primary,\n                                    unfocusedBorderColor = MaterialTheme.colorScheme.outline,\n                                    focusedLabelColor = MaterialTheme.colorScheme.primary,\n                                ),\n                            modifier = Modifier.fillMaxWidth(),\n                        )\n\n                        HorizontalDivider()\n\n                        Text(\n                            text = stringResource(R.string.join_existing_room),\n                            style = MaterialTheme.typography.titleSmall,\n                            fontWeight = FontWeight.Bold,\n                            color = MaterialTheme.colorScheme.primary,\n                        )\n\n                        OutlinedTextField(\n                            value = roomCodeInput,\n                            onValueChange = { roomCodeInput = it.uppercase().filter { c -> c.isLetterOrDigit() }.take(8) },\n                            label = { Text(stringResource(R.string.room_code)) },\n                            placeholder = { Text(\"ABCD1234\") },\n                            supportingText = {\n                                Text(\n                                    text = \"${roomCodeInput.length}/8\",\n                                    style = MaterialTheme.typography.labelSmall,\n                                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                                )\n                            },\n                            leadingIcon = {\n                                Icon(\n                                    painterResource(R.drawable.token),\n                                    null,\n                                    tint = MaterialTheme.colorScheme.primary,\n                                )\n                            },\n                            singleLine = true,\n                            shape = RoundedCornerShape(12.dp),\n                            colors =\n                                OutlinedTextFieldDefaults.colors(\n                                    focusedBorderColor = MaterialTheme.colorScheme.primary,\n                                    unfocusedBorderColor = MaterialTheme.colorScheme.outline,\n                                    focusedLabelColor = MaterialTheme.colorScheme.primary,\n                                ),\n                            modifier = Modifier.fillMaxWidth(),\n                        )\n\n                        // Status messages\n                        if (isJoiningRoom) {\n                            Row(\n                                verticalAlignment = Alignment.CenterVertically,\n                                horizontalArrangement = Arrangement.Center,\n                                modifier = Modifier.fillMaxWidth(),\n                            ) {\n                                CircularProgressIndicator(\n                                    modifier = Modifier.size(18.dp),\n                                    strokeWidth = 2.dp,\n                                    color = MaterialTheme.colorScheme.primary,\n                                )\n                                Spacer(modifier = Modifier.width(8.dp))\n                                Text(\n                                    text = waitingForApprovalText,\n                                    style = MaterialTheme.typography.bodyMedium,\n                                    color = MaterialTheme.colorScheme.primary,\n                                    fontWeight = FontWeight.Medium,\n                                )\n                            }\n                        }\n\n                        joinErrorMessage?.let { msg ->\n                            Surface(\n                                modifier = Modifier.fillMaxWidth(),\n                                shape = RoundedCornerShape(8.dp),\n                                color = MaterialTheme.colorScheme.error.copy(alpha = 0.1f),\n                            ) {\n                                Row(\n                                    verticalAlignment = Alignment.CenterVertically,\n                                    horizontalArrangement = Arrangement.Center,\n                                    modifier = Modifier.padding(12.dp),\n                                ) {\n                                    Icon(\n                                        painterResource(R.drawable.error),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(18.dp),\n                                        tint = MaterialTheme.colorScheme.error,\n                                    )\n                                    Spacer(modifier = Modifier.width(8.dp))\n                                    Text(\n                                        text = msg,\n                                        style = MaterialTheme.typography.bodyMedium,\n                                        color = MaterialTheme.colorScheme.error,\n                                        fontWeight = FontWeight.Medium,\n                                    )\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n\n            // Action buttons\n            item {\n                Spacer(modifier = Modifier.height(20.dp))\n                Column(\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(horizontal = 16.dp),\n                    verticalArrangement = Arrangement.spacedBy(8.dp),\n                ) {\n                    Row(\n                        modifier = Modifier.fillMaxWidth(),\n                        horizontalArrangement = Arrangement.spacedBy(12.dp),\n                    ) {\n                        // Create Room button (left side)\n                        Button(\n                            onClick = {\n                                val username = usernameInput.takeIf { it.isNotBlank() } ?: savedUsername\n                                val finalUsername = username.trim()\n                                if (finalUsername.isNotBlank()) {\n                                    savedUsername = finalUsername\n                                    Toast.makeText(context, R.string.creating_room, Toast.LENGTH_SHORT).show()\n                                    isCreatingRoom = true\n                                    isJoiningRoom = false\n                                    joinErrorMessage = null\n                                    listenTogetherManager.connect()\n                                    listenTogetherManager.createRoom(finalUsername)\n                                } else {\n                                    Toast.makeText(context, R.string.error_username_empty, Toast.LENGTH_SHORT).show()\n                                }\n                            },\n                            modifier = Modifier.weight(1f),\n                            enabled = (usernameInput.trim().isNotBlank() || savedUsername.isNotBlank()),\n                            colors =\n                                ButtonDefaults.buttonColors(\n                                    containerColor = MaterialTheme.colorScheme.primary,\n                                ),\n                        ) {\n                            Icon(\n                                painter = painterResource(R.drawable.add),\n                                contentDescription = null,\n                                modifier = Modifier.size(18.dp),\n                            )\n                            Spacer(Modifier.width(8.dp))\n                            Text(stringResource(R.string.create_room), fontWeight = FontWeight.SemiBold)\n                        }\n\n                        // Join Room button (right side - only visible when room code is complete)\n                        if (roomCodeInput.length == 8) {\n                            Button(\n                                onClick = {\n                                    val username = usernameInput.takeIf { it.isNotBlank() } ?: savedUsername\n                                    val finalUsername = username.trim()\n                                    if (finalUsername.isNotBlank()) {\n                                        savedUsername = finalUsername\n                                        Toast\n                                            .makeText(\n                                                context,\n                                                String.format(joiningRoomTemplate, roomCodeInput),\n                                                Toast.LENGTH_SHORT,\n                                            ).show()\n                                        isJoiningRoom = true\n                                        isCreatingRoom = false\n                                        joinErrorMessage = null\n                                        listenTogetherManager.connect()\n                                        listenTogetherManager.joinRoom(roomCodeInput, finalUsername)\n                                    } else {\n                                        Toast.makeText(context, R.string.error_username_empty, Toast.LENGTH_SHORT).show()\n                                    }\n                                },\n                                modifier = Modifier.weight(1f),\n                                enabled = (usernameInput.trim().isNotBlank() || savedUsername.isNotBlank()),\n                                colors =\n                                    ButtonDefaults.buttonColors(\n                                        containerColor = MaterialTheme.colorScheme.secondary,\n                                    ),\n                            ) {\n                                Icon(\n                                    painter = painterResource(R.drawable.login),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(18.dp),\n                                )\n                                Spacer(Modifier.width(8.dp))\n                                Text(stringResource(R.string.join_room), fontWeight = FontWeight.SemiBold)\n                            }\n                        }\n                    }\n\n                    TextButton(\n                        onClick = onDismiss,\n                        modifier = Modifier.fillMaxWidth(),\n                    ) {\n                        Text(\n                            stringResource(R.string.cancel),\n                            fontWeight = FontWeight.Medium,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        )\n                    }\n                }\n                Spacer(modifier = Modifier.height(16.dp))\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/menu/PlaylistMenu.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.menu\n\nimport android.content.Intent\nimport android.content.res.Configuration\nimport android.widget.Toast\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.TextRange\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.unit.dp\nimport androidx.core.net.toUri\nimport androidx.media3.exoplayer.offline.Download\nimport androidx.media3.exoplayer.offline.DownloadRequest\nimport androidx.media3.exoplayer.offline.DownloadService\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalDownloadUtil\nimport com.metrolist.music.LocalListenTogetherManager\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.db.entities.Playlist\nimport com.metrolist.music.db.entities.PlaylistSong\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.db.entities.SpeedDialItem\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.playback.ExoDownloadService\nimport com.metrolist.music.playback.queues.ListQueue\nimport com.metrolist.music.playback.queues.YouTubeQueue\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.Material3MenuGroup\nimport com.metrolist.music.ui.component.Material3MenuItemData\nimport com.metrolist.music.ui.component.NewAction\nimport com.metrolist.music.ui.component.NewActionGrid\nimport com.metrolist.music.ui.component.PlaylistListItem\nimport com.metrolist.music.ui.component.TextFieldDialog\nimport com.metrolist.music.ui.menu.ExportDialog\nimport com.metrolist.music.utils.PlaylistExporter\nimport com.metrolist.music.utils.getExportFileUri\nimport com.metrolist.music.utils.saveToPublicDocuments\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport java.time.LocalDateTime\n\n@Composable\nfun PlaylistMenu(\n    playlist: Playlist,\n    coroutineScope: CoroutineScope,\n    onDismiss: () -> Unit,\n    autoPlaylist: Boolean? = false,\n    downloadPlaylist: Boolean? = false,\n    songList: List<Song>? = emptyList(),\n) {\n    val context = LocalContext.current\n    val database = LocalDatabase.current\n    val downloadUtil = LocalDownloadUtil.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val isGuest = listenTogetherManager?.isInRoom == true && !listenTogetherManager.isHost\n    val dbPlaylist by database.playlist(playlist.id).collectAsState(initial = playlist)\n    var songs by remember {\n        mutableStateOf(emptyList<Song>())\n    }\n\n    LaunchedEffect(Unit) {\n        if (autoPlaylist == false) {\n            database.playlistSongs(playlist.id).collect {\n                songs = it.map(PlaylistSong::song)\n            }\n        } else {\n            if (songList != null) {\n                songs = songList\n            }\n        }\n    }\n\n    var downloadState by remember {\n        mutableIntStateOf(Download.STATE_STOPPED)\n    }\n\n    val editable: Boolean = playlist.playlist.isEditable == true\n\n    val isPinned by database.speedDialDao.isPinned(playlist.id).collectAsState(initial = false)\n\n    var showExportDialog by remember { mutableStateOf(false) }\n\n    LaunchedEffect(songs) {\n        if (songs.isEmpty()) return@LaunchedEffect\n        downloadUtil.downloads.collect { downloads ->\n            downloadState =\n                if (songs.all { downloads[it.id]?.state == Download.STATE_COMPLETED }) {\n                    Download.STATE_COMPLETED\n                } else if (songs.all {\n                        downloads[it.id]?.state == Download.STATE_QUEUED ||\n                            downloads[it.id]?.state == Download.STATE_DOWNLOADING ||\n                            downloads[it.id]?.state == Download.STATE_COMPLETED\n                    }\n                ) {\n                    Download.STATE_DOWNLOADING\n                } else {\n                    Download.STATE_STOPPED\n                }\n        }\n    }\n\n    var showEditDialog by remember {\n        mutableStateOf(false)\n    }\n\n    if (showEditDialog) {\n        TextFieldDialog(\n            icon = { Icon(painter = painterResource(R.drawable.edit), contentDescription = null) },\n            title = { Text(text = stringResource(R.string.edit_playlist)) },\n            onDismiss = { showEditDialog = false },\n            initialTextFieldValue =\n                TextFieldValue(\n                    playlist.playlist.name,\n                    TextRange(playlist.playlist.name.length),\n                ),\n            onDone = { name ->\n                onDismiss()\n                database.query {\n                    update(\n                        playlist.playlist.copy(\n                            name = name,\n                            lastUpdateTime = LocalDateTime.now(),\n                        ),\n                    )\n                }\n                coroutineScope.launch(Dispatchers.IO) {\n                    playlist.playlist.browseId?.let { YouTube.renamePlaylist(it, name) }\n                }\n            },\n        )\n    }\n\n    var showRemoveDownloadDialog by remember {\n        mutableStateOf(false)\n    }\n\n    if (showRemoveDownloadDialog) {\n        DefaultDialog(\n            onDismiss = { showRemoveDownloadDialog = false },\n            content = {\n                Text(\n                    text =\n                        stringResource(\n                            R.string.remove_download_playlist_confirm,\n                            playlist.playlist.name,\n                        ),\n                    style = MaterialTheme.typography.bodyLarge,\n                    modifier = Modifier.padding(horizontal = 18.dp),\n                )\n            },\n            buttons = {\n                TextButton(\n                    onClick = {\n                        showRemoveDownloadDialog = false\n                    },\n                ) {\n                    Text(text = stringResource(android.R.string.cancel))\n                }\n\n                TextButton(\n                    onClick = {\n                        showRemoveDownloadDialog = false\n                        songs.forEach { song ->\n                            DownloadService.sendRemoveDownload(\n                                context,\n                                ExoDownloadService::class.java,\n                                song.id,\n                                false,\n                            )\n                        }\n                    },\n                ) {\n                    Text(text = stringResource(android.R.string.ok))\n                }\n            },\n        )\n    }\n\n    var showDeletePlaylistDialog by remember {\n        mutableStateOf(false)\n    }\n\n    if (showDeletePlaylistDialog) {\n        DefaultDialog(\n            onDismiss = { showDeletePlaylistDialog = false },\n            content = {\n                Text(\n                    text = stringResource(R.string.delete_playlist_confirm, playlist.playlist.name),\n                    style = MaterialTheme.typography.bodyLarge,\n                    modifier = Modifier.padding(horizontal = 18.dp),\n                )\n            },\n            buttons = {\n                TextButton(\n                    onClick = {\n                        showDeletePlaylistDialog = false\n                    },\n                ) {\n                    Text(text = stringResource(android.R.string.cancel))\n                }\n\n                TextButton(\n                    onClick = {\n                        showDeletePlaylistDialog = false\n                        onDismiss()\n                        database.transaction {\n                            // First toggle the like using the same logic as the like button\n                            if (playlist.playlist.bookmarkedAt != null) {\n                                // Using the same toggleLike() method that's used in the like button\n                                update(playlist.playlist.toggleLike())\n                            }\n                            // Then delete the playlist\n                            delete(playlist.playlist)\n                        }\n\n                        coroutineScope.launch(Dispatchers.IO) {\n                            playlist.playlist.browseId?.let { YouTube.deletePlaylist(it) }\n                        }\n                    },\n                ) {\n                    Text(text = stringResource(android.R.string.ok))\n                }\n            },\n        )\n    }\n\n    PlaylistListItem(\n        playlist = playlist,\n        trailingContent = {\n            if (playlist.playlist.isEditable != true) {\n                IconButton(\n                    onClick = {\n                        database.query {\n                            dbPlaylist?.playlist?.toggleLike()?.let { update(it) }\n                        }\n                    },\n                ) {\n                    Icon(\n                        painter =\n                            painterResource(\n                                if (dbPlaylist?.playlist?.bookmarkedAt !=\n                                    null\n                                ) {\n                                    R.drawable.favorite\n                                } else {\n                                    R.drawable.favorite_border\n                                },\n                            ),\n                        tint =\n                            if (dbPlaylist?.playlist?.bookmarkedAt !=\n                                null\n                            ) {\n                                MaterialTheme.colorScheme.error\n                            } else {\n                                LocalContentColor.current\n                            },\n                        contentDescription = null,\n                    )\n                }\n            }\n        },\n    )\n\n    HorizontalDivider()\n\n    Spacer(modifier = Modifier.height(12.dp))\n\n    val configuration = LocalConfiguration.current\n    val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT\n\n    LazyColumn(\n        contentPadding =\n            PaddingValues(\n                start = 0.dp,\n                top = 0.dp,\n                end = 0.dp,\n                bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(),\n            ),\n    ) {\n        item {\n            NewActionGrid(\n                actions =\n                    listOfNotNull(\n                        if (!isGuest) {\n                            NewAction(\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.play),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(28.dp),\n                                        tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                                    )\n                                },\n                                text = stringResource(R.string.play),\n                                onClick = {\n                                    onDismiss()\n                                    if (songs.isNotEmpty()) {\n                                        playerConnection.playQueue(\n                                            ListQueue(\n                                                title = playlist.playlist.name,\n                                                items = songs.map(Song::toMediaItem),\n                                            ),\n                                        )\n                                    }\n                                },\n                            )\n                            NewAction(\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.shuffle),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(28.dp),\n                                        tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                                    )\n                                },\n                                text = stringResource(R.string.shuffle),\n                                onClick = {\n                                    onDismiss()\n                                    if (songs.isNotEmpty()) {\n                                        playerConnection.playQueue(\n                                            ListQueue(\n                                                title = playlist.playlist.name,\n                                                items = songs.shuffled().map(Song::toMediaItem),\n                                            ),\n                                        )\n                                    }\n                                },\n                            )\n                        } else {\n                            null\n                        },\n                        NewAction(\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.share),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(28.dp),\n                                    tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                                )\n                            },\n                            text = stringResource(R.string.share),\n                            onClick = {\n                                onDismiss()\n                                val intent =\n                                    Intent().apply {\n                                        action = Intent.ACTION_SEND\n                                        type = \"text/plain\"\n                                        putExtra(\n                                            Intent.EXTRA_TEXT,\n                                            \"https://music.youtube.com/playlist?list=${dbPlaylist?.playlist?.browseId}\",\n                                        )\n                                    }\n                                context.startActivity(Intent.createChooser(intent, null))\n                            },\n                        ),\n                    ),\n                modifier = Modifier.padding(horizontal = 4.dp, vertical = 16.dp),\n                columns = if (isGuest) 1 else 3,\n            )\n        }\n\n        item {\n            Material3MenuGroup(\n                items =\n                    buildList {\n                        if (!isGuest) {\n                            playlist.playlist.browseId?.let { browseId ->\n                                add(\n                                    Material3MenuItemData(\n                                        title = { Text(text = stringResource(R.string.start_radio)) },\n                                        description = { Text(text = stringResource(R.string.start_radio_desc)) },\n                                        icon = {\n                                            Icon(\n                                                painter = painterResource(R.drawable.radio),\n                                                contentDescription = null,\n                                            )\n                                        },\n                                        onClick = {\n                                            coroutineScope.launch(Dispatchers.IO) {\n                                                YouTube.playlist(browseId).getOrNull()?.playlist?.let { playlistItem ->\n                                                    playlistItem.radioEndpoint?.let { radioEndpoint ->\n                                                        withContext(Dispatchers.Main) {\n                                                            playerConnection.playQueue(YouTubeQueue(radioEndpoint))\n                                                        }\n                                                    }\n                                                }\n                                            }\n                                            onDismiss()\n                                        },\n                                    ),\n                                )\n                            }\n                        }\n                        if (!isGuest) {\n                            add(\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.play_next)) },\n                                    description = { Text(text = stringResource(R.string.play_next_desc)) },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.playlist_play),\n                                            contentDescription = null,\n                                        )\n                                    },\n                                    onClick = {\n                                        coroutineScope.launch {\n                                            playerConnection.playNext(songs.map { it.toMediaItem() })\n                                        }\n                                        onDismiss()\n                                    },\n                                ),\n                            )\n                        }\n                        if (!isGuest) {\n                            add(\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.add_to_queue)) },\n                                    description = { Text(text = stringResource(R.string.add_to_queue_desc)) },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.queue_music),\n                                            contentDescription = null,\n                                        )\n                                    },\n                                    onClick = {\n                                        onDismiss()\n                                        playerConnection.addToQueue(songs.map { it.toMediaItem() })\n                                    },\n                                ),\n                            )\n                        }\n                    },\n            )\n        }\n\n        item { Spacer(modifier = Modifier.height(12.dp)) }\n\n        item {\n            Material3MenuGroup(\n                items =\n                    buildList {\n                        if (editable && autoPlaylist != true && !isGuest) {\n                            add(\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.edit)) },\n                                    description = { Text(text = stringResource(R.string.edit_desc)) },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.edit),\n                                            contentDescription = null,\n                                        )\n                                    },\n                                    onClick = {\n                                        showEditDialog = true\n                                    },\n                                ),\n                            )\n                        }\n                        add(\n                            Material3MenuItemData(\n                                title = {\n                                    Text(\n                                        text = if (isPinned) \"Unpin from Speed dial\" else \"Pin to Speed dial\",\n                                    )\n                                },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(if (isPinned) R.drawable.remove else R.drawable.add),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    coroutineScope.launch(Dispatchers.IO) {\n                                        if (isPinned) {\n                                            database.speedDialDao.delete(playlist.id)\n                                        } else {\n                                            database.speedDialDao.insert(\n                                                SpeedDialItem(\n                                                    id = playlist.id,\n                                                    title = playlist.playlist.name,\n                                                    subtitle = null,\n                                                    thumbnailUrl = playlist.thumbnails.firstOrNull(),\n                                                    type = \"LOCAL_PLAYLIST\",\n                                                ),\n                                            )\n                                        }\n                                    }\n                                    onDismiss()\n                                },\n                            ),\n                        )\n                        if (downloadPlaylist != true) {\n                            add(\n                                when (downloadState) {\n                                    Download.STATE_COMPLETED -> {\n                                        Material3MenuItemData(\n                                            title = {\n                                                Text(\n                                                    text = stringResource(R.string.remove_download),\n                                                )\n                                            },\n                                            icon = {\n                                                Icon(\n                                                    painter = painterResource(R.drawable.offline),\n                                                    contentDescription = null,\n                                                )\n                                            },\n                                            onClick = {\n                                                showRemoveDownloadDialog = true\n                                            },\n                                        )\n                                    }\n\n                                    Download.STATE_QUEUED, Download.STATE_DOWNLOADING -> {\n                                        Material3MenuItemData(\n                                            title = { Text(text = stringResource(R.string.downloading)) },\n                                            icon = {\n                                                CircularProgressIndicator(\n                                                    modifier = Modifier.size(24.dp),\n                                                    strokeWidth = 2.dp,\n                                                )\n                                            },\n                                            onClick = {\n                                                showRemoveDownloadDialog = true\n                                            },\n                                        )\n                                    }\n\n                                    else -> {\n                                        Material3MenuItemData(\n                                            title = { Text(text = stringResource(R.string.action_download)) },\n                                            description = { Text(text = stringResource(R.string.download_desc)) },\n                                            icon = {\n                                                Icon(\n                                                    painter = painterResource(R.drawable.download),\n                                                    contentDescription = null,\n                                                )\n                                            },\n                                            onClick = {\n                                                songs.forEach { song ->\n                                                    val downloadRequest =\n                                                        DownloadRequest\n                                                            .Builder(song.id, song.id.toUri())\n                                                            .setCustomCacheKey(song.id)\n                                                            .setData(song.song.title.toByteArray())\n                                                            .build()\n                                                    DownloadService.sendAddDownload(\n                                                        context,\n                                                        ExoDownloadService::class.java,\n                                                        downloadRequest,\n                                                        false,\n                                                    )\n                                                }\n                                            },\n                                        )\n                                    }\n                                },\n                            )\n                        }\n                        // Export playlist\n                        add(\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.export_playlist)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.share),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = { showExportDialog = true },\n                            ),\n                        )\n                        if (autoPlaylist != true && !isGuest) {\n                            add(\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.delete)) },\n                                    description = { Text(text = stringResource(R.string.delete_desc)) },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.delete),\n                                            contentDescription = null,\n                                        )\n                                    },\n                                    onClick = {\n                                        showDeletePlaylistDialog = true\n                                    },\n                                ),\n                            )\n                        }\n                        playlist.playlist.shareLink?.let { shareLink ->\n                            add(\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.share)) },\n                                    description = { Text(text = stringResource(R.string.share_desc)) },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.share),\n                                            contentDescription = null,\n                                        )\n                                    },\n                                    onClick = {\n                                        val intent =\n                                            Intent().apply {\n                                                action = Intent.ACTION_SEND\n                                                type = \"text/plain\"\n                                                putExtra(Intent.EXTRA_TEXT, shareLink)\n                                            }\n                                        context.startActivity(Intent.createChooser(intent, null))\n                                        onDismiss()\n                                    },\n                                ),\n                            )\n                        }\n                    },\n            )\n        }\n    }\n\n    val exportPlaylistStr = stringResource(R.string.export_playlist)\n\n    if (showExportDialog) {\n        ExportDialog(\n            onDismiss = { showExportDialog = false },\n            onShare = { format ->\n                val playlistSongs =\n                    songs.map { s ->\n                        com.metrolist.music.db.entities.PlaylistSong(\n                            map =\n                                com.metrolist.music.db.entities.PlaylistSongMap(\n                                    songId = s.id,\n                                    playlistId = playlist.id,\n                                    position = 0,\n                                ),\n                            song = s,\n                        )\n                    }\n                val result =\n                    when (format) {\n                        \"csv\" -> PlaylistExporter.exportPlaylistAsCSV(context, playlist.playlist.name, playlistSongs)\n                        \"m3u\" -> PlaylistExporter.exportPlaylistAsM3U(context, playlist.playlist.name, playlistSongs)\n                        else -> Result.failure(IllegalArgumentException(\"Unknown format\"))\n                    }\n                result\n                    .onSuccess { file ->\n                        val uri = getExportFileUri(context, file)\n                        val mimeType = if (format == \"csv\") \"text/csv\" else \"audio/x-mpegurl\"\n                        val shareIntent =\n                            Intent(Intent.ACTION_SEND).apply {\n                                type = mimeType\n                                putExtra(Intent.EXTRA_STREAM, uri)\n                                addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n                            }\n                        context.startActivity(Intent.createChooser(shareIntent, exportPlaylistStr))\n                    }.onFailure {\n                        Toast.makeText(context, R.string.export_failed, Toast.LENGTH_SHORT).show()\n                    }\n                showExportDialog = false\n            },\n            onSave = { format ->\n                val playlistSongs =\n                    songs.map { s ->\n                        com.metrolist.music.db.entities.PlaylistSong(\n                            map =\n                                com.metrolist.music.db.entities.PlaylistSongMap(\n                                    songId = s.id,\n                                    playlistId = playlist.id,\n                                    position = 0,\n                                ),\n                            song = s,\n                        )\n                    }\n                val export =\n                    when (format) {\n                        \"csv\" -> PlaylistExporter.exportPlaylistAsCSV(context, playlist.playlist.name, playlistSongs)\n                        \"m3u\" -> PlaylistExporter.exportPlaylistAsM3U(context, playlist.playlist.name, playlistSongs)\n                        else -> Result.failure(IllegalArgumentException(\"Unknown format\"))\n                    }\n                export\n                    .onSuccess { file ->\n                        val mimeType = if (format == \"csv\") \"text/csv\" else \"audio/x-mpegurl\"\n                        val save = saveToPublicDocuments(context, file, mimeType)\n                        save\n                            .onSuccess { Toast.makeText(context, R.string.export_success, Toast.LENGTH_SHORT).show() }\n                            .onFailure { Toast.makeText(context, R.string.export_failed, Toast.LENGTH_SHORT).show() }\n                    }.onFailure {\n                        Toast.makeText(context, R.string.export_failed, Toast.LENGTH_SHORT).show()\n                    }\n                showExportDialog = false\n            },\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/menu/PlaylistScreenMenus.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.menu\n\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport android.widget.Toast\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.RadioButton\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.media3.exoplayer.offline.Download\nimport com.metrolist.music.LocalListenTogetherManager\nimport com.metrolist.music.R\nimport com.metrolist.music.db.entities.Playlist\nimport com.metrolist.music.db.entities.PlaylistSong\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.Material3MenuGroup\nimport com.metrolist.music.ui.component.Material3MenuItemData\nimport com.metrolist.music.utils.PlaylistExporter\nimport com.metrolist.music.utils.getExportFileUri\nimport com.metrolist.music.utils.saveToPublicDocuments\nimport kotlinx.coroutines.launch\n\n/**\n * Menu for Local Playlist Screen\n */\n@Composable\nfun LocalPlaylistMenu(\n    playlist: Playlist,\n    songs: List<PlaylistSong>,\n    context: Context,\n    downloadState: Int,\n    onEdit: () -> Unit,\n    onSync: () -> Unit,\n    onDelete: () -> Unit,\n    onDownload: () -> Unit,\n    onQueue: () -> Unit,\n    onDismiss: () -> Unit,\n) {\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val isGuest = listenTogetherManager?.isInRoom == true && !listenTogetherManager.isHost\n    val coroutineScope = rememberCoroutineScope()\n    val localContext = LocalContext.current\n\n    val (showExportDialog, setShowExportDialog) = remember { mutableStateOf(false) }\n\n    val downloadMenuItem =\n        when (downloadState) {\n            Download.STATE_COMPLETED -> {\n                Material3MenuItemData(\n                    title = { Text(stringResource(R.string.remove_download)) },\n                    description = { Text(stringResource(R.string.remove_download_playlist_desc)) },\n                    icon = {\n                        Icon(\n                            painter = painterResource(R.drawable.offline),\n                            contentDescription = null,\n                        )\n                    },\n                    onClick = {\n                        onDownload()\n                        onDismiss()\n                    },\n                )\n            }\n\n            Download.STATE_QUEUED, Download.STATE_DOWNLOADING -> {\n                Material3MenuItemData(\n                    title = { Text(stringResource(R.string.downloading)) },\n                    description = { Text(stringResource(R.string.download_in_progress_desc)) },\n                    icon = {\n                        CircularProgressIndicator(\n                            modifier = Modifier.size(24.dp),\n                            strokeWidth = 2.dp,\n                        )\n                    },\n                    onClick = {\n                        onDownload()\n                        onDismiss()\n                    },\n                )\n            }\n\n            else -> {\n                Material3MenuItemData(\n                    title = { Text(stringResource(R.string.action_download)) },\n                    description = { Text(stringResource(R.string.download_playlist_desc)) },\n                    icon = {\n                        Icon(\n                            painter = painterResource(R.drawable.download),\n                            contentDescription = null,\n                        )\n                    },\n                    onClick = {\n                        onDownload()\n                        onDismiss()\n                    },\n                )\n            }\n        }\n\n    val isYouTubePlaylist = playlist.playlist.browseId != null\n\n    val menuItems =\n        buildList {\n            add(\n                Material3MenuItemData(\n                    title = { Text(stringResource(R.string.edit)) },\n                    description = { Text(stringResource(R.string.edit_playlist)) },\n                    icon = {\n                        Icon(\n                            painter = painterResource(R.drawable.edit),\n                            contentDescription = null,\n                        )\n                    },\n                    onClick = {\n                        onEdit()\n                        onDismiss()\n                    },\n                ),\n            )\n\n            // Show sync button only for YouTube playlists\n            if (isYouTubePlaylist) {\n                add(\n                    Material3MenuItemData(\n                        title = { Text(stringResource(R.string.action_sync)) },\n                        description = { Text(stringResource(R.string.sync_playlist_desc)) },\n                        icon = {\n                            Icon(\n                                painter = painterResource(R.drawable.sync),\n                                contentDescription = null,\n                            )\n                        },\n                        onClick = {\n                            onSync()\n                            onDismiss()\n                        },\n                    ),\n                )\n            }\n\n            if (!isGuest) {\n                add(\n                    Material3MenuItemData(\n                        title = { Text(stringResource(R.string.add_to_queue)) },\n                        description = { Text(stringResource(R.string.add_to_queue_desc)) },\n                        icon = {\n                            Icon(\n                                painter = painterResource(R.drawable.queue_music),\n                                contentDescription = null,\n                            )\n                        },\n                        onClick = {\n                            onQueue()\n                            onDismiss()\n                        },\n                    ),\n                )\n            }\n\n            add(downloadMenuItem)\n\n            add(\n                Material3MenuItemData(\n                    title = { Text(stringResource(R.string.share)) },\n                    description = { Text(stringResource(R.string.share_playlist_desc)) },\n                    icon = {\n                        Icon(\n                            painter = painterResource(R.drawable.share),\n                            contentDescription = null,\n                        )\n                    },\n                    onClick = {\n                        val shareText =\n                            if (isYouTubePlaylist) {\n                                \"https://music.youtube.com/playlist?list=${playlist.playlist.browseId}\"\n                            } else {\n                                songs.joinToString(\"\\n\") { it.song.song.title }\n                            }\n                        val sendIntent: Intent =\n                            Intent().apply {\n                                action = Intent.ACTION_SEND\n                                putExtra(Intent.EXTRA_TEXT, shareText)\n                                type = \"text/plain\"\n                            }\n                        val shareIntent = Intent.createChooser(sendIntent, null)\n                        context.startActivity(shareIntent)\n                        onDismiss()\n                    },\n                ),\n            )\n\n            // Export menu group\n            add(\n                Material3MenuItemData(\n                    title = { Text(stringResource(R.string.export_playlist)) },\n                    icon = {\n                        Icon(\n                            painter = painterResource(R.drawable.share),\n                            contentDescription = null,\n                        )\n                    },\n                    onClick = { setShowExportDialog(true) },\n                ),\n            )\n\n            add(\n                Material3MenuItemData(\n                    title = { Text(stringResource(R.string.delete)) },\n                    description = { Text(stringResource(R.string.delete_playlist_desc)) },\n                    icon = {\n                        Icon(\n                            painter = painterResource(R.drawable.delete),\n                            contentDescription = null,\n                        )\n                    },\n                    onClick = {\n                        onDelete()\n                        onDismiss()\n                    },\n                ),\n            )\n        }\n\n    Material3MenuGroup(items = menuItems)\n\n    if (showExportDialog) {\n        ExportDialog(\n            onDismiss = { setShowExportDialog(false) },\n            onShare = { format ->\n                coroutineScope.launch {\n                    val result =\n                        when (format) {\n                            \"csv\" -> PlaylistExporter.exportPlaylistAsCSV(localContext, playlist.playlist.name, songs)\n                            \"m3u\" -> PlaylistExporter.exportPlaylistAsM3U(localContext, playlist.playlist.name, songs)\n                            else -> Result.failure(IllegalArgumentException(\"Unknown format\"))\n                        }\n                    result\n                        .onSuccess { file ->\n                            val uri = getExportFileUri(localContext, file)\n                            val mimeType =\n                                when (format) {\n                                    \"csv\" -> \"text/csv\"\n                                    \"m3u\" -> \"audio/x-mpegurl\"\n                                    else -> \"*/*\"\n                                }\n                            shareExportFile(localContext, uri, mimeType)\n                        }.onFailure {\n                            Toast.makeText(localContext, R.string.export_failed, Toast.LENGTH_SHORT).show()\n                        }\n                }\n                onDismiss()\n            },\n            onSave = { format ->\n                coroutineScope.launch {\n                    val exportResult =\n                        when (format) {\n                            \"csv\" -> PlaylistExporter.exportPlaylistAsCSV(localContext, playlist.playlist.name, songs)\n                            \"m3u\" -> PlaylistExporter.exportPlaylistAsM3U(localContext, playlist.playlist.name, songs)\n                            else -> Result.failure(IllegalArgumentException(\"Unknown format\"))\n                        }\n                    exportResult\n                        .onSuccess { file ->\n                            val mimeType =\n                                when (format) {\n                                    \"csv\" -> \"text/csv\"\n                                    \"m3u\" -> \"audio/x-mpegurl\"\n                                    else -> \"application/octet-stream\"\n                                }\n                            val saveResult = saveToPublicDocuments(localContext, file, mimeType)\n                            saveResult\n                                .onSuccess {\n                                    Toast.makeText(localContext, R.string.export_success, Toast.LENGTH_SHORT).show()\n                                }.onFailure {\n                                    Toast.makeText(localContext, R.string.export_failed, Toast.LENGTH_SHORT).show()\n                                }\n                        }.onFailure {\n                            Toast.makeText(localContext, R.string.export_failed, Toast.LENGTH_SHORT).show()\n                        }\n                }\n                onDismiss()\n            },\n        )\n    }\n}\n\n/**\n * Menu for Auto Playlist Screen (Liked Songs, Downloaded Songs, etc.)\n */\n@Composable\nfun AutoPlaylistMenu(\n    downloadState: Int,\n    onQueue: () -> Unit,\n    onDownload: () -> Unit,\n    onDismiss: () -> Unit,\n    songs: List<Song> = emptyList(),\n    playlistName: String = \"Playlist\",\n) {\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val isGuest = listenTogetherManager?.isInRoom == true && !listenTogetherManager.isHost\n    val coroutineScope = rememberCoroutineScope()\n    val localContext = LocalContext.current\n\n    val (showExportDialog, setShowExportDialog) = remember { mutableStateOf(false) }\n\n    val downloadMenuItem =\n        when (downloadState) {\n            Download.STATE_COMPLETED -> {\n                Material3MenuItemData(\n                    title = { Text(stringResource(R.string.remove_download)) },\n                    description = { Text(stringResource(R.string.remove_download_playlist_desc)) },\n                    icon = {\n                        Icon(\n                            painter = painterResource(R.drawable.offline),\n                            contentDescription = null,\n                        )\n                    },\n                    onClick = {\n                        onDownload()\n                        onDismiss()\n                    },\n                )\n            }\n\n            Download.STATE_QUEUED, Download.STATE_DOWNLOADING -> {\n                Material3MenuItemData(\n                    title = { Text(stringResource(R.string.downloading)) },\n                    description = { Text(stringResource(R.string.download_in_progress_desc)) },\n                    icon = {\n                        CircularProgressIndicator(\n                            modifier = Modifier.size(24.dp),\n                            strokeWidth = 2.dp,\n                        )\n                    },\n                    onClick = {\n                        onDownload()\n                        onDismiss()\n                    },\n                )\n            }\n\n            else -> {\n                Material3MenuItemData(\n                    title = { Text(stringResource(R.string.action_download)) },\n                    description = { Text(stringResource(R.string.download_playlist_desc)) },\n                    icon = {\n                        Icon(\n                            painter = painterResource(R.drawable.download),\n                            contentDescription = null,\n                        )\n                    },\n                    onClick = {\n                        onDownload()\n                        onDismiss()\n                    },\n                )\n            }\n        }\n\n    Material3MenuGroup(\n        items =\n            listOfNotNull(\n                if (!isGuest) {\n                    Material3MenuItemData(\n                        title = { Text(stringResource(R.string.add_to_queue)) },\n                        description = { Text(stringResource(R.string.add_to_queue_desc)) },\n                        icon = {\n                            Icon(\n                                painter = painterResource(R.drawable.queue_music),\n                                contentDescription = null,\n                            )\n                        },\n                        onClick = {\n                            onQueue()\n                            onDismiss()\n                        },\n                    )\n                } else {\n                    null\n                },\n                if (songs.isNotEmpty()) {\n                    Material3MenuItemData(\n                        title = { Text(stringResource(R.string.export_playlist)) },\n                        icon = {\n                            Icon(\n                                painter = painterResource(R.drawable.share),\n                                contentDescription = null,\n                            )\n                        },\n                        onClick = { setShowExportDialog(true) },\n                    )\n                } else {\n                    null\n                },\n                downloadMenuItem,\n            ),\n    )\n\n    if (showExportDialog) {\n        // Convert Song objects to a format that PlaylistExporter can handle\n        val playlistSongs =\n            songs.map { song ->\n                PlaylistSong(\n                    map =\n                        com.metrolist.music.db.entities.PlaylistSongMap(\n                            songId = song.id,\n                            playlistId = \"auto_playlist\",\n                            position = 0,\n                        ),\n                    song = song,\n                )\n            }\n\n        ExportDialog(\n            onDismiss = { setShowExportDialog(false) },\n            onShare = { format ->\n                coroutineScope.launch {\n                    val result =\n                        when (format) {\n                            \"csv\" -> PlaylistExporter.exportPlaylistAsCSV(localContext, playlistName, playlistSongs)\n                            \"m3u\" -> PlaylistExporter.exportPlaylistAsM3U(localContext, playlistName, playlistSongs)\n                            else -> Result.failure(IllegalArgumentException(\"Unknown format\"))\n                        }\n                    result\n                        .onSuccess { file ->\n                            val uri = getExportFileUri(localContext, file)\n                            val mimeType =\n                                when (format) {\n                                    \"csv\" -> \"text/csv\"\n                                    \"m3u\" -> \"audio/x-mpegurl\"\n                                    else -> \"*/*\"\n                                }\n                            shareExportFile(localContext, uri, mimeType)\n                        }.onFailure {\n                            Toast.makeText(localContext, R.string.export_failed, Toast.LENGTH_SHORT).show()\n                        }\n                }\n                onDismiss()\n            },\n            onSave = { format ->\n                coroutineScope.launch {\n                    val exportResult =\n                        when (format) {\n                            \"csv\" -> PlaylistExporter.exportPlaylistAsCSV(localContext, playlistName, playlistSongs)\n                            \"m3u\" -> PlaylistExporter.exportPlaylistAsM3U(localContext, playlistName, playlistSongs)\n                            else -> Result.failure(IllegalArgumentException(\"Unknown format\"))\n                        }\n                    exportResult\n                        .onSuccess { file ->\n                            val mimeType =\n                                when (format) {\n                                    \"csv\" -> \"text/csv\"\n                                    \"m3u\" -> \"audio/x-mpegurl\"\n                                    else -> \"application/octet-stream\"\n                                }\n                            val saveResult = saveToPublicDocuments(localContext, file, mimeType)\n                            saveResult\n                                .onSuccess {\n                                    Toast.makeText(localContext, R.string.export_success, Toast.LENGTH_SHORT).show()\n                                }.onFailure {\n                                    Toast.makeText(localContext, R.string.export_failed, Toast.LENGTH_SHORT).show()\n                                }\n                        }.onFailure {\n                            Toast.makeText(localContext, R.string.export_failed, Toast.LENGTH_SHORT).show()\n                        }\n                }\n                onDismiss()\n            },\n        )\n    }\n}\n\n/**\n * Menu for Top Playlist Screen\n */\n@Composable\nfun TopPlaylistMenu(\n    downloadState: Int,\n    onQueue: () -> Unit,\n    onDownload: () -> Unit,\n    onDismiss: () -> Unit,\n) {\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val isGuest = listenTogetherManager?.isInRoom == true && !listenTogetherManager.isHost\n\n    val downloadMenuItem =\n        when (downloadState) {\n            Download.STATE_COMPLETED -> {\n                Material3MenuItemData(\n                    title = { Text(stringResource(R.string.remove_download)) },\n                    description = { Text(stringResource(R.string.remove_download_playlist_desc)) },\n                    icon = {\n                        Icon(\n                            painter = painterResource(R.drawable.offline),\n                            contentDescription = null,\n                        )\n                    },\n                    onClick = {\n                        onDownload()\n                        onDismiss()\n                    },\n                )\n            }\n\n            Download.STATE_QUEUED, Download.STATE_DOWNLOADING -> {\n                Material3MenuItemData(\n                    title = { Text(stringResource(R.string.downloading)) },\n                    description = { Text(stringResource(R.string.download_in_progress_desc)) },\n                    icon = {\n                        CircularProgressIndicator(\n                            modifier = Modifier.size(24.dp),\n                            strokeWidth = 2.dp,\n                        )\n                    },\n                    onClick = {\n                        onDownload()\n                        onDismiss()\n                    },\n                )\n            }\n\n            else -> {\n                Material3MenuItemData(\n                    title = { Text(stringResource(R.string.action_download)) },\n                    description = { Text(stringResource(R.string.download_playlist_desc)) },\n                    icon = {\n                        Icon(\n                            painter = painterResource(R.drawable.download),\n                            contentDescription = null,\n                        )\n                    },\n                    onClick = {\n                        onDownload()\n                        onDismiss()\n                    },\n                )\n            }\n        }\n\n    Material3MenuGroup(\n        items =\n            listOfNotNull(\n                if (!isGuest) {\n                    Material3MenuItemData(\n                        title = { Text(stringResource(R.string.add_to_queue)) },\n                        description = { Text(stringResource(R.string.add_to_queue_desc)) },\n                        icon = {\n                            Icon(\n                                painter = painterResource(R.drawable.queue_music),\n                                contentDescription = null,\n                            )\n                        },\n                        onClick = {\n                            onQueue()\n                            onDismiss()\n                        },\n                    )\n                } else {\n                    null\n                },\n                downloadMenuItem,\n            ),\n    )\n}\n\nprivate fun shareExportFile(\n    context: Context,\n    uri: Uri,\n    mimeType: String,\n) {\n    val shareIntent =\n        Intent(Intent.ACTION_SEND).apply {\n            type = mimeType\n            putExtra(Intent.EXTRA_STREAM, uri)\n            addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n        }\n    context.startActivity(Intent.createChooser(shareIntent, context.getString(R.string.export_playlist)))\n}\n\n@Composable\nfun ExportDialog(\n    onDismiss: () -> Unit,\n    initialFormat: String = \"csv\",\n    onShare: (format: String) -> Unit,\n    onSave: (format: String) -> Unit,\n) {\n    val (selected, setSelected) = remember { mutableStateOf(initialFormat) }\n\n    DefaultDialog(\n        onDismiss = onDismiss,\n        title = { Text(stringResource(R.string.export_playlist)) },\n        buttons = {\n            TextButton(onClick = onDismiss) {\n                Text(text = stringResource(android.R.string.cancel))\n            }\n            TextButton(onClick = { onSave(selected) }) {\n                Text(text = stringResource(R.string.export_option_save))\n            }\n            TextButton(onClick = { onShare(selected) }) {\n                Text(text = stringResource(R.string.export_option_share))\n            }\n        },\n        horizontalAlignment = Alignment.Start,\n    ) {\n        Column(modifier = Modifier.fillMaxWidth()) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .clickable { setSelected(\"csv\") }\n                        .padding(horizontal = 8.dp, vertical = 8.dp),\n            ) {\n                RadioButton(selected = selected == \"csv\", onClick = null)\n                Column(modifier = Modifier.padding(start = 12.dp)) {\n                    Text(text = stringResource(R.string.export_as_csv))\n                }\n            }\n\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .clickable { setSelected(\"m3u\") }\n                        .padding(horizontal = 8.dp, vertical = 8.dp),\n            ) {\n                RadioButton(selected = selected == \"m3u\", onClick = null)\n                Column(modifier = Modifier.padding(start = 12.dp)) {\n                    Text(text = stringResource(R.string.export_as_m3u))\n                }\n            }\n        }\n    }\n}\n\n/**\n * Menu for Cache Playlist Screen\n */\n@Composable\nfun CachePlaylistMenu(\n    downloadState: Int,\n    onQueue: () -> Unit,\n    onDownload: () -> Unit,\n    onDismiss: () -> Unit,\n) {\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val isGuest = listenTogetherManager?.isInRoom == true && !listenTogetherManager.isHost\n\n    val downloadMenuItem =\n        when (downloadState) {\n            Download.STATE_COMPLETED -> {\n                Material3MenuItemData(\n                    title = { Text(stringResource(R.string.remove_download)) },\n                    description = { Text(stringResource(R.string.remove_download_playlist_desc)) },\n                    icon = {\n                        Icon(\n                            painter = painterResource(R.drawable.offline),\n                            contentDescription = null,\n                        )\n                    },\n                    onClick = {\n                        onDownload()\n                        onDismiss()\n                    },\n                )\n            }\n\n            Download.STATE_QUEUED, Download.STATE_DOWNLOADING -> {\n                Material3MenuItemData(\n                    title = { Text(stringResource(R.string.downloading)) },\n                    description = { Text(stringResource(R.string.download_in_progress_desc)) },\n                    icon = {\n                        CircularProgressIndicator(\n                            modifier = Modifier.size(24.dp),\n                            strokeWidth = 2.dp,\n                        )\n                    },\n                    onClick = {\n                        onDownload()\n                        onDismiss()\n                    },\n                )\n            }\n\n            else -> {\n                Material3MenuItemData(\n                    title = { Text(stringResource(R.string.action_download)) },\n                    description = { Text(stringResource(R.string.download_playlist_desc)) },\n                    icon = {\n                        Icon(\n                            painter = painterResource(R.drawable.download),\n                            contentDescription = null,\n                        )\n                    },\n                    onClick = {\n                        onDownload()\n                        onDismiss()\n                    },\n                )\n            }\n        }\n\n    Material3MenuGroup(\n        items =\n            listOfNotNull(\n                if (!isGuest) {\n                    Material3MenuItemData(\n                        title = { Text(stringResource(R.string.add_to_queue)) },\n                        description = { Text(stringResource(R.string.add_to_queue_desc)) },\n                        icon = {\n                            Icon(\n                                painter = painterResource(R.drawable.queue_music),\n                                contentDescription = null,\n                            )\n                        },\n                        onClick = {\n                            onQueue()\n                            onDismiss()\n                        },\n                    )\n                } else {\n                    null\n                },\n                downloadMenuItem,\n            ),\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/menu/QueueMenu.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.menu\n\nimport android.content.Intent\nimport android.content.res.Configuration\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.core.net.toUri\nimport androidx.media3.exoplayer.offline.Download\nimport androidx.media3.exoplayer.offline.DownloadRequest\nimport androidx.media3.exoplayer.offline.DownloadService\nimport androidx.navigation.NavController\nimport coil3.compose.AsyncImage\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalDownloadUtil\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.LocalSyncUtils\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.ListItemHeight\nimport com.metrolist.music.constants.ListThumbnailSize\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.models.MediaMetadata\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.playback.ExoDownloadService\nimport com.metrolist.music.playback.queues.YouTubeQueue\nimport com.metrolist.music.ui.component.BottomSheetState\nimport com.metrolist.music.ui.component.ListDialog\nimport com.metrolist.music.ui.component.Material3MenuGroup\nimport com.metrolist.music.ui.component.Material3MenuItemData\nimport com.metrolist.music.ui.component.MediaMetadataListItem\nimport com.metrolist.music.ui.component.NewAction\nimport com.metrolist.music.ui.component.NewActionGrid\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.firstOrNull\nimport kotlinx.coroutines.launch\n\n@Composable\nfun QueueMenu(\n    mediaMetadata: MediaMetadata?,\n    navController: NavController,\n    playerBottomSheetState: BottomSheetState,\n    onShowDetailsDialog: () -> Unit,\n    onDismiss: () -> Unit,\n) {\n    mediaMetadata ?: return\n    val context = LocalContext.current\n    val database = LocalDatabase.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val coroutineScope = rememberCoroutineScope()\n    val syncUtils = LocalSyncUtils.current\n\n    val librarySong by database.song(mediaMetadata.id).collectAsState(initial = null)\n    val download by LocalDownloadUtil.current.getDownload(mediaMetadata.id)\n        .collectAsState(initial = null)\n\n    var refetchIconDegree by remember { mutableFloatStateOf(0f) }\n    val rotationAnimation by animateFloatAsState(\n        targetValue = refetchIconDegree,\n        animationSpec = tween(durationMillis = 800),\n        label = \"\",\n    )\n\n    val artists = remember(mediaMetadata.artists) {\n        mediaMetadata.artists.filter { it.id != null }\n    }\n\n    var showChoosePlaylistDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    AddToPlaylistDialog(\n        isVisible = showChoosePlaylistDialog,\n        onGetSong = { playlist ->\n            database.withTransaction {\n                insert(mediaMetadata)\n            }\n            coroutineScope.launch(Dispatchers.IO) {\n                playlist.playlist.browseId?.let { YouTube.addToPlaylist(it, mediaMetadata.id) }\n            }\n            listOf(mediaMetadata.id)\n        },\n        onDismiss = {\n            showChoosePlaylistDialog = false\n        }\n    )\n\n    var showSelectArtistDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    if (showSelectArtistDialog) {\n        ListDialog(\n            onDismiss = { showSelectArtistDialog = false },\n        ) {\n            items(artists) { artist ->\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    modifier = Modifier\n                        .height(ListItemHeight)\n                        .clickable {\n                            navController.navigate(\"artist/${artist.id}\")\n                            showSelectArtistDialog = false\n                            playerBottomSheetState.collapseSoft()\n                            onDismiss()\n                        }\n                        .padding(horizontal = 12.dp),\n                ) {\n                    Box(\n                        modifier = Modifier.padding(8.dp),\n                        contentAlignment = Alignment.Center,\n                    ) {\n                        AsyncImage(\n                            model = null,\n                            contentDescription = null,\n                            modifier = Modifier\n                                .size(ListThumbnailSize)\n                                .clip(CircleShape),\n                        )\n                    }\n                    Text(\n                        text = artist.name,\n                        fontSize = 18.sp,\n                        fontWeight = FontWeight.Bold,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis,\n                        modifier = Modifier\n                            .weight(1f)\n                            .padding(horizontal = 8.dp),\n                    )\n                }\n            }\n        }\n    }\n\n    // Song header with like button (for episodes, this toggles save for later)\n    val isEpisode = librarySong?.song?.isEpisode == true || mediaMetadata.isEpisode\n    val isFavorite = if (isEpisode) librarySong?.song?.inLibrary != null else librarySong?.song?.liked == true\n    MediaMetadataListItem(\n        mediaMetadata = mediaMetadata,\n        trailingContent = {\n            IconButton(\n                onClick = {\n                    coroutineScope.launch(Dispatchers.IO) {\n                        database.transaction {\n                            if (librarySong == null) {\n                                insert(mediaMetadata)\n                            }\n                        }\n                        val dbSong = database.song(mediaMetadata.id).firstOrNull()\n                        dbSong?.let { songWithArtists ->\n                            val songEntity = songWithArtists.song\n                            if (songEntity.isEpisode) {\n                                // Episode: toggle save for later\n                                val isCurrentlySaved = songEntity.inLibrary != null\n                                database.query {\n                                    update(songEntity.copy(inLibrary = if (isCurrentlySaved) null else java.time.LocalDateTime.now()))\n                                }\n                                launch {\n                                    if (isCurrentlySaved) {\n                                        val setVideoIdEntity = database.getSetVideoId(songEntity.id)\n                                        val setVideoId = setVideoIdEntity?.setVideoId\n                                        if (setVideoId != null) {\n                                            YouTube.removeEpisodeFromSavedEpisodes(songEntity.id, setVideoId).onSuccess {\n                                                timber.log.Timber.d(\"[EPISODE_SAVE] Removed episode from Episodes for Later: ${songEntity.id}\")\n                                            }.onFailure { e ->\n                                                timber.log.Timber.e(e, \"[EPISODE_SAVE] Failed to remove episode: ${songEntity.id}\")\n                                                kotlinx.coroutines.withContext(Dispatchers.Main) {\n                                                    android.widget.Toast.makeText(context, R.string.error_episode_remove, android.widget.Toast.LENGTH_SHORT).show()\n                                                }\n                                            }\n                                        }\n                                    } else {\n                                        YouTube.addEpisodeToSavedEpisodes(songEntity.id).onSuccess {\n                                            timber.log.Timber.d(\"[EPISODE_SAVE] Saved episode to Episodes for Later: ${songEntity.id}\")\n                                        }.onFailure { e ->\n                                            timber.log.Timber.e(e, \"[EPISODE_SAVE] Failed to save episode: ${songEntity.id}\")\n                                            kotlinx.coroutines.withContext(Dispatchers.Main) {\n                                                android.widget.Toast.makeText(context, R.string.error_episode_save, android.widget.Toast.LENGTH_SHORT).show()\n                                            }\n                                        }\n                                    }\n                                }\n                            } else {\n                                // Regular song: toggle like\n                                val s = songEntity.toggleLike()\n                                database.query {\n                                    update(s)\n                                }\n                                syncUtils.likeSong(s)\n                            }\n                        }\n                    }\n                },\n            ) {\n                Icon(\n                    painter = painterResource(\n                        if (isFavorite) R.drawable.favorite\n                        else R.drawable.favorite_border\n                    ),\n                    tint = if (isFavorite) MaterialTheme.colorScheme.error\n                    else LocalContentColor.current,\n                    contentDescription = null,\n                )\n            }\n        },\n    )\n\n    HorizontalDivider()\n\n    Spacer(modifier = Modifier.height(12.dp))\n\n    val configuration = LocalConfiguration.current\n    val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT\n\n    LazyColumn(\n        contentPadding = PaddingValues(\n            start = 0.dp,\n            top = 0.dp,\n            end = 0.dp,\n            bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(),\n        ),\n    ) {\n        // Quick actions grid\n        item {\n            NewActionGrid(\n                actions = listOf(\n                    NewAction(\n                        icon = {\n                            Icon(\n                                painter = painterResource(R.drawable.radio),\n                                contentDescription = null,\n                                modifier = Modifier.size(28.dp),\n                                tint = MaterialTheme.colorScheme.onSurfaceVariant\n                            )\n                        },\n                        text = stringResource(R.string.start_radio),\n                        onClick = {\n                            onDismiss()\n                            val currentMediaId = playerConnection.player.currentMediaItemIndex.let {\n                                playerConnection.player.getMediaItemAt(it).mediaId\n                            }\n                            if (mediaMetadata.id == currentMediaId) {\n                                playerConnection.startRadioSeamlessly()\n                            } else {\n                                playerConnection.playQueue(\n                                    YouTubeQueue.radio(mediaMetadata)\n                                )\n                            }\n                        }\n                    ),\n                    NewAction(\n                        icon = {\n                            Icon(\n                                painter = painterResource(R.drawable.playlist_add),\n                                contentDescription = null,\n                                modifier = Modifier.size(28.dp),\n                                tint = MaterialTheme.colorScheme.onSurfaceVariant\n                            )\n                        },\n                        text = stringResource(R.string.add_to_playlist),\n                        onClick = { showChoosePlaylistDialog = true }\n                    ),\n                    NewAction(\n                        icon = {\n                            Icon(\n                                painter = painterResource(R.drawable.share),\n                                contentDescription = null,\n                                modifier = Modifier.size(28.dp),\n                                tint = MaterialTheme.colorScheme.onSurfaceVariant\n                            )\n                        },\n                        text = stringResource(R.string.share),\n                        onClick = {\n                            onDismiss()\n                            val intent = Intent().apply {\n                                action = Intent.ACTION_SEND\n                                type = \"text/plain\"\n                                putExtra(\n                                    Intent.EXTRA_TEXT,\n                                    \"https://music.youtube.com/watch?v=${mediaMetadata.id}\"\n                                )\n                            }\n                            context.startActivity(Intent.createChooser(intent, null))\n                        }\n                    )\n                ),\n                modifier = Modifier.padding(horizontal = 4.dp, vertical = 16.dp)\n            )\n        }\n\n        // Play next / Add to queue\n        item {\n            Material3MenuGroup(\n                items = listOf(\n                    Material3MenuItemData(\n                        title = { Text(text = stringResource(R.string.play_next)) },\n                        description = { Text(text = stringResource(R.string.play_next_desc)) },\n                        icon = {\n                            Icon(\n                                painter = painterResource(R.drawable.playlist_play),\n                                contentDescription = null,\n                            )\n                        },\n                        onClick = {\n                            onDismiss()\n                            librarySong?.let {\n                                playerConnection.playNext(it.toMediaItem())\n                            } ?: run {\n                                playerConnection.playNext(mediaMetadata.toMediaItem())\n                            }\n                        }\n                    ),\n                    Material3MenuItemData(\n                        title = { Text(text = stringResource(R.string.add_to_queue)) },\n                        description = { Text(text = stringResource(R.string.add_to_queue_desc)) },\n                        icon = {\n                            Icon(\n                                painter = painterResource(R.drawable.queue_music),\n                                contentDescription = null,\n                            )\n                        },\n                        onClick = {\n                            onDismiss()\n                            librarySong?.let {\n                                playerConnection.addToQueue(it.toMediaItem())\n                            } ?: run {\n                                playerConnection.addToQueue(mediaMetadata.toMediaItem())\n                            }\n                        }\n                    )\n                )\n            )\n        }\n\n        item { Spacer(modifier = Modifier.height(12.dp)) }\n\n        // Download section\n        item {\n            Material3MenuGroup(\n                items = listOf(\n                    when (download?.state) {\n                        Download.STATE_COMPLETED -> {\n                            Material3MenuItemData(\n                                title = {\n                                    Text(text = stringResource(R.string.remove_download))\n                                },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.offline),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(24.dp)\n                                    )\n                                },\n                                onClick = {\n                                    DownloadService.sendRemoveDownload(\n                                        context,\n                                        ExoDownloadService::class.java,\n                                        mediaMetadata.id,\n                                        false,\n                                    )\n                                }\n                            )\n                        }\n\n                        Download.STATE_QUEUED, Download.STATE_DOWNLOADING -> {\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.downloading)) },\n                                icon = {\n                                    CircularProgressIndicator(\n                                        modifier = Modifier.size(24.dp),\n                                        strokeWidth = 2.dp\n                                    )\n                                },\n                                onClick = {\n                                    DownloadService.sendRemoveDownload(\n                                        context,\n                                        ExoDownloadService::class.java,\n                                        mediaMetadata.id,\n                                        false,\n                                    )\n                                }\n                            )\n                        }\n\n                        else -> {\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.action_download)) },\n                                description = { Text(text = stringResource(R.string.download_desc)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.download),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(24.dp)\n                                    )\n                                },\n                                onClick = {\n                                    database.transaction {\n                                        insert(mediaMetadata)\n                                    }\n                                    val downloadRequest =\n                                        DownloadRequest\n                                            .Builder(mediaMetadata.id, mediaMetadata.id.toUri())\n                                            .setCustomCacheKey(mediaMetadata.id)\n                                            .setData(mediaMetadata.title.toByteArray())\n                                            .build()\n                                    DownloadService.sendAddDownload(\n                                        context,\n                                        ExoDownloadService::class.java,\n                                        downloadRequest,\n                                        false,\n                                    )\n                                }\n                            )\n                        }\n                    }\n                )\n            )\n        }\n\n        item { Spacer(modifier = Modifier.height(12.dp)) }\n\n        // Navigation section (Artist, Album)\n        item {\n            Material3MenuGroup(\n                items = buildList {\n                    if (artists.isNotEmpty()) {\n                        add(\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.view_artist)) },\n                                description = {\n                                    Text(\n                                        text = mediaMetadata.artists.joinToString { it.name },\n                                        maxLines = 1,\n                                        overflow = TextOverflow.Ellipsis\n                                    )\n                                },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.artist),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(24.dp)\n                                    )\n                                },\n                                onClick = {\n                                    if (mediaMetadata.artists.size == 1) {\n                                        navController.navigate(\"artist/${mediaMetadata.artists[0].id}\")\n                                        playerBottomSheetState.collapseSoft()\n                                        onDismiss()\n                                    } else {\n                                        showSelectArtistDialog = true\n                                    }\n                                }\n                            )\n                        )\n                    }\n                    if (mediaMetadata.album != null) {\n                        add(\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.view_album)) },\n                                description = {\n                                    Text(\n                                        text = mediaMetadata.album.title,\n                                        maxLines = 1,\n                                        overflow = TextOverflow.Ellipsis\n                                    )\n                                },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.album),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(24.dp)\n                                    )\n                                },\n                                onClick = {\n                                    navController.navigate(\"album/${mediaMetadata.album.id}\")\n                                    playerBottomSheetState.collapseSoft()\n                                    onDismiss()\n                                }\n                            )\n                        )\n                    }\n                }\n            )\n        }\n\n        item { Spacer(modifier = Modifier.height(12.dp)) }\n\n        // Details and refetch section\n        item {\n            Material3MenuGroup(\n                items = buildList {\n                    add(\n                        Material3MenuItemData(\n                            title = { Text(text = stringResource(R.string.refetch)) },\n                            description = { Text(text = stringResource(R.string.refetch_desc)) },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.sync),\n                                    contentDescription = null,\n                                    modifier = Modifier.graphicsLayer(rotationZ = rotationAnimation),\n                                )\n                            },\n                            onClick = {\n                                refetchIconDegree -= 360\n                                coroutineScope.launch(Dispatchers.IO) {\n                                    YouTube.queue(listOf(mediaMetadata.id)).onSuccess {\n                                        val newSong = it.firstOrNull()\n                                        if (newSong != null && librarySong != null) {\n                                            database.transaction {\n                                                update(librarySong!!, newSong.toMediaMetadata())\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                        )\n                    )\n                    add(\n                        Material3MenuItemData(\n                            title = { Text(text = stringResource(R.string.details)) },\n                            description = { Text(text = stringResource(R.string.details_desc)) },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.info),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(24.dp)\n                                )\n                            },\n                            onClick = {\n                                onShowDetailsDialog()\n                                onDismiss()\n                            }\n                        )\n                    )\n                }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/menu/SelectionSongsMenu.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.menu\n\nimport android.annotation.SuppressLint\nimport android.content.res.Configuration\nimport android.widget.Toast\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LinearProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.core.net.toUri\nimport androidx.media3.common.Player\nimport androidx.media3.common.Timeline\nimport androidx.media3.exoplayer.offline.Download\nimport androidx.media3.exoplayer.offline.DownloadRequest\nimport androidx.media3.exoplayer.offline.DownloadService\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalDownloadUtil\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.LocalSyncUtils\nimport com.metrolist.music.R\nimport com.metrolist.music.db.entities.PlaylistSongMap\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.models.MediaMetadata\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.playback.ExoDownloadService\nimport com.metrolist.music.playback.queues.ListQueue\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.Material3MenuGroup\nimport com.metrolist.music.ui.component.Material3MenuItemData\nimport com.metrolist.music.ui.component.NewAction\nimport com.metrolist.music.ui.component.NewActionGrid\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.withContext\nimport java.time.LocalDateTime\n\n@SuppressLint(\"MutableCollectionMutableState\")\n@Composable\nfun SelectionSongMenu(\n    songSelection: List<Song>,\n    onDismiss: () -> Unit,\n    clearAction: () -> Unit,\n    songPosition: List<PlaylistSongMap>? = emptyList(),\n    isUploadedPlaylist: Boolean = false,\n) {\n    val context = LocalContext.current\n    val database = LocalDatabase.current\n    val downloadUtil = LocalDownloadUtil.current\n    val coroutineScope = rememberCoroutineScope()\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val syncUtils = LocalSyncUtils.current\n    val deletedNSongsTemplate = stringResource(R.string.deleted_n_songs)\n    val listenTogetherManager = com.metrolist.music.LocalListenTogetherManager.current\n    val isGuest = listenTogetherManager?.isInRoom == true && listenTogetherManager.isHost == false\n\n    val allInLibrary by remember {\n        mutableStateOf(\n            songSelection.all {\n                it.song.inLibrary != null\n            },\n        )\n    }\n\n    val allLiked by remember(songSelection) {\n        mutableStateOf(\n            songSelection.isNotEmpty() &&\n                songSelection.all {\n                    it.song.liked\n                },\n        )\n    }\n\n    var downloadState by remember {\n        mutableIntStateOf(Download.STATE_STOPPED)\n    }\n\n    LaunchedEffect(songSelection) {\n        if (songSelection.isEmpty()) return@LaunchedEffect\n        downloadUtil.downloads.collect { downloads ->\n            downloadState =\n                if (songSelection.all { downloads[it.id]?.state == Download.STATE_COMPLETED }) {\n                    Download.STATE_COMPLETED\n                } else if (songSelection.all {\n                        downloads[it.id]?.state == Download.STATE_QUEUED ||\n                            downloads[it.id]?.state == Download.STATE_DOWNLOADING ||\n                            downloads[it.id]?.state == Download.STATE_COMPLETED\n                    }\n                ) {\n                    Download.STATE_DOWNLOADING\n                } else {\n                    Download.STATE_STOPPED\n                }\n        }\n    }\n\n    var showChoosePlaylistDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    val notAddedList by remember {\n        mutableStateOf(mutableListOf<Song>())\n    }\n\n    AddToPlaylistDialog(\n        isVisible = showChoosePlaylistDialog,\n        onGetSong = { playlist ->\n            coroutineScope.launch(Dispatchers.IO) {\n                songSelection.forEach { song ->\n                    playlist.playlist.browseId?.let { browseId ->\n                        YouTube.addToPlaylist(browseId, song.id)\n                    }\n                }\n            }\n            songSelection.map { it.id }\n        },\n        onDismiss = {\n            showChoosePlaylistDialog = false\n        },\n    )\n\n    var showRemoveDownloadDialog by remember {\n        mutableStateOf(false)\n    }\n\n    var showDeleteUploadedDialog by remember {\n        mutableStateOf(false)\n    }\n    var isDeleting by remember { mutableStateOf(false) }\n    var deleteProgress by remember { mutableIntStateOf(0) }\n    var totalToDelete by remember { mutableIntStateOf(0) }\n\n    if (showRemoveDownloadDialog) {\n        DefaultDialog(\n            onDismiss = { showRemoveDownloadDialog = false },\n            content = {\n                Text(\n                    text = stringResource(R.string.remove_download_playlist_confirm, \"selection\"),\n                    style = MaterialTheme.typography.bodyLarge,\n                    modifier = Modifier.padding(horizontal = 18.dp),\n                )\n            },\n            buttons = {\n                TextButton(\n                    onClick = {\n                        showRemoveDownloadDialog = false\n                    },\n                ) {\n                    Text(text = stringResource(android.R.string.cancel))\n                }\n\n                TextButton(\n                    onClick = {\n                        showRemoveDownloadDialog = false\n                        songSelection.forEach { song ->\n                            DownloadService.sendRemoveDownload(\n                                context,\n                                ExoDownloadService::class.java,\n                                song.song.id,\n                                false,\n                            )\n                        }\n                    },\n                ) {\n                    Text(text = stringResource(android.R.string.ok))\n                }\n            },\n        )\n    }\n\n    if (showDeleteUploadedDialog) {\n        DefaultDialog(\n            onDismiss = {\n                if (!isDeleting) {\n                    showDeleteUploadedDialog = false\n                }\n            },\n            icon = {\n                Icon(\n                    painter = painterResource(R.drawable.delete),\n                    contentDescription = null,\n                )\n            },\n            title = {\n                Text(\n                    if (isDeleting) {\n                        stringResource(R.string.deleting)\n                    } else {\n                        stringResource(R.string.delete_uploaded_songs)\n                    },\n                )\n            },\n            buttons = {\n                if (!isDeleting) {\n                    TextButton(\n                        onClick = { showDeleteUploadedDialog = false },\n                    ) {\n                        Text(text = stringResource(android.R.string.cancel))\n                    }\n\n                    TextButton(\n                        onClick = {\n                            totalToDelete = songSelection.size\n                            deleteProgress = 0\n                            isDeleting = true\n                            val songsToDelete = songSelection.toList()\n                            coroutineScope.launch(Dispatchers.IO) {\n                                var successCount = 0\n                                songsToDelete.forEachIndexed { index, song ->\n                                    deleteProgress = index + 1\n                                    val entityId = song.song.uploadEntityId\n                                    if (entityId != null) {\n                                        YouTube.deleteUploadedSong(entityId).onSuccess {\n                                            database.query {\n                                                delete(song.song)\n                                            }\n                                            successCount++\n                                        }\n                                    }\n                                }\n                                withContext(Dispatchers.Main) {\n                                    Toast\n                                        .makeText(\n                                            context,\n                                            String.format(deletedNSongsTemplate, successCount),\n                                            Toast.LENGTH_SHORT,\n                                        ).show()\n                                    isDeleting = false\n                                    showDeleteUploadedDialog = false\n                                    onDismiss()\n                                    clearAction()\n                                }\n                            }\n                        },\n                    ) {\n                        Text(text = stringResource(R.string.delete))\n                    }\n                }\n            },\n        ) {\n            if (isDeleting) {\n                Text(\n                    text = stringResource(R.string.upload_progress, deleteProgress, totalToDelete),\n                    style = MaterialTheme.typography.bodyMedium,\n                )\n                Spacer(modifier = Modifier.height(16.dp))\n                LinearProgressIndicator(\n                    progress = { if (totalToDelete > 0) deleteProgress.toFloat() / totalToDelete else 0f },\n                    modifier = Modifier.fillMaxWidth(),\n                )\n            } else {\n                Text(\n                    text = stringResource(R.string.delete_uploaded_songs_confirm, songSelection.size),\n                    style = MaterialTheme.typography.bodyLarge,\n                )\n            }\n        }\n    }\n\n    val configuration = LocalConfiguration.current\n    val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT\n\n    LazyColumn(\n        contentPadding =\n            PaddingValues(\n                start = 0.dp,\n                top = 0.dp,\n                end = 0.dp,\n                bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(),\n            ),\n    ) {\n        item {\n            NewActionGrid(\n                actions =\n                    listOfNotNull(\n                        if (!isGuest) {\n                            NewAction(\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.play),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(28.dp),\n                                        tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                                    )\n                                },\n                                text = stringResource(R.string.play),\n                                onClick = {\n                                    onDismiss()\n                                    playerConnection.playQueue(\n                                        ListQueue(\n                                            title = \"Selection\",\n                                            items = songSelection.map { it.toMediaItem() },\n                                        ),\n                                    )\n                                    clearAction()\n                                },\n                            )\n                        } else {\n                            null\n                        },\n                        if (!isGuest) {\n                            NewAction(\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.shuffle),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(28.dp),\n                                        tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                                    )\n                                },\n                                text = stringResource(R.string.shuffle),\n                                onClick = {\n                                    onDismiss()\n                                    playerConnection.playQueue(\n                                        ListQueue(\n                                            title = \"Selection\",\n                                            items = songSelection.shuffled().map { it.toMediaItem() },\n                                        ),\n                                    )\n                                    clearAction()\n                                },\n                            )\n                        } else {\n                            null\n                        },\n                        NewAction(\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.playlist_add),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(28.dp),\n                                    tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                                )\n                            },\n                            text = stringResource(R.string.add_to_playlist),\n                            onClick = {\n                                showChoosePlaylistDialog = true\n                            },\n                        ),\n                    ),\n                modifier = Modifier.padding(horizontal = 4.dp, vertical = 16.dp),\n            )\n        }\n        item {\n            Material3MenuGroup(\n                items =\n                    buildList {\n                        if (!isGuest) {\n                            add(\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.play_next)) },\n                                    description = { Text(text = stringResource(R.string.play_next_desc)) },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.playlist_play),\n                                            contentDescription = null,\n                                        )\n                                    },\n                                    onClick = {\n                                        onDismiss()\n                                        playerConnection.playNext(songSelection.map { it.toMediaItem() })\n                                        clearAction()\n                                    },\n                                ),\n                            )\n                            add(\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.shuffle)) },\n                                    description = { Text(text = stringResource(R.string.add_to_queue_desc)) },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.shuffle),\n                                            contentDescription = null,\n                                        )\n                                    },\n                                    onClick = {\n                                        onDismiss()\n                                        playerConnection.playQueue(\n                                            ListQueue(\n                                                title = \"Selection\",\n                                                items = songSelection.shuffled().map { it.toMediaItem() },\n                                            ),\n                                        )\n                                        clearAction()\n                                    },\n                                ),\n                            )\n                            add(\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.add_to_queue)) },\n                                    description = { Text(text = stringResource(R.string.add_to_queue_desc)) },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.queue_music),\n                                            contentDescription = null,\n                                        )\n                                    },\n                                    onClick = {\n                                        onDismiss()\n                                        playerConnection.addToQueue(songSelection.map { it.toMediaItem() })\n                                        clearAction()\n                                    },\n                                ),\n                            )\n                        }\n                        add(\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.add_to_playlist)) },\n                                description = { Text(text = stringResource(R.string.add_to_playlist_desc)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.playlist_add),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    showChoosePlaylistDialog = true\n                                },\n                            ),\n                        )\n                        add(\n                            Material3MenuItemData(\n                                title = {\n                                    Text(\n                                        text =\n                                            stringResource(\n                                                if (allInLibrary) R.string.remove_from_library else R.string.add_to_library,\n                                            ),\n                                    )\n                                },\n                                icon = {\n                                    Icon(\n                                        painter =\n                                            painterResource(\n                                                if (allInLibrary) R.drawable.library_add_check else R.drawable.library_add,\n                                            ),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    if (allInLibrary) {\n                                        database.query {\n                                            songSelection.forEach { song ->\n                                                inLibrary(song.id, null)\n                                            }\n                                        }\n                                        coroutineScope.launch {\n                                            // Use the new reliable method that fetches fresh tokens\n                                            songSelection.forEach { song ->\n                                                YouTube.toggleSongLibrary(song.id, false)\n                                            }\n                                        }\n                                    } else {\n                                        database.transaction {\n                                            songSelection.forEach { song ->\n                                                insert(song.toMediaMetadata())\n                                                inLibrary(song.id, LocalDateTime.now())\n                                            }\n                                        }\n                                        coroutineScope.launch {\n                                            // Use the new reliable method that fetches fresh tokens\n                                            songSelection\n                                                .filter { it.song.inLibrary == null }\n                                                .forEach { song ->\n                                                    YouTube.toggleSongLibrary(song.id, true)\n                                                }\n                                        }\n                                    }\n                                },\n                            ),\n                        )\n                    },\n            )\n        }\n\n        item { Spacer(modifier = Modifier.height(12.dp)) }\n\n        item {\n            Material3MenuGroup(\n                items =\n                    buildList {\n                        add(\n                            when (downloadState) {\n                                Download.STATE_COMPLETED -> {\n                                    Material3MenuItemData(\n                                        title = {\n                                            Text(\n                                                text = stringResource(R.string.remove_download),\n                                            )\n                                        },\n                                        icon = {\n                                            Icon(\n                                                painter = painterResource(R.drawable.offline),\n                                                contentDescription = null,\n                                            )\n                                        },\n                                        onClick = {\n                                            showRemoveDownloadDialog = true\n                                        },\n                                    )\n                                }\n\n                                Download.STATE_QUEUED, Download.STATE_DOWNLOADING -> {\n                                    Material3MenuItemData(\n                                        title = { Text(text = stringResource(R.string.downloading)) },\n                                        icon = {\n                                            CircularProgressIndicator(\n                                                modifier = Modifier.size(24.dp),\n                                                strokeWidth = 2.dp,\n                                            )\n                                        },\n                                        onClick = {\n                                            showRemoveDownloadDialog = true\n                                        },\n                                    )\n                                }\n\n                                else -> {\n                                    Material3MenuItemData(\n                                        title = { Text(text = stringResource(R.string.action_download)) },\n                                        icon = {\n                                            Icon(\n                                                painter = painterResource(R.drawable.download),\n                                                contentDescription = null,\n                                            )\n                                        },\n                                        onClick = {\n                                            songSelection.forEach { song ->\n                                                val downloadRequest =\n                                                    DownloadRequest\n                                                        .Builder(song.id, song.id.toUri())\n                                                        .setCustomCacheKey(song.id)\n                                                        .setData(song.song.title.toByteArray())\n                                                        .build()\n                                                DownloadService.sendAddDownload(\n                                                    context,\n                                                    ExoDownloadService::class.java,\n                                                    downloadRequest,\n                                                    false,\n                                                )\n                                            }\n                                        },\n                                    )\n                                }\n                            },\n                        )\n                        add(\n                            Material3MenuItemData(\n                                title = {\n                                    Text(\n                                        text =\n                                            stringResource(\n                                                if (allLiked) R.string.dislike_all else R.string.like_all,\n                                            ),\n                                    )\n                                },\n                                icon = {\n                                    Icon(\n                                        painter =\n                                            painterResource(\n                                                if (allLiked) R.drawable.favorite else R.drawable.favorite_border,\n                                            ),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    val allLiked = songSelection.all { it.song.liked }\n                                    onDismiss()\n                                    database.query {\n                                        songSelection.forEach { song ->\n                                            if ((!allLiked && !song.song.liked) || allLiked) {\n                                                val s = song.song.toggleLike()\n                                                update(s)\n                                                syncUtils.likeSong(s)\n                                            }\n                                        }\n                                    }\n                                },\n                            ),\n                        )\n                        if (songPosition?.isNotEmpty() == true) {\n                            add(\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.delete)) },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.delete),\n                                            contentDescription = null,\n                                        )\n                                    },\n                                    onClick = {\n                                        onDismiss()\n                                        var i = 0\n                                        database.query {\n                                            songPosition.forEach { cur ->\n                                                move(cur.playlistId, cur.position - i, Int.MAX_VALUE)\n                                                delete(cur.copy(position = Int.MAX_VALUE))\n                                                i++\n                                            }\n                                        }\n                                        clearAction()\n                                    },\n                                ),\n                            )\n                        }\n                        if (isUploadedPlaylist) {\n                            add(\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.delete_uploaded_songs)) },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.delete),\n                                            contentDescription = null,\n                                        )\n                                    },\n                                    onClick = {\n                                        showDeleteUploadedDialog = true\n                                    },\n                                ),\n                            )\n                        }\n                    },\n            )\n        }\n    }\n}\n\n@SuppressLint(\"MutableCollectionMutableState\")\n@Composable\nfun SelectionMediaMetadataMenu(\n    songSelection: List<MediaMetadata>,\n    currentItems: List<Timeline.Window>,\n    onDismiss: () -> Unit,\n    clearAction: () -> Unit,\n) {\n    val context = LocalContext.current\n    val database = LocalDatabase.current\n    val downloadUtil = LocalDownloadUtil.current\n    val coroutineScope = rememberCoroutineScope()\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val listenTogetherManager = com.metrolist.music.LocalListenTogetherManager.current\n    val isGuest = listenTogetherManager?.isInRoom == true && listenTogetherManager.isHost == false\n\n    val allLiked by remember(songSelection) {\n        mutableStateOf(songSelection.isNotEmpty() && songSelection.all { it.liked })\n    }\n\n    var showChoosePlaylistDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    val notAddedList by remember {\n        mutableStateOf(mutableListOf<Song>())\n    }\n\n    AddToPlaylistDialog(\n        isVisible = showChoosePlaylistDialog,\n        onGetSong = {\n            songSelection.map {\n                runBlocking {\n                    withContext(Dispatchers.IO) {\n                        database.insert(it)\n                    }\n                }\n                it.id\n            }\n        },\n        onDismiss = { showChoosePlaylistDialog = false },\n    )\n\n    var downloadState by remember {\n        mutableIntStateOf(Download.STATE_STOPPED)\n    }\n\n    LaunchedEffect(songSelection) {\n        if (songSelection.isEmpty()) return@LaunchedEffect\n        downloadUtil.downloads.collect { downloads ->\n            downloadState =\n                if (songSelection.all { downloads[it.id]?.state == Download.STATE_COMPLETED }) {\n                    Download.STATE_COMPLETED\n                } else if (songSelection.all {\n                        downloads[it.id]?.state == Download.STATE_QUEUED ||\n                            downloads[it.id]?.state == Download.STATE_DOWNLOADING ||\n                            downloads[it.id]?.state == Download.STATE_COMPLETED\n                    }\n                ) {\n                    Download.STATE_DOWNLOADING\n                } else {\n                    Download.STATE_STOPPED\n                }\n        }\n    }\n\n    var showRemoveDownloadDialog by remember {\n        mutableStateOf(false)\n    }\n\n    if (showRemoveDownloadDialog) {\n        DefaultDialog(\n            onDismiss = { showRemoveDownloadDialog = false },\n            content = {\n                Text(\n                    text = stringResource(R.string.remove_download_playlist_confirm, \"selection\"),\n                    style = MaterialTheme.typography.bodyLarge,\n                    modifier = Modifier.padding(horizontal = 18.dp),\n                )\n            },\n            buttons = {\n                TextButton(\n                    onClick = {\n                        showRemoveDownloadDialog = false\n                    },\n                ) {\n                    Text(text = stringResource(android.R.string.cancel))\n                }\n\n                TextButton(\n                    onClick = {\n                        showRemoveDownloadDialog = false\n                        songSelection.forEach { song ->\n                            DownloadService.sendRemoveDownload(\n                                context,\n                                ExoDownloadService::class.java,\n                                song.id,\n                                false,\n                            )\n                        }\n                    },\n                ) {\n                    Text(text = stringResource(android.R.string.ok))\n                }\n            },\n        )\n    }\n\n    LazyColumn(\n        contentPadding =\n            PaddingValues(\n                start = 0.dp,\n                top = 0.dp,\n                end = 0.dp,\n                bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(),\n            ),\n    ) {\n        item {\n            Material3MenuGroup(\n                items =\n                    buildList {\n                        if (currentItems.isNotEmpty() && !isGuest) {\n                            add(\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.delete)) },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.delete),\n                                            contentDescription = null,\n                                        )\n                                    },\n                                    onClick = {\n                                        onDismiss()\n                                        var i = 0\n                                        currentItems.forEach { cur ->\n                                            if (playerConnection.player.availableCommands.contains(\n                                                    Player.COMMAND_CHANGE_MEDIA_ITEMS,\n                                                )\n                                            ) {\n                                                playerConnection.player.removeMediaItem(cur.firstPeriodIndex - i++)\n                                            }\n                                        }\n                                        clearAction()\n                                    },\n                                ),\n                            )\n                        }\n                        if (!isGuest) {\n                            add(\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.play)) },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.play),\n                                            contentDescription = null,\n                                        )\n                                    },\n                                    onClick = {\n                                        onDismiss()\n                                        playerConnection.playQueue(\n                                            ListQueue(\n                                                title = \"Selection\",\n                                                items = songSelection.map { it.toMediaItem() },\n                                            ),\n                                        )\n                                        clearAction()\n                                    },\n                                ),\n                            )\n                            add(\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.shuffle)) },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.shuffle),\n                                            contentDescription = null,\n                                        )\n                                    },\n                                    onClick = {\n                                        onDismiss()\n                                        playerConnection.playQueue(\n                                            ListQueue(\n                                                title = \"Selection\",\n                                                items = songSelection.shuffled().map { it.toMediaItem() },\n                                            ),\n                                        )\n                                        clearAction()\n                                    },\n                                ),\n                            )\n                            if (!isGuest) {\n                                add(\n                                    Material3MenuItemData(\n                                        title = { Text(text = stringResource(R.string.add_to_queue)) },\n                                        icon = {\n                                            Icon(\n                                                painter = painterResource(R.drawable.queue_music),\n                                                contentDescription = null,\n                                            )\n                                        },\n                                        onClick = {\n                                            onDismiss()\n                                            playerConnection.addToQueue(songSelection.map { it.toMediaItem() })\n                                            clearAction()\n                                        },\n                                    ),\n                                )\n                            }\n                        }\n                        add(\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.add_to_playlist)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.playlist_add),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    showChoosePlaylistDialog = true\n                                },\n                            ),\n                        )\n                    },\n            )\n        }\n\n        item { Spacer(modifier = Modifier.height(12.dp)) }\n\n        item {\n            Material3MenuGroup(\n                items =\n                    buildList {\n                        add(\n                            Material3MenuItemData(\n                                title = {\n                                    Text(\n                                        text = stringResource(R.string.like_all),\n                                    )\n                                },\n                                icon = {\n                                    Icon(\n                                        painter =\n                                            painterResource(\n                                                if (allLiked) R.drawable.favorite else R.drawable.favorite_border,\n                                            ),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    database.query {\n                                        if (allLiked) {\n                                            songSelection.forEach { song ->\n                                                update(song.toSongEntity().toggleLike())\n                                            }\n                                        } else {\n                                            songSelection.filter { !it.liked }.forEach { song ->\n                                                update(song.toSongEntity().toggleLike())\n                                            }\n                                        }\n                                    }\n                                },\n                            ),\n                        )\n                        add(\n                            when (downloadState) {\n                                Download.STATE_COMPLETED -> {\n                                    Material3MenuItemData(\n                                        title = {\n                                            Text(\n                                                text = stringResource(R.string.remove_download),\n                                                color = MaterialTheme.colorScheme.surface,\n                                            )\n                                        },\n                                        icon = {\n                                            Icon(\n                                                painter = painterResource(R.drawable.offline),\n                                                contentDescription = null,\n                                                tint = MaterialTheme.colorScheme.surface,\n                                            )\n                                        },\n                                        onClick = {\n                                            showRemoveDownloadDialog = true\n                                        },\n                                        cardColors =\n                                            CardDefaults.cardColors(\n                                                containerColor = MaterialTheme.colorScheme.onSurface,\n                                            ),\n                                    )\n                                }\n\n                                Download.STATE_QUEUED, Download.STATE_DOWNLOADING -> {\n                                    Material3MenuItemData(\n                                        title = { Text(text = stringResource(R.string.downloading)) },\n                                        icon = {\n                                            CircularProgressIndicator(\n                                                modifier = Modifier.size(24.dp),\n                                                strokeWidth = 2.dp,\n                                            )\n                                        },\n                                        onClick = {\n                                            showRemoveDownloadDialog = true\n                                        },\n                                    )\n                                }\n\n                                else -> {\n                                    Material3MenuItemData(\n                                        title = { Text(text = stringResource(R.string.action_download)) },\n                                        icon = {\n                                            Icon(\n                                                painter = painterResource(R.drawable.download),\n                                                contentDescription = null,\n                                            )\n                                        },\n                                        onClick = {\n                                            songSelection.forEach { song ->\n                                                val downloadRequest =\n                                                    DownloadRequest\n                                                        .Builder(song.id, song.id.toUri())\n                                                        .setCustomCacheKey(song.id)\n                                                        .setData(song.title.toByteArray())\n                                                        .build()\n                                                DownloadService.sendAddDownload(\n                                                    context,\n                                                    ExoDownloadService::class.java,\n                                                    downloadRequest,\n                                                    false,\n                                                )\n                                            }\n                                        },\n                                    )\n                                }\n                            },\n                        )\n                    },\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/menu/SongMenu.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.menu\n\nimport android.content.Intent\nimport android.content.res.Configuration\nimport java.time.LocalDateTime\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.ListItem\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.produceState\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.Saver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.ColorFilter\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.TextRange\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.core.net.toUri\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.media3.exoplayer.offline.Download\nimport androidx.media3.exoplayer.offline.DownloadRequest\nimport androidx.media3.exoplayer.offline.DownloadService\nimport android.widget.Toast\nimport androidx.navigation.NavController\nimport coil3.compose.AsyncImage\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalDownloadUtil\nimport com.metrolist.music.LocalListenTogetherManager\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.LocalSyncUtils\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.ListItemHeight\nimport com.metrolist.music.constants.ListThumbnailSize\nimport com.metrolist.music.db.entities.ArtistEntity\nimport com.metrolist.music.db.entities.Event\nimport com.metrolist.music.db.entities.PodcastEntity\nimport com.metrolist.music.db.entities.SpeedDialItem\nimport com.metrolist.music.db.entities.PlaylistSong\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.playback.ExoDownloadService\nimport com.metrolist.music.playback.queues.YouTubeQueue\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.ListDialog\nimport com.metrolist.music.ui.component.LocalBottomSheetPageState\nimport com.metrolist.music.ui.component.Material3MenuGroup\nimport com.metrolist.music.ui.component.Material3MenuItemData\nimport com.metrolist.music.ui.component.NewAction\nimport com.metrolist.music.ui.component.NewActionGrid\nimport com.metrolist.music.ui.component.SongListItem\nimport com.metrolist.music.ui.component.TextFieldDialog\nimport com.metrolist.music.ui.utils.ShowMediaInfo\nimport com.metrolist.music.viewmodels.CachePlaylistViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport timber.log.Timber\n\n@Composable\nfun SongMenu(\n    originalSong: Song,\n    event: Event? = null,\n    navController: NavController,\n    playlistSong: PlaylistSong? = null,\n    playlistBrowseId: String? = null,\n    onDismiss: () -> Unit,\n    isFromCache: Boolean = false,\n) {\n    val context = LocalContext.current\n    val database = LocalDatabase.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val songState = database.song(originalSong.id).collectAsState(initial = originalSong)\n    val song = songState.value ?: originalSong\n    val download by LocalDownloadUtil.current.getDownload(originalSong.id)\n        .collectAsState(initial = null)\n    val coroutineScope = rememberCoroutineScope()\n    val syncUtils = LocalSyncUtils.current\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val scope = rememberCoroutineScope()\n    var refetchIconDegree by remember { mutableFloatStateOf(0f) }\n\n    val cacheViewModel = hiltViewModel<CachePlaylistViewModel>()\n\n    val rotationAnimation by animateFloatAsState(\n        targetValue = refetchIconDegree,\n        animationSpec = tween(durationMillis = 800),\n        label = \"\",\n    )\n\n    val isPinned by database.speedDialDao.isPinned(song.id).collectAsState(initial = false)\n\n    // Podcast subscription state for episodes\n    val podcastEntity by produceState<PodcastEntity?>(initialValue = null, song) {\n        val podcastId = song.song.albumId\n        if (song.song.isEpisode && podcastId != null) {\n            database.podcast(podcastId).collect { value = it }\n        }\n    }\n    val isPodcastSubscribed = podcastEntity?.bookmarkedAt != null\n\n    val orderedArtists by produceState(initialValue = emptyList<ArtistEntity>(), song) {\n        withContext(Dispatchers.IO) {\n            val artistMaps = database.songArtistMap(song.id).sortedBy { it.position }\n            val sorted = artistMaps.mapNotNull { map ->\n                song.artists.firstOrNull { it.id == map.artistId }\n            }\n            value = sorted\n        }\n    }\n\n    var showEditDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    val TextFieldValueSaver: Saver<TextFieldValue, *> = Saver(\n        save = { it.text },\n        restore = { text -> TextFieldValue(text, TextRange(text.length)) }\n    )\n\n    var titleField by rememberSaveable(stateSaver = TextFieldValueSaver) {\n        mutableStateOf(TextFieldValue(song.song.title))\n    }\n\n    var artistField by rememberSaveable(stateSaver = TextFieldValueSaver) {\n        mutableStateOf(TextFieldValue(song.artists.firstOrNull()?.name.orEmpty()))\n    }\n\n    if (showEditDialog) {\n        TextFieldDialog(\n            icon = {\n                Icon(\n                    painter = painterResource(R.drawable.edit),\n                    contentDescription = null\n                )\n            },\n            title = {\n                Text(text = stringResource(R.string.edit_song))\n            },\n            textFields = listOf(\n                stringResource(R.string.song_title) to titleField,\n                stringResource(R.string.artist_name) to artistField\n            ),\n            onTextFieldsChange = { index, newValue ->\n                if (index == 0) titleField = newValue\n                else artistField = newValue\n            },\n            onDoneMultiple = { values ->\n                val newTitle = values[0]\n                val newArtist = values[1]\n\n                coroutineScope.launch {\n                    database.query {\n                        update(song.song.copy(title = newTitle))\n                        val artist = song.artists.firstOrNull()\n                        if (artist != null) {\n                            update(artist.copy(name = newArtist))\n                        }\n                    }\n\n                    showEditDialog = false\n                    onDismiss()\n                }\n            },\n            onDismiss = { showEditDialog = false }\n        )\n    }\n\n    var showChoosePlaylistDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    var showErrorPlaylistAddDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    AddToPlaylistDialog(\n        isVisible = showChoosePlaylistDialog,\n        onGetSong = { playlist ->\n            coroutineScope.launch(Dispatchers.IO) {\n                playlist.playlist.browseId?.let { browseId ->\n                    YouTube.addToPlaylist(browseId, song.id)\n                }\n            }\n            listOf(song.id)\n        },\n        onDismiss = {\n            showChoosePlaylistDialog = false\n        },\n    )\n\n    if (showErrorPlaylistAddDialog) {\n        ListDialog(\n            onDismiss = {\n                showErrorPlaylistAddDialog = false\n                onDismiss()\n            },\n        ) {\n            item {\n                ListItem(\n                    headlineContent = { Text(text = stringResource(R.string.already_in_playlist)) },\n                    leadingContent = {\n                        Image(\n                            painter = painterResource(R.drawable.close),\n                            contentDescription = null,\n                            colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),\n                            modifier = Modifier.size(ListThumbnailSize),\n                        )\n                    },\n                    modifier = Modifier.clickable { showErrorPlaylistAddDialog = false },\n                )\n            }\n\n            items(listOf(song)) { song ->\n                SongListItem(song = song)\n            }\n        }\n    }\n\n    var showSelectArtistDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    var showDeleteUploadedDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n    var isDeleting by remember { mutableStateOf(false) }\n\n    if (showDeleteUploadedDialog) {\n        DefaultDialog(\n            onDismiss = { if (!isDeleting) showDeleteUploadedDialog = false },\n            icon = {\n                Icon(\n                    painter = painterResource(R.drawable.delete),\n                    contentDescription = null,\n                    tint = MaterialTheme.colorScheme.error,\n                )\n            },\n            title = { Text(stringResource(R.string.delete_uploaded_song)) },\n            buttons = {\n                TextButton(\n                    onClick = { showDeleteUploadedDialog = false },\n                    enabled = !isDeleting\n                ) {\n                    Text(stringResource(R.string.cancel))\n                }\n                TextButton(\n                    onClick = {\n                        val entityId = song.song.uploadEntityId\n                        if (entityId == null) {\n                            Toast.makeText(\n                                context,\n                                R.string.delete_uploaded_song_failed,\n                                Toast.LENGTH_SHORT\n                            ).show()\n                            showDeleteUploadedDialog = false\n                            return@TextButton\n                        }\n                        isDeleting = true\n                        coroutineScope.launch(Dispatchers.IO) {\n                            YouTube.deleteUploadedSong(entityId).onSuccess {\n                                database.query {\n                                    delete(song.song)\n                                }\n                                withContext(Dispatchers.Main) {\n                                    Toast.makeText(\n                                        context,\n                                        R.string.delete_uploaded_song_success,\n                                        Toast.LENGTH_SHORT\n                                    ).show()\n                                    isDeleting = false\n                                    showDeleteUploadedDialog = false\n                                    onDismiss()\n                                }\n                            }.onFailure {\n                                withContext(Dispatchers.Main) {\n                                    Toast.makeText(\n                                        context,\n                                        R.string.delete_uploaded_song_failed,\n                                        Toast.LENGTH_SHORT\n                                    ).show()\n                                    isDeleting = false\n                                    showDeleteUploadedDialog = false\n                                }\n                            }\n                        }\n                    },\n                    enabled = !isDeleting\n                ) {\n                    if (isDeleting) {\n                        CircularProgressIndicator(\n                            modifier = Modifier.size(16.dp),\n                            strokeWidth = 2.dp\n                        )\n                    } else {\n                        Text(\n                            text = stringResource(R.string.delete),\n                            color = MaterialTheme.colorScheme.error\n                        )\n                    }\n                }\n            }\n        ) {\n            Text(\n                text = stringResource(R.string.delete_uploaded_song_confirm),\n                style = MaterialTheme.typography.bodyMedium\n            )\n        }\n    }\n\n    if (showSelectArtistDialog) {\n        ListDialog(\n            onDismiss = { showSelectArtistDialog = false },\n        ) {\n            items(\n                items = song.artists.distinctBy { it.id },\n                key = { it.id },\n            ) { artist ->\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    modifier = Modifier\n                        .height(ListItemHeight)\n                        .clickable {\n                            navController.navigate(\"artist/${artist.id}\")\n                            showSelectArtistDialog = false\n                            onDismiss()\n                        }\n                        .padding(horizontal = 12.dp),\n                ) {\n                    Box(\n                        modifier = Modifier.padding(8.dp),\n                        contentAlignment = Alignment.Center,\n                    ) {\n                        AsyncImage(\n                            model = artist.thumbnailUrl,\n                            contentDescription = null,\n                            modifier = Modifier\n                                .size(ListThumbnailSize)\n                                .clip(CircleShape),\n                        )\n                    }\n                    Text(\n                        text = artist.name,\n                        fontSize = 18.sp,\n                        fontWeight = FontWeight.Bold,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis,\n                        modifier = Modifier\n                            .weight(1f)\n                            .padding(horizontal = 8.dp),\n                    )\n                }\n            }\n        }\n    }\n\n    SongListItem(\n        song = song,\n        badges = {},\n        trailingContent = {\n            // For episodes, show saved state and toggle save for later\n            val isEpisode = song.song.isEpisode\n            val isFavorite = if (isEpisode) song.song.inLibrary != null else song.song.liked\n            IconButton(\n                onClick = {\n                    if (isEpisode) {\n                        // Episode: toggle save for later (same pattern as songs)\n                        val isCurrentlySaved = song.song.inLibrary != null\n                        database.query {\n                            update(song.song.copy(\n                                inLibrary = if (isCurrentlySaved) null else LocalDateTime.now(),\n                                isEpisode = true\n                            ))\n                        }\n                        coroutineScope.launch(Dispatchers.IO) {\n                            if (isCurrentlySaved) {\n                                val setVideoIdEntity = database.getSetVideoId(song.id)\n                                val setVideoId = setVideoIdEntity?.setVideoId\n                                if (setVideoId != null) {\n                                    YouTube.removeEpisodeFromSavedEpisodes(song.id, setVideoId).onSuccess {\n                                        Timber.d(\"[EPISODE_SAVE] Removed episode from Episodes for Later: ${song.id}\")\n                                    }.onFailure { e ->\n                                        Timber.e(e, \"[EPISODE_SAVE] Failed to remove episode: ${song.id}\")\n                                        withContext(Dispatchers.Main) {\n                                            Toast.makeText(context, R.string.error_episode_remove, Toast.LENGTH_SHORT).show()\n                                        }\n                                    }\n                                }\n                            } else {\n                                YouTube.addEpisodeToSavedEpisodes(song.id).onSuccess {\n                                    Timber.d(\"[EPISODE_SAVE] Saved episode to Episodes for Later: ${song.id}\")\n                                }.onFailure { e ->\n                                    Timber.e(e, \"[EPISODE_SAVE] Failed to save episode: ${song.id}\")\n                                    withContext(Dispatchers.Main) {\n                                        Toast.makeText(context, R.string.error_episode_save, Toast.LENGTH_SHORT).show()\n                                    }\n                                }\n                            }\n                        }\n                    } else {\n                        // Regular song: toggle like\n                        val s = song.song.toggleLike()\n                        database.query {\n                            update(s)\n                        }\n                        syncUtils.likeSong(s)\n                    }\n                },\n            ) {\n                Icon(\n                    painter = painterResource(if (isFavorite) R.drawable.favorite else R.drawable.favorite_border),\n                    tint = if (isFavorite) MaterialTheme.colorScheme.error else LocalContentColor.current,\n                    contentDescription = null,\n                )\n            }\n        },\n    )\n\n    HorizontalDivider()\n\n    Spacer(modifier = Modifier.height(12.dp))\n\n    val bottomSheetPageState = LocalBottomSheetPageState.current\n    val configuration = LocalConfiguration.current\n    val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT\n\n    val isGuest = listenTogetherManager?.isInRoom == true && !listenTogetherManager.isHost\n\n    LazyColumn(\n        contentPadding = PaddingValues(\n            start = 0.dp,\n            top = 0.dp,\n            end = 0.dp,\n            bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(),\n        ),\n    ) {\n        item {\n            NewActionGrid(\n                actions = listOf(\n                    NewAction(\n                        icon = {\n                            Icon(\n                                painter = painterResource(R.drawable.edit),\n                                contentDescription = null,\n                                modifier = Modifier.size(28.dp),\n                                tint = MaterialTheme.colorScheme.onSurfaceVariant\n                            )\n                        },\n                        text = stringResource(R.string.edit),\n                        onClick = { showEditDialog = true }\n                    ),\n                    NewAction(\n                        icon = {\n                            Icon(\n                                painter = painterResource(R.drawable.playlist_add),\n                                contentDescription = null,\n                                modifier = Modifier.size(28.dp),\n                                tint = MaterialTheme.colorScheme.onSurfaceVariant\n                            )\n                        },\n                        text = stringResource(R.string.add_to_playlist),\n                        onClick = { showChoosePlaylistDialog = true }\n                    ),\n                    NewAction(\n                        icon = {\n                            Icon(\n                                painter = painterResource(R.drawable.share),\n                                contentDescription = null,\n                                modifier = Modifier.size(28.dp),\n                                tint = MaterialTheme.colorScheme.onSurfaceVariant\n                            )\n                        },\n                        text = stringResource(R.string.share),\n                        onClick = {\n                            onDismiss()\n                            val intent = Intent().apply {\n                                action = Intent.ACTION_SEND\n                                type = \"text/plain\"\n                                putExtra(Intent.EXTRA_TEXT, \"https://music.youtube.com/watch?v=${song.id}\")\n                            }\n                            context.startActivity(Intent.createChooser(intent, null))\n                        }\n                    )\n                ),\n                modifier = Modifier.padding(horizontal = 4.dp, vertical = 16.dp)\n            )\n        }\n        item {\n            Material3MenuGroup(\n                items = listOfNotNull(\n                    if (listenTogetherManager != null && listenTogetherManager.isInRoom && !listenTogetherManager.isHost) {\n                        Material3MenuItemData(\n                            title = { Text(text = stringResource(R.string.suggest_to_host)) },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.queue_music),\n                                    contentDescription = null,\n                                )\n                            },\n                            onClick = {\n                                val durationMs = if (song.song.duration > 0) song.song.duration.toLong() * 1000 else 180000L\n                                val trackInfo = com.metrolist.music.listentogether.TrackInfo(\n                                    id = song.id,\n                                    title = song.song.title,\n                                    artist = orderedArtists.joinToString(\", \") { it.name },\n                                    album = song.song.albumName,\n                                    duration = durationMs,\n                                    thumbnail = song.thumbnailUrl\n                                )\n                                listenTogetherManager.suggestTrack(trackInfo)\n                                onDismiss()\n                            }\n                        )\n                    } else null,\n                    if (!isGuest) {\n                        Material3MenuItemData(\n                            title = { Text(text = stringResource(R.string.start_radio)) },\n                            description = { Text(text = stringResource(R.string.start_radio_desc)) },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.radio),\n                                    contentDescription = null,\n                                )\n                            },\n                            onClick = {\n                                onDismiss()\n                                playerConnection.playQueue(YouTubeQueue.radio(song.toMediaMetadata()))\n                            }\n                        )\n                    } else null,\n                    if (!isGuest) {\n                        Material3MenuItemData(\n                            title = { Text(text = stringResource(R.string.play_next)) },\n                            description = { Text(text = stringResource(R.string.play_next_desc)) },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.playlist_play),\n                                    contentDescription = null,\n                                )\n                            },\n                            onClick = {\n                                onDismiss()\n                                playerConnection.playNext(song.toMediaItem())\n                            }\n                        )\n                    } else null,\n                    if (!isGuest) {\n                        Material3MenuItemData(\n                            title = { Text(text = stringResource(R.string.add_to_queue)) },\n                            description = { Text(text = stringResource(R.string.add_to_queue_desc)) },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.queue_music),\n                                    contentDescription = null,\n                                )\n                            },\n                            onClick = {\n                                onDismiss()\n                                playerConnection.addToQueue(song.toMediaItem())\n                            }\n                        )\n                    } else null\n                )\n            )\n        }\n\n        item { Spacer(modifier = Modifier.height(12.dp)) }\n\n        item {\n            Material3MenuGroup(\n                items = buildList {\n                    add(\n                        Material3MenuItemData(\n                            title = { \n                                Text(\n                                    text = if (isPinned) \"Unpin from Speed dial\" else \"Pin to Speed dial\" \n                                ) \n                            },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(if (isPinned) R.drawable.remove else R.drawable.add),\n                                    contentDescription = null,\n                                )\n                            },\n                            onClick = {\n                                coroutineScope.launch(Dispatchers.IO) {\n                                    if (isPinned) {\n                                        database.speedDialDao.delete(song.id)\n                                    } else {\n                                        database.speedDialDao.insert(\n                                            SpeedDialItem(\n                                                id = song.id,\n                                                title = song.song.title,\n                                                subtitle = song.artists.joinToString(\", \") { it.name },\n                                                thumbnailUrl = song.song.thumbnailUrl,\n                                                type = \"SONG\",\n                                                explicit = song.song.explicit\n                                            )\n                                        )\n                                    }\n                                }\n                                onDismiss()\n                            }\n                        )\n                    )\n                    // For episodes, use \"Save for later\" / \"Remove from saved\" (Episodes for Later playlist)\n                    // For regular songs, use \"Add to library\"\n                    if (song.song.isEpisode) {\n                        val isEpisodeSaved = song.song.inLibrary != null\n                        add(\n                            Material3MenuItemData(\n                                title = {\n                                    Text(text = stringResource(\n                                        if (isEpisodeSaved) R.string.remove_episode_from_saved\n                                        else R.string.save_episode_for_later\n                                    ))\n                                },\n                                description = { Text(text = stringResource(R.string.episodes_for_later)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(\n                                            if (isEpisodeSaved) R.drawable.library_add_check\n                                            else R.drawable.library_add\n                                        ),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    coroutineScope.launch(Dispatchers.IO) {\n                                        val shouldBeSaved = !isEpisodeSaved\n\n                                        // Update local database first (optimistic update)\n                                        database.query {\n                                            update(song.song.copy(\n                                                inLibrary = if (shouldBeSaved) LocalDateTime.now() else null,\n                                                isEpisode = true\n                                            ))\n                                        }\n\n                                        // Sync with YouTube (handles login check internally)\n                                        val setVideoId = if (isEpisodeSaved) database.getSetVideoId(song.id)?.setVideoId else null\n                                        syncUtils.saveEpisode(song.id, shouldBeSaved, setVideoId)\n                                    }\n                                    onDismiss()\n                                }\n                            )\n                        )\n                    } else {\n                        add(\n                            Material3MenuItemData(\n                                title = {\n                                    Text(\n                                        text = stringResource(\n                                            if (song.song.inLibrary == null) R.string.add_to_library\n                                            else R.string.remove_from_library\n                                        )\n                                    )\n                                },\n                                description = { Text(text = stringResource(R.string.add_to_library_desc)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(\n                                            if (song.song.inLibrary == null) R.drawable.library_add\n                                            else R.drawable.library_add_check\n                                        ),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    val currentSong = song.song\n                                    val isInLibrary = currentSong.inLibrary != null\n                                    val token =\n                                        if (isInLibrary) currentSong.libraryRemoveToken else currentSong.libraryAddToken\n\n                                    token?.let {\n                                        coroutineScope.launch {\n                                            YouTube.feedback(listOf(it))\n                                        }\n                                    }\n\n                                    database.query {\n                                        update(song.song.toggleLibrary())\n                                    }\n                                }\n                            )\n                        )\n                    }\n                    if (event != null) {\n                        add(\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.remove_from_history)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.delete),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    onDismiss()\n                                    database.query {\n                                        delete(event)\n                                    }\n                                }\n                            )\n                        )\n                    }\n                    if (playlistSong != null) {\n                        add(\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.remove_from_playlist)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.delete),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    database.transaction {\n                                        coroutineScope.launch {\n                                            playlistBrowseId?.let { playlistId ->\n                                                if (playlistSong.map.setVideoId != null) {\n                                                    YouTube.removeFromPlaylist(\n                                                        playlistId,\n                                                        playlistSong.map.songId,\n                                                        playlistSong.map.setVideoId\n                                                    )\n                                                }\n                                            }\n                                        }\n                                        move(\n                                            playlistSong.map.playlistId,\n                                            playlistSong.map.position,\n                                            Int.MAX_VALUE\n                                        )\n                                        delete(playlistSong.map.copy(position = Int.MAX_VALUE))\n                                    }\n                                    onDismiss()\n                                }\n                            )\n                        )\n                    }\n                    if (isFromCache) {\n                        add(\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.remove_from_cache)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.delete),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    onDismiss()\n                                    cacheViewModel.removeSongFromCache(song.id)\n                                }\n                            )\n                        )\n                    }\n                    // Delete uploaded song option\n                    if (song.song.isUploaded) {\n                        add(\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.delete_uploaded_song)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.delete),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    showDeleteUploadedDialog = true\n                                }\n                            )\n                        )\n                    }\n                }\n            )\n        }\n\n        item { Spacer(modifier = Modifier.height(12.dp)) }\n\n        item {\n            Material3MenuGroup(\n                items = listOf(\n                    when (download?.state) {\n                        Download.STATE_COMPLETED -> {\n                            Material3MenuItemData(\n                                title = {\n                                    Text(\n                                        text = stringResource(R.string.remove_download)\n                                    )\n                                },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.offline),\n                                        contentDescription = null\n                                    )\n                                },\n                                onClick = {\n                                    DownloadService.sendRemoveDownload(\n                                        context,\n                                        ExoDownloadService::class.java,\n                                        song.id,\n                                        false,\n                                    )\n                                }\n                            )\n                        }\n                        Download.STATE_QUEUED, Download.STATE_DOWNLOADING -> {\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.downloading)) },\n                                icon = {\n                                    CircularProgressIndicator(\n                                        modifier = Modifier.size(24.dp),\n                                        strokeWidth = 2.dp\n                                    )\n                                },\n                                onClick = {\n                                    DownloadService.sendRemoveDownload(\n                                        context,\n                                        ExoDownloadService::class.java,\n                                        song.id,\n                                        false,\n                                    )\n                                }\n                            )\n                        }\n                        else -> {\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.action_download)) },\n                                description = { Text(text = stringResource(R.string.download_desc)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.download),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    val downloadRequest =\n                                        DownloadRequest\n                                            .Builder(song.id, song.id.toUri())\n                                            .setCustomCacheKey(song.id)\n                                            .setData(song.song.title.toByteArray())\n                                            .build()\n                                    DownloadService.sendAddDownload(\n                                        context,\n                                        ExoDownloadService::class.java,\n                                        downloadRequest,\n                                        false,\n                                    )\n                                }\n                            )\n                        }\n                    }\n                )\n            )\n        }\n\n        item { Spacer(modifier = Modifier.height(12.dp)) }\n\n        item {\n            Material3MenuGroup(\n                items = buildList {\n                    // Don't show \"View Artist\" for podcast episodes\n                    if (!song.song.isEpisode) {\n                        add(\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.view_artist)) },\n                                description = { Text(text = song.artists.joinToString { it.name }) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.artist),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    if (song.artists.size == 1) {\n                                        navController.navigate(\"artist/${song.artists[0].id}\")\n                                        onDismiss()\n                                    } else {\n                                        showSelectArtistDialog = true\n                                    }\n                                }\n                            )\n                        )\n                    }\n                    if (song.song.albumId != null) {\n                        // Show \"View Podcast\" for episodes, \"View Album\" for songs\n                        val isPodcast = song.song.isEpisode\n                        add(\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(if (isPodcast) R.string.view_podcast else R.string.view_album)) },\n                                description = {\n                                    song.song.albumName?.let {\n                                        Text(text = it)\n                                    }\n                                },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(if (isPodcast) R.drawable.mic else R.drawable.album),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    onDismiss()\n                                    if (isPodcast) {\n                                        navController.navigate(\"online_podcast/${song.song.albumId}\")\n                                    } else {\n                                        navController.navigate(\"album/${song.song.albumId}\")\n                                    }\n                                }\n                            )\n                        )\n                    }\n                    // Subscribe to podcast option for episodes\n                    song.song.albumId?.takeIf { song.song.isEpisode }?.let { podcastId ->\n                        add(\n                            Material3MenuItemData(\n                                title = {\n                                    Text(\n                                        text = stringResource(\n                                            if (isPodcastSubscribed) R.string.subscribed\n                                            else R.string.subscribe_to_podcast\n                                        )\n                                    )\n                                },\n                                description = {\n                                    song.song.albumName?.let {\n                                        Text(text = it)\n                                    }\n                                },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(\n                                            if (isPodcastSubscribed) R.drawable.library_add_check\n                                            else R.drawable.library_add\n                                        ),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    Timber.d(\"[PODCAST_LIB] Toggling podcast save for: $podcastId\")\n                                    coroutineScope.launch(Dispatchers.IO) {\n                                        val existingPodcast = podcastEntity\n                                        val isCurrentlySaved = existingPodcast?.bookmarkedAt != null\n\n                                        // Call the API to save/unsave on YTM\n                                        YouTube.savePodcast(podcastId, !isCurrentlySaved).onSuccess {\n                                            Timber.d(\"[PODCAST_LIB] savePodcast API success!\")\n                                        }.onFailure { e ->\n                                            Timber.e(e, \"[PODCAST_LIB] savePodcast API failed\")\n                                        }\n\n                                        // Update local database\n                                        if (existingPodcast != null) {\n                                            Timber.d(\"[PODCAST_LIB] Updating existing podcast\")\n                                            database.query {\n                                                update(existingPodcast.toggleBookmark())\n                                            }\n                                        } else {\n                                            Timber.d(\"[PODCAST_LIB] Creating new podcast entry\")\n                                            database.query {\n                                                insert(\n                                                    PodcastEntity(\n                                                        id = podcastId,\n                                                        title = song.song.albumName ?: \"Unknown Podcast\",\n                                                        author = song.artists.firstOrNull()?.name,\n                                                        thumbnailUrl = song.song.thumbnailUrl,\n                                                    ).toggleBookmark()\n                                                )\n                                            }\n                                        }\n                                    }\n                                    onDismiss()\n                                }\n                            )\n                        )\n                    }\n                    add(\n                        Material3MenuItemData(\n                            title = { Text(text = stringResource(R.string.refetch)) },\n                            description = { Text(text = stringResource(R.string.refetch_desc)) },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.sync),\n                                    contentDescription = null,\n                                    modifier = Modifier.graphicsLayer(rotationZ = rotationAnimation),\n                                )\n                            },\n                            onClick = {\n                                refetchIconDegree -= 360\n                                scope.launch(Dispatchers.IO) {\n                                    YouTube.queue(listOf(song.id)).onSuccess {\n                                        val newSong = it.firstOrNull()\n                                        if (newSong != null) {\n                                            database.transaction {\n                                                update(song, newSong.toMediaMetadata())\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                        )\n                    )\n                    add(\n                        Material3MenuItemData(\n                            title = { Text(text = stringResource(R.string.details)) },\n                            description = { Text(text = stringResource(R.string.details_desc)) },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.info),\n                                    contentDescription = null,\n                                )\n                            },\n                            onClick = {\n                                onDismiss()\n                                bottomSheetPageState.show {\n                                    ShowMediaInfo(song.id)\n                                }\n                            }\n                        )\n                    )\n                }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/menu/YouTubeAlbumMenu.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.menu\n\nimport android.annotation.SuppressLint\nimport android.content.Intent\nimport android.content.res.Configuration\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.ListItem\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.ColorFilter\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.core.net.toUri\nimport androidx.media3.exoplayer.offline.Download\nimport androidx.media3.exoplayer.offline.DownloadRequest\nimport androidx.media3.exoplayer.offline.DownloadService\nimport androidx.navigation.NavController\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalDownloadUtil\nimport com.metrolist.music.LocalListenTogetherManager\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.ListItemHeight\nimport com.metrolist.music.constants.ListThumbnailSize\nimport com.metrolist.music.db.entities.SpeedDialItem\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.playback.ExoDownloadService\nimport com.metrolist.music.playback.queues.YouTubeAlbumRadio\nimport com.metrolist.music.ui.component.ListDialog\nimport com.metrolist.music.ui.component.Material3MenuGroup\nimport com.metrolist.music.ui.component.Material3MenuItemData\nimport com.metrolist.music.ui.component.NewAction\nimport com.metrolist.music.ui.component.NewActionGrid\nimport com.metrolist.music.ui.component.SongListItem\nimport com.metrolist.music.ui.component.YouTubeListItem\nimport com.metrolist.music.utils.reportException\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\n\n@OptIn(ExperimentalMaterial3Api::class)\n@SuppressLint(\"MutableCollectionMutableState\")\n@Composable\nfun YouTubeAlbumMenu(\n    albumItem: AlbumItem,\n    navController: NavController,\n    onDismiss: () -> Unit,\n) {\n    val context = LocalContext.current\n    val database = LocalDatabase.current\n    val downloadUtil = LocalDownloadUtil.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val isGuest = listenTogetherManager?.isInRoom == true && !listenTogetherManager.isHost\n    val album by database.albumWithSongs(albumItem.id).collectAsState(initial = null)\n    val isPinned by database.speedDialDao.isPinned(albumItem.id).collectAsState(initial = false)\n    val coroutineScope = rememberCoroutineScope()\n\n    LaunchedEffect(Unit) {\n        database.album(albumItem.id).collect { album ->\n            if (album == null) {\n                YouTube\n                    .album(albumItem.id)\n                    .onSuccess { albumPage ->\n                        database.transaction {\n                            insert(albumPage)\n                        }\n                    }.onFailure {\n                        reportException(it)\n                    }\n            }\n        }\n    }\n\n    var downloadState by remember {\n        mutableIntStateOf(Download.STATE_STOPPED)\n    }\n\n    LaunchedEffect(album) {\n        val songs = album?.songs?.map { it.id } ?: return@LaunchedEffect\n        downloadUtil.downloads.collect { downloads ->\n            downloadState =\n                if (songs.all { downloads[it]?.state == Download.STATE_COMPLETED }) {\n                    Download.STATE_COMPLETED\n                } else if (songs.all {\n                        downloads[it]?.state == Download.STATE_QUEUED ||\n                                downloads[it]?.state == Download.STATE_DOWNLOADING ||\n                                downloads[it]?.state == Download.STATE_COMPLETED\n                    }\n                ) {\n                    Download.STATE_DOWNLOADING\n                } else {\n                    Download.STATE_STOPPED\n                }\n        }\n    }\n\n    var showChoosePlaylistDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    var showErrorPlaylistAddDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    val notAddedList by remember {\n        mutableStateOf(mutableListOf<Song>())\n    }\n\n    AddToPlaylistDialog(\n        isVisible = showChoosePlaylistDialog,\n        onGetSong = { playlist ->\n            coroutineScope.launch(Dispatchers.IO) {\n                playlist.playlist.browseId?.let { playlistId ->\n                    album?.album?.playlistId?.let { addPlaylistId ->\n                        YouTube.addPlaylistToPlaylist(playlistId, addPlaylistId)\n                    }\n                }\n            }\n            album?.songs?.map { it.id }.orEmpty()\n        },\n        onDismiss = { showChoosePlaylistDialog = false }\n    )\n\n    if (showErrorPlaylistAddDialog) {\n        ListDialog(\n            onDismiss = {\n                showErrorPlaylistAddDialog = false\n                onDismiss()\n            },\n        ) {\n            item {\n                ListItem(\n                    headlineContent = { Text(text = stringResource(R.string.already_in_playlist)) },\n                    leadingContent = {\n                        Image(\n                            painter = painterResource(R.drawable.close),\n                            contentDescription = null,\n                            colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),\n                            modifier = Modifier.size(ListThumbnailSize),\n                        )\n                    },\n                    modifier = Modifier.clickable { showErrorPlaylistAddDialog = false },\n                )\n            }\n\n            items(notAddedList) { song ->\n                SongListItem(song = song)\n            }\n        }\n    }\n\n    var showSelectArtistDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    if (showSelectArtistDialog) {\n        ListDialog(\n            onDismiss = { showSelectArtistDialog = false },\n        ) {\n            items(\n                items = album?.artists.orEmpty().distinctBy { it.id },\n                key = { it.id },\n            ) { artist ->\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    modifier = Modifier\n                        .height(ListItemHeight)\n                        .clickable {\n                            navController.navigate(\"artist/${artist.id}\")\n                            showSelectArtistDialog = false\n                            onDismiss()\n                        }\n                        .padding(horizontal = 12.dp),\n                ) {\n                    Box(\n                        contentAlignment = Alignment.CenterStart,\n                        modifier = Modifier\n                            .fillParentMaxWidth()\n                            .height(ListItemHeight)\n                            .clickable {\n                                showSelectArtistDialog = false\n                                onDismiss()\n                                navController.navigate(\"artist/${artist.id}\")\n                            }\n                            .padding(horizontal = 24.dp),\n                    ) {\n                        Text(\n                            text = artist.name,\n                            fontSize = 18.sp,\n                            fontWeight = FontWeight.Bold,\n                            maxLines = 1,\n                            overflow = TextOverflow.Ellipsis,\n                        )\n                    }\n                }\n            }\n        }\n    }\n\n    YouTubeListItem(\n        item = albumItem,\n        badges = {},\n        trailingContent = {\n            IconButton(\n                onClick = {\n                    database.query {\n                        album?.album?.toggleLike()?.let(::update)\n                    }\n                },\n            ) {\n                Icon(\n                    painter = painterResource(if (album?.album?.bookmarkedAt != null) R.drawable.favorite else R.drawable.favorite_border),\n                    tint = if (album?.album?.bookmarkedAt != null) MaterialTheme.colorScheme.error else LocalContentColor.current,\n                    contentDescription = null,\n                )\n            }\n        },\n    )\n\n    HorizontalDivider()\n\n    Spacer(modifier = Modifier.height(12.dp))\n\n    val configuration = LocalConfiguration.current\n    val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT\n\n    LazyColumn(\n        contentPadding = PaddingValues(\n            start = 0.dp,\n            top = 0.dp,\n            end = 0.dp,\n            bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(),\n        ),\n    ) {\n        item {\n            NewActionGrid(\n                actions = listOfNotNull(\n                    if (!isGuest) {\n                        NewAction(\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.play),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(28.dp),\n                                    tint = MaterialTheme.colorScheme.onSurfaceVariant\n                                )\n                            },\n                            text = stringResource(R.string.play),\n                            onClick = {\n                                onDismiss()\n                                album?.songs?.let { songs ->\n                                    if (songs.isNotEmpty()) {\n                                        playerConnection.playQueue(YouTubeAlbumRadio(albumItem.playlistId))\n                                    }\n                                }\n                            }\n                        )\n                        NewAction(\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.shuffle),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(28.dp),\n                                    tint = MaterialTheme.colorScheme.onSurfaceVariant\n                                )\n                            },\n                            text = stringResource(R.string.shuffle),\n                            onClick = {\n                                onDismiss()\n                                album?.songs?.let { songs ->\n                                    if (songs.isNotEmpty()) {\n                                        playerConnection.playQueue(YouTubeAlbumRadio(albumItem.playlistId))\n                                    }\n                                }\n                            }\n                        )\n                    } else null,\n                    NewAction(\n                        icon = {\n                            Icon(\n                                painter = painterResource(R.drawable.share),\n                                contentDescription = null,\n                                modifier = Modifier.size(28.dp),\n                                tint = MaterialTheme.colorScheme.onSurfaceVariant\n                            )\n                        },\n                        text = stringResource(R.string.share),\n                        onClick = {\n                            onDismiss()\n                            val intent = Intent().apply {\n                                action = Intent.ACTION_SEND\n                                type = \"text/plain\"\n                            putExtra(Intent.EXTRA_TEXT, albumItem.shareLink)\n                            }\n                            context.startActivity(Intent.createChooser(intent, null))\n                        }\n                    )\n                ),\n                modifier = Modifier.padding(horizontal = 4.dp, vertical = 16.dp),\n                columns = if (isGuest) 1 else 3\n            )\n        }\n        item {\n            Material3MenuGroup(\n                items = listOfNotNull(\n                    if (!isGuest) {\n                        Material3MenuItemData(\n                            title = { Text(text = stringResource(R.string.play_next)) },\n                            description = { Text(text = stringResource(R.string.play_next_desc)) },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.playlist_play),\n                                    contentDescription = null,\n                                )\n                            },\n                            onClick = {\n                                album\n                                    ?.songs\n                                    ?.map { it.toMediaItem() }\n                                    ?.let(playerConnection::playNext)\n                                onDismiss()\n                            }\n                        )\n                    } else null,\n                    if (!isGuest) {\n                        Material3MenuItemData(\n                            title = { Text(text = stringResource(R.string.add_to_queue)) },\n                            description = { Text(text = stringResource(R.string.add_to_queue_desc)) },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.queue_music),\n                                    contentDescription = null,\n                                )\n                            },\n                            onClick = {\n                                album\n                                    ?.songs\n                                    ?.map { it.toMediaItem() }\n                                    ?.let(playerConnection::addToQueue)\n                                onDismiss()\n                            }\n                        )\n                    } else null,\n                    Material3MenuItemData(\n                        title = { Text(text = stringResource(R.string.add_to_playlist)) },\n                        description = { Text(text = stringResource(R.string.add_to_playlist_desc)) },\n                        icon = {\n                            Icon(\n                                painter = painterResource(R.drawable.playlist_add),\n                                contentDescription = null,\n                            )\n                        },\n                        onClick = {\n                            showChoosePlaylistDialog = true\n                        }\n                    ),\n                    Material3MenuItemData(\n                        title = { \n                            Text(\n                                text = if (isPinned) \"Unpin from Speed dial\" else \"Pin to Speed dial\" \n                            ) \n                        },\n                        icon = {\n                            Icon(\n                                painter = painterResource(if (isPinned) R.drawable.remove else R.drawable.add),\n                                contentDescription = null,\n                            )\n                        },\n                        onClick = {\n                            coroutineScope.launch(Dispatchers.IO) {\n                                if (isPinned) {\n                                    database.speedDialDao.delete(albumItem.id)\n                                } else {\n                                    database.speedDialDao.insert(SpeedDialItem.fromYTItem(albumItem))\n                                }\n                            }\n                            onDismiss()\n                        }\n                    )\n                )\n            )\n        }\n\n        item { Spacer(modifier = Modifier.height(12.dp)) }\n\n        item {\n            Material3MenuGroup(\n                items = listOf(\n                    when (downloadState) {\n                        Download.STATE_COMPLETED -> {\n                            Material3MenuItemData(\n                                title = {\n                                    Text(\n                                        text = stringResource(R.string.remove_download)\n                                    )\n                                },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.offline),\n                                        contentDescription = null\n                                    )\n                                },\n                                onClick = {\n                                    album?.songs?.forEach { song ->\n                                        DownloadService.sendRemoveDownload(\n                                            context,\n                                            ExoDownloadService::class.java,\n                                            song.id,\n                                            false,\n                                        )\n                                    }\n                                }\n                            )\n                        }\n                        Download.STATE_QUEUED, Download.STATE_DOWNLOADING -> {\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.downloading)) },\n                                icon = {\n                                    CircularProgressIndicator(\n                                        modifier = Modifier.size(24.dp),\n                                        strokeWidth = 2.dp\n                                    )\n                                },\n                                onClick = {\n                                    album?.songs?.forEach { song ->\n                                        DownloadService.sendRemoveDownload(\n                                            context,\n                                            ExoDownloadService::class.java,\n                                            song.id,\n                                            false,\n                                        )\n                                    }\n                                }\n                            )\n                        }\n                        else -> {\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.action_download)) },\n                                description = { Text(text = stringResource(R.string.download_desc)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.download),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    album?.songs?.forEach { song ->\n                                        val downloadRequest =\n                                            DownloadRequest\n                                                .Builder(song.id, song.id.toUri())\n                                                .setCustomCacheKey(song.id)\n                                                .setData(song.song.title.toByteArray())\n                                                .build()\n                                        DownloadService.sendAddDownload(\n                                            context,\n                                            ExoDownloadService::class.java,\n                                            downloadRequest,\n                                            false,\n                                        )\n                                    }\n                                }\n                            )\n                        }\n                    }\n                )\n            )\n        }\n\n        albumItem.artists?.let { artists ->\n            item { Spacer(modifier = Modifier.height(12.dp)) }\n            item {\n                Material3MenuGroup(\n                    items = listOf(\n                        Material3MenuItemData(\n                            title = { Text(text = stringResource(R.string.view_artist)) },\n                            description = { Text(text = artists.joinToString { it.name }) },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.artist),\n                                    contentDescription = null,\n                                )\n                            },\n                            onClick = {\n                                if (artists.size == 1) {\n                                    navController.navigate(\"artist/${artists[0].id}\")\n                                    onDismiss()\n                                } else {\n                                    showSelectArtistDialog = true\n                                }\n                            }\n                        )\n                    )\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/menu/YouTubeArtistMenu.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.menu\n\nimport android.content.Intent\nimport android.content.res.Configuration\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport com.metrolist.innertube.models.ArtistItem\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalListenTogetherManager\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.db.entities.SpeedDialItem\nimport com.metrolist.music.db.entities.ArtistEntity\nimport com.metrolist.music.playback.queues.YouTubeQueue\nimport com.metrolist.music.ui.component.Material3MenuGroup\nimport com.metrolist.music.ui.component.Material3MenuItemData\nimport com.metrolist.music.ui.component.NewAction\nimport com.metrolist.music.ui.component.NewActionGrid\nimport com.metrolist.music.ui.component.YouTubeListItem\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport androidx.compose.runtime.rememberCoroutineScope\n\n@OptIn(ExperimentalMaterial3Api::class)\n\n@Composable\nfun YouTubeArtistMenu(\n    artist: ArtistItem,\n    onDismiss: () -> Unit,\n) {\n    val context = LocalContext.current\n    val database = LocalDatabase.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val libraryArtist by database.artist(artist.id).collectAsState(initial = null)\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val isGuest = listenTogetherManager?.isInRoom == true && !listenTogetherManager.isHost\n    val isPinned by database.speedDialDao.isPinned(artist.id).collectAsState(initial = false)\n    val coroutineScope = rememberCoroutineScope()\n\n    YouTubeListItem(\n        item = artist,\n        trailingContent = {},\n    )\n\n    HorizontalDivider()\n\n    Spacer(modifier = Modifier.height(12.dp))\n\n    val configuration = LocalConfiguration.current\n    val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT\n\n    LazyColumn(\n        contentPadding = PaddingValues(\n            start = 0.dp,\n            top = 0.dp,\n            end = 0.dp,\n            bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(),\n        ),\n    ) {\n        item {\n            NewActionGrid(\n                actions = buildList {\n                    if (!isGuest) {\n                        artist.radioEndpoint?.let { watchEndpoint ->\n                            add(\n                                NewAction(\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.radio),\n                                            contentDescription = null,\n                                            modifier = Modifier.size(28.dp),\n                                            tint = MaterialTheme.colorScheme.onSurfaceVariant\n                                        )\n                                    },\n                                    text = stringResource(R.string.start_radio),\n                                    onClick = {\n                                        playerConnection.playQueue(YouTubeQueue(watchEndpoint))\n                                        onDismiss()\n                                    }\n                                )\n                            )\n                        }\n                    }\n\n                    add(\n                        NewAction(\n                            icon = {\n                                Icon(\n                                    painter = painterResource(if (isPinned) R.drawable.remove else R.drawable.add),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(28.dp),\n                                    tint = MaterialTheme.colorScheme.onSurfaceVariant\n                                )\n                            },\n                            text = if (isPinned) \"Unpin\" else \"Pin\",\n                            onClick = {\n                                coroutineScope.launch(Dispatchers.IO) {\n                                    if (isPinned) {\n                                        database.speedDialDao.delete(artist.id)\n                                    } else {\n                                        database.speedDialDao.insert(SpeedDialItem.fromYTItem(artist))\n                                    }\n                                }\n                                onDismiss()\n                            }\n                        )\n                    )\n\n                    add(\n                        NewAction(\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.share),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(28.dp),\n                                    tint = MaterialTheme.colorScheme.onSurfaceVariant\n                                )\n                            },\n                            text = stringResource(R.string.share),\n                            onClick = {\n                                val intent = Intent().apply {\n                                    action = Intent.ACTION_SEND\n                                    type = \"text/plain\"\n                                    putExtra(Intent.EXTRA_TEXT, artist.shareLink)\n                                }\n                                context.startActivity(Intent.createChooser(intent, null))\n                                onDismiss()\n                            }\n                        )\n                    )\n                },\n                modifier = Modifier.padding(horizontal = 4.dp, vertical = 16.dp),\n                columns = if (isGuest) 1 else 3\n            )\n        }\n\n        item {\n            Material3MenuGroup(\n                items = listOf(\n                    Material3MenuItemData(\n                        title = {\n                            Text(text = if (libraryArtist?.artist?.bookmarkedAt != null) stringResource(R.string.subscribed) else stringResource(R.string.subscribe))\n                        },\n                        icon = {\n                            Icon(\n                                painter = painterResource(\n                                    if (libraryArtist?.artist?.bookmarkedAt != null) {\n                                        R.drawable.subscribed\n                                    } else {\n                                        R.drawable.subscribe\n                                    }\n                                ),\n                                contentDescription = null,\n                            )\n                        },\n                        onClick = {\n                            database.query {\n                                val libraryArtist = libraryArtist\n                                if (libraryArtist != null) {\n                                    update(libraryArtist.artist.toggleLike())\n                                } else {\n                                    insert(\n                                        ArtistEntity(\n                                            id = artist.id,\n                                            name = artist.title,\n                                            channelId = artist.channelId,\n                                            thumbnailUrl = artist.thumbnail,\n                                        ).toggleLike()\n                                    )\n                                }\n                            }\n                        }\n                    )\n                )\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/menu/YouTubePlaylistMenu.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.menu\n\nimport android.annotation.SuppressLint\nimport android.content.Intent\nimport android.content.res.Configuration\nimport android.widget.Toast\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.ListItem\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.ColorFilter\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.core.net.toUri\nimport androidx.media3.exoplayer.offline.Download\nimport androidx.media3.exoplayer.offline.DownloadRequest\nimport androidx.media3.exoplayer.offline.DownloadService\nimport coil3.compose.AsyncImage\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.utils.completed\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalDownloadUtil\nimport com.metrolist.music.LocalListenTogetherManager\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.ListThumbnailSize\nimport com.metrolist.music.constants.ThumbnailCornerRadius\nimport com.metrolist.music.db.entities.PlaylistEntity\nimport com.metrolist.music.db.entities.PlaylistSongMap\nimport com.metrolist.music.db.entities.SpeedDialItem\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.models.MediaMetadata\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.playback.ExoDownloadService\nimport com.metrolist.music.playback.queues.YouTubeQueue\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.ListDialog\nimport com.metrolist.music.ui.component.Material3MenuGroup\nimport com.metrolist.music.ui.component.Material3MenuItemData\nimport com.metrolist.music.ui.component.NewAction\nimport com.metrolist.music.ui.component.NewActionGrid\nimport com.metrolist.music.ui.component.YouTubeListItem\nimport com.metrolist.music.ui.utils.resize\nimport com.metrolist.music.utils.exportYouTubePlaylistAsCSV\nimport com.metrolist.music.utils.exportYouTubePlaylistAsM3U\nimport com.metrolist.music.utils.getExportFileUri\nimport com.metrolist.music.utils.joinByBullet\nimport com.metrolist.music.utils.makeTimeString\nimport com.metrolist.music.utils.saveToPublicDocuments\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\n\n@OptIn(ExperimentalMaterial3Api::class)\n@SuppressLint(\"MutableCollectionMutableState\")\n@Composable\nfun YouTubePlaylistMenu(\n    playlist: PlaylistItem,\n    songs: List<SongItem> = emptyList(),\n    coroutineScope: CoroutineScope,\n    onDismiss: () -> Unit,\n    selectAction: () -> Unit = {},\n    canSelect: Boolean = false,\n) {\n    val context = LocalContext.current\n    val database = LocalDatabase.current\n    val downloadUtil = LocalDownloadUtil.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val isGuest = listenTogetherManager?.isInRoom == true && !listenTogetherManager.isHost\n    val dbPlaylist by database.playlistByBrowseId(playlist.id).collectAsState(initial = null)\n    val isPinned by database.speedDialDao.isPinned(playlist.id).collectAsState(initial = false)\n\n    var showChoosePlaylistDialog by rememberSaveable { mutableStateOf(false) }\n    var showImportPlaylistDialog by rememberSaveable { mutableStateOf(false) }\n    var showErrorPlaylistAddDialog by rememberSaveable { mutableStateOf(false) }\n\n    val notAddedList by remember {\n        mutableStateOf(mutableListOf<MediaMetadata>())\n    }\n\n    AddToPlaylistDialog(\n        isVisible = showChoosePlaylistDialog,\n        onGetSong = { targetPlaylist ->\n            val allSongs =\n                songs\n                    .ifEmpty {\n                        YouTube\n                            .playlist(targetPlaylist.id)\n                            .completed()\n                            .getOrNull()\n                            ?.songs\n                            .orEmpty()\n                    }.map {\n                        it.toMediaMetadata()\n                    }\n            database.withTransaction {\n                allSongs.forEach(::insert)\n            }\n            coroutineScope.launch(Dispatchers.IO) {\n                targetPlaylist.playlist.browseId?.let { playlistId ->\n                    YouTube.addPlaylistToPlaylist(playlistId, targetPlaylist.id)\n                }\n            }\n            allSongs.map { it.id }\n        },\n        onDismiss = { showChoosePlaylistDialog = false },\n    )\n\n    YouTubeListItem(\n        item = playlist,\n        trailingContent = {\n            if (playlist.id != \"LM\" && !playlist.isEditable) {\n                IconButton(\n                    onClick = {\n                        val isCurrentlySaved = dbPlaylist?.playlist?.bookmarkedAt != null\n                        if (dbPlaylist?.playlist == null) {\n                            database.transaction {\n                                val playlistEntity =\n                                    PlaylistEntity(\n                                        name = playlist.title,\n                                        browseId = playlist.id,\n                                        thumbnailUrl = playlist.thumbnail,\n                                        isEditable = playlist.isEditable,\n                                        remoteSongCount =\n                                            playlist.songCountText?.let {\n                                                Regex(\"\"\"\\d+\"\"\").find(it)?.value?.toIntOrNull()\n                                            },\n                                        playEndpointParams = playlist.playEndpoint?.params,\n                                        shuffleEndpointParams = playlist.shuffleEndpoint?.params,\n                                        radioEndpointParams = playlist.radioEndpoint?.params,\n                                    ).toggleLike()\n                                insert(playlistEntity)\n                            }\n                        } else {\n                            database.transaction {\n                                val currentPlaylist = dbPlaylist!!.playlist\n                                update(currentPlaylist, playlist)\n                                update(currentPlaylist.toggleLike())\n                            }\n                        }\n                        coroutineScope.launch(Dispatchers.IO) {\n                            if (!isCurrentlySaved) {\n                                val playlistEntity = database.playlistByBrowseId(playlist.id).first()?.playlist\n                                if (playlistEntity != null) {\n                                    songs\n                                        .ifEmpty {\n                                            YouTube\n                                                .playlist(playlist.id)\n                                                .completed()\n                                                .getOrNull()\n                                                ?.songs\n                                                .orEmpty()\n                                        }.map { it.toMediaMetadata() }\n                                        .onEach { database.transaction { insert(it) } }\n                                        .mapIndexed { index, song ->\n                                            PlaylistSongMap(\n                                                songId = song.id,\n                                                playlistId = playlistEntity.id,\n                                                position = index,\n                                                setVideoId = song.setVideoId,\n                                            )\n                                        }.forEach { database.transaction { insert(it) } }\n                                }\n                            }\n                            if (playlist.isPodcast) {\n                                YouTube\n                                    .savePodcast(playlist.id, !isCurrentlySaved)\n                                    .onSuccess {\n                                        timber.log.Timber.d(\"[PODCAST_SAVE] savePodcast API success for ${playlist.id}\")\n                                    }.onFailure { e ->\n                                        timber.log.Timber.e(e, \"[PODCAST_SAVE] savePodcast API failed for ${playlist.id}\")\n                                        withContext(Dispatchers.Main) {\n                                            android.widget.Toast\n                                                .makeText(\n                                                    context,\n                                                    if (isCurrentlySaved) R.string.error_podcast_unsubscribe else R.string.error_podcast_subscribe,\n                                                    android.widget.Toast.LENGTH_SHORT,\n                                                ).show()\n                                        }\n                                    }\n                            }\n                        }\n                    },\n                ) {\n                    Icon(\n                        painter =\n                            painterResource(\n                                if (dbPlaylist?.playlist?.bookmarkedAt !=\n                                    null\n                                ) {\n                                    R.drawable.favorite\n                                } else {\n                                    R.drawable.favorite_border\n                                },\n                            ),\n                        tint =\n                            if (dbPlaylist?.playlist?.bookmarkedAt !=\n                                null\n                            ) {\n                                MaterialTheme.colorScheme.error\n                            } else {\n                                LocalContentColor.current\n                            },\n                        contentDescription = null,\n                    )\n                }\n            }\n        },\n    )\n    HorizontalDivider()\n\n    var downloadState by remember {\n        mutableIntStateOf(Download.STATE_STOPPED)\n    }\n    LaunchedEffect(songs) {\n        if (songs.isEmpty()) return@LaunchedEffect\n        downloadUtil.downloads.collect { downloads ->\n            downloadState =\n                if (songs.all { downloads[it.id]?.state == Download.STATE_COMPLETED }) {\n                    Download.STATE_COMPLETED\n                } else if (songs.all {\n                        downloads[it.id]?.state == Download.STATE_QUEUED ||\n                            downloads[it.id]?.state == Download.STATE_DOWNLOADING ||\n                            downloads[it.id]?.state == Download.STATE_COMPLETED\n                    }\n                ) {\n                    Download.STATE_DOWNLOADING\n                } else {\n                    Download.STATE_STOPPED\n                }\n        }\n    }\n    var showRemoveDownloadDialog by remember {\n        mutableStateOf(false)\n    }\n    var showExportDialog by remember { mutableStateOf(false) }\n    if (showRemoveDownloadDialog) {\n        DefaultDialog(\n            onDismiss = { showRemoveDownloadDialog = false },\n            content = {\n                Text(\n                    text =\n                        stringResource(\n                            R.string.remove_download_playlist_confirm,\n                            playlist.title,\n                        ),\n                    style = MaterialTheme.typography.bodyLarge,\n                    modifier = Modifier.padding(horizontal = 18.dp),\n                )\n            },\n            buttons = {\n                TextButton(\n                    onClick = { showRemoveDownloadDialog = false },\n                ) {\n                    Text(text = stringResource(android.R.string.cancel))\n                }\n                TextButton(\n                    onClick = {\n                        showRemoveDownloadDialog = false\n                        songs.forEach { song ->\n                            DownloadService.sendRemoveDownload(\n                                context,\n                                ExoDownloadService::class.java,\n                                song.id,\n                                false,\n                            )\n                        }\n                    },\n                ) {\n                    Text(text = stringResource(android.R.string.ok))\n                }\n            },\n        )\n    }\n\n    ImportPlaylistDialog(\n        isVisible = showImportPlaylistDialog,\n        onGetSong = {\n            val allSongs =\n                songs\n                    .ifEmpty {\n                        YouTube\n                            .playlist(playlist.id)\n                            .completed()\n                            .getOrNull()\n                            ?.songs\n                            .orEmpty()\n                    }.map {\n                        it.toMediaMetadata()\n                    }\n            database.withTransaction {\n                allSongs.forEach(::insert)\n            }\n            allSongs.map { it.id }\n        },\n        playlistTitle = playlist.title,\n        onDismiss = { showImportPlaylistDialog = false },\n    )\n\n    if (showErrorPlaylistAddDialog) {\n        ListDialog(\n            onDismiss = {\n                showErrorPlaylistAddDialog = false\n                onDismiss()\n            },\n        ) {\n            item {\n                ListItem(\n                    headlineContent = { Text(text = stringResource(R.string.already_in_playlist)) },\n                    leadingContent = {\n                        Image(\n                            painter = painterResource(R.drawable.close),\n                            contentDescription = null,\n                            colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),\n                            modifier = Modifier.size(ListThumbnailSize),\n                        )\n                    },\n                    modifier = Modifier.clickable { showErrorPlaylistAddDialog = false },\n                )\n            }\n\n            items(notAddedList) { song ->\n                ListItem(\n                    headlineContent = { Text(text = song.title) },\n                    leadingContent = {\n                        Box(\n                            contentAlignment = Alignment.Center,\n                            modifier = Modifier.size(ListThumbnailSize),\n                        ) {\n                            AsyncImage(\n                                model = song.thumbnailUrl,\n                                contentDescription = null,\n                                modifier =\n                                    Modifier\n                                        .fillMaxSize()\n                                        .clip(RoundedCornerShape(ThumbnailCornerRadius)),\n                            )\n                        }\n                    },\n                    supportingContent = {\n                        Text(\n                            text =\n                                joinByBullet(\n                                    song.artists.joinToString { it.name },\n                                    makeTimeString(song.duration * 1000L),\n                                ),\n                        )\n                    },\n                )\n            }\n        }\n    }\n\n    val configuration = LocalConfiguration.current\n    val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT\n\n    LazyColumn(\n        contentPadding =\n            PaddingValues(\n                start = 0.dp,\n                top = 0.dp,\n                end = 0.dp,\n                bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(),\n            ),\n    ) {\n        item {\n            NewActionGrid(\n                actions =\n                    buildList {\n                        if (!isGuest) {\n                            playlist.playEndpoint?.let { playEndpoint ->\n                                add(\n                                    NewAction(\n                                        icon = {\n                                            Icon(\n                                                painter = painterResource(R.drawable.play),\n                                                contentDescription = null,\n                                                modifier = Modifier.size(28.dp),\n                                                tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                                            )\n                                        },\n                                        text = stringResource(R.string.play),\n                                        onClick = {\n                                            playerConnection.playQueue(YouTubeQueue(playEndpoint))\n                                            onDismiss()\n                                        },\n                                    ),\n                                )\n                            }\n                            playlist.shuffleEndpoint?.let { shuffleEndpoint ->\n                                add(\n                                    NewAction(\n                                        icon = {\n                                            Icon(\n                                                painter = painterResource(R.drawable.shuffle),\n                                                contentDescription = null,\n                                                modifier = Modifier.size(28.dp),\n                                                tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                                            )\n                                        },\n                                        text = stringResource(R.string.shuffle),\n                                        onClick = {\n                                            playerConnection.playQueue(YouTubeQueue(shuffleEndpoint))\n                                            onDismiss()\n                                        },\n                                    ),\n                                )\n                            }\n                            playlist.radioEndpoint?.let { radioEndpoint ->\n                                add(\n                                    NewAction(\n                                        icon = {\n                                            Icon(\n                                                painter = painterResource(R.drawable.radio),\n                                                contentDescription = null,\n                                                modifier = Modifier.size(28.dp),\n                                                tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                                            )\n                                        },\n                                        text = stringResource(R.string.start_radio),\n                                        onClick = {\n                                            playerConnection.playQueue(YouTubeQueue(radioEndpoint))\n                                            onDismiss()\n                                        },\n                                    ),\n                                )\n                            }\n                        }\n                    },\n                modifier = Modifier.padding(horizontal = 4.dp, vertical = 16.dp),\n            )\n        }\n\n        item {\n            Material3MenuGroup(\n                items =\n                    listOfNotNull(\n                        if (!isGuest) {\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.play_next)) },\n                                description = { Text(text = stringResource(R.string.play_next_desc)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.playlist_play),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    coroutineScope.launch {\n                                        songs\n                                            .ifEmpty {\n                                                withContext(Dispatchers.IO) {\n                                                    YouTube\n                                                        .playlist(playlist.id)\n                                                        .completed()\n                                                        .getOrNull()\n                                                        ?.songs\n                                                        .orEmpty()\n                                                }\n                                            }.let { songs ->\n                                                playerConnection.playNext(\n                                                    songs.map {\n                                                        it\n                                                            .copy(thumbnail = it.thumbnail.resize(544, 544))\n                                                            .toMediaItem()\n                                                    },\n                                                )\n                                            }\n                                    }\n                                    onDismiss()\n                                },\n                            )\n                        } else {\n                            null\n                        },\n                        if (!isGuest) {\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.add_to_queue)) },\n                                description = { Text(text = stringResource(R.string.add_to_queue_desc)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.queue_music),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    coroutineScope.launch {\n                                        songs\n                                            .ifEmpty {\n                                                withContext(Dispatchers.IO) {\n                                                    YouTube\n                                                        .playlist(playlist.id)\n                                                        .completed()\n                                                        .getOrNull()\n                                                        ?.songs\n                                                        .orEmpty()\n                                                }\n                                            }.let { songs ->\n                                                playerConnection.addToQueue(songs.map { it.toMediaItem() })\n                                            }\n                                    }\n                                    onDismiss()\n                                },\n                            )\n                        } else {\n                            null\n                        },\n                        Material3MenuItemData(\n                            title = { Text(text = stringResource(R.string.add_to_playlist)) },\n                            description = { Text(text = stringResource(R.string.add_to_playlist_desc)) },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.playlist_add),\n                                    contentDescription = null,\n                                )\n                            },\n                            onClick = {\n                                showChoosePlaylistDialog = true\n                            },\n                        ),\n                        Material3MenuItemData(\n                            title = {\n                                Text(\n                                    text = if (isPinned) \"Unpin from Speed dial\" else \"Pin to Speed dial\",\n                                )\n                            },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(if (isPinned) R.drawable.remove else R.drawable.add),\n                                    contentDescription = null,\n                                )\n                            },\n                            onClick = {\n                                coroutineScope.launch(Dispatchers.IO) {\n                                    if (isPinned) {\n                                        database.speedDialDao.delete(playlist.id)\n                                    } else {\n                                        database.speedDialDao.insert(SpeedDialItem.fromYTItem(playlist))\n                                    }\n                                }\n                                onDismiss()\n                            },\n                        ),\n                    ),\n            )\n        }\n\n        item { Spacer(modifier = Modifier.height(12.dp)) }\n\n        item {\n            Material3MenuGroup(\n                items =\n                    buildList {\n                        if (songs.isNotEmpty()) {\n                            add(\n                                when (downloadState) {\n                                    Download.STATE_COMPLETED -> {\n                                        Material3MenuItemData(\n                                            title = {\n                                                Text(\n                                                    text = stringResource(R.string.remove_download),\n                                                )\n                                            },\n                                            icon = {\n                                                Icon(\n                                                    painter = painterResource(R.drawable.offline),\n                                                    contentDescription = null,\n                                                )\n                                            },\n                                            onClick = {\n                                                showRemoveDownloadDialog = true\n                                            },\n                                        )\n                                    }\n\n                                    Download.STATE_QUEUED, Download.STATE_DOWNLOADING -> {\n                                        Material3MenuItemData(\n                                            title = { Text(text = stringResource(R.string.downloading)) },\n                                            icon = {\n                                                CircularProgressIndicator(\n                                                    modifier = Modifier.size(24.dp),\n                                                    strokeWidth = 2.dp,\n                                                )\n                                            },\n                                            onClick = {\n                                                showRemoveDownloadDialog = true\n                                            },\n                                        )\n                                    }\n\n                                    else -> {\n                                        Material3MenuItemData(\n                                            title = { Text(text = stringResource(R.string.action_download)) },\n                                            description = { Text(text = stringResource(R.string.download_desc)) },\n                                            icon = {\n                                                Icon(\n                                                    painter = painterResource(R.drawable.download),\n                                                    contentDescription = null,\n                                                )\n                                            },\n                                            onClick = {\n                                                songs.forEach { song ->\n                                                    val downloadRequest =\n                                                        DownloadRequest\n                                                            .Builder(song.id, song.id.toUri())\n                                                            .setCustomCacheKey(song.id)\n                                                            .setData(song.title.toByteArray())\n                                                            .build()\n                                                    DownloadService.sendAddDownload(\n                                                        context,\n                                                        ExoDownloadService::class.java,\n                                                        downloadRequest,\n                                                        false,\n                                                    )\n                                                }\n                                            },\n                                        )\n                                    }\n                                },\n                            )\n                        }\n                        add(\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.export_playlist)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.share),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = { showExportDialog = true },\n                            ),\n                        )\n                        add(\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.share)) },\n                                description = { Text(text = stringResource(R.string.share_desc)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.share),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    val intent =\n                                        Intent().apply {\n                                            action = Intent.ACTION_SEND\n                                            type = \"text/plain\"\n                                            putExtra(Intent.EXTRA_TEXT, playlist.shareLink)\n                                        }\n                                    context.startActivity(Intent.createChooser(intent, null))\n                                    onDismiss()\n                                },\n                            ),\n                        )\n                        if (canSelect) {\n                            add(\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.select)) },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.select_all),\n                                            contentDescription = null,\n                                        )\n                                    },\n                                    onClick = {\n                                        onDismiss()\n                                        selectAction()\n                                    },\n                                ),\n                            )\n                        }\n                    },\n            )\n        }\n    }\n\n    if (showExportDialog) {\n        ExportDialog(\n            onDismiss = { showExportDialog = false },\n            onShare = { format ->\n                coroutineScope.launch {\n                    val ytSongs =\n                        if (songs.isEmpty()) {\n                            withContext(Dispatchers.IO) {\n                                YouTube\n                                    .playlist(playlist.id)\n                                    .completed()\n                                    .getOrNull()\n                                    ?.songs\n                                    .orEmpty()\n                            }\n                        } else {\n                            songs\n                        }\n\n                    val result =\n                        when (format) {\n                            \"csv\" -> exportYouTubePlaylistAsCSV(context, playlist.title, ytSongs)\n                            \"m3u\" -> exportYouTubePlaylistAsM3U(context, playlist.title, ytSongs)\n                            else -> Result.failure(IllegalArgumentException(\"Unknown format\"))\n                        }\n                    result\n                        .onSuccess { file ->\n                            val uri = getExportFileUri(context, file)\n                            val mime = if (format == \"csv\") \"text/csv\" else \"audio/x-mpegurl\"\n                            shareFile(context, uri, mime)\n                        }.onFailure {\n                            Toast.makeText(context, R.string.export_failed, Toast.LENGTH_SHORT).show()\n                        }\n                }\n                onDismiss()\n            },\n            onSave = { format ->\n                coroutineScope.launch {\n                    val ytSongs =\n                        if (songs.isEmpty()) {\n                            withContext(Dispatchers.IO) {\n                                YouTube\n                                    .playlist(playlist.id)\n                                    .completed()\n                                    .getOrNull()\n                                    ?.songs\n                                    .orEmpty()\n                            }\n                        } else {\n                            songs\n                        }\n\n                    val export =\n                        when (format) {\n                            \"csv\" -> exportYouTubePlaylistAsCSV(context, playlist.title, ytSongs)\n                            \"m3u\" -> exportYouTubePlaylistAsM3U(context, playlist.title, ytSongs)\n                            else -> Result.failure(IllegalArgumentException(\"Unknown format\"))\n                        }\n                    export\n                        .onSuccess { file ->\n                            val mime = if (format == \"csv\") \"text/csv\" else \"audio/x-mpegurl\"\n                            val save = saveToPublicDocuments(context, file, mime)\n                            save\n                                .onSuccess { Toast.makeText(context, R.string.export_success, Toast.LENGTH_SHORT).show() }\n                                .onFailure { Toast.makeText(context, R.string.export_failed, Toast.LENGTH_SHORT).show() }\n                        }.onFailure { Toast.makeText(context, R.string.export_failed, Toast.LENGTH_SHORT).show() }\n                }\n                onDismiss()\n            },\n        )\n    }\n}\n\nprivate fun shareFile(\n    context: android.content.Context,\n    uri: android.net.Uri,\n    mimeType: String,\n) {\n    val shareIntent =\n        Intent(Intent.ACTION_SEND).apply {\n            type = mimeType\n            putExtra(Intent.EXTRA_STREAM, uri)\n            addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n        }\n    context.startActivity(Intent.createChooser(shareIntent, context.getString(R.string.export_playlist)))\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/menu/YouTubeSelectionSongMenu.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.menu\n\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.toMutableStateList\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.core.net.toUri\nimport androidx.media3.exoplayer.offline.Download\nimport androidx.media3.exoplayer.offline.DownloadRequest\nimport androidx.media3.exoplayer.offline.DownloadService\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalDownloadUtil\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.LocalSyncUtils\nimport com.metrolist.music.R\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.playback.ExoDownloadService\nimport com.metrolist.music.playback.queues.ListQueue\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.Material3MenuGroup\nimport com.metrolist.music.ui.component.Material3MenuItemData\nimport kotlinx.coroutines.launch\nimport java.time.LocalDateTime\n\n@Composable\nfun YouTubeSelectionSongMenu(\n    songSelection: List<SongItem>,\n    onDismiss: () -> Unit,\n    clearAction: () -> Unit,\n) {\n    val context = LocalContext.current\n    val database = LocalDatabase.current\n    val downloadUtil = LocalDownloadUtil.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val coroutineScope = rememberCoroutineScope()\n    val syncUtils = LocalSyncUtils.current\n\n    var showChoosePlaylistDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    val listenTogetherManager = com.metrolist.music.LocalListenTogetherManager.current\n    val isGuest = listenTogetherManager?.isInRoom == true && listenTogetherManager.isHost == false\n\n    var downloadState by remember {\n        mutableIntStateOf(Download.STATE_STOPPED)\n    }\n\n    var showRemoveDownloadDialog by remember {\n        mutableStateOf(false)\n    }\n\n    // Check if all songs are liked\n    val allLiked by remember(songSelection) {\n        mutableStateOf(\n            songSelection.isNotEmpty() &&\n                songSelection.all { song ->\n                    // Convert to MediaMetadata to check liked status\n                    val metadata = song.toMediaMetadata()\n                    metadata.liked\n                },\n        )\n    }\n\n    // Check if all songs are in library\n    val allInLibrary by remember(songSelection) {\n        mutableStateOf(\n            songSelection.all { song ->\n                val metadata = song.toMediaMetadata()\n                metadata.inLibrary != null\n            },\n        )\n    }\n\n    LaunchedEffect(songSelection) {\n        if (songSelection.isEmpty()) return@LaunchedEffect\n        downloadUtil.downloads.collect { downloads ->\n            downloadState =\n                if (songSelection.all { downloads[it.id]?.state == Download.STATE_COMPLETED }) {\n                    Download.STATE_COMPLETED\n                } else if (songSelection.all {\n                        downloads[it.id]?.state == Download.STATE_QUEUED ||\n                            downloads[it.id]?.state == Download.STATE_DOWNLOADING ||\n                            downloads[it.id]?.state == Download.STATE_COMPLETED\n                    }\n                ) {\n                    Download.STATE_DOWNLOADING\n                } else {\n                    Download.STATE_STOPPED\n                }\n        }\n    }\n\n    AddToPlaylistDialogOnline(\n        isVisible = showChoosePlaylistDialog,\n        songs =\n            remember {\n                songSelection\n                    .map { song ->\n                        // Convert SongItem to Song entity\n                        val metadata = song.toMediaMetadata()\n                        com.metrolist.music.db.entities.Song(\n                            song =\n                                com.metrolist.music.db.entities.SongEntity(\n                                    id = metadata.id,\n                                    title = metadata.title,\n                                    duration = metadata.duration,\n                                    thumbnailUrl = metadata.thumbnailUrl,\n                                    albumId = metadata.album?.id,\n                                    albumName = metadata.album?.title,\n                                    liked = metadata.liked,\n                                    totalPlayTime = 0,\n                                    inLibrary = metadata.inLibrary,\n                                    isLocal = false,\n                                    libraryAddToken = metadata.libraryAddToken,\n                                    libraryRemoveToken = metadata.libraryRemoveToken,\n                                ),\n                            artists =\n                                metadata.artists.map { artist ->\n                                    com.metrolist.music.db.entities.ArtistEntity(\n                                        id = artist.id ?: \"\",\n                                        name = artist.name,\n                                    )\n                                },\n                            album =\n                                metadata.album?.let { album ->\n                                    com.metrolist.music.db.entities.AlbumEntity(\n                                        id = album.id,\n                                        title = album.title,\n                                        thumbnailUrl = metadata.thumbnailUrl, // Use song's thumbnail as album thumbnail\n                                        songCount = 0,\n                                        duration = 0,\n                                    )\n                                },\n                        )\n                    }.toMutableStateList()\n            },\n        onProgressStart = { },\n        onPercentageChange = { },\n        onDismiss = {\n            showChoosePlaylistDialog = false\n        },\n    )\n\n    if (showRemoveDownloadDialog) {\n        DefaultDialog(\n            onDismiss = { showRemoveDownloadDialog = false },\n            content = {\n                Text(\n                    text = stringResource(R.string.remove_download_playlist_confirm, \"selection\"),\n                    style = MaterialTheme.typography.bodyLarge,\n                    modifier = Modifier.padding(horizontal = 18.dp),\n                )\n            },\n            buttons = {\n                TextButton(\n                    onClick = {\n                        showRemoveDownloadDialog = false\n                    },\n                ) {\n                    Text(text = stringResource(android.R.string.cancel))\n                }\n\n                TextButton(\n                    onClick = {\n                        showRemoveDownloadDialog = false\n                        songSelection.forEach { song ->\n                            DownloadService.sendRemoveDownload(\n                                context,\n                                ExoDownloadService::class.java,\n                                song.id,\n                                false,\n                            )\n                        }\n                    },\n                ) {\n                    Text(text = stringResource(android.R.string.ok))\n                }\n            },\n        )\n    }\n\n    val queueAllSongsStr = stringResource(R.string.queue_all_songs)\n\n    LazyColumn(\n        contentPadding =\n            PaddingValues(\n                start = 8.dp,\n                top = 8.dp,\n                end = 8.dp,\n                bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(),\n            ),\n    ) {\n        item {\n            Material3MenuGroup(\n                listOfNotNull(\n                    if (!isGuest) {\n                        Material3MenuItemData(\n                            icon = { Icon(painterResource(R.drawable.play), null) },\n                            title = { Text(stringResource(R.string.play)) },\n                            onClick = {\n                                playerConnection.playQueue(\n                                    ListQueue(\n                                        title = queueAllSongsStr,\n                                        items = songSelection.map { it.toMediaItem() },\n                                    ),\n                                )\n                                clearAction()\n                                onDismiss()\n                            },\n                        )\n                    } else {\n                        null\n                    },\n                    if (!isGuest) {\n                        Material3MenuItemData(\n                            icon = { Icon(painterResource(R.drawable.shuffle), null) },\n                            title = { Text(stringResource(R.string.shuffle)) },\n                            onClick = {\n                                playerConnection.playQueue(\n                                    ListQueue(\n                                        title = queueAllSongsStr,\n                                        items = songSelection.shuffled().map { it.toMediaItem() },\n                                    ),\n                                )\n                                clearAction()\n                                onDismiss()\n                            },\n                        )\n                    } else {\n                        null\n                    },\n                    if (!isGuest) {\n                        Material3MenuItemData(\n                            icon = { Icon(painterResource(R.drawable.queue_music), null) },\n                            title = { Text(stringResource(R.string.add_to_queue)) },\n                            onClick = {\n                                playerConnection.addToQueue(songSelection.map { it.toMediaItem() })\n                                clearAction()\n                                onDismiss()\n                            },\n                        )\n                    } else {\n                        null\n                    },\n                    Material3MenuItemData(\n                        icon = { Icon(painterResource(R.drawable.playlist_add), null) },\n                        title = { Text(stringResource(R.string.add_to_playlist)) },\n                        onClick = {\n                            showChoosePlaylistDialog = true\n                        },\n                    ),\n                    Material3MenuItemData(\n                        title = {\n                            Text(\n                                text =\n                                    stringResource(\n                                        if (allInLibrary) R.string.remove_from_library else R.string.add_to_library,\n                                    ),\n                            )\n                        },\n                        icon = {\n                            Icon(\n                                painter =\n                                    painterResource(\n                                        if (allInLibrary) R.drawable.library_add_check else R.drawable.library_add,\n                                    ),\n                                contentDescription = null,\n                            )\n                        },\n                        onClick = {\n                            if (allInLibrary) {\n                                database.query {\n                                    songSelection.forEach { song ->\n                                        inLibrary(song.id, null)\n                                    }\n                                }\n                                coroutineScope.launch {\n                                    // Use the new reliable method that fetches fresh tokens\n                                    songSelection.forEach { song ->\n                                        YouTube.toggleSongLibrary(song.id, false)\n                                    }\n                                }\n                            } else {\n                                database.transaction {\n                                    songSelection.forEach { song ->\n                                        insert(song.toMediaMetadata())\n                                        inLibrary(song.id, LocalDateTime.now())\n                                    }\n                                }\n                                coroutineScope.launch {\n                                    // Use the new reliable method that fetches fresh tokens\n                                    songSelection\n                                        .filter { song ->\n                                            song.toMediaMetadata().inLibrary == null\n                                        }.forEach { song ->\n                                            YouTube.toggleSongLibrary(song.id, true)\n                                        }\n                                }\n                            }\n                            clearAction()\n                            onDismiss()\n                        },\n                    ),\n                    when (downloadState) {\n                        Download.STATE_COMPLETED -> {\n                            Material3MenuItemData(\n                                title = {\n                                    Text(\n                                        text = stringResource(R.string.remove_download),\n                                    )\n                                },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.offline),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    showRemoveDownloadDialog = true\n                                },\n                            )\n                        }\n\n                        Download.STATE_QUEUED, Download.STATE_DOWNLOADING -> {\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.downloading)) },\n                                icon = {\n                                    CircularProgressIndicator(\n                                        modifier = Modifier.size(24.dp),\n                                        strokeWidth = 2.dp,\n                                    )\n                                },\n                                onClick = {\n                                    showRemoveDownloadDialog = true\n                                },\n                            )\n                        }\n\n                        else -> {\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.action_download)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.download),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    songSelection.forEach { song ->\n                                        val downloadRequest =\n                                            DownloadRequest\n                                                .Builder(song.id, song.id.toUri())\n                                                .setCustomCacheKey(song.id)\n                                                .setData(song.title.toByteArray())\n                                                .build()\n                                        DownloadService.sendAddDownload(\n                                            context,\n                                            ExoDownloadService::class.java,\n                                            downloadRequest,\n                                            false,\n                                        )\n                                    }\n                                    clearAction()\n                                    onDismiss()\n                                },\n                            )\n                        }\n                    },\n                    Material3MenuItemData(\n                        title = {\n                            Text(\n                                text =\n                                    stringResource(\n                                        if (allLiked) R.string.dislike_all else R.string.like_all,\n                                    ),\n                            )\n                        },\n                        icon = {\n                            Icon(\n                                painter =\n                                    painterResource(\n                                        if (allLiked) R.drawable.favorite else R.drawable.favorite_border,\n                                    ),\n                                contentDescription = null,\n                            )\n                        },\n                        onClick = {\n                            database.transaction {\n                                songSelection.forEach { song ->\n                                    val metadata = song.toMediaMetadata()\n                                    if ((!allLiked && !metadata.liked) || allLiked) {\n                                        // Insert the song first if it doesn't exist\n                                        insert(metadata)\n                                        // Create SongEntity with toggled like status\n                                        val songEntity =\n                                            com.metrolist.music.db.entities.SongEntity(\n                                                id = metadata.id,\n                                                title = metadata.title,\n                                                duration = metadata.duration,\n                                                thumbnailUrl = metadata.thumbnailUrl,\n                                                albumId = metadata.album?.id,\n                                                albumName = metadata.album?.title,\n                                                liked = !metadata.liked,\n                                                totalPlayTime = 0,\n                                                inLibrary = metadata.inLibrary,\n                                                isLocal = false,\n                                                libraryAddToken = metadata.libraryAddToken,\n                                                libraryRemoveToken = metadata.libraryRemoveToken,\n                                            )\n                                        update(songEntity)\n                                        syncUtils.likeSong(songEntity)\n                                    }\n                                }\n                            }\n                            clearAction()\n                            onDismiss()\n                        },\n                    ),\n                ),\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/menu/YouTubeSongMenu.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.menu\n\nimport android.annotation.SuppressLint\nimport android.content.Intent\nimport android.content.res.Configuration\nimport androidx.compose.foundation.basicMarquee\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.ListItem\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.core.net.toUri\nimport androidx.media3.exoplayer.offline.Download\nimport androidx.media3.exoplayer.offline.DownloadRequest\nimport androidx.media3.exoplayer.offline.DownloadService\nimport androidx.navigation.NavController\nimport coil3.compose.AsyncImage\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalDownloadUtil\nimport com.metrolist.music.LocalListenTogetherManager\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.LocalSyncUtils\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.ListItemHeight\nimport com.metrolist.music.constants.ListThumbnailSize\nimport com.metrolist.music.constants.ThumbnailCornerRadius\nimport com.metrolist.music.db.entities.SpeedDialItem\nimport com.metrolist.music.db.entities.SongEntity\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.models.MediaMetadata\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.playback.ExoDownloadService\nimport com.metrolist.music.playback.queues.YouTubeQueue\nimport com.metrolist.music.ui.component.ListDialog\nimport com.metrolist.music.ui.component.LocalBottomSheetPageState\nimport com.metrolist.music.ui.component.Material3MenuGroup\nimport com.metrolist.music.ui.component.Material3MenuItemData\nimport com.metrolist.music.ui.component.NewAction\nimport com.metrolist.music.ui.component.NewActionGrid\nimport com.metrolist.music.ui.utils.ShowMediaInfo\nimport com.metrolist.music.ui.utils.resize\nimport com.metrolist.music.utils.joinByBullet\nimport com.metrolist.music.utils.makeTimeString\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport timber.log.Timber\nimport java.time.LocalDateTime\n\n@SuppressLint(\"MutableCollectionMutableState\")\n@Composable\nfun YouTubeSongMenu(\n    song: SongItem,\n    navController: NavController,\n    onDismiss: () -> Unit,\n    onHistoryRemoved: () -> Unit = {}\n) {\n    val context = LocalContext.current\n    val database = LocalDatabase.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val librarySong by database.song(song.id).collectAsState(initial = null)\n    val download by LocalDownloadUtil.current.getDownload(song.id).collectAsState(initial = null)\n    val coroutineScope = rememberCoroutineScope()\n    val syncUtils = LocalSyncUtils.current\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val isPinned by database.speedDialDao.isPinned(song.id).collectAsState(initial = false)\n    val artists = remember {\n        song.artists.mapNotNull {\n            it.id?.let { artistId ->\n                MediaMetadata.Artist(id = artistId, name = it.name)\n            }\n        }\n    }\n\n    var showChoosePlaylistDialog by rememberSaveable {  \n        mutableStateOf(false)  \n    }  \n\n    AddToPlaylistDialog(\n        isVisible = showChoosePlaylistDialog,\n        onGetSong = { playlist ->\n            database.withTransaction {\n                insert(song.toMediaMetadata())\n            }\n            coroutineScope.launch(Dispatchers.IO) {\n                playlist.playlist.browseId?.let { browseId ->\n                    YouTube.addToPlaylist(browseId, song.id)\n                }\n            }\n            listOf(song.id)\n        },\n        onDismiss = { showChoosePlaylistDialog = false }\n    )  \n\n    var showSelectArtistDialog by rememberSaveable {  \n        mutableStateOf(false)  \n    }  \n\n    if (showSelectArtistDialog) {  \n        ListDialog(  \n            onDismiss = { showSelectArtistDialog = false },  \n        ) {  \n            items(artists) { artist ->  \n                Row(  \n                    verticalAlignment = Alignment.CenterVertically,  \n                    modifier =  \n                    Modifier  \n                        .height(ListItemHeight)  \n                        .clickable {  \n                            navController.navigate(\"artist/${artist.id}\")  \n                            showSelectArtistDialog = false  \n                            onDismiss()  \n                        }  \n                        .padding(horizontal = 12.dp),  \n                ) {  \n                    Box(  \n                        contentAlignment = Alignment.CenterStart,  \n                        modifier =  \n                        Modifier  \n                            .fillParentMaxWidth()  \n                            .height(ListItemHeight)  \n                            .clickable {  \n                                navController.navigate(\"artist/${artist.id}\")  \n                                showSelectArtistDialog = false  \n                                onDismiss()  \n                            }  \n                            .padding(horizontal = 24.dp),  \n                    ) {  \n                        Text(  \n                            text = artist.name,  \n                            fontSize = 18.sp,  \n                            fontWeight = FontWeight.Bold,  \n                            maxLines = 1,  \n                            overflow = TextOverflow.Ellipsis,  \n                        )  \n                    }  \n                }  \n            }  \n        }  \n    }  \n\n    ListItem(  \n        headlineContent = {\n            Text(\n                text = song.title,\n                modifier = Modifier.basicMarquee(),\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n            )\n        },  \n        supportingContent = {  \n            Text(  \n                text = joinByBullet(\n                    song.artists.joinToString { it.name },\n                    song.duration?.let { makeTimeString(it * 1000L) },\n                )\n            )  \n        },  \n        leadingContent = {\n            Box(\n                contentAlignment = Alignment.Center,\n                modifier = Modifier\n                    .size(ListThumbnailSize)\n                    .clip(RoundedCornerShape(ThumbnailCornerRadius))\n            ) {\n                AsyncImage(\n                    model = song.thumbnail,\n                    contentDescription = null,\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .clip(RoundedCornerShape(ThumbnailCornerRadius))\n                )\n            }\n        },\n        trailingContent = {\n            // For episodes, show saved state and toggle save for later\n            val isEpisode = song.isEpisode\n            val isFavorite = if (isEpisode) librarySong?.song?.inLibrary != null else librarySong?.song?.liked == true\n            IconButton(\n                onClick = {\n                    if (isEpisode) {\n                        // Episode: toggle save for later\n                        val currentLibrarySong = librarySong\n                        val isCurrentlySaved = currentLibrarySong?.song?.inLibrary != null\n                        val shouldBeSaved = !isCurrentlySaved\n\n                        // Update local database first (optimistic update)\n                        database.query {\n                            if (currentLibrarySong != null) {\n                                update(currentLibrarySong.song.copy(inLibrary = if (shouldBeSaved) LocalDateTime.now() else null))\n                            } else {\n                                insert(song.toMediaMetadata().toSongEntity().copy(inLibrary = LocalDateTime.now(), isEpisode = true))\n                            }\n                        }\n\n                        // Sync with YouTube (handles login check internally)\n                        coroutineScope.launch(Dispatchers.IO) {\n                            val setVideoId = if (isCurrentlySaved) song.setVideoId ?: database.getSetVideoId(song.id)?.setVideoId else null\n                            syncUtils.saveEpisode(song.id, shouldBeSaved, setVideoId)\n                        }\n                    } else {\n                        // Regular song: toggle like\n                        database.transaction {\n                            librarySong.let { librarySong ->\n                                val s: SongEntity\n                                if (librarySong == null) {\n                                    insert(song.toMediaMetadata(), SongEntity::toggleLike)\n                                    s = song.toMediaMetadata().toSongEntity().let(SongEntity::toggleLike)\n                                } else {\n                                    s = librarySong.song.toggleLike()\n                                    update(s)\n                                }\n                                syncUtils.likeSong(s)\n                            }\n                        }\n                    }\n                },\n            ) {\n                Icon(\n                    painter = painterResource(if (isFavorite) R.drawable.favorite else R.drawable.favorite_border),\n                    tint = if (isFavorite) MaterialTheme.colorScheme.error else LocalContentColor.current,\n                    contentDescription = null,\n                )\n            }\n        },  \n    )  \n\n    HorizontalDivider()\n\n    Spacer(modifier = Modifier.height(12.dp))\n\n    val bottomSheetPageState = LocalBottomSheetPageState.current\n    val configuration = LocalConfiguration.current\n    val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT\n\n    val isGuest = listenTogetherManager?.isInRoom == true && !listenTogetherManager.isHost\n\n    LazyColumn(\n        contentPadding = PaddingValues(\n            start = 0.dp,\n            top = 0.dp,\n            end = 0.dp,\n            bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(),\n        ),\n    ) {\n        item {\n            NewActionGrid(\n                actions = listOfNotNull(\n                    if (!isGuest) {\n                        NewAction(\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.playlist_play),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(28.dp),\n                                    tint = MaterialTheme.colorScheme.onSurfaceVariant\n                                )\n                            },\n                            text = stringResource(R.string.play_next),\n                            onClick = {\n                                playerConnection.playNext(song.copy(thumbnail = song.thumbnail.resize(544,544)).toMediaItem())\n                                onDismiss()\n                            }\n                        )\n                    } else null,\n                    NewAction(\n                        icon = {\n                            Icon(\n                                painter = painterResource(R.drawable.playlist_add),\n                                contentDescription = null,\n                                modifier = Modifier.size(28.dp),\n                                tint = MaterialTheme.colorScheme.onSurfaceVariant\n                            )\n                        },\n                        text = stringResource(R.string.add_to_playlist),\n                        onClick = {\n                            showChoosePlaylistDialog = true\n                        }\n                    ),\n                    NewAction(\n                        icon = {\n                            Icon(\n                                painter = painterResource(R.drawable.share),\n                                contentDescription = null,\n                                modifier = Modifier.size(28.dp),\n                                tint = MaterialTheme.colorScheme.onSurfaceVariant\n                            )\n                        },\n                        text = stringResource(R.string.share),\n                        onClick = {\n                            val intent = Intent().apply {\n                                action = Intent.ACTION_SEND\n                                type = \"text/plain\"\n                                putExtra(Intent.EXTRA_TEXT, song.shareLink)\n                            }\n                            context.startActivity(Intent.createChooser(intent, null))\n                            onDismiss()\n                        }\n                    )\n                ),\n                columns = if (isGuest) 2 else 3,\n                modifier = Modifier.padding(horizontal = 4.dp, vertical = 16.dp)\n            )\n        }\n\n        item {\n            Material3MenuGroup(\n                items = listOfNotNull(\n                    if (listenTogetherManager != null && listenTogetherManager.isInRoom && !listenTogetherManager.isHost) {\n                        Material3MenuItemData(\n                            title = { Text(text = stringResource(R.string.suggest_to_host)) },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.queue_music),\n                                    contentDescription = null,\n                                )\n                            },\n                            onClick = {\n                                val durationMs = if (song.duration != null && song.duration!! > 0) song.duration!! * 1000L else 180000L\n                                val trackInfo = com.metrolist.music.listentogether.TrackInfo(\n                                    id = song.id,\n                                    title = song.title,\n                                    artist = artists.joinToString(\", \") { it.name },\n                                    album = song.album?.name,\n                                    duration = durationMs,\n                                    thumbnail = song.thumbnail\n                                )\n                                listenTogetherManager.suggestTrack(trackInfo)\n                                onDismiss()\n                            }\n                        )\n                    } else null,\n                    if (!isGuest) {\n                        Material3MenuItemData(\n                            title = { Text(text = stringResource(R.string.start_radio)) },\n                            description = { Text(text = stringResource(R.string.start_radio_desc)) },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.radio),\n                                    contentDescription = null,\n                                )\n                            },\n                            onClick = {\n                                playerConnection.playQueue(YouTubeQueue.radio(song.toMediaMetadata()))\n                                onDismiss()\n                            }\n                        )\n                    } else null,\n                    if (!isGuest) {\n                        Material3MenuItemData(\n                            title = { Text(text = stringResource(R.string.add_to_queue)) },\n                            description = { Text(text = stringResource(R.string.add_to_queue_desc)) },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.queue_music),\n                                    contentDescription = null,\n                                )\n                            },\n                            onClick = {\n                                playerConnection.addToQueue(song.toMediaItem())\n                                onDismiss()\n                            }\n                        )\n                    } else null\n                )\n            )\n        }\n\n        item { Spacer(modifier = Modifier.height(12.dp)) }\n\n        item {\n            Material3MenuGroup(\n                items = buildList {\n                    // Save/Remove for Later option for podcast episodes\n                    if (song.isEpisode) {\n                        if (song.setVideoId != null) {\n                            // Episode is saved - show remove option\n                            add(\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.remove_episode_from_saved)) },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.remove),\n                                            contentDescription = null,\n                                        )\n                                    },\n                                    onClick = {\n                                        // Update local database first (optimistic update)\n                                        database.query {\n                                            librarySong?.song?.let { update(it.copy(inLibrary = null)) }\n                                        }\n                                        // Sync with YouTube (handles login check internally)\n                                        syncUtils.saveEpisode(song.id, false, song.setVideoId)\n                                        onDismiss()\n                                    }\n                                )\n                            )\n                        } else {\n                            // Episode not saved - show save option\n                            add(\n                                Material3MenuItemData(\n                                    title = { Text(text = stringResource(R.string.save_episode_for_later)) },\n                                    description = { Text(text = stringResource(R.string.save_episode_for_later_desc)) },\n                                    icon = {\n                                        Icon(\n                                            painter = painterResource(R.drawable.playlist_add),\n                                            contentDescription = null,\n                                        )\n                                    },\n                                    onClick = {\n                                        // Update local database first (optimistic update)\n                                        database.query {\n                                            if (librarySong != null) {\n                                                update(librarySong!!.song.copy(inLibrary = java.time.LocalDateTime.now()))\n                                            } else {\n                                                insert(song.toMediaMetadata().toSongEntity().copy(inLibrary = java.time.LocalDateTime.now(), isEpisode = true))\n                                            }\n                                        }\n                                        // Sync with YouTube (handles login check internally)\n                                        syncUtils.saveEpisode(song.id, true, null)\n                                        onDismiss()\n                                    }\n                                )\n                            )\n                        }\n                    }\n                    if (song.historyRemoveToken != null) {\n                        add(\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.remove_from_history)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.delete),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    coroutineScope.launch {\n                                        Timber.d(\"[HISTORY_REMOVE] Removing song ${song.id} from YTM history\")\n                                        YouTube.feedback(listOf(song.historyRemoveToken!!))\n                                            .onSuccess {\n                                                Timber.d(\"[HISTORY_REMOVE] Successfully removed from YTM history\")\n                                            }\n                                            .onFailure { e ->\n                                                Timber.e(e, \"[HISTORY_REMOVE] Failed to remove from YTM history\")\n                                            }\n                                        delay(500)\n                                        onHistoryRemoved()\n                                        onDismiss()\n                                    }\n                                }\n                            )\n                        )\n                    }\n                    add(\n                        Material3MenuItemData(\n                            title = { \n                                Text(\n                                    text = if (isPinned) stringResource(R.string.unpin_from_speed_dial) else stringResource(R.string.pin_to_speed_dial)\n                                ) \n                            },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(if (isPinned) R.drawable.remove else R.drawable.add),\n                                    contentDescription = null,\n                                )\n                            },\n                            onClick = {\n                                coroutineScope.launch(Dispatchers.IO) {\n                                    if (isPinned) {\n                                        database.speedDialDao.delete(song.id)\n                                    } else {\n                                        database.speedDialDao.insert(SpeedDialItem.fromYTItem(song))\n                                    }\n                                }\n                                onDismiss()\n                            }\n                        )\n                    )\n                    add(\n                        Material3MenuItemData(\n                            title = {\n                                Text(text = if (librarySong?.song?.inLibrary != null) stringResource(R.string.remove_from_library) else stringResource(R.string.add_to_library))\n                            },\n                            description = { Text(text = stringResource(R.string.add_to_library_desc)) },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(if (librarySong?.song?.inLibrary != null) R.drawable.library_add_check else R.drawable.library_add),\n                                    contentDescription = null,\n                                )\n                            },\n                            onClick = {\n                                val isInLibrary = librarySong?.song?.inLibrary != null\n\n                                // Use the new reliable method that fetches fresh tokens\n                                coroutineScope.launch {\n                                    YouTube.toggleSongLibrary(song.id, !isInLibrary)\n                                }\n\n                                if (isInLibrary) {\n                                    database.query {\n                                        inLibrary(song.id, null)\n                                    }\n                                } else {\n                                    database.transaction {\n                                        insert(song.toMediaMetadata())\n                                        inLibrary(song.id, LocalDateTime.now())\n                                        addLibraryTokens(\n                                            song.id,\n                                            song.libraryAddToken,\n                                            song.libraryRemoveToken\n                                        )\n                                    }\n                                }\n                            }\n                        )\n                    )\n                }\n            )\n        }\n\n        item { Spacer(modifier = Modifier.height(12.dp)) }\n\n        item {\n            Material3MenuGroup(\n                items = listOf(\n                    when (download?.state) {\n                        Download.STATE_COMPLETED -> {\n                            Material3MenuItemData(\n                                title = {\n                                    Text(\n                                        text = stringResource(R.string.remove_download)\n                                    )\n                                },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.offline),\n                                        contentDescription = null\n                                    )\n                                },\n                                onClick = {\n                                    DownloadService.sendRemoveDownload(\n                                        context,\n                                        ExoDownloadService::class.java,\n                                        song.id,\n                                        false,\n                                    )\n                                }\n                            )\n                        }\n                        Download.STATE_QUEUED, Download.STATE_DOWNLOADING -> {\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.downloading)) },\n                                icon = {\n                                    CircularProgressIndicator(\n                                        modifier = Modifier.size(24.dp),\n                                        strokeWidth = 2.dp\n                                    )\n                                },\n                                onClick = {\n                                    DownloadService.sendRemoveDownload(\n                                        context,\n                                        ExoDownloadService::class.java,\n                                        song.id,\n                                        false,\n                                    )\n                                }\n                            )\n                        }\n                        else -> {\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.action_download)) },\n                                description = { Text(text = stringResource(R.string.download_desc)) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.download),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    database.transaction {\n                                        insert(song.toMediaMetadata())\n                                    }\n                                    val downloadRequest = DownloadRequest\n                                        .Builder(song.id, song.id.toUri())\n                                        .setCustomCacheKey(song.id)\n                                        .setData(song.title.toByteArray())\n                                        .build()\n                                    DownloadService.sendAddDownload(\n                                        context,\n                                        ExoDownloadService::class.java,\n                                        downloadRequest,\n                                        false,\n                                    )\n                                }\n                            )\n                        }\n                    }\n                )\n            )\n        }\n\n        item { Spacer(modifier = Modifier.height(12.dp)) }\n\n        item {\n            // Check if this is a podcast episode (album ID doesn't start with MPREb_)\n            val isPodcast = song.album?.let { !it.id.startsWith(\"MPREb_\") } ?: false\n\n            Material3MenuGroup(\n                items = buildList {\n                    // Don't show \"View Artist\" for podcasts - only show \"View Podcast\"\n                    if (artists.isNotEmpty() && !isPodcast) {\n                        add(\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(R.string.view_artist)) },\n                                description = { Text(text = song.artists.joinToString { it.name }) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(R.drawable.artist),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    if (artists.size == 1) {\n                                        navController.navigate(\"artist/${artists[0].id}\")\n                                        onDismiss()\n                                    } else {\n                                        showSelectArtistDialog = true\n                                    }\n                                }\n                            )\n                        )\n                    }\n                    song.album?.let { album ->\n                        add(\n                            Material3MenuItemData(\n                                title = { Text(text = stringResource(if (isPodcast) R.string.view_podcast else R.string.view_album)) },\n                                description = { Text(text = album.name) },\n                                icon = {\n                                    Icon(\n                                        painter = painterResource(if (isPodcast) R.drawable.mic else R.drawable.album),\n                                        contentDescription = null,\n                                    )\n                                },\n                                onClick = {\n                                    if (isPodcast) {\n                                        navController.navigate(\"online_podcast/${album.id}\")\n                                    } else {\n                                        navController.navigate(\"album/${album.id}\")\n                                    }\n                                    onDismiss()\n                                }\n                            )\n                        )\n                    }\n                    add(\n                        Material3MenuItemData(\n                            title = { Text(text = stringResource(R.string.details)) },\n                            description = { Text(text = stringResource(R.string.details_desc)) },\n                            icon = {\n                                Icon(\n                                    painter = painterResource(R.drawable.info),\n                                    contentDescription = null,\n                                )\n                            },\n                            onClick = {\n                                onDismiss()\n                                bottomSheetPageState.show {\n                                    ShowMediaInfo(song.id)\n                                }\n                            }\n                        )\n                    )\n                }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/player/MiniPlayer.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n *\n * Performance optimized MiniPlayer - prevents unnecessary recomposition\n */\n\npackage com.metrolist.music.ui.player\n\nimport android.content.res.Configuration\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.animation.core.spring\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.basicMarquee\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.gestures.detectHorizontalDragGestures\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.MutableLongState\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.drawWithContent\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.platform.LocalWindowInfo\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.media3.common.Player\nimport coil3.compose.AsyncImage\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalListenTogetherManager\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.CropAlbumArtKey\nimport com.metrolist.music.constants.DarkModeKey\nimport com.metrolist.music.constants.MiniPlayerHeight\nimport com.metrolist.music.constants.PureBlackMiniPlayerKey\nimport com.metrolist.music.constants.SwipeSensitivityKey\nimport com.metrolist.music.constants.SwipeThumbnailKey\nimport com.metrolist.music.constants.ThumbnailCornerRadius\nimport com.metrolist.music.constants.UseNewMiniPlayerDesignKey\nimport com.metrolist.music.db.entities.ArtistEntity\nimport com.metrolist.music.listentogether.ListenTogetherManager\nimport com.metrolist.music.models.MediaMetadata\nimport com.metrolist.music.playback.CastConnectionHandler\nimport com.metrolist.music.playback.PlayerConnection\nimport com.metrolist.music.ui.screens.settings.DarkMode\nimport com.metrolist.music.ui.utils.resize\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport kotlinx.coroutines.launch\nimport kotlin.math.absoluteValue\nimport kotlin.math.roundToInt\nimport com.metrolist.music.ui.component.Icon as MIcon\n\n/**\n * Stable wrapper for progress state - reads values only during draw phase\n * This prevents recomposition when position/duration change\n */\n@Stable\nclass ProgressState(\n    private val positionState: MutableLongState,\n    private val durationState: MutableLongState,\n) {\n    val progress: Float\n        get() {\n            val duration = durationState.longValue\n            return if (duration > 0) (positionState.longValue.toFloat() / duration).coerceIn(0f, 1f) else 0f\n        }\n}\n\n@Composable\nfun MiniPlayer(\n    positionState: MutableLongState,\n    durationState: MutableLongState,\n    modifier: Modifier = Modifier,\n) {\n    val useNewMiniPlayerDesign by rememberPreference(UseNewMiniPlayerDesignKey, true)\n\n    // Create stable progress state - doesn't cause recomposition on position changes\n    val progressState = remember { ProgressState(positionState, durationState) }\n\n    if (useNewMiniPlayerDesign) {\n        NewMiniPlayer(\n            progressState = progressState,\n            modifier = modifier,\n        )\n    } else {\n        Box(modifier = modifier.fillMaxWidth()) {\n            LegacyMiniPlayer(\n                progressState = progressState,\n                modifier = Modifier.align(Alignment.Center),\n            )\n        }\n    }\n}\n\n// ============================================================================\n// NEW MINI PLAYER DESIGN\n// ============================================================================\n\n@Composable\nprivate fun NewMiniPlayer(\n    progressState: ProgressState,\n    modifier: Modifier = Modifier,\n) {\n    val playerConnection = LocalPlayerConnection.current ?: return\n\n    // Theme settings - these rarely change\n    val pureBlack by rememberPreference(PureBlackMiniPlayerKey, defaultValue = false)\n    val isSystemInDarkTheme = isSystemInDarkTheme()\n    val darkTheme by rememberEnumPreference(DarkModeKey, defaultValue = DarkMode.AUTO)\n    val useDarkTheme =\n        remember(darkTheme, isSystemInDarkTheme) {\n            if (darkTheme == DarkMode.AUTO) isSystemInDarkTheme else darkTheme == DarkMode.ON\n        }\n\n    // Player states - only collect what's needed at this level\n    val playbackState by playerConnection.playbackState.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n    val canSkipNext by playerConnection.canSkipNext.collectAsState()\n    val canSkipPrevious by playerConnection.canSkipPrevious.collectAsState()\n\n    // Cast state - safely access castConnectionHandler to prevent crashes during service lifecycle changes\n    val castHandler =\n        remember(playerConnection) {\n            try {\n                playerConnection.service.castConnectionHandler\n            } catch (e: Exception) {\n                null\n            }\n        }\n    val isCasting by castHandler?.isCasting?.collectAsState() ?: remember { mutableStateOf(false) }\n\n    // Swipe settings\n    val swipeSensitivity by rememberPreference(SwipeSensitivityKey, 0.73f)\n    val swipeThumbnailPref by rememberPreference(SwipeThumbnailKey, true)\n\n    // Disable swipe for Listen Together guests\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val isListenTogetherGuest = listenTogetherManager?.let { it.isInRoom && !it.isHost } ?: false\n    val swipeThumbnail = swipeThumbnailPref && !isListenTogetherGuest\n\n    val layoutDirection = LocalLayoutDirection.current\n    val coroutineScope = rememberCoroutineScope()\n\n    val windowInfo = LocalWindowInfo.current\n    val configuration = LocalConfiguration.current\n    val density = LocalDensity.current\n    val isTabletLandscape =\n        remember(windowInfo.containerSize.width, configuration.orientation) {\n            (windowInfo.containerSize.width / density.density) >= 600f && configuration.orientation == Configuration.ORIENTATION_LANDSCAPE\n        }\n\n    // Swipe animation state\n    val offsetXAnimatable = remember { Animatable(0f) }\n    var dragStartTime by remember { mutableLongStateOf(0L) }\n    var totalDragDistance by remember { mutableFloatStateOf(0f) }\n\n    val animationSpec =\n        remember {\n            spring<Float>(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessLow)\n        }\n\n    val autoSwipeThreshold =\n        remember(swipeSensitivity) {\n            (600 / (1f + kotlin.math.exp(-(-11.44748 * swipeSensitivity + 9.04945)))).roundToInt()\n        }\n\n    // Memoize colors\n    val backgroundColor = if (pureBlack && useDarkTheme) Color.Black else MaterialTheme.colorScheme.surfaceContainer\n    val primaryColor = MaterialTheme.colorScheme.primary\n    val outlineColor = MaterialTheme.colorScheme.outline\n    val onSurfaceColor = MaterialTheme.colorScheme.onSurface\n    val errorColor = MaterialTheme.colorScheme.error\n\n    Box(\n        modifier =\n            modifier\n                .fillMaxWidth()\n                .height(MiniPlayerHeight)\n                .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal))\n                .padding(horizontal = 12.dp)\n                .let { baseModifier ->\n                    if (swipeThumbnail) {\n                        baseModifier.pointerInput(Unit) {\n                            detectHorizontalDragGestures(\n                                onDragStart = {\n                                    dragStartTime = System.currentTimeMillis()\n                                    totalDragDistance = 0f\n                                },\n                                onDragCancel = {\n                                    coroutineScope.launch {\n                                        offsetXAnimatable.animateTo(0f, animationSpec)\n                                    }\n                                },\n                                onHorizontalDrag = { _, dragAmount ->\n                                    val adjustedDragAmount =\n                                        if (layoutDirection == LayoutDirection.Rtl) -dragAmount else dragAmount\n                                    val canSkipPrevious = playerConnection.player.previousMediaItemIndex != -1\n                                    val canSkipNext = playerConnection.player.nextMediaItemIndex != -1\n                                    val tryingToSwipeRight = adjustedDragAmount > 0\n                                    val tryingToSwipeLeft = adjustedDragAmount < 0\n                                    val allowLeft = tryingToSwipeLeft && canSkipNext\n                                    val allowRight = tryingToSwipeRight && canSkipPrevious\n\n                                    val canReturnToCenter =\n                                        (tryingToSwipeRight && !canSkipPrevious && offsetXAnimatable.value < 0) ||\n                                            (tryingToSwipeLeft && !canSkipNext && offsetXAnimatable.value > 0)\n\n                                    if (allowLeft || allowRight || canReturnToCenter) {\n                                        totalDragDistance += kotlin.math.abs(adjustedDragAmount)\n                                        coroutineScope.launch {\n                                            offsetXAnimatable.snapTo(offsetXAnimatable.value + adjustedDragAmount)\n                                        }\n                                    }\n                                },\n                                onDragEnd = {\n                                    val dragDuration = System.currentTimeMillis() - dragStartTime\n                                    val velocity = if (dragDuration > 0) totalDragDistance / dragDuration else 0f\n                                    val currentOffset = offsetXAnimatable.value\n                                    val minDistanceThreshold = 50f\n                                    val velocityThreshold = (swipeSensitivity * -8.25f) + 8.5f\n\n                                    val shouldChangeSong =\n                                        (kotlin.math.abs(currentOffset) > minDistanceThreshold && velocity > velocityThreshold) ||\n                                            (kotlin.math.abs(currentOffset) > autoSwipeThreshold)\n\n                                    if (shouldChangeSong) {\n                                        if (currentOffset > 0 && canSkipPrevious) {\n                                            playerConnection.player.seekToPreviousMediaItem()\n                                        } else if (currentOffset <= 0 && canSkipNext) {\n                                            playerConnection.player.seekToNext()\n                                        }\n                                    }\n                                    coroutineScope.launch {\n                                        offsetXAnimatable.animateTo(0f, animationSpec)\n                                    }\n                                },\n                            )\n                        }\n                    } else {\n                        baseModifier\n                    }\n                },\n    ) {\n        Box(\n            modifier =\n                Modifier\n                    .then(if (isTabletLandscape) Modifier.width(500.dp).align(Alignment.Center) else Modifier.fillMaxWidth())\n                    .height(64.dp)\n                    .offset { IntOffset(offsetXAnimatable.value.roundToInt(), 0) }\n                    .clip(RoundedCornerShape(32.dp))\n                    .background(color = backgroundColor)\n                    .border(1.dp, outlineColor.copy(alpha = 0.3f), RoundedCornerShape(32.dp)),\n        ) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp, vertical = 8.dp),\n            ) {\n                // Play button with progress - isolated composable\n                NewMiniPlayerPlayButton(\n                    progressState = progressState,\n                    playbackState = playbackState,\n                    isCasting = isCasting,\n                    castHandler = castHandler,\n                    playerConnection = playerConnection,\n                    mediaMetadata = mediaMetadata,\n                    primaryColor = primaryColor,\n                    outlineColor = outlineColor,\n                    listenTogetherManager = listenTogetherManager,\n                )\n\n                Spacer(modifier = Modifier.width(16.dp))\n\n                // Song info - isolated composable\n                NewMiniPlayerSongInfo(\n                    mediaMetadata = mediaMetadata,\n                    onSurfaceColor = onSurfaceColor,\n                    errorColor = errorColor,\n                    modifier = Modifier.weight(1f),\n                )\n\n                Spacer(modifier = Modifier.width(12.dp))\n\n                // Cast indicator\n                if (isCasting) {\n                    Icon(\n                        painter = painterResource(R.drawable.cast_connected),\n                        contentDescription = \"Casting\",\n                        tint = primaryColor,\n                        modifier = Modifier.size(20.dp),\n                    )\n                    Spacer(modifier = Modifier.width(12.dp))\n                }\n\n                // Subscribe button - isolated composable\n                mediaMetadata?.artists?.firstOrNull()?.id?.let { artistId ->\n                    SubscribeButton(artistId = artistId, metadata = mediaMetadata!!)\n                }\n\n                Spacer(modifier = Modifier.width(8.dp))\n\n                // Favorite button - isolated composable\n                mediaMetadata?.let { FavoriteButton(songId = it.id) }\n            }\n        }\n    }\n}\n\n/**\n * Play button with circular progress indicator\n * Uses drawWithContent to update progress without recomposition\n */\n@Composable\nprivate fun NewMiniPlayerPlayButton(\n    progressState: ProgressState,\n    playbackState: Int,\n    isCasting: Boolean,\n    castHandler: CastConnectionHandler?,\n    playerConnection: PlayerConnection,\n    mediaMetadata: MediaMetadata?,\n    primaryColor: Color,\n    outlineColor: Color,\n    listenTogetherManager: ListenTogetherManager?,\n) {\n    val isPlaying by playerConnection.isPlaying.collectAsState()\n    val castIsPlaying by castHandler?.castIsPlaying?.collectAsState() ?: remember { mutableStateOf(false) }\n    val effectiveIsPlaying = if (isCasting) castIsPlaying else isPlaying\n    val isListenTogetherGuest = listenTogetherManager?.let { it.isInRoom && !it.isHost } ?: false\n    val isMuted by playerConnection.isMuted.collectAsState()\n\n    val trackColor = outlineColor.copy(alpha = 0.2f)\n    val strokeWidth = 3.dp\n\n    Box(\n        contentAlignment = Alignment.Center,\n        modifier =\n            Modifier\n                .size(48.dp)\n                .drawWithContent {\n                    drawContent()\n                    // Draw progress arc - this reads progressState.progress during draw phase only\n                    val progress = progressState.progress\n                    val stroke = Stroke(width = strokeWidth.toPx(), cap = StrokeCap.Round)\n                    val startAngle = -90f\n                    val sweepAngle = 360f * progress\n                    val diameter = size.minDimension\n                    val topLeft = Offset((size.width - diameter) / 2, (size.height - diameter) / 2)\n\n                    // Draw track\n                    drawArc(\n                        color = trackColor,\n                        startAngle = 0f,\n                        sweepAngle = 360f,\n                        useCenter = false,\n                        topLeft = topLeft,\n                        size = Size(diameter, diameter),\n                        style = stroke,\n                    )\n                    // Draw progress\n                    drawArc(\n                        color = primaryColor,\n                        startAngle = startAngle,\n                        sweepAngle = sweepAngle,\n                        useCenter = false,\n                        topLeft = topLeft,\n                        size = Size(diameter, diameter),\n                        style = stroke,\n                    )\n                },\n    ) {\n        // Thumbnail with play/pause overlay\n        Box(\n            contentAlignment = Alignment.Center,\n            modifier =\n                Modifier\n                    .size(40.dp)\n                    .clip(CircleShape)\n                    .border(1.dp, outlineColor.copy(alpha = 0.3f), CircleShape)\n                    .clickable {\n                        if (isListenTogetherGuest) {\n                            playerConnection.toggleMute()\n                            return@clickable\n                        }\n                        if (isCasting) {\n                            if (castIsPlaying) castHandler?.pause() else castHandler?.play()\n                        } else if (playbackState == Player.STATE_ENDED) {\n                            playerConnection.player.seekTo(0, 0)\n                            playerConnection.player.playWhenReady = true\n                        } else {\n                            playerConnection.togglePlayPause()\n                        }\n                    },\n        ) {\n            mediaMetadata?.let { metadata ->\n                val thumbnailUrl =\n                    remember(metadata.thumbnailUrl) {\n                        metadata.thumbnailUrl?.resize(120, 120)\n                    }\n                AsyncImage(\n                    model = thumbnailUrl,\n                    contentDescription = null,\n                    contentScale = ContentScale.Crop,\n                    modifier = Modifier.fillMaxSize().clip(CircleShape),\n                )\n            }\n\n            // Overlay for paused state or muted (guest)\n            if (isListenTogetherGuest && isMuted ||\n                (!isListenTogetherGuest && (!effectiveIsPlaying || playbackState == Player.STATE_ENDED))\n            ) {\n                Box(\n                    modifier =\n                        Modifier\n                            .fillMaxSize()\n                            .background(Color.Black.copy(alpha = 0.4f), CircleShape),\n                )\n                Icon(\n                    painter =\n                        painterResource(\n                            if (isListenTogetherGuest) {\n                                if (isMuted) R.drawable.volume_off else R.drawable.volume_up\n                            } else if (playbackState == Player.STATE_ENDED) {\n                                R.drawable.replay\n                            } else {\n                                R.drawable.play\n                            },\n                        ),\n                    contentDescription = null,\n                    tint = Color.White,\n                    modifier = Modifier.size(20.dp),\n                )\n            }\n        }\n    }\n}\n\n/**\n * Song info display - title and artist\n */\n@Composable\nprivate fun NewMiniPlayerSongInfo(\n    mediaMetadata: MediaMetadata?,\n    onSurfaceColor: Color,\n    errorColor: Color,\n    modifier: Modifier = Modifier,\n) {\n    val error by LocalPlayerConnection.current?.error?.collectAsState() ?: remember { mutableStateOf(null) }\n\n    Column(\n        modifier = modifier,\n        verticalArrangement = Arrangement.Center,\n    ) {\n        mediaMetadata?.let { metadata ->\n            Text(\n                text = metadata.title,\n                color = onSurfaceColor,\n                fontSize = 14.sp,\n                fontWeight = FontWeight.Medium,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                modifier = Modifier.basicMarquee(iterations = 1, initialDelayMillis = 3000, velocity = 30.dp),\n            )\n            Row(\n                horizontalArrangement = Arrangement.SpaceBetween,\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                if (metadata.explicit) MIcon.Explicit()\n                if (metadata.artists.any { it.name.isNotBlank() }) {\n                    Text(\n                        text = metadata.artists.joinToString { it.name },\n                        color = onSurfaceColor.copy(alpha = 0.7f),\n                        fontSize = 12.sp,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis,\n                        modifier = Modifier.basicMarquee(iterations = 1, initialDelayMillis = 3000, velocity = 30.dp),\n                    )\n                }\n            }\n\n            AnimatedVisibility(visible = error != null, enter = fadeIn(), exit = fadeOut()) {\n                Text(\n                    text = stringResource(R.string.error_playing),\n                    color = errorColor,\n                    fontSize = 10.sp,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                )\n            }\n        }\n    }\n}\n\n// ============================================================================\n// LEGACY MINI PLAYER DESIGN\n// ============================================================================\n\n@Composable\nprivate fun LegacyMiniPlayer(\n    progressState: ProgressState,\n    modifier: Modifier = Modifier,\n) {\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val pureBlack by rememberPreference(PureBlackMiniPlayerKey, defaultValue = false)\n\n    val playbackState by playerConnection.playbackState.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n    val canSkipNext by playerConnection.canSkipNext.collectAsState()\n    val canSkipPrevious by playerConnection.canSkipPrevious.collectAsState()\n\n    val castHandler =\n        remember(playerConnection) {\n            try {\n                playerConnection.service.castConnectionHandler\n            } catch (e: Exception) {\n                null\n            }\n        }\n    val isCasting by castHandler?.isCasting?.collectAsState() ?: remember { mutableStateOf(false) }\n\n    val swipeSensitivity by rememberPreference(SwipeSensitivityKey, 0.73f)\n    val swipeThumbnailPref by rememberPreference(SwipeThumbnailKey, true)\n\n    // Disable swipe for Listen Together guests\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val isListenTogetherGuest = listenTogetherManager?.let { it.isInRoom && !it.isHost } ?: false\n    val swipeThumbnail = swipeThumbnailPref && !isListenTogetherGuest\n\n    val layoutDirection = LocalLayoutDirection.current\n    val coroutineScope = rememberCoroutineScope()\n\n    val windowInfo = LocalWindowInfo.current\n    val configuration = LocalConfiguration.current\n    val density = LocalDensity.current\n    val isTabletLandscape =\n        remember(windowInfo.containerSize.width, configuration.orientation) {\n            (windowInfo.containerSize.width / density.density) >= 600f && configuration.orientation == Configuration.ORIENTATION_LANDSCAPE\n        }\n\n    val offsetXAnimatable = remember { Animatable(0f) }\n    var dragStartTime by remember { mutableLongStateOf(0L) }\n    var totalDragDistance by remember { mutableFloatStateOf(0f) }\n\n    val animationSpec =\n        remember {\n            spring<Float>(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessLow)\n        }\n\n    val autoSwipeThreshold =\n        remember(swipeSensitivity) {\n            (600 / (1f + kotlin.math.exp(-(-11.44748 * swipeSensitivity + 9.04945)))).roundToInt()\n        }\n\n    val primaryColor = MaterialTheme.colorScheme.primary\n    val trackColor = MaterialTheme.colorScheme.surfaceVariant\n\n    Box(\n        modifier =\n            modifier\n                .then(if (isTabletLandscape) Modifier.width(500.dp) else Modifier.fillMaxWidth())\n                .height(MiniPlayerHeight)\n                .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal))\n                .clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp))\n                .background(\n                    if (pureBlack && isSystemInDarkTheme()) {\n                        Color.Black\n                    } else {\n                        MaterialTheme.colorScheme.surfaceContainer\n                    },\n                ).let { baseModifier ->\n                    if (swipeThumbnail) {\n                        baseModifier.pointerInput(Unit) {\n                            detectHorizontalDragGestures(\n                                onDragStart = {\n                                    dragStartTime = System.currentTimeMillis()\n                                    totalDragDistance = 0f\n                                },\n                                onDragCancel = {\n                                    coroutineScope.launch { offsetXAnimatable.animateTo(0f, animationSpec) }\n                                },\n                                onHorizontalDrag = { _, dragAmount ->\n                                    val adjustedDragAmount =\n                                        if (layoutDirection == LayoutDirection.Rtl) -dragAmount else dragAmount\n                                    val canSkipPrevious = playerConnection.player.previousMediaItemIndex != -1\n                                    val canSkipNext = playerConnection.player.nextMediaItemIndex != -1\n                                    val tryingToSwipeRight = adjustedDragAmount > 0\n                                    val tryingToSwipeLeft = adjustedDragAmount < 0\n                                    val allowLeft = tryingToSwipeLeft && canSkipNext\n                                    val allowRight = tryingToSwipeRight && canSkipPrevious\n\n                                    val canReturnToCenter =\n                                        (tryingToSwipeRight && !canSkipPrevious && offsetXAnimatable.value < 0) ||\n                                            (tryingToSwipeLeft && !canSkipNext && offsetXAnimatable.value > 0)\n\n                                    if (allowLeft || allowRight || canReturnToCenter) {\n                                        totalDragDistance += kotlin.math.abs(adjustedDragAmount)\n                                        coroutineScope.launch {\n                                            offsetXAnimatable.snapTo(offsetXAnimatable.value + adjustedDragAmount)\n                                        }\n                                    }\n                                },\n                                onDragEnd = {\n                                    val dragDuration = System.currentTimeMillis() - dragStartTime\n                                    val velocity = if (dragDuration > 0) totalDragDistance / dragDuration else 0f\n                                    val currentOffset = offsetXAnimatable.value\n                                    val minDistanceThreshold = 50f\n                                    val velocityThreshold = (swipeSensitivity * -8.25f) + 8.5f\n\n                                    val shouldChangeSong =\n                                        (kotlin.math.abs(currentOffset) > minDistanceThreshold && velocity > velocityThreshold) ||\n                                            (kotlin.math.abs(currentOffset) > autoSwipeThreshold)\n\n                                    if (shouldChangeSong) {\n                                        if (currentOffset > 0 && canSkipPrevious) {\n                                            playerConnection.player.seekToPreviousMediaItem()\n                                        } else if (currentOffset <= 0 && canSkipNext) {\n                                            playerConnection.player.seekToNext()\n                                        }\n                                    }\n                                    coroutineScope.launch { offsetXAnimatable.animateTo(0f, animationSpec) }\n                                },\n                            )\n                        }\n                    } else {\n                        baseModifier\n                    }\n                },\n    ) {\n        // Progress bar - uses drawWithContent to avoid recomposition\n        Box(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .height(2.dp)\n                    .align(Alignment.BottomCenter)\n                    .drawWithContent {\n                        val progress = progressState.progress\n                        drawRect(trackColor)\n                        drawRect(primaryColor, size = Size(size.width * progress, size.height))\n                    },\n        )\n\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n            modifier =\n                Modifier\n                    .fillMaxSize()\n                    .offset { IntOffset(offsetXAnimatable.value.roundToInt(), 0) }\n                    .padding(end = 12.dp),\n        ) {\n            Box(Modifier.weight(1f)) {\n                mediaMetadata?.let {\n                    LegacyMiniMediaInfo(\n                        mediaMetadata = it,\n                        pureBlack = pureBlack,\n                        modifier = Modifier.padding(horizontal = 6.dp),\n                    )\n                }\n            }\n\n            LegacyPlayPauseButton(\n                playbackState = playbackState,\n                isCasting = isCasting,\n                castHandler = castHandler,\n                playerConnection = playerConnection,\n                listenTogetherManager = listenTogetherManager,\n            )\n\n            IconButton(\n                enabled = canSkipNext && !isListenTogetherGuest,\n                onClick = if (isListenTogetherGuest) ({}) else ({ playerConnection.seekToNext() }),\n            ) {\n                Icon(painter = painterResource(R.drawable.skip_next), contentDescription = null)\n            }\n        }\n\n        // Swipe indicator\n        if (offsetXAnimatable.value.absoluteValue > 50f) {\n            Box(\n                modifier =\n                    Modifier\n                        .align(if (offsetXAnimatable.value > 0) Alignment.CenterStart else Alignment.CenterEnd)\n                        .padding(horizontal = 16.dp),\n            ) {\n                Icon(\n                    painter =\n                        painterResource(\n                            if (offsetXAnimatable.value > 0) R.drawable.skip_previous else R.drawable.skip_next,\n                        ),\n                    contentDescription = null,\n                    tint =\n                        primaryColor.copy(\n                            alpha = (offsetXAnimatable.value.absoluteValue / autoSwipeThreshold).coerceIn(0f, 1f),\n                        ),\n                    modifier = Modifier.size(24.dp),\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun LegacyPlayPauseButton(\n    playbackState: Int,\n    isCasting: Boolean,\n    castHandler: CastConnectionHandler?,\n    playerConnection: PlayerConnection,\n    listenTogetherManager: ListenTogetherManager?,\n) {\n    val isPlaying by playerConnection.isPlaying.collectAsState()\n    val castIsPlaying by castHandler?.castIsPlaying?.collectAsState() ?: remember { mutableStateOf(false) }\n    val effectiveIsPlaying = if (isCasting) castIsPlaying else isPlaying\n    val isListenTogetherGuest = listenTogetherManager?.let { it.isInRoom && !it.isHost } ?: false\n    val isMuted by playerConnection.isMuted.collectAsState()\n\n    IconButton(\n        onClick = {\n            if (isListenTogetherGuest) {\n                playerConnection.toggleMute()\n                return@IconButton\n            }\n            if (isCasting) {\n                if (castIsPlaying) castHandler?.pause() else castHandler?.play()\n            } else if (playbackState == Player.STATE_ENDED) {\n                playerConnection.player.seekTo(0, 0)\n                playerConnection.player.playWhenReady = true\n            } else {\n                playerConnection.togglePlayPause()\n            }\n        },\n    ) {\n        Icon(\n            painter =\n                painterResource(\n                    when {\n                        isListenTogetherGuest -> if (isMuted) R.drawable.volume_off else R.drawable.volume_up\n                        playbackState == Player.STATE_ENDED -> R.drawable.replay\n                        effectiveIsPlaying -> R.drawable.pause\n                        else -> R.drawable.play\n                    },\n                ),\n            contentDescription = null,\n        )\n    }\n}\n\n@Composable\nprivate fun LegacyMiniMediaInfo(\n    mediaMetadata: MediaMetadata,\n    pureBlack: Boolean,\n    modifier: Modifier = Modifier,\n) {\n    val error by LocalPlayerConnection.current?.error?.collectAsState() ?: remember { mutableStateOf(null) }\n    val cropAlbumArt by rememberPreference(CropAlbumArtKey, false)\n\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = modifier,\n    ) {\n        Box(\n            modifier =\n                Modifier\n                    .padding(6.dp)\n                    .size(48.dp)\n                    .clip(RoundedCornerShape(ThumbnailCornerRadius)),\n        ) {\n            Box(\n                modifier =\n                    Modifier\n                        .fillMaxSize()\n                        .background(MaterialTheme.colorScheme.surfaceVariant),\n            )\n\n            val thumbnailUrl =\n                remember(mediaMetadata.thumbnailUrl) {\n                    mediaMetadata.thumbnailUrl?.resize(144, 144)\n                }\n            AsyncImage(\n                model = thumbnailUrl,\n                contentDescription = null,\n                contentScale = if (cropAlbumArt) ContentScale.Crop else ContentScale.Fit,\n                modifier =\n                    Modifier\n                        .fillMaxSize()\n                        .clip(RoundedCornerShape(ThumbnailCornerRadius)),\n            )\n\n            androidx.compose.animation.AnimatedVisibility(visible = error != null, enter = fadeIn(), exit = fadeOut()) {\n                Box(\n                    Modifier\n                        .fillMaxSize()\n                        .background(\n                            color = if (pureBlack) Color.Black else Color.Black.copy(alpha = 0.6f),\n                            shape = RoundedCornerShape(ThumbnailCornerRadius),\n                        ),\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.info),\n                        contentDescription = null,\n                        tint = MaterialTheme.colorScheme.error,\n                        modifier = Modifier.align(Alignment.Center),\n                    )\n                }\n            }\n        }\n\n        Column(\n            modifier =\n                Modifier\n                    .weight(1f)\n                    .padding(horizontal = 6.dp),\n        ) {\n            Text(\n                text = mediaMetadata.title,\n                color = MaterialTheme.colorScheme.onSurface,\n                fontSize = 16.sp,\n                fontWeight = FontWeight.Bold,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                modifier = Modifier.basicMarquee(),\n            )\n\n            if (mediaMetadata.artists.any { it.name.isNotBlank() }) {\n                Text(\n                    text = mediaMetadata.artists.joinToString { it.name },\n                    color = MaterialTheme.colorScheme.secondary,\n                    fontSize = 12.sp,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                )\n            }\n        }\n    }\n}\n\n// ============================================================================\n// ISOLATED BUTTON COMPOSABLES - Prevent parent recomposition\n// ============================================================================\n\n@Composable\nprivate fun SubscribeButton(\n    artistId: String,\n    metadata: MediaMetadata,\n) {\n    val database = LocalDatabase.current\n    val libraryArtist by database.artist(artistId).collectAsState(initial = null)\n    val isSubscribed = libraryArtist?.artist?.bookmarkedAt != null\n\n    val primaryColor = MaterialTheme.colorScheme.primary\n    val outlineColor = MaterialTheme.colorScheme.outline\n    val onSurfaceColor = MaterialTheme.colorScheme.onSurface\n\n    Box(\n        contentAlignment = Alignment.Center,\n        modifier =\n            Modifier\n                .size(40.dp)\n                .clip(CircleShape)\n                .border(\n                    width = 1.dp,\n                    color = if (isSubscribed) primaryColor.copy(alpha = 0.5f) else outlineColor.copy(alpha = 0.3f),\n                    shape = CircleShape,\n                ).background(\n                    color = if (isSubscribed) primaryColor.copy(alpha = 0.1f) else Color.Transparent,\n                    shape = CircleShape,\n                ).clickable {\n                    database.transaction {\n                        val artist = libraryArtist?.artist\n                        if (artist != null) {\n                            update(artist.toggleLike())\n                        } else {\n                            metadata.artists.firstOrNull()?.let { artistInfo ->\n                                insert(\n                                    ArtistEntity(\n                                        id = artistInfo.id ?: \"\",\n                                        name = artistInfo.name,\n                                        channelId = null,\n                                        thumbnailUrl = null,\n                                    ).toggleLike(),\n                                )\n                            }\n                        }\n                    }\n                },\n    ) {\n        Icon(\n            painter = painterResource(if (isSubscribed) R.drawable.subscribed else R.drawable.subscribe),\n            contentDescription = null,\n            tint = if (isSubscribed) primaryColor else onSurfaceColor.copy(alpha = 0.7f),\n            modifier = Modifier.size(20.dp),\n        )\n    }\n}\n\n@Composable\nprivate fun FavoriteButton(songId: String) {\n    val database = LocalDatabase.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val librarySong by database.song(songId).collectAsState(initial = null)\n    // For episodes, show saved state (inLibrary); for songs, show liked state\n    val isEpisode = librarySong?.song?.isEpisode == true\n    val isLiked = if (isEpisode) librarySong?.song?.inLibrary != null else librarySong?.song?.liked == true\n\n    val errorColor = MaterialTheme.colorScheme.error\n    val outlineColor = MaterialTheme.colorScheme.outline\n    val onSurfaceColor = MaterialTheme.colorScheme.onSurface\n\n    Box(\n        contentAlignment = Alignment.Center,\n        modifier =\n            Modifier\n                .size(40.dp)\n                .clip(CircleShape)\n                .border(\n                    width = 1.dp,\n                    color = if (isLiked) errorColor.copy(alpha = 0.5f) else outlineColor.copy(alpha = 0.3f),\n                    shape = CircleShape,\n                ).background(\n                    color = if (isLiked) errorColor.copy(alpha = 0.1f) else Color.Transparent,\n                    shape = CircleShape,\n                ).clickable { playerConnection.service.toggleLike() },\n    ) {\n        Icon(\n            painter = painterResource(if (isLiked) R.drawable.favorite else R.drawable.favorite_border),\n            contentDescription = null,\n            tint = if (isLiked) errorColor else onSurfaceColor.copy(alpha = 0.7f),\n            modifier = Modifier.size(20.dp),\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/player/PlaybackError.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.player\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.media3.common.PlaybackException\nimport com.metrolist.music.R\n\n@Composable\nfun PlaybackError(\n    error: PlaybackException,\n    retry: () -> Unit,\n) {\n    // Build detailed error info for debugging\n    val rawErrorMessage = error.cause?.cause?.message \n        ?: error.cause?.message \n        ?: error.message \n        ?: stringResource(R.string.error_unknown)\n    \n    // Check if this is an age-restricted content error\n    // Age-restricted content typically returns 403 Forbidden or contains age-related messages\n    val isAgeRestricted = rawErrorMessage.contains(\"age\", ignoreCase = true) ||\n            rawErrorMessage.contains(\"Sign in to confirm your age\", ignoreCase = true) ||\n            rawErrorMessage.contains(\"LOGIN_REQUIRED\", ignoreCase = true) ||\n            rawErrorMessage.contains(\"confirm your age\", ignoreCase = true) ||\n            rawErrorMessage.contains(\"403\", ignoreCase = true) ||\n            rawErrorMessage.contains(\"Response code: 403\", ignoreCase = true) ||\n            error.errorCode == PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS\n    \n    val errorMessage = if (isAgeRestricted) {\n        \"This app does not support playing age-restricted songs. We are working on fixing this issue.\"\n    } else {\n        rawErrorMessage\n    }\n    \n    Column(\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.Center,\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(16.dp)\n    ) {\n        // Error icon\n        Icon(\n            painter = painterResource(R.drawable.error),\n            contentDescription = null,\n            tint = MaterialTheme.colorScheme.error,\n            modifier = Modifier.size(48.dp)\n        )\n        \n        Spacer(modifier = Modifier.height(12.dp))\n        \n        // Main error message\n        Text(\n            text = stringResource(R.string.error_playback_failed),\n            style = MaterialTheme.typography.titleMedium,\n            color = MaterialTheme.colorScheme.error,\n            textAlign = TextAlign.Center\n        )\n        \n        Spacer(modifier = Modifier.height(8.dp))\n        \n        // Error details\n        Text(\n            text = errorMessage,\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n            textAlign = TextAlign.Center,\n            maxLines = 3,\n            overflow = TextOverflow.Ellipsis\n        )\n        \n        Spacer(modifier = Modifier.height(4.dp))\n        \n        // Error code\n        Text(\n            text = \"Code: ${getErrorCodeName(error.errorCode)} (${error.errorCode})\",\n            style = MaterialTheme.typography.bodySmall.copy(\n                fontFamily = FontFamily.Monospace,\n                fontSize = 11.sp\n            ),\n            color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),\n            textAlign = TextAlign.Center\n        )\n        \n        Spacer(modifier = Modifier.height(16.dp))\n        \n        // Retry button\n        Button(\n            onClick = retry,\n            shape = RoundedCornerShape(20.dp),\n            colors = ButtonDefaults.buttonColors(\n                containerColor = MaterialTheme.colorScheme.primary\n            )\n        ) {\n            Icon(\n                painter = painterResource(R.drawable.replay),\n                contentDescription = null,\n                modifier = Modifier.size(18.dp)\n            )\n            Spacer(modifier = Modifier.width(6.dp))\n            Text(text = stringResource(R.string.retry))\n        }\n    }\n}\n\n/**\n * Get human-readable error code name from PlaybackException error code\n */\nprivate fun getErrorCodeName(errorCode: Int): String {\n    return when (errorCode) {\n        PlaybackException.ERROR_CODE_UNSPECIFIED -> \"UNSPECIFIED\"\n        PlaybackException.ERROR_CODE_REMOTE_ERROR -> \"REMOTE_ERROR\"\n        PlaybackException.ERROR_CODE_BEHIND_LIVE_WINDOW -> \"BEHIND_LIVE_WINDOW\"\n        PlaybackException.ERROR_CODE_TIMEOUT -> \"TIMEOUT\"\n        PlaybackException.ERROR_CODE_FAILED_RUNTIME_CHECK -> \"FAILED_RUNTIME_CHECK\"\n        PlaybackException.ERROR_CODE_IO_UNSPECIFIED -> \"IO_UNSPECIFIED\"\n        PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED -> \"IO_NETWORK_CONNECTION_FAILED\"\n        PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT -> \"IO_NETWORK_CONNECTION_TIMEOUT\"\n        PlaybackException.ERROR_CODE_IO_INVALID_HTTP_CONTENT_TYPE -> \"IO_INVALID_HTTP_CONTENT_TYPE\"\n        PlaybackException.ERROR_CODE_IO_BAD_HTTP_STATUS -> \"IO_BAD_HTTP_STATUS\"\n        PlaybackException.ERROR_CODE_IO_FILE_NOT_FOUND -> \"IO_FILE_NOT_FOUND\"\n        PlaybackException.ERROR_CODE_IO_NO_PERMISSION -> \"IO_NO_PERMISSION\"\n        PlaybackException.ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED -> \"IO_CLEARTEXT_NOT_PERMITTED\"\n        PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE -> \"IO_READ_POSITION_OUT_OF_RANGE\"\n        PlaybackException.ERROR_CODE_PARSING_CONTAINER_MALFORMED -> \"PARSING_CONTAINER_MALFORMED\"\n        PlaybackException.ERROR_CODE_PARSING_MANIFEST_MALFORMED -> \"PARSING_MANIFEST_MALFORMED\"\n        PlaybackException.ERROR_CODE_PARSING_CONTAINER_UNSUPPORTED -> \"PARSING_CONTAINER_UNSUPPORTED\"\n        PlaybackException.ERROR_CODE_PARSING_MANIFEST_UNSUPPORTED -> \"PARSING_MANIFEST_UNSUPPORTED\"\n        PlaybackException.ERROR_CODE_DECODER_INIT_FAILED -> \"DECODER_INIT_FAILED\"\n        PlaybackException.ERROR_CODE_DECODER_QUERY_FAILED -> \"DECODER_QUERY_FAILED\"\n        PlaybackException.ERROR_CODE_DECODING_FAILED -> \"DECODING_FAILED\"\n        PlaybackException.ERROR_CODE_DECODING_FORMAT_EXCEEDS_CAPABILITIES -> \"DECODING_FORMAT_EXCEEDS_CAPABILITIES\"\n        PlaybackException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED -> \"DECODING_FORMAT_UNSUPPORTED\"\n        PlaybackException.ERROR_CODE_AUDIO_TRACK_INIT_FAILED -> \"AUDIO_TRACK_INIT_FAILED\"\n        PlaybackException.ERROR_CODE_AUDIO_TRACK_WRITE_FAILED -> \"AUDIO_TRACK_WRITE_FAILED\"\n        PlaybackException.ERROR_CODE_DRM_UNSPECIFIED -> \"DRM_UNSPECIFIED\"\n        PlaybackException.ERROR_CODE_DRM_SCHEME_UNSUPPORTED -> \"DRM_SCHEME_UNSUPPORTED\"\n        PlaybackException.ERROR_CODE_DRM_PROVISIONING_FAILED -> \"DRM_PROVISIONING_FAILED\"\n        PlaybackException.ERROR_CODE_DRM_CONTENT_ERROR -> \"DRM_CONTENT_ERROR\"\n        PlaybackException.ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED -> \"DRM_LICENSE_ACQUISITION_FAILED\"\n        PlaybackException.ERROR_CODE_DRM_DISALLOWED_OPERATION -> \"DRM_DISALLOWED_OPERATION\"\n        PlaybackException.ERROR_CODE_DRM_SYSTEM_ERROR -> \"DRM_SYSTEM_ERROR\"\n        PlaybackException.ERROR_CODE_DRM_DEVICE_REVOKED -> \"DRM_DEVICE_REVOKED\"\n        PlaybackException.ERROR_CODE_DRM_LICENSE_EXPIRED -> \"DRM_LICENSE_EXPIRED\"\n        else -> \"UNKNOWN_ERROR_$errorCode\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/player/Player.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.player\n\nimport android.content.ClipData\nimport android.content.ClipboardManager\nimport android.content.Context\nimport android.content.Intent\nimport android.content.res.Configuration\nimport android.view.WindowManager\nimport android.widget.Toast\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.animateColorAsState\nimport androidx.compose.animation.animateContentSize\nimport androidx.compose.animation.core.LinearEasing\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.spring\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.animation.slideInVertically\nimport androidx.compose.animation.slideOutVertically\nimport androidx.compose.animation.togetherWith\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.basicMarquee\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsPressedAsState\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.ColumnScope\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.add\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.ContainedLoadingIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.FilledIconButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButtonDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedIconButton\nimport androidx.compose.material3.ProvideTextStyle\nimport androidx.compose.material3.Slider\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.blur\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.ColorFilter\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.pluralStringResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.TextLayoutResult\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.text.withStyle\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.compose.ui.window.DialogProperties\nimport androidx.core.view.WindowCompat\nimport androidx.datastore.preferences.core.edit\nimport androidx.media3.common.C\nimport androidx.media3.common.Player\nimport androidx.media3.common.Player.STATE_ENDED\nimport androidx.navigation.NavController\nimport androidx.palette.graphics.Palette\nimport coil3.compose.AsyncImage\nimport coil3.imageLoader\nimport coil3.request.ImageRequest\nimport coil3.request.allowHardware\nimport coil3.toBitmap\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalDownloadUtil\nimport com.metrolist.music.LocalListenTogetherManager\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.CropAlbumArtKey\nimport com.metrolist.music.constants.DarkModeKey\nimport com.metrolist.music.constants.HidePlayerThumbnailKey\nimport com.metrolist.music.constants.KeepScreenOn\nimport com.metrolist.music.constants.PlayerBackgroundStyle\nimport com.metrolist.music.constants.PlayerBackgroundStyleKey\nimport com.metrolist.music.constants.PlayerButtonsStyle\nimport com.metrolist.music.constants.PlayerButtonsStyleKey\nimport com.metrolist.music.constants.PlayerHorizontalPadding\nimport com.metrolist.music.constants.QueuePeekHeight\nimport com.metrolist.music.constants.SleepTimerDefaultKey\nimport com.metrolist.music.constants.SleepTimerFadeOutKey\nimport com.metrolist.music.constants.SleepTimerStopAfterCurrentSongKey\nimport com.metrolist.music.constants.SliderStyle\nimport com.metrolist.music.constants.SliderStyleKey\nimport com.metrolist.music.constants.SquigglySliderKey\nimport com.metrolist.music.constants.ThumbnailCornerRadius\nimport com.metrolist.music.constants.UseNewPlayerDesignKey\nimport com.metrolist.music.db.entities.LyricsEntity\nimport com.metrolist.music.extensions.togglePlayPause\nimport com.metrolist.music.extensions.toggleRepeatMode\nimport com.metrolist.music.listentogether.RoomRole\nimport com.metrolist.music.models.MediaMetadata\nimport com.metrolist.music.ui.component.BottomSheet\nimport com.metrolist.music.ui.component.BottomSheetState\nimport com.metrolist.music.ui.component.LocalBottomSheetPageState\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.Lyrics\nimport com.metrolist.music.ui.component.PlayerSliderTrack\nimport com.metrolist.music.ui.component.ResizableIconButton\nimport com.metrolist.music.ui.component.SquigglySlider\nimport com.metrolist.music.ui.component.WavySlider\nimport com.metrolist.music.ui.component.rememberBottomSheetState\nimport com.metrolist.music.ui.menu.PlayerMenu\nimport com.metrolist.music.ui.screens.settings.DarkMode\nimport com.metrolist.music.ui.theme.PlayerColorExtractor\nimport com.metrolist.music.ui.theme.PlayerSliderColors\nimport com.metrolist.music.ui.utils.ShowMediaInfo\nimport com.metrolist.music.ui.utils.ShowOffsetDialog\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.makeTimeString\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport dagger.hilt.android.EntryPointAccessors\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport kotlin.math.max\nimport kotlin.math.roundToInt\nimport com.metrolist.music.ui.component.Icon as MIcon\nimport com.metrolist.music.constants.SleepTimerDefaultKey\nimport com.metrolist.music.utils.dataStore\nimport androidx.datastore.preferences.core.edit\nimport com.metrolist.music.constants.SleepTimerFadeOutKey\nimport com.metrolist.music.constants.SleepTimerStopAfterCurrentSongKey\n\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun BottomSheetPlayer(\n    state: BottomSheetState,\n    navController: NavController,\n    modifier: Modifier = Modifier,\n    pureBlack: Boolean,\n) {\n    val context = LocalContext.current\n    val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager\n    val menuState = LocalMenuState.current\n    val sleepTimerDefaultSetTemplate = stringResource(R.string.sleep_timer_default_set)\n    val copiedTitleStr = stringResource(R.string.copied_title)\n    val copiedArtistStr = stringResource(R.string.copied_artist)\n    val bottomSheetPageState = LocalBottomSheetPageState.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n\n    val (useNewPlayerDesign, onUseNewPlayerDesignChange) =\n        rememberPreference(\n            UseNewPlayerDesignKey,\n            defaultValue = true,\n        )\n    val (hidePlayerThumbnail, onHidePlayerThumbnailChange) = rememberPreference(HidePlayerThumbnailKey, false)\n    val cropAlbumArt by rememberPreference(CropAlbumArtKey, false)\n    val playerBackground by rememberEnumPreference(\n        key = PlayerBackgroundStyleKey,\n        defaultValue = PlayerBackgroundStyle.DEFAULT,\n    )\n    val playerButtonsStyle by rememberEnumPreference(\n        key = PlayerButtonsStyleKey,\n        defaultValue = PlayerButtonsStyle.DEFAULT,\n    )\n\n    val isSystemInDarkTheme = isSystemInDarkTheme()\n    val darkTheme by rememberEnumPreference(DarkModeKey, defaultValue = DarkMode.AUTO)\n    val useDarkTheme =\n        remember(darkTheme, isSystemInDarkTheme) {\n            if (darkTheme == DarkMode.AUTO) isSystemInDarkTheme else darkTheme == DarkMode.ON\n        }\n\n    val shouldUseDarkButtonColors =\n        remember(playerBackground, useDarkTheme) {\n            when (playerBackground) {\n                PlayerBackgroundStyle.BLUR, PlayerBackgroundStyle.GRADIENT -> true\n                PlayerBackgroundStyle.DEFAULT -> useDarkTheme\n            }\n        }\n\n    val isPlaying by playerConnection.isPlaying.collectAsState()\n    val isKeepScreenOn by rememberPreference(KeepScreenOn, false)\n    val keepScreenOn = isPlaying && isKeepScreenOn\n\n    DisposableEffect(playerBackground, state.isExpanded, useDarkTheme, keepScreenOn) {\n        val window = (context as? android.app.Activity)?.window\n        if (window != null && state.isExpanded) {\n            val insetsController = WindowCompat.getInsetsController(window, window.decorView)\n\n            when (playerBackground) {\n                PlayerBackgroundStyle.BLUR, PlayerBackgroundStyle.GRADIENT -> {\n                    insetsController.isAppearanceLightStatusBars = false\n                }\n\n                PlayerBackgroundStyle.DEFAULT -> {\n                    insetsController.isAppearanceLightStatusBars = !useDarkTheme\n                }\n            }\n\n            if (keepScreenOn && state.isExpanded) {\n                window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)\n            } else {\n                window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)\n            }\n        }\n\n        onDispose {\n            if (window != null) {\n                val insetsController = WindowCompat.getInsetsController(window, window.decorView)\n                insetsController.isAppearanceLightStatusBars = !useDarkTheme\n                window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)\n            }\n        }\n    }\n    val onBackgroundColor =\n        when (playerBackground) {\n            PlayerBackgroundStyle.DEFAULT -> MaterialTheme.colorScheme.secondary\n            else -> MaterialTheme.colorScheme.onSurface\n        }\n    val useBlackBackground =\n        remember(isSystemInDarkTheme, darkTheme, pureBlack) {\n            val useDarkTheme =\n                if (darkTheme == DarkMode.AUTO) isSystemInDarkTheme else darkTheme == DarkMode.ON\n            useDarkTheme && pureBlack\n        }\n\n    val playbackState by playerConnection.playbackState.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n    val currentSong by playerConnection.currentSong.collectAsState(initial = null)\n    val automix by playerConnection.service.automixItems.collectAsState()\n    val repeatMode by playerConnection.repeatMode.collectAsState()\n    val canSkipPrevious by playerConnection.canSkipPrevious.collectAsState()\n    val canSkipNext by playerConnection.canSkipNext.collectAsState()\n    val isMuted by playerConnection.isMuted.collectAsState()\n\n    val sliderStyle by rememberEnumPreference(SliderStyleKey, SliderStyle.DEFAULT)\n    val squigglySlider by rememberPreference(SquigglySliderKey, defaultValue = false)\n\n    // Listen Together state (reactive)\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val listenTogetherRoleState = listenTogetherManager?.role?.collectAsState(initial = RoomRole.NONE)\n    val isListenTogetherGuest = listenTogetherRoleState?.value == RoomRole.GUEST\n\n    // Cast state - safely access castConnectionHandler to prevent crashes during service lifecycle changes\n    val castHandler =\n        remember(playerConnection) {\n            try {\n                playerConnection.service.castConnectionHandler\n            } catch (e: Exception) {\n                null\n            }\n        }\n    val isCasting by castHandler?.isCasting?.collectAsState() ?: remember { mutableStateOf(false) }\n    val castPosition by castHandler?.castPosition?.collectAsState() ?: remember { mutableLongStateOf(0L) }\n    val castDuration by castHandler?.castDuration?.collectAsState() ?: remember { mutableLongStateOf(0L) }\n    val castIsPlaying by castHandler?.castIsPlaying?.collectAsState() ?: remember { mutableStateOf(false) }\n\n    // Use Cast state when casting, otherwise local player\n    val effectiveIsPlaying = if (isCasting) castIsPlaying else isPlaying\n\n    // Use State objects for position/duration to pass to MiniPlayer without causing recomposition\n    // These states persist across playback state changes to ensure continuous progress updates\n    val positionState = remember { mutableLongStateOf(0L) }\n    val durationState = remember { mutableLongStateOf(0L) }\n\n    // Convenience accessors for local use\n    var position by positionState\n    var duration by durationState\n\n    val effectivePosition by remember {\n        derivedStateOf {\n            if (isCasting) {\n                castPosition\n            } else {\n                position\n            }\n        }\n    }\n\n    var sliderPosition by remember {\n        mutableStateOf<Long?>(null)\n    }\n    // Track when we last manually set position to avoid Cast overwriting it\n    var lastManualSeekTime by remember { mutableLongStateOf(0L) }\n\n    var gradientColors by remember {\n        mutableStateOf<List<Color>>(emptyList())\n    }\n    val gradientColorsCache = remember { mutableMapOf<String, List<Color>>() }\n\n    if (!canSkipNext && automix.isNotEmpty()) {\n        playerConnection.service.addToQueueAutomix(automix[0], 0)\n    }\n\n    val defaultGradientColors = listOf(MaterialTheme.colorScheme.surface, MaterialTheme.colorScheme.surfaceVariant)\n    val fallbackColor = MaterialTheme.colorScheme.surface.toArgb()\n\n    LaunchedEffect(mediaMetadata?.id, playerBackground) {\n        if (playerBackground == PlayerBackgroundStyle.GRADIENT) {\n            val currentMetadata = mediaMetadata\n            if (currentMetadata != null && currentMetadata.thumbnailUrl != null) {\n                val cachedColors = gradientColorsCache[currentMetadata.id]\n                if (cachedColors != null) {\n                    gradientColors = cachedColors\n                    return@LaunchedEffect\n                }\n                withContext(Dispatchers.IO) {\n                    val request =\n                        ImageRequest\n                            .Builder(context)\n                            .data(currentMetadata.thumbnailUrl)\n                            .size(100, 100)\n                            .allowHardware(false)\n                            .memoryCacheKey(\"gradient_${currentMetadata.id}\")\n                            .build()\n\n                    val result = runCatching { context.imageLoader.execute(request) }.getOrNull()\n                    if (result != null) {\n                        val bitmap = result.image?.toBitmap()\n                        if (bitmap != null) {\n                            val palette =\n                                withContext(Dispatchers.Default) {\n                                    Palette\n                                        .from(bitmap)\n                                        .maximumColorCount(8)\n                                        .resizeBitmapArea(100 * 100)\n                                        .generate()\n                                }\n                            val extractedColors =\n                                PlayerColorExtractor.extractGradientColors(\n                                    palette = palette,\n                                    fallbackColor = fallbackColor,\n                                )\n                            gradientColorsCache[currentMetadata.id] = extractedColors\n                            withContext(Dispatchers.Main) { gradientColors = extractedColors }\n                        }\n                    }\n                }\n            }\n        } else {\n            gradientColors = emptyList()\n        }\n    }\n\n    val TextBackgroundColor by animateColorAsState(\n        targetValue =\n            when (playerBackground) {\n                PlayerBackgroundStyle.DEFAULT -> MaterialTheme.colorScheme.onBackground\n                PlayerBackgroundStyle.BLUR -> Color.White\n                PlayerBackgroundStyle.GRADIENT -> Color.White\n            },\n        label = \"TextBackgroundColor\",\n    )\n\n    val icBackgroundColor by animateColorAsState(\n        targetValue =\n            when (playerBackground) {\n                PlayerBackgroundStyle.DEFAULT -> MaterialTheme.colorScheme.surface\n                PlayerBackgroundStyle.BLUR -> Color.Black\n                PlayerBackgroundStyle.GRADIENT -> Color.Black\n            },\n        label = \"icBackgroundColor\",\n    )\n\n    val (textButtonColor, iconButtonColor) =\n        when {\n            playerBackground == PlayerBackgroundStyle.BLUR ||\n                playerBackground == PlayerBackgroundStyle.GRADIENT -> {\n                when (playerButtonsStyle) {\n                    PlayerButtonsStyle.DEFAULT -> {\n                        Pair(Color.White, Color.Black)\n                    }\n\n                    PlayerButtonsStyle.PRIMARY -> {\n                        Pair(\n                            MaterialTheme.colorScheme.primary,\n                            MaterialTheme.colorScheme.onPrimary,\n                        )\n                    }\n\n                    PlayerButtonsStyle.TERTIARY -> {\n                        Pair(\n                            MaterialTheme.colorScheme.tertiary,\n                            MaterialTheme.colorScheme.onTertiary,\n                        )\n                    }\n                }\n            }\n\n            else -> {\n                when (playerButtonsStyle) {\n                    PlayerButtonsStyle.DEFAULT -> {\n                        if (useDarkTheme) {\n                            Pair(Color.White, Color.Black)\n                        } else {\n                            Pair(Color.Black, Color.White)\n                        }\n                    }\n\n                    PlayerButtonsStyle.PRIMARY -> {\n                        Pair(\n                            MaterialTheme.colorScheme.primary,\n                            MaterialTheme.colorScheme.onPrimary,\n                        )\n                    }\n\n                    PlayerButtonsStyle.TERTIARY -> {\n                        Pair(\n                            MaterialTheme.colorScheme.tertiary,\n                            MaterialTheme.colorScheme.onTertiary,\n                        )\n                    }\n                }\n            }\n        }\n\n    // Separate colors for Previous/Next buttons in PRIMARY/TERTIARY modes\n    val (sideButtonContainerColor, sideButtonContentColor) =\n        when {\n            playerBackground == PlayerBackgroundStyle.BLUR ||\n                playerBackground == PlayerBackgroundStyle.GRADIENT -> {\n                when (playerButtonsStyle) {\n                    PlayerButtonsStyle.DEFAULT -> {\n                        Pair(\n                            Color.White.copy(alpha = 0.2f),\n                            Color.White,\n                        )\n                    }\n\n                    PlayerButtonsStyle.PRIMARY -> {\n                        Pair(\n                            MaterialTheme.colorScheme.primaryContainer,\n                            MaterialTheme.colorScheme.onPrimaryContainer,\n                        )\n                    }\n\n                    PlayerButtonsStyle.TERTIARY -> {\n                        Pair(\n                            MaterialTheme.colorScheme.tertiaryContainer,\n                            MaterialTheme.colorScheme.onTertiaryContainer,\n                        )\n                    }\n                }\n            }\n\n            else -> {\n                when (playerButtonsStyle) {\n                    PlayerButtonsStyle.DEFAULT -> {\n                        Pair(\n                            MaterialTheme.colorScheme.surfaceContainerHighest,\n                            MaterialTheme.colorScheme.onSurface,\n                        )\n                    }\n\n                    PlayerButtonsStyle.PRIMARY -> {\n                        Pair(\n                            MaterialTheme.colorScheme.primaryContainer,\n                            MaterialTheme.colorScheme.onPrimaryContainer,\n                        )\n                    }\n\n                    PlayerButtonsStyle.TERTIARY -> {\n                        Pair(\n                            MaterialTheme.colorScheme.tertiaryContainer,\n                            MaterialTheme.colorScheme.onTertiaryContainer,\n                        )\n                    }\n                }\n            }\n        }\n\n    val download by LocalDownloadUtil.current\n        .getDownload(mediaMetadata?.id ?: \"\")\n        .collectAsState(initial = null)\n\n    val sleepTimerEnabled =\n        remember(\n            playerConnection.service.sleepTimer.triggerTime,\n            playerConnection.service.sleepTimer.pauseWhenSongEnd,\n        ) {\n            playerConnection.service.sleepTimer.isActive\n        }\n\n    var sleepTimerTimeLeft by remember {\n        mutableLongStateOf(0L)\n    }\n\n    LaunchedEffect(sleepTimerEnabled) {\n        if (sleepTimerEnabled) {\n            while (isActive) {\n                sleepTimerTimeLeft =\n                    if (playerConnection.service.sleepTimer.pauseWhenSongEnd) {\n                        playerConnection.player.duration - playerConnection.player.currentPosition\n                    } else {\n                        playerConnection.service.sleepTimer.triggerTime - System.currentTimeMillis()\n                    }\n                delay(1000L)\n            }\n        }\n    }\n\n    val scope = rememberCoroutineScope()\n    var showSleepTimerDialog by remember {\n        mutableStateOf(false)\n    }\n\n    val sleepTimerDefault by rememberPreference(SleepTimerDefaultKey, 30f)\n    var sleepTimerValue by remember {\n        mutableFloatStateOf(sleepTimerDefault)\n    }\n    val isAtDefault by remember {\n        derivedStateOf { sleepTimerValue.roundToInt() == sleepTimerDefault.roundToInt() }\n    }\n    val sleepTimerStopAfterCurrentSong by rememberPreference(SleepTimerStopAfterCurrentSongKey, false)\n    val sleepTimerFadeOut by rememberPreference(SleepTimerFadeOutKey, false)\n\n\n    if (showSleepTimerDialog) {\n        AlertDialog(\n            properties = DialogProperties(usePlatformDefaultWidth = false),\n            onDismissRequest = { showSleepTimerDialog = false },\n            icon = {\n                Icon(\n                    painter = painterResource(R.drawable.bedtime),\n                    contentDescription = null,\n                )\n            },\n            title = { Text(stringResource(R.string.sleep_timer)) },\n            confirmButton = {\n                TextButton(\n                    onClick = {\n                        showSleepTimerDialog = false\n                        playerConnection.service.sleepTimer.start(\n                            minute = sleepTimerValue.roundToInt(),\n                            stopAfterCurrentSong = sleepTimerStopAfterCurrentSong,\n                            fadeOut = sleepTimerFadeOut,\n                        )\n                    },\n                ) {\n                    Text(stringResource(android.R.string.ok))\n                }\n            },\n            dismissButton = {\n                TextButton(\n                    onClick = { showSleepTimerDialog = false },\n                ) {\n                    Text(stringResource(android.R.string.cancel))\n                }\n            },\n            text = {\n                Column(horizontalAlignment = Alignment.CenterHorizontally) {\n                    Text(\n                        text =\n                            pluralStringResource(\n                                R.plurals.minute,\n                                sleepTimerValue.roundToInt(),\n                                sleepTimerValue.roundToInt(),\n                            ),\n                        style = MaterialTheme.typography.bodyLarge,\n                    )\n\n                    Slider(\n                        value = sleepTimerValue,\n                        onValueChange = { sleepTimerValue = it },\n                        valueRange = 5f..120f,\n                        steps = (120 - 5) / 5 - 1,\n                    )\n\n                    Row(\n                        horizontalArrangement = Arrangement.spacedBy(8.dp),\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        if (isAtDefault) {\n                            FilledIconButton(\n                                onClick = {\n                                    scope.launch {\n                                        context.dataStore.edit { settings ->\n                                            settings[SleepTimerDefaultKey] = sleepTimerValue\n                                        }\n                                    }\n                                    Toast.makeText(\n                                        context,\n                                        String.format(sleepTimerDefaultSetTemplate, sleepTimerValue.roundToInt()),\n                                        Toast.LENGTH_SHORT,\n                                    ).show()\n                                },\n                                colors = IconButtonDefaults.filledIconButtonColors(\n                                    containerColor = MaterialTheme.colorScheme.primary,\n                                    contentColor = MaterialTheme.colorScheme.onPrimary,\n                                ),\n                            ) {\n                                Text(stringResource(R.string.set_as_default))\n                            }\n                        } else {\n                            OutlinedIconButton(\n                                onClick = {\n                                    scope.launch {\n                                        context.dataStore.edit { settings ->\n                                            settings[SleepTimerDefaultKey] = sleepTimerValue\n                                        }\n                                    }\n                                    Toast.makeText(\n                                        context,\n                                        String.format(sleepTimerDefaultSetTemplate, sleepTimerValue.roundToInt()),\n                                        Toast.LENGTH_SHORT,\n                                    ).show()\n                                },\n                            ) {\n                                Text(stringResource(R.string.set_as_default))\n                            }\n                        }\n\n                        OutlinedIconButton(\n                            onClick = {\n                                showSleepTimerDialog = false\n                                playerConnection.service.sleepTimer.start(minute = -1)\n                            },\n                        ) {\n                            Text(stringResource(R.string.end_of_song))\n                        }\n                    }\n                }\n            },\n        )\n    }\n\n    var showChoosePlaylistDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    var showInlineLyrics by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    var isFullScreen by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    // Position update - only for local playback\n    // When casting, we use castPosition directly to avoid sync issues\n    // Use isPlaying instead of playbackState to ensure continuous updates during playback\n    LaunchedEffect(isPlaying, isCasting) {\n        if (!isCasting && isPlaying) {\n            while (isActive) {\n                delay(100) // Update more frequently for smoother progress bar\n                if (sliderPosition == null) { // Only update if user isn't dragging\n                    position = playerConnection.player.currentPosition\n                    duration = playerConnection.player.duration\n                }\n            }\n        }\n    }\n\n    // Also update position when playback state changes (e.g., song change, seek)\n    LaunchedEffect(playbackState, mediaMetadata?.id) {\n        if (!isCasting) {\n            position = playerConnection.player.currentPosition\n            duration = playerConnection.player.duration\n        }\n    }\n\n    // When casting, use Cast position/duration directly\n    // But wait a bit after manual seeks to let Cast catch up\n    LaunchedEffect(isCasting, castPosition, castDuration) {\n        if (isCasting && sliderPosition == null) {\n            val timeSinceManualSeek = System.currentTimeMillis() - lastManualSeekTime\n            if (timeSinceManualSeek > 1500) {\n                // Only update from Cast if we haven't manually seeked recently\n                position = castPosition\n                if (castDuration > 0) duration = castDuration\n            }\n        }\n    }\n\n    val dismissedBound = QueuePeekHeight + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()\n\n    val queueSheetState =\n        rememberBottomSheetState(\n            dismissedBound = dismissedBound,\n            expandedBound = state.expandedBound,\n            collapsedBound = dismissedBound + 1.dp,\n            initialAnchor = 1,\n        )\n\n    val bottomSheetBackgroundColor =\n        when (playerBackground) {\n            PlayerBackgroundStyle.BLUR, PlayerBackgroundStyle.GRADIENT -> {\n                MaterialTheme.colorScheme.surfaceContainer\n            }\n\n            else -> {\n                if (useBlackBackground) {\n                    Color.Black\n                } else {\n                    MaterialTheme.colorScheme.surfaceContainer\n                }\n            }\n        }\n\n    val backgroundAlpha = state.progress.coerceIn(0f, 1f)\n\n    BottomSheet(\n        state = state,\n        modifier = modifier,\n        background = {\n            Box(\n                modifier =\n                    Modifier\n                        .fillMaxSize()\n                        .background(bottomSheetBackgroundColor),\n            ) {\n                when (playerBackground) {\n                    PlayerBackgroundStyle.BLUR -> {\n                        AnimatedContent(\n                            targetState = mediaMetadata?.thumbnailUrl,\n                            transitionSpec = {\n                                fadeIn(tween(800)).togetherWith(fadeOut(tween(800)))\n                            },\n                            label = \"blurBackground\",\n                        ) { thumbnailUrl ->\n                            if (thumbnailUrl != null) {\n                                Box(modifier = Modifier.alpha(backgroundAlpha)) {\n                                    AsyncImage(\n                                        model =\n                                            ImageRequest\n                                                .Builder(context)\n                                                .data(thumbnailUrl)\n                                                .size(100, 100)\n                                                .allowHardware(false)\n                                                .build(),\n                                        contentDescription = null,\n                                        contentScale = ContentScale.Crop,\n                                        modifier =\n                                            Modifier\n                                                .fillMaxSize()\n                                                .blur(if (useDarkTheme) 150.dp else 100.dp),\n                                    )\n                                    Box(\n                                        modifier =\n                                            Modifier\n                                                .fillMaxSize()\n                                                .background(Color.Black.copy(alpha = 0.3f)),\n                                    )\n                                }\n                            }\n                        }\n                    }\n\n                    PlayerBackgroundStyle.GRADIENT -> {\n                        AnimatedContent(\n                            targetState = gradientColors,\n                            transitionSpec = {\n                                fadeIn(tween(800)).togetherWith(fadeOut(tween(800)))\n                            },\n                            label = \"gradientBackground\",\n                        ) { colors ->\n                            if (colors.isNotEmpty()) {\n                                val gradientColorStops =\n                                    if (colors.size >= 3) {\n                                        arrayOf(\n                                            0.0f to colors[0],\n                                            0.5f to colors[1],\n                                            1.0f to colors[2],\n                                        )\n                                    } else {\n                                        arrayOf(\n                                            0.0f to colors[0],\n                                            0.6f to colors[0].copy(alpha = 0.7f),\n                                            1.0f to Color.Black,\n                                        )\n                                    }\n                                Box(\n                                    Modifier\n                                        .fillMaxSize()\n                                        .alpha(backgroundAlpha)\n                                        .background(Brush.verticalGradient(colorStops = gradientColorStops))\n                                        .background(Color.Black.copy(alpha = 0.2f)),\n                                )\n                            }\n                        }\n                    }\n\n                    else -> {\n                        PlayerBackgroundStyle.DEFAULT\n                    }\n                }\n            }\n        },\n        onDismiss =\n            if (!isListenTogetherGuest) {\n                {\n                    playerConnection.service.clearAutomix()\n                    playerConnection.player.stop()\n                    playerConnection.player.clearMediaItems()\n                }\n            } else {\n                null\n            },\n        collapsedContent = {\n            MiniPlayer(\n                positionState = positionState,\n                durationState = durationState,\n            )\n        },\n    ) {\n        val controlsContent: @Composable ColumnScope.(MediaMetadata) -> Unit = { mediaMetadata ->\n            val playPauseRoundness by animateDpAsState(\n                targetValue = if (isPlaying) 24.dp else 36.dp,\n                animationSpec = tween(durationMillis = 90, easing = LinearEasing),\n                label = \"playPauseRoundness\",\n            )\n\n            Row(\n                horizontalArrangement = Arrangement.SpaceBetween,\n                verticalAlignment = Alignment.CenterVertically,\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = PlayerHorizontalPadding),\n            ) {\n                AnimatedContent(\n                    targetState = showInlineLyrics,\n                    label = \"ThumbnailAnimation\",\n                ) { showLyrics ->\n                    if (showLyrics) {\n                        Row {\n                            if (hidePlayerThumbnail) {\n                                Box(\n                                    modifier =\n                                        Modifier\n                                            .size(56.dp)\n                                            .clip(RoundedCornerShape(ThumbnailCornerRadius))\n                                            .background(MaterialTheme.colorScheme.surfaceVariant),\n                                    contentAlignment = Alignment.Center,\n                                ) {\n                                    Icon(\n                                        painter = painterResource(R.drawable.small_icon),\n                                        contentDescription = null,\n                                        modifier =\n                                            Modifier\n                                                .size(32.dp),\n                                        tint = textButtonColor.copy(alpha = 0.7f),\n                                    )\n                                }\n                            } else {\n                                AsyncImage(\n                                    model = mediaMetadata.thumbnailUrl,\n                                    contentDescription = null,\n                                    contentScale = if (cropAlbumArt) ContentScale.Crop else ContentScale.Fit,\n                                    modifier =\n                                        Modifier\n                                            .size(56.dp)\n                                            .clip(RoundedCornerShape(ThumbnailCornerRadius)),\n                                )\n                            }\n                            Spacer(modifier = Modifier.width(12.dp))\n                        }\n                    } else {\n                        Spacer(modifier = Modifier.width(0.dp))\n                    }\n                }\n                Column(\n                    modifier = Modifier.weight(1f),\n                ) {\n                    AnimatedContent(\n                        targetState = mediaMetadata.title,\n                        transitionSpec = { fadeIn() togetherWith fadeOut() },\n                        label = \"\",\n                    ) { title ->\n                        Text(\n                            text = title,\n                            style = MaterialTheme.typography.titleLarge,\n                            fontWeight = FontWeight.Bold,\n                            maxLines = 1,\n                            overflow = TextOverflow.Ellipsis,\n                            color = TextBackgroundColor,\n                            modifier =\n                                Modifier\n                                    .basicMarquee(iterations = 1, initialDelayMillis = 3000, velocity = 30.dp)\n                                    .combinedClickable(\n                                        enabled = true,\n                                        indication = null,\n                                        interactionSource = remember { MutableInteractionSource() },\n                                        onClick = {\n                                            if (mediaMetadata.album != null) {\n                                                navController.navigate(\"album/${mediaMetadata.album.id}\")\n                                                state.collapseSoft()\n                                            }\n                                        },\n                                        onLongClick = {\n                                            val clip = ClipData.newPlainText(copiedTitleStr, title)\n                                            clipboardManager.setPrimaryClip(clip)\n                                            Toast\n                                                .makeText(context, copiedTitleStr, Toast.LENGTH_SHORT)\n                                                .show()\n                                        },\n                                    ),\n                        )\n                    }\n\n                    Row(\n                        horizontalArrangement = Arrangement.SpaceBetween,\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        if (mediaMetadata.explicit) MIcon.Explicit()\n\n                        if (mediaMetadata.artists.any { it.name.isNotBlank() }) {\n                            val annotatedString =\n                                buildAnnotatedString {\n                                    mediaMetadata.artists.forEachIndexed { index, artist ->\n                                        val tag = \"artist_${artist.id.orEmpty()}\"\n                                        pushStringAnnotation(tag = tag, annotation = artist.id.orEmpty())\n                                        withStyle(SpanStyle(color = TextBackgroundColor, fontSize = 16.sp)) {\n                                            append(artist.name)\n                                        }\n                                        pop()\n                                        if (index != mediaMetadata.artists.lastIndex) append(\", \")\n                                    }\n                                }\n\n                            Box(\n                                modifier =\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .basicMarquee(iterations = 1, initialDelayMillis = 3000, velocity = 30.dp)\n                                        .padding(end = 12.dp),\n                            ) {\n                                var layoutResult by remember { mutableStateOf<TextLayoutResult?>(null) }\n                                var clickOffset by remember { mutableStateOf<Offset?>(null) }\n                                Text(\n                                    text = annotatedString,\n                                    style = MaterialTheme.typography.titleMedium.copy(color = TextBackgroundColor),\n                                    maxLines = 1,\n                                    overflow = TextOverflow.Ellipsis,\n                                    onTextLayout = { layoutResult = it },\n                                    modifier =\n                                        Modifier\n                                            .pointerInput(Unit) {\n                                                awaitPointerEventScope {\n                                                    while (true) {\n                                                        val event = awaitPointerEvent()\n                                                        val tapPosition = event.changes.firstOrNull()?.position\n                                                        if (tapPosition != null) {\n                                                            clickOffset = tapPosition\n                                                        }\n                                                    }\n                                                }\n                                            }.combinedClickable(\n                                                enabled = true,\n                                                indication = null,\n                                                interactionSource = remember { MutableInteractionSource() },\n                                                onClick = {\n                                                    val tapPosition = clickOffset\n                                                    val layout = layoutResult\n                                                    if (tapPosition != null && layout != null) {\n                                                        val offset = layout.getOffsetForPosition(tapPosition)\n                                                        annotatedString\n                                                            .getStringAnnotations(offset, offset)\n                                                            .firstOrNull()\n                                                            ?.let { ann ->\n                                                                val artistId = ann.item\n                                                                if (artistId.isNotBlank()) {\n                                                                    navController.navigate(\"artist/$artistId\")\n                                                                    state.collapseSoft()\n                                                                }\n                                                            }\n                                                    }\n                                                },\n                                                onLongClick = {\n                                                    val clip =\n                                                        ClipData.newPlainText(\n                                                            copiedArtistStr,\n                                                            annotatedString,\n                                                        )\n                                                    clipboardManager.setPrimaryClip(clip)\n                                                    Toast\n                                                        .makeText(\n                                                            context,\n                                                            copiedArtistStr,\n                                                            Toast.LENGTH_SHORT,\n                                                        ).show()\n                                                },\n                                            ),\n                                )\n                            }\n                        }\n                    }\n                }\n\n                Spacer(modifier = Modifier.width(12.dp))\n\n                if (useNewPlayerDesign) {\n                    val shareShape =\n                        RoundedCornerShape(\n                            topStart = 50.dp,\n                            bottomStart = 50.dp,\n                            topEnd = 3.dp,\n                            bottomEnd = 3.dp,\n                        )\n\n                    val favShape =\n                        RoundedCornerShape(\n                            topStart = 3.dp,\n                            bottomStart = 3.dp,\n                            topEnd = 50.dp,\n                            bottomEnd = 50.dp,\n                        )\n\n                    val middleShape = RoundedCornerShape(3.dp)\n\n                    Row(\n                        horizontalArrangement = Arrangement.spacedBy(6.dp),\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        AnimatedContent(targetState = showInlineLyrics, label = \"ShareButton\") { showLyrics ->\n                            if (showLyrics) {\n                                FilledIconButton(\n                                    onClick = { isFullScreen = !isFullScreen },\n                                    shape = shareShape,\n                                    colors =\n                                        IconButtonDefaults.filledIconButtonColors(\n                                            containerColor = textButtonColor,\n                                            contentColor = iconButtonColor,\n                                        ),\n                                    modifier = Modifier.size(42.dp),\n                                ) {\n                                    Icon(\n                                        painter = painterResource(R.drawable.fullscreen),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(24.dp),\n                                    )\n                                }\n                            } else {\n                                FilledIconButton(\n                                    onClick = {\n                                        val intent =\n                                            Intent().apply {\n                                                action = Intent.ACTION_SEND\n                                                type = \"text/plain\"\n                                                putExtra(\n                                                    Intent.EXTRA_TEXT,\n                                                    \"https://music.youtube.com/watch?v=${mediaMetadata.id}\",\n                                                )\n                                            }\n                                        context.startActivity(Intent.createChooser(intent, null))\n                                    },\n                                    shape = shareShape,\n                                    colors =\n                                        IconButtonDefaults.filledIconButtonColors(\n                                            containerColor = textButtonColor,\n                                            contentColor = iconButtonColor,\n                                        ),\n                                    modifier = Modifier.size(42.dp),\n                                ) {\n                                    Icon(\n                                        painter = painterResource(R.drawable.share),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(24.dp),\n                                    )\n                                }\n                            }\n                        }\n\n                        AnimatedContent(targetState = showInlineLyrics, label = \"LikeButton\") { showLyrics ->\n                            if (showLyrics) {\n                                val currentLyrics by playerConnection.currentLyrics.collectAsState(initial = null)\n                                FilledIconButton(\n                                    onClick = {\n                                        menuState.show {\n                                            com.metrolist.music.ui.menu.LyricsMenu(\n                                                lyricsProvider = { currentLyrics },\n                                                songProvider = { currentSong?.song },\n                                                mediaMetadataProvider = { mediaMetadata },\n                                                onDismiss = menuState::dismiss,\n                                                onShowOffsetDialog = {\n                                                    bottomSheetPageState.show {\n                                                        ShowOffsetDialog(\n                                                            songProvider = { currentSong?.song },\n                                                        )\n                                                    }\n                                                },\n                                            )\n                                        }\n                                    },\n                                    shape = favShape,\n                                    colors =\n                                        IconButtonDefaults.filledIconButtonColors(\n                                            containerColor = textButtonColor,\n                                            contentColor = iconButtonColor,\n                                        ),\n                                    modifier = Modifier.size(42.dp),\n                                ) {\n                                    Icon(\n                                        painter = painterResource(R.drawable.more_horiz),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(24.dp),\n                                    )\n                                }\n                            } else {\n                                // For episodes, show saved state (inLibrary); for songs, show liked state\n                                val isEpisode = currentSong?.song?.isEpisode == true\n                                val isFavorite = if (isEpisode) currentSong?.song?.inLibrary != null else currentSong?.song?.liked == true\n                                FilledIconButton(\n                                    onClick = playerConnection::toggleLike,\n                                    shape = favShape,\n                                    colors =\n                                        IconButtonDefaults.filledIconButtonColors(\n                                            containerColor = textButtonColor,\n                                            contentColor = iconButtonColor,\n                                        ),\n                                    modifier = Modifier.size(42.dp),\n                                ) {\n                                    Icon(\n                                        painter =\n                                            painterResource(\n                                                if (isFavorite) {\n                                                    R.drawable.favorite\n                                                } else {\n                                                    R.drawable.favorite_border\n                                                },\n                                            ),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(24.dp),\n                                    )\n                                }\n                            }\n                        }\n                    }\n                } else {\n                    AnimatedContent(targetState = showInlineLyrics, label = \"ShareButton\") { showLyrics ->\n                        if (showLyrics) {\n                            Box(\n                                modifier =\n                                    Modifier\n                                        .size(40.dp)\n                                        .clip(RoundedCornerShape(24.dp))\n                                        .background(textButtonColor)\n                                        .clickable { isFullScreen = !isFullScreen },\n                            ) {\n                                Icon(\n                                    painter = painterResource(R.drawable.fullscreen),\n                                    contentDescription = null,\n                                    tint = iconButtonColor,\n                                    modifier =\n                                        Modifier\n                                            .align(Alignment.Center)\n                                            .size(24.dp),\n                                )\n                            }\n                        } else {\n                            Box(\n                                modifier =\n                                    Modifier\n                                        .size(40.dp)\n                                        .clip(RoundedCornerShape(24.dp))\n                                        .background(textButtonColor)\n                                        .clickable {\n                                            val intent =\n                                                Intent().apply {\n                                                    action = Intent.ACTION_SEND\n                                                    type = \"text/plain\"\n                                                    putExtra(\n                                                        Intent.EXTRA_TEXT,\n                                                        \"https://music.youtube.com/watch?v=${mediaMetadata.id}\",\n                                                    )\n                                                }\n                                            context.startActivity(Intent.createChooser(intent, null))\n                                        },\n                            ) {\n                                Icon(\n                                    painter = painterResource(R.drawable.share),\n                                    contentDescription = null,\n                                    tint = iconButtonColor,\n                                    modifier =\n                                        Modifier\n                                            .align(Alignment.Center)\n                                            .size(24.dp),\n                                )\n                            }\n                        }\n                    }\n\n                    Spacer(modifier = Modifier.size(12.dp))\n\n                    AnimatedContent(targetState = showInlineLyrics, label = \"LikeButton\") { showLyrics ->\n                        if (showLyrics) {\n                            val currentLyrics by playerConnection.currentLyrics.collectAsState(initial = null)\n                            Box(\n                                modifier =\n                                    Modifier\n                                        .size(40.dp)\n                                        .clip(RoundedCornerShape(24.dp))\n                                        .background(textButtonColor)\n                                        .clickable {\n                                            menuState.show {\n                                                com.metrolist.music.ui.menu.LyricsMenu(\n                                                    lyricsProvider = { currentLyrics },\n                                                    songProvider = { currentSong?.song },\n                                                    mediaMetadataProvider = { mediaMetadata },\n                                                    onDismiss = menuState::dismiss,\n                                                    onShowOffsetDialog = {\n                                                        bottomSheetPageState.show {\n                                                            ShowOffsetDialog(\n                                                                songProvider = { currentSong?.song },\n                                                            )\n                                                        }\n                                                    },\n                                                )\n                                            }\n                                        },\n                            ) {\n                                Icon(\n                                    painter = painterResource(R.drawable.more_horiz),\n                                    contentDescription = null,\n                                    tint = iconButtonColor,\n                                    modifier =\n                                        Modifier\n                                            .align(Alignment.Center)\n                                            .size(24.dp),\n                                )\n                            }\n                        } else {\n                            PlayerMoreMenuButton(\n                                mediaMetadata = mediaMetadata,\n                                navController = navController,\n                                state = state,\n                                textButtonColor = textButtonColor,\n                                iconButtonColor = iconButtonColor,\n                            )\n                        }\n                    }\n                }\n            }\n\n            Spacer(Modifier.height(24.dp))\n\n            when (sliderStyle) {\n                SliderStyle.DEFAULT -> {\n                    Slider(\n                        value = (sliderPosition ?: effectivePosition).toFloat(),\n                        valueRange = 0f..(if (duration == C.TIME_UNSET) 0f else duration.toFloat()),\n                        onValueChange = {\n                            if (!isListenTogetherGuest) {\n                                sliderPosition = it.toLong()\n                            }\n                        },\n                        onValueChangeFinished = {\n                            if (!isListenTogetherGuest) {\n                                sliderPosition?.let {\n                                    if (isCasting) {\n                                        castHandler?.seekTo(it)\n                                        lastManualSeekTime = System.currentTimeMillis()\n                                    } else {\n                                        playerConnection.player.seekTo(it)\n                                    }\n                                    position = it\n                                }\n                                sliderPosition = null\n                            }\n                        },\n                        enabled = !isListenTogetherGuest,\n                        colors = PlayerSliderColors.getSliderColors(textButtonColor, playerBackground, useDarkTheme),\n                        modifier = Modifier.padding(horizontal = PlayerHorizontalPadding),\n                    )\n                }\n\n                SliderStyle.WAVY -> {\n                    if (squigglySlider) {\n                        SquigglySlider(\n                            value = (sliderPosition ?: effectivePosition).toFloat(),\n                            valueRange = 0f..(if (duration == C.TIME_UNSET) 0f else duration.toFloat()),\n                            onValueChange = {\n                                sliderPosition = it.toLong()\n                            },\n                            onValueChangeFinished = {\n                                sliderPosition?.let {\n                                    if (isCasting) {\n                                        castHandler?.seekTo(it)\n                                        lastManualSeekTime = System.currentTimeMillis()\n                                    } else {\n                                        playerConnection.player.seekTo(it)\n                                    }\n                                    position = it\n                                }\n                                sliderPosition = null\n                            },\n                            modifier = Modifier.padding(horizontal = PlayerHorizontalPadding),\n                            colors = PlayerSliderColors.getSliderColors(textButtonColor, playerBackground, useDarkTheme),\n                            isPlaying = effectiveIsPlaying,\n                        )\n                    } else {\n                        WavySlider(\n                            value = (sliderPosition ?: effectivePosition).toFloat(),\n                            valueRange = 0f..(if (duration == C.TIME_UNSET) 0f else duration.toFloat()),\n                            onValueChange = {\n                                sliderPosition = it.toLong()\n                            },\n                            onValueChangeFinished = {\n                                sliderPosition?.let {\n                                    if (isCasting) {\n                                        castHandler?.seekTo(it)\n                                        lastManualSeekTime = System.currentTimeMillis()\n                                    } else {\n                                        playerConnection.player.seekTo(it)\n                                    }\n                                    position = it\n                                }\n                                sliderPosition = null\n                            },\n                            colors = PlayerSliderColors.getSliderColors(textButtonColor, playerBackground, useDarkTheme),\n                            modifier = Modifier.padding(horizontal = PlayerHorizontalPadding),\n                            isPlaying = effectiveIsPlaying,\n                        )\n                    }\n                }\n\n                SliderStyle.SLIM -> {\n                    Slider(\n                        value = (sliderPosition ?: effectivePosition).toFloat(),\n                        valueRange = 0f..(if (duration == C.TIME_UNSET) 0f else duration.toFloat()),\n                        onValueChange = {\n                            if (!isListenTogetherGuest) {\n                                sliderPosition = it.toLong()\n                            }\n                        },\n                        onValueChangeFinished = {\n                            if (!isListenTogetherGuest) {\n                                sliderPosition?.let {\n                                    if (isCasting) {\n                                        castHandler?.seekTo(it)\n                                        lastManualSeekTime = System.currentTimeMillis()\n                                    } else {\n                                        playerConnection.player.seekTo(it)\n                                    }\n                                    position = it\n                                }\n                                sliderPosition = null\n                            }\n                        },\n                        enabled = !isListenTogetherGuest,\n                        thumb = { Spacer(modifier = Modifier.size(0.dp)) },\n                        track = { sliderState ->\n                            PlayerSliderTrack(\n                                sliderState = sliderState,\n                                colors = PlayerSliderColors.getSliderColors(textButtonColor, playerBackground, useDarkTheme),\n                            )\n                        },\n                        modifier = Modifier.padding(horizontal = PlayerHorizontalPadding),\n                    )\n                }\n            }\n\n            Spacer(Modifier.height(4.dp))\n\n            Row(\n                horizontalArrangement = Arrangement.SpaceBetween,\n                verticalAlignment = Alignment.CenterVertically,\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = PlayerHorizontalPadding + 4.dp),\n            ) {\n                Text(\n                    text = makeTimeString(sliderPosition ?: effectivePosition),\n                    style = MaterialTheme.typography.labelMedium,\n                    color = TextBackgroundColor,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                )\n\n                Text(\n                    text = if (duration != C.TIME_UNSET) makeTimeString(duration) else \"\",\n                    style = MaterialTheme.typography.labelMedium,\n                    color = TextBackgroundColor,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                )\n            }\n\n            Spacer(Modifier.height(24.dp))\n\n            AnimatedVisibility(\n                visible = !isFullScreen,\n                enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),\n                exit = shrinkVertically(shrinkTowards = Alignment.Top) + slideOutVertically(targetOffsetY = { it }) + fadeOut(),\n            ) {\n                Column {\n                    if (useNewPlayerDesign) {\n                        Row(\n                            horizontalArrangement = Arrangement.Center,\n                            verticalAlignment = Alignment.CenterVertically,\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .padding(horizontal = PlayerHorizontalPadding),\n                        ) {\n                            val backInteractionSource = remember { MutableInteractionSource() }\n                            val nextInteractionSource = remember { MutableInteractionSource() }\n                            val playPauseInteractionSource = remember { MutableInteractionSource() }\n\n                            val isPlayPausePressed by playPauseInteractionSource.collectIsPressedAsState()\n                            val isBackPressed by backInteractionSource.collectIsPressedAsState()\n                            val isNextPressed by nextInteractionSource.collectIsPressedAsState()\n\n                            val playPauseWeight by animateFloatAsState(\n                                targetValue =\n                                    if (isPlayPausePressed) {\n                                        1.9f\n                                    } else if (isBackPressed || isNextPressed) {\n                                        1.1f\n                                    } else {\n                                        1.3f\n                                    },\n                                animationSpec =\n                                    spring(\n                                        dampingRatio = 0.6f,\n                                        stiffness = 500f,\n                                    ),\n                                label = \"playPauseWeight\",\n                            )\n\n                            val backButtonWeight by animateFloatAsState(\n                                targetValue =\n                                    if (isBackPressed) {\n                                        0.65f\n                                    } else if (isPlayPausePressed) {\n                                        0.35f\n                                    } else {\n                                        0.45f\n                                    },\n                                animationSpec =\n                                    spring(\n                                        dampingRatio = 0.6f,\n                                        stiffness = 500f,\n                                    ),\n                                label = \"backButtonWeight\",\n                            )\n\n                            val nextButtonWeight by animateFloatAsState(\n                                targetValue =\n                                    if (isNextPressed) {\n                                        0.65f\n                                    } else if (isPlayPausePressed) {\n                                        0.35f\n                                    } else {\n                                        0.45f\n                                    },\n                                animationSpec =\n                                    spring(\n                                        dampingRatio = 0.6f,\n                                        stiffness = 500f,\n                                    ),\n                                label = \"nextButtonWeight\",\n                            )\n\n                            FilledIconButton(\n                                onClick = playerConnection::seekToPrevious,\n                                enabled = canSkipPrevious && !isListenTogetherGuest,\n                                shape = RoundedCornerShape(50),\n                                interactionSource = backInteractionSource,\n                                colors =\n                                    IconButtonDefaults.filledIconButtonColors(\n                                        containerColor = sideButtonContainerColor,\n                                        contentColor = sideButtonContentColor,\n                                    ),\n                                modifier =\n                                    Modifier\n                                        .height(68.dp)\n                                        .weight(backButtonWeight),\n                            ) {\n                                Icon(\n                                    painter = painterResource(R.drawable.skip_previous),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(32.dp),\n                                )\n                            }\n\n                            Spacer(modifier = Modifier.width(8.dp))\n\n                            FilledIconButton(\n                                onClick = {\n                                    if (isListenTogetherGuest) {\n                                        playerConnection.toggleMute()\n                                        return@FilledIconButton\n                                    }\n                                    if (isCasting) {\n                                        if (castIsPlaying) {\n                                            castHandler?.pause()\n                                        } else {\n                                            castHandler?.play()\n                                        }\n                                    } else if (playbackState == STATE_ENDED) {\n                                        playerConnection.player.seekTo(0, 0)\n                                        playerConnection.player.playWhenReady = true\n                                    } else {\n                                        playerConnection.togglePlayPause()\n                                    }\n                                },\n                                shape = RoundedCornerShape(50),\n                                interactionSource = playPauseInteractionSource,\n                                colors =\n                                    IconButtonDefaults.filledIconButtonColors(\n                                        containerColor = textButtonColor,\n                                        contentColor = iconButtonColor,\n                                    ),\n                                modifier =\n                                    Modifier\n                                        .height(68.dp)\n                                        .weight(playPauseWeight),\n                            ) {\n                                Row(\n                                    verticalAlignment = Alignment.CenterVertically,\n                                    horizontalArrangement = Arrangement.Center,\n                                ) {\n                                    Icon(\n                                        painter =\n                                            painterResource(\n                                                if (isListenTogetherGuest) {\n                                                    if (isMuted) R.drawable.volume_off else R.drawable.volume_up\n                                                } else {\n                                                    if (effectiveIsPlaying) R.drawable.pause else R.drawable.play\n                                                },\n                                            ),\n                                        contentDescription =\n                                            if (isListenTogetherGuest) {\n                                                if (isMuted) stringResource(R.string.unmute) else stringResource(R.string.mute)\n                                            } else {\n                                                if (effectiveIsPlaying) stringResource(R.string.pause) else stringResource(R.string.play)\n                                            },\n                                        modifier = Modifier.size(32.dp),\n                                    )\n                                    Spacer(modifier = Modifier.width(8.dp))\n                                    Text(\n                                        text =\n                                            if (isListenTogetherGuest) {\n                                                if (isMuted) stringResource(R.string.unmute) else stringResource(R.string.mute)\n                                            } else {\n                                                if (effectiveIsPlaying) stringResource(R.string.pause) else stringResource(R.string.play)\n                                            },\n                                        style = MaterialTheme.typography.titleMedium,\n                                    )\n                                }\n                            }\n\n                            Spacer(modifier = Modifier.width(8.dp))\n\n                            FilledIconButton(\n                                onClick = playerConnection::seekToNext,\n                                enabled = canSkipNext && !isListenTogetherGuest,\n                                shape = RoundedCornerShape(50),\n                                interactionSource = nextInteractionSource,\n                                colors =\n                                    IconButtonDefaults.filledIconButtonColors(\n                                        containerColor = sideButtonContainerColor,\n                                        contentColor = sideButtonContentColor,\n                                    ),\n                                modifier =\n                                    Modifier\n                                        .height(68.dp)\n                                        .weight(nextButtonWeight),\n                            ) {\n                                Icon(\n                                    painter = painterResource(R.drawable.skip_next),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(32.dp),\n                                )\n                            }\n                        }\n                    } else {\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .padding(horizontal = PlayerHorizontalPadding),\n                        ) {\n                            Box(modifier = Modifier.weight(1f)) {\n                                ResizableIconButton(\n                                    icon =\n                                        when (repeatMode) {\n                                            Player.REPEAT_MODE_OFF, Player.REPEAT_MODE_ALL -> R.drawable.repeat\n                                            Player.REPEAT_MODE_ONE -> R.drawable.repeat_one\n                                            else -> throw IllegalStateException()\n                                        },\n                                    color = TextBackgroundColor,\n                                    modifier =\n                                        Modifier\n                                            .size(32.dp)\n                                            .padding(4.dp)\n                                            .align(Alignment.Center)\n                                            .alpha(if (isListenTogetherGuest) 0.5f else 1f),\n                                    enabled = !isListenTogetherGuest,\n                                    onClick = {\n                                        playerConnection.player.toggleRepeatMode()\n                                    },\n                                )\n                            }\n\n                            Box(modifier = Modifier.weight(1f)) {\n                                ResizableIconButton(\n                                    icon = R.drawable.skip_previous,\n                                    enabled = canSkipPrevious && !isListenTogetherGuest,\n                                    color = TextBackgroundColor,\n                                    modifier =\n                                        Modifier\n                                            .size(32.dp)\n                                            .align(Alignment.Center)\n                                            .alpha(if (isListenTogetherGuest) 0.5f else 1f),\n                                    onClick = playerConnection::seekToPrevious,\n                                )\n                            }\n\n                            Spacer(Modifier.width(8.dp))\n\n                            Box(\n                                modifier =\n                                    Modifier\n                                        .size(72.dp)\n                                        .clip(RoundedCornerShape(playPauseRoundness))\n                                        .background(textButtonColor)\n                                        .clickable {\n                                            if (isListenTogetherGuest) {\n                                                playerConnection.toggleMute()\n                                                return@clickable\n                                            }\n                                            if (isCasting) {\n                                                if (castIsPlaying) {\n                                                    castHandler?.pause()\n                                                } else {\n                                                    castHandler?.play()\n                                                }\n                                            } else if (playbackState == STATE_ENDED) {\n                                                playerConnection.player.seekTo(0, 0)\n                                                playerConnection.player.playWhenReady = true\n                                            } else {\n                                                playerConnection.player.togglePlayPause()\n                                            }\n                                        },\n                            ) {\n                                Image(\n                                    painter =\n                                        painterResource(\n                                            if (isListenTogetherGuest) {\n                                                if (isMuted) R.drawable.volume_off else R.drawable.volume_up\n                                            } else if (playbackState ==\n                                                STATE_ENDED\n                                            ) {\n                                                R.drawable.replay\n                                            } else if (effectiveIsPlaying) {\n                                                R.drawable.pause\n                                            } else {\n                                                R.drawable.play\n                                            },\n                                        ),\n                                    contentDescription = null,\n                                    colorFilter = ColorFilter.tint(iconButtonColor),\n                                    modifier =\n                                        Modifier\n                                            .align(Alignment.Center)\n                                            .size(36.dp),\n                                )\n                            }\n\n                            Spacer(Modifier.width(8.dp))\n\n                            Box(modifier = Modifier.weight(1f)) {\n                                ResizableIconButton(\n                                    icon = R.drawable.skip_next,\n                                    enabled = canSkipNext && !isListenTogetherGuest,\n                                    color = TextBackgroundColor,\n                                    modifier =\n                                        Modifier\n                                            .size(32.dp)\n                                            .align(Alignment.Center)\n                                            .alpha(if (isListenTogetherGuest) 0.5f else 1f),\n                                    onClick = playerConnection::seekToNext,\n                                )\n                            }\n\n                            Box(modifier = Modifier.weight(1f)) {\n                                // For episodes, show saved state (inLibrary); for songs, show liked state\n                                val isEpisode = currentSong?.song?.isEpisode == true\n                                val isFavorite = if (isEpisode) currentSong?.song?.inLibrary != null else currentSong?.song?.liked == true\n                                ResizableIconButton(\n                                    icon = if (isFavorite) R.drawable.favorite else R.drawable.favorite_border,\n                                    color = if (isFavorite) MaterialTheme.colorScheme.error else TextBackgroundColor,\n                                    modifier =\n                                        Modifier\n                                            .size(32.dp)\n                                            .padding(4.dp)\n                                            .align(Alignment.Center),\n                                    onClick = playerConnection::toggleLike,\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        when (LocalConfiguration.current.orientation) {\n            Configuration.ORIENTATION_LANDSCAPE -> {\n                // Calculate vertical padding like OuterTune\n                val density = LocalDensity.current\n                val verticalPadding =\n                    max(\n                        WindowInsets.systemBars.getTop(density),\n                        WindowInsets.systemBars.getBottom(density),\n                    )\n                val verticalPaddingDp = with(density) { verticalPadding.toDp() }\n                val verticalWindowInsets = WindowInsets(left = 0.dp, top = verticalPaddingDp, right = 0.dp, bottom = verticalPaddingDp)\n\n                Row(\n                    modifier =\n                        Modifier\n                            .windowInsetsPadding(\n                                WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).add(verticalWindowInsets),\n                            ).padding(bottom = 24.dp)\n                            .fillMaxSize(),\n                ) {\n                    Box(\n                        contentAlignment = Alignment.Center,\n                        modifier =\n                            Modifier\n                                .weight(1f)\n                                .nestedScroll(state.preUpPostDownNestedScrollConnection),\n                    ) {\n                        // Remember lambdas to prevent unnecessary recomposition\n                        val currentSliderPosition by rememberUpdatedState(sliderPosition)\n                        val sliderPositionProvider = remember { { currentSliderPosition } }\n                        val isExpandedProvider = remember(state) { { state.isExpanded } }\n                        AnimatedContent(\n                            targetState = showInlineLyrics,\n                            label = \"Lyrics\",\n                            transitionSpec = { fadeIn() togetherWith fadeOut() },\n                        ) { showLyrics ->\n                            if (showLyrics) {\n                                InlineLyricsView(\n                                    mediaMetadata = mediaMetadata,\n                                    showLyrics = showLyrics,\n                                    positionProvider = { effectivePosition },\n                                )\n                            } else {\n                                Thumbnail(\n                                    sliderPositionProvider = sliderPositionProvider,\n                                    modifier = Modifier.animateContentSize(),\n                                    isPlayerExpanded = isExpandedProvider,\n                                    isLandscape = true,\n                                    isListenTogetherGuest = isListenTogetherGuest,\n                                )\n                            }\n                        }\n                    }\n\n                    Column(\n                        horizontalAlignment = Alignment.CenterHorizontally,\n                        modifier =\n                            Modifier\n                                .weight(if (showInlineLyrics) 0.65f else 1f, false)\n                                .animateContentSize()\n                                .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top)),\n                    ) {\n                        Spacer(Modifier.weight(1f))\n\n                        mediaMetadata?.let {\n                            controlsContent(it)\n                        }\n\n                        Spacer(Modifier.weight(1f))\n                    }\n                }\n            }\n\n            else -> {\n                val bottomPadding by animateDpAsState(\n                    targetValue = if (isFullScreen) 0.dp else queueSheetState.collapsedBound,\n                    label = \"bottomPadding\",\n                )\n                Column(\n                    horizontalAlignment = Alignment.CenterHorizontally,\n                    modifier =\n                        Modifier\n                            .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal))\n                            .padding(bottom = bottomPadding)\n                            .animateContentSize(),\n                ) {\n                    Box(\n                        contentAlignment = Alignment.Center,\n                        modifier = Modifier.weight(1f),\n                    ) {\n                        // Remember lambdas to prevent unnecessary recomposition\n                        val currentSliderPosition by rememberUpdatedState(sliderPosition)\n                        val sliderPositionProvider = remember { { currentSliderPosition } }\n                        val isExpandedProvider = remember(state) { { state.isExpanded } }\n                        AnimatedContent(\n                            targetState = showInlineLyrics,\n                            label = \"Lyrics\",\n                            transitionSpec = { fadeIn() togetherWith fadeOut() },\n                        ) { showLyrics ->\n                            if (showLyrics) {\n                                InlineLyricsView(\n                                    mediaMetadata = mediaMetadata,\n                                    showLyrics = showLyrics,\n                                    positionProvider = { effectivePosition },\n                                )\n                            } else {\n                                Thumbnail(\n                                    sliderPositionProvider = sliderPositionProvider,\n                                    modifier = Modifier.nestedScroll(state.preUpPostDownNestedScrollConnection),\n                                    isPlayerExpanded = isExpandedProvider,\n                                    isListenTogetherGuest = isListenTogetherGuest,\n                                )\n                            }\n                        }\n                    }\n\n                    mediaMetadata?.let {\n                        controlsContent(it)\n                    }\n\n                    Spacer(Modifier.height(30.dp))\n                }\n            }\n        }\n\n        AnimatedVisibility(\n            visible = !isFullScreen,\n            enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),\n            exit = shrinkVertically(shrinkTowards = Alignment.Top) + slideOutVertically(targetOffsetY = { it }) + fadeOut(),\n        ) {\n            Queue(\n                state = queueSheetState,\n                playerBottomSheetState = state,\n                navController = navController,\n                background =\n                    if (useBlackBackground) {\n                        Color.Black\n                    } else {\n                        MaterialTheme.colorScheme.surfaceContainer\n                    },\n                onBackgroundColor = onBackgroundColor,\n                TextBackgroundColor = TextBackgroundColor,\n                textButtonColor = textButtonColor,\n                iconButtonColor = iconButtonColor,\n                pureBlack = pureBlack,\n                showInlineLyrics = showInlineLyrics,\n                playerBackground = playerBackground,\n                onToggleLyrics = {\n                    showInlineLyrics = !showInlineLyrics\n                },\n            )\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun InlineLyricsView(\n    mediaMetadata: MediaMetadata?,\n    showLyrics: Boolean,\n    positionProvider: () -> Long,\n) {\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val currentLyrics by playerConnection.currentLyrics.collectAsState(initial = null)\n    val lyrics = remember(currentLyrics) { currentLyrics?.lyrics?.trim() }\n    val context = LocalContext.current\n    val database = LocalDatabase.current\n    val coroutineScope = rememberCoroutineScope()\n\n    LaunchedEffect(mediaMetadata?.id, currentLyrics) {\n        if (mediaMetadata != null && currentLyrics == null) {\n            delay(500)\n            coroutineScope.launch(Dispatchers.IO) {\n                try {\n                    val entryPoint =\n                        EntryPointAccessors.fromApplication(\n                            context.applicationContext,\n                            com.metrolist.music.di.LyricsHelperEntryPoint::class.java,\n                        )\n                    val lyricsHelper = entryPoint.lyricsHelper()\n                    val fetchedLyricsWithProvider = lyricsHelper.getLyrics(mediaMetadata)\n                    database.query {\n                        upsert(LyricsEntity(mediaMetadata.id, fetchedLyricsWithProvider.lyrics, fetchedLyricsWithProvider.provider))\n                    }\n                } catch (e: Exception) {\n                    // Handle error\n                }\n            }\n        }\n    }\n\n    Box(\n        modifier =\n            Modifier\n                .fillMaxSize()\n                .clip(RoundedCornerShape(12.dp)),\n        contentAlignment = Alignment.Center,\n    ) {\n        when {\n            lyrics == null -> {\n                ContainedLoadingIndicator()\n            }\n\n            lyrics == LyricsEntity.LYRICS_NOT_FOUND -> {\n                Text(\n                    text = stringResource(R.string.lyrics_not_found),\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),\n                    textAlign = TextAlign.Center,\n                )\n            }\n\n            else -> {\n                val lyricsContent: @Composable () -> Unit = {\n                    Lyrics(\n                        sliderPositionProvider = positionProvider,\n                        modifier = Modifier.padding(horizontal = 24.dp),\n                        showLyrics = showLyrics,\n                    )\n                }\n                ProvideTextStyle(\n                    value =\n                        MaterialTheme.typography.bodyMedium.copy(\n                            fontSize = 14.sp,\n                            textAlign = TextAlign.Center,\n                        ),\n                ) {\n                    lyricsContent()\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun MoreActionsButton(\n    mediaMetadata: MediaMetadata,\n    navController: NavController,\n    state: BottomSheetState,\n    textButtonColor: Color,\n    iconButtonColor: Color,\n) {\n    val menuState = LocalMenuState.current\n    val bottomSheetPageState = LocalBottomSheetPageState.current\n\n    Box(\n        modifier =\n            Modifier\n                .size(40.dp)\n                .clip(RoundedCornerShape(24.dp))\n                .background(textButtonColor)\n                .clickable {\n                    menuState.show {\n                        PlayerMenu(\n                            mediaMetadata = mediaMetadata,\n                            navController = navController,\n                            playerBottomSheetState = state,\n                            onShowDetailsDialog = {\n                                mediaMetadata.id.let {\n                                    bottomSheetPageState.show {\n                                        ShowMediaInfo(it)\n                                    }\n                                }\n                            },\n                            onDismiss = menuState::dismiss,\n                        )\n                    }\n                },\n    ) {\n        Image(\n            painter = painterResource(R.drawable.more_horiz),\n            contentDescription = null,\n            colorFilter = ColorFilter.tint(iconButtonColor),\n        )\n    }\n}\n\n@Composable\nprivate fun PlayerMoreMenuButton(\n    mediaMetadata: MediaMetadata,\n    navController: NavController,\n    state: BottomSheetState,\n    textButtonColor: Color,\n    iconButtonColor: Color,\n) {\n    val menuState = LocalMenuState.current\n    val bottomSheetPageState = LocalBottomSheetPageState.current\n\n    Box(\n        contentAlignment = Alignment.Center,\n        modifier =\n            Modifier\n                .size(40.dp)\n                .clip(RoundedCornerShape(24.dp))\n                .background(textButtonColor)\n                .clickable {\n                    menuState.show {\n                        PlayerMenu(\n                            mediaMetadata = mediaMetadata,\n                            navController = navController,\n                            playerBottomSheetState = state,\n                            onShowDetailsDialog = {\n                                mediaMetadata.id.let {\n                                    bottomSheetPageState.show {\n                                        ShowMediaInfo(it)\n                                    }\n                                }\n                            },\n                            onDismiss = menuState::dismiss,\n                        )\n                    }\n                },\n    ) {\n        Image(\n            painter = painterResource(R.drawable.more_horiz),\n            contentDescription = null,\n            colorFilter = ColorFilter.tint(iconButtonColor),\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/player/Queue.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.player\n\nimport android.annotation.SuppressLint\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.animateContentSize\nimport androidx.compose.animation.expandVertically\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.animation.slideInVertically\nimport androidx.compose.animation.slideOutVertically\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.basicMarquee\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.add\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedButton\nimport androidx.compose.material3.Slider\nimport androidx.compose.material3.SnackbarDuration\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.SnackbarResult\nimport androidx.compose.material3.SwipeToDismissBox\nimport androidx.compose.material3.SwipeToDismissBoxValue\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.rememberSwipeToDismissBoxState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.runtime.saveable.listSaver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.toMutableStateList\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalClipboard\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.pluralStringResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.datastore.preferences.core.edit\nimport androidx.media3.common.Player\nimport androidx.media3.common.Timeline\nimport androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder\nimport androidx.navigation.NavController\nimport com.metrolist.music.LocalListenTogetherManager\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.ListItemHeight\nimport com.metrolist.music.constants.PlayerBackgroundStyle\nimport com.metrolist.music.constants.QueueEditLockKey\nimport com.metrolist.music.constants.UseNewPlayerDesignKey\nimport com.metrolist.music.extensions.metadata\nimport com.metrolist.music.extensions.move\nimport com.metrolist.music.extensions.toggleRepeatMode\nimport com.metrolist.music.listentogether.RoomRole\nimport com.metrolist.music.models.MediaMetadata\nimport com.metrolist.music.ui.component.ActionPromptDialog\nimport com.metrolist.music.ui.component.BottomSheet\nimport com.metrolist.music.ui.component.BottomSheetState\nimport com.metrolist.music.ui.component.LocalBottomSheetPageState\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.MediaMetadataListItem\nimport com.metrolist.music.ui.menu.PlayerMenu\nimport com.metrolist.music.ui.menu.QueueMenu\nimport com.metrolist.music.ui.menu.SelectionMediaMetadataMenu\nimport com.metrolist.music.ui.utils.ShowMediaInfo\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.makeTimeString\nimport com.metrolist.music.utils.rememberPreference\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\nimport sh.calvin.reorderable.ReorderableItem\nimport sh.calvin.reorderable.rememberReorderableLazyListState\nimport kotlin.math.roundToInt\nimport com.metrolist.music.constants.SleepTimerDefaultKey\nimport com.metrolist.music.utils.dataStore\nimport androidx.datastore.preferences.core.edit\nimport android.widget.Toast\nimport androidx.compose.runtime.derivedStateOf\nimport com.metrolist.music.constants.SleepTimerFadeOutKey\nimport com.metrolist.music.constants.SleepTimerStopAfterCurrentSongKey\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.material3.Button\n\n\n@SuppressLint(\"UnrememberedMutableState\")\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nfun Queue(\n    state: BottomSheetState,\n    playerBottomSheetState: BottomSheetState,\n    navController: NavController,\n    modifier: Modifier = Modifier,\n    background: Color,\n    onBackgroundColor: Color,\n    TextBackgroundColor: Color,\n    textButtonColor: Color,\n    iconButtonColor: Color,\n    pureBlack: Boolean,\n    showInlineLyrics: Boolean,\n    playerBackground: PlayerBackgroundStyle = PlayerBackgroundStyle.DEFAULT,\n    onToggleLyrics: () -> Unit = {},\n) {\n    val context = LocalContext.current\n    val haptic = LocalHapticFeedback.current\n    val clipboardManager = LocalClipboard.current\n    val menuState = LocalMenuState.current\n    val sleepTimerDefaultSetTemplate = stringResource(R.string.sleep_timer_default_set)\n    val bottomSheetPageState = LocalBottomSheetPageState.current\n\n    // Listen Together state (reactive)\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val listenTogetherRoleState = listenTogetherManager?.role?.collectAsState(initial = com.metrolist.music.listentogether.RoomRole.NONE)\n    val isListenTogetherGuest = listenTogetherRoleState?.value == RoomRole.GUEST\n\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val repeatMode by playerConnection.repeatMode.collectAsState()\n\n    val currentWindowIndex by playerConnection.currentWindowIndex.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n\n    val currentFormat by playerConnection.currentFormat.collectAsState(initial = null)\n\n    val selectedSongs = remember { mutableStateListOf<MediaMetadata>() }\n    val selectedItems = remember { mutableStateListOf<Timeline.Window>() }\n\n    // Cast state - safely access castConnectionHandler to prevent crashes during service lifecycle changes\n    val castHandler =\n        remember(playerConnection) {\n            try {\n                playerConnection.service.castConnectionHandler\n            } catch (e: Exception) {\n                null\n            }\n        }\n    val isCasting by castHandler?.isCasting?.collectAsState() ?: remember { mutableStateOf(false) }\n    val castIsPlaying by castHandler?.castIsPlaying?.collectAsState() ?: remember { mutableStateOf(false) }\n\n    var inSelectMode by rememberSaveable { mutableStateOf(false) }\n    val selection =\n        rememberSaveable(\n            saver =\n                listSaver<MutableList<String>, String>(\n                    save = { it.toList() },\n                    restore = { it.toMutableStateList() },\n                ),\n        ) { mutableStateListOf() }\n    val onExitSelectionMode = {\n        inSelectMode = false\n        selection.clear()\n    }\n    if (inSelectMode) {\n        BackHandler(onBack = onExitSelectionMode)\n    }\n\n    var locked by rememberPreference(QueueEditLockKey, defaultValue = true)\n\n    val (useNewPlayerDesign, onUseNewPlayerDesignChange) =\n        rememberPreference(\n            UseNewPlayerDesignKey,\n            defaultValue = true,\n        )\n\n    val snackbarHostState = remember { SnackbarHostState() }\n    var dismissJob: Job? by remember { mutableStateOf(null) }\n\n    val coroutineScope = rememberCoroutineScope()\n    var showSleepTimerDialog by remember { mutableStateOf(false) }\n    val sleepTimerDefault by rememberPreference(SleepTimerDefaultKey, 30f)\n    var sleepTimerValue by remember { mutableFloatStateOf(sleepTimerDefault) }\n    val isAtDefault by remember {\n        derivedStateOf { sleepTimerValue.roundToInt() == sleepTimerDefault.roundToInt() }\n    }\n    val sleepTimerStopAfterCurrentSong by rememberPreference(SleepTimerStopAfterCurrentSongKey, false)\n    val sleepTimerFadeOut by rememberPreference(SleepTimerFadeOutKey, false)\n    val sleepTimerEnabled = remember(\n        playerConnection.service.sleepTimer.triggerTime,\n        playerConnection.service.sleepTimer.pauseWhenSongEnd\n    ) {\n        playerConnection.service.sleepTimer.isActive\n    }\n    var sleepTimerTimeLeft by remember { mutableLongStateOf(0L) }\n\n    LaunchedEffect(sleepTimerEnabled) {\n        if (sleepTimerEnabled) {\n            while (isActive) {\n                sleepTimerTimeLeft =\n                    if (playerConnection.service.sleepTimer.pauseWhenSongEnd) {\n                        playerConnection.player.duration - playerConnection.player.currentPosition\n                    } else {\n                        playerConnection.service.sleepTimer.triggerTime - System.currentTimeMillis()\n                    }\n                delay(1000L)\n            }\n        }\n    }\n\n    BottomSheet(\n        state = state,\n        modifier = modifier,\n        background = {\n            Box(Modifier.fillMaxSize().background(Color.Unspecified))\n        },\n        collapsedContent = {\n            if (useNewPlayerDesign) {\n                // New design\n                Row(\n                    horizontalArrangement = Arrangement.spacedBy(6.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(horizontal = 30.dp, vertical = 12.dp)\n                            .windowInsetsPadding(\n                                WindowInsets.systemBars.only(\n                                    WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal,\n                                ),\n                            ),\n                ) {\n                    val buttonSize = 42.dp\n                    val iconSize = 24.dp\n                    val queueShape =\n                        RoundedCornerShape(\n                            topStart = 50.dp,\n                            bottomStart = 50.dp,\n                            topEnd = 3.dp,\n                            bottomEnd = 3.dp,\n                        )\n                    val middleShape = RoundedCornerShape(3.dp)\n                    val repeatShape =\n                        RoundedCornerShape(\n                            topStart = 3.dp,\n                            bottomStart = 3.dp,\n                            topEnd = 50.dp,\n                            bottomEnd = 50.dp,\n                        )\n\n                    PlayerQueueButton(\n                        icon = R.drawable.queue_music,\n                        onClick = { state.expandSoft() },\n                        isActive = false,\n                        shape = queueShape,\n                        modifier = Modifier.size(buttonSize),\n                        textButtonColor = textButtonColor,\n                        iconButtonColor = iconButtonColor,\n                        iconSize = iconSize,\n                        textBackgroundColor = TextBackgroundColor,\n                        playerBackground = playerBackground,\n                    )\n\n                    PlayerQueueButton(\n                        icon = R.drawable.bedtime,\n                        onClick = {\n                            if (sleepTimerEnabled) {\n                                playerConnection.service.sleepTimer.clear()\n                            } else {\n                                showSleepTimerDialog = true\n                            }\n                        },\n                        isActive = sleepTimerEnabled,\n                        enabled = !isListenTogetherGuest,\n                        shape = middleShape,\n                        modifier = Modifier.size(buttonSize),\n                        textButtonColor = textButtonColor,\n                        iconButtonColor = iconButtonColor,\n                        text = if (sleepTimerEnabled) makeTimeString(sleepTimerTimeLeft) else null,\n                        iconSize = iconSize,\n                        textBackgroundColor = TextBackgroundColor,\n                        playerBackground = playerBackground,\n                    )\n\n                    val shuffleModeEnabled by playerConnection.shuffleModeEnabled.collectAsState()\n                    PlayerQueueButton(\n                        icon = R.drawable.shuffle,\n                        onClick = {\n                            playerConnection.player.shuffleModeEnabled = !shuffleModeEnabled\n                        },\n                        isActive = shuffleModeEnabled,\n                        enabled = !isListenTogetherGuest,\n                        shape = middleShape,\n                        modifier = Modifier.size(buttonSize),\n                        textButtonColor = textButtonColor,\n                        iconButtonColor = iconButtonColor,\n                        iconSize = iconSize,\n                        textBackgroundColor = TextBackgroundColor,\n                        playerBackground = playerBackground,\n                    )\n\n                    PlayerQueueButton(\n                        icon = R.drawable.lyrics,\n                        onClick = { onToggleLyrics() },\n                        isActive = showInlineLyrics,\n                        shape = middleShape,\n                        modifier = Modifier.size(buttonSize),\n                        textButtonColor = textButtonColor,\n                        iconButtonColor = iconButtonColor,\n                        iconSize = iconSize,\n                        textBackgroundColor = TextBackgroundColor,\n                        playerBackground = playerBackground,\n                    )\n\n                    PlayerQueueButton(\n                        icon =\n                            when (repeatMode) {\n                                Player.REPEAT_MODE_ALL -> R.drawable.repeat\n                                Player.REPEAT_MODE_ONE -> R.drawable.repeat_one\n                                else -> R.drawable.repeat\n                            },\n                        onClick = {\n                            playerConnection.player.toggleRepeatMode()\n                        },\n                        isActive = repeatMode != Player.REPEAT_MODE_OFF,\n                        enabled = !isListenTogetherGuest,\n                        shape = repeatShape,\n                        modifier = Modifier.size(buttonSize),\n                        textButtonColor = textButtonColor,\n                        iconButtonColor = iconButtonColor,\n                        iconSize = iconSize,\n                        textBackgroundColor = TextBackgroundColor,\n                        playerBackground = playerBackground,\n                    )\n\n                    Spacer(modifier = Modifier.weight(1f))\n\n                    Box(\n                        modifier =\n                            Modifier\n                                .size(buttonSize)\n                                .clip(CircleShape)\n                                .background(textButtonColor)\n                                .clickable {\n                                    menuState.show {\n                                        PlayerMenu(\n                                            mediaMetadata = mediaMetadata,\n                                            navController = navController,\n                                            playerBottomSheetState = playerBottomSheetState,\n                                            onShowDetailsDialog = {\n                                                mediaMetadata?.id?.let {\n                                                    bottomSheetPageState.show {\n                                                        ShowMediaInfo(it)\n                                                    }\n                                                }\n                                            },\n                                            onDismiss = menuState::dismiss,\n                                        )\n                                    }\n                                },\n                        contentAlignment = Alignment.Center,\n                    ) {\n                        Icon(\n                            painter = painterResource(id = R.drawable.more_vert),\n                            contentDescription = null,\n                            modifier = Modifier.size(iconSize),\n                            tint = iconButtonColor,\n                        )\n                    }\n                }\n            } else {\n                // Old design\n                Row(\n                    horizontalArrangement = Arrangement.SpaceBetween,\n                    verticalAlignment = Alignment.CenterVertically,\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(horizontal = 30.dp, vertical = 12.dp)\n                            .windowInsetsPadding(\n                                WindowInsets.systemBars\n                                    .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal),\n                            ),\n                ) {\n                    TextButton(\n                        onClick = { state.expandSoft() },\n                        modifier = Modifier.weight(1f),\n                    ) {\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            horizontalArrangement = Arrangement.Center,\n                            modifier = Modifier.fillMaxWidth(),\n                        ) {\n                            Icon(\n                                painter = painterResource(id = R.drawable.queue_music),\n                                contentDescription = null,\n                                modifier = Modifier.size(20.dp),\n                                tint = TextBackgroundColor,\n                            )\n                            Spacer(modifier = Modifier.width(6.dp))\n                            Text(\n                                text = stringResource(id = R.string.queue),\n                                color = TextBackgroundColor,\n                                maxLines = 1,\n                                overflow = TextOverflow.Ellipsis,\n                                textAlign = TextAlign.Center,\n                                modifier = Modifier.basicMarquee(),\n                            )\n                        }\n                    }\n\n                    TextButton(\n                        enabled = !isListenTogetherGuest,\n                        onClick = {\n                            if (!isListenTogetherGuest) {\n                                if (sleepTimerEnabled) {\n                                    playerConnection.service.sleepTimer.clear()\n                                } else {\n                                    showSleepTimerDialog = true\n                                }\n                            }\n                        },\n                        modifier = Modifier.weight(1.2f),\n                    ) {\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            horizontalArrangement = Arrangement.Center,\n                            modifier = Modifier.fillMaxWidth(),\n                        ) {\n                            Icon(\n                                painter = painterResource(id = R.drawable.bedtime),\n                                contentDescription = null,\n                                modifier = Modifier.size(20.dp),\n                                tint = TextBackgroundColor,\n                            )\n                            Spacer(modifier = Modifier.width(6.dp))\n                            AnimatedContent(\n                                label = \"sleepTimer\",\n                                targetState = sleepTimerEnabled,\n                            ) { enabled ->\n                                if (enabled) {\n                                    Text(\n                                        text = makeTimeString(sleepTimerTimeLeft),\n                                        color = TextBackgroundColor,\n                                        maxLines = 1,\n                                        overflow = TextOverflow.Ellipsis,\n                                        textAlign = TextAlign.Center,\n                                        modifier = Modifier.basicMarquee(),\n                                    )\n                                } else {\n                                    Text(\n                                        text = stringResource(id = R.string.sleep_timer),\n                                        color = TextBackgroundColor,\n                                        maxLines = 1,\n                                        overflow = TextOverflow.Ellipsis,\n                                        textAlign = TextAlign.Center,\n                                        modifier = Modifier.basicMarquee(),\n                                    )\n                                }\n                            }\n                        }\n                    }\n\n                    TextButton(\n                        onClick = {\n                            onToggleLyrics()\n                        },\n                        modifier = Modifier.weight(1f),\n                    ) {\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            horizontalArrangement = Arrangement.Center,\n                            modifier = Modifier.fillMaxWidth(),\n                        ) {\n                            Icon(\n                                painter = painterResource(id = R.drawable.lyrics),\n                                contentDescription = null,\n                                modifier = Modifier.size(20.dp),\n                                tint = TextBackgroundColor,\n                            )\n                            Spacer(modifier = Modifier.width(6.dp))\n                            Text(\n                                text = stringResource(R.string.lyrics),\n                                color = TextBackgroundColor,\n                                maxLines = 1,\n                                overflow = TextOverflow.Ellipsis,\n                                textAlign = TextAlign.Center,\n                                modifier = Modifier.basicMarquee(),\n                            )\n                        }\n                    }\n                }\n            }\n\n            if (showSleepTimerDialog) {\n                ActionPromptDialog(\n                    titleBar = {\n                        Row(\n                            modifier = Modifier.fillMaxWidth(),\n                            horizontalArrangement = Arrangement.Center,\n                        ) {\n                            Text(\n                                text = stringResource(R.string.sleep_timer),\n                                overflow = TextOverflow.Ellipsis,\n                                maxLines = 1,\n                                style = MaterialTheme.typography.headlineSmall,\n                            )\n                        }\n                    },\n                    onDismiss = { showSleepTimerDialog = false },\n                    onConfirm = {\n                        showSleepTimerDialog = false\n                        playerConnection.service.sleepTimer.start(\n                            minute = sleepTimerValue.roundToInt(),\n                            stopAfterCurrentSong = sleepTimerStopAfterCurrentSong,\n                            fadeOut = sleepTimerFadeOut,\n                        )\n                    },\n                    onCancel = {\n                        showSleepTimerDialog = false\n                    },\n                    onReset = {\n                        sleepTimerValue = sleepTimerDefault\n                    },\n                    content = {\n                        Column(horizontalAlignment = Alignment.CenterHorizontally) {\n                            Text(\n                                text =\n                                    pluralStringResource(\n                                        R.plurals.minute,\n                                        sleepTimerValue.roundToInt(),\n                                        sleepTimerValue.roundToInt(),\n                                    ),\n                                style = MaterialTheme.typography.bodyLarge,\n                            )\n\n                            Spacer(Modifier.height(16.dp))\n\n                            Slider(\n                                value = sleepTimerValue,\n                                onValueChange = { sleepTimerValue = it },\n                                valueRange = 5f..120f,\n                                steps = (120 - 5) / 5 - 1,\n                                modifier = Modifier.fillMaxWidth(),\n                            )\n\n                            Spacer(Modifier.height(8.dp))\n\n                            Row(\n                                horizontalArrangement = Arrangement.spacedBy(8.dp),\n                                verticalAlignment = Alignment.CenterVertically,\n                            ) {\n                                if (isAtDefault) {\n                                    Button(\n                                        onClick = {\n                                            coroutineScope.launch {\n                                                context.dataStore.edit { settings ->\n                                                    settings[SleepTimerDefaultKey] = sleepTimerValue\n                                                }\n                                            }\n                                            Toast.makeText(\n                                                context,\n                                                String.format(sleepTimerDefaultSetTemplate, sleepTimerValue.roundToInt()),\n                                                Toast.LENGTH_SHORT,\n                                            ).show()\n                                        },\n                                        colors = androidx.compose.material3.ButtonDefaults.buttonColors(\n                                            containerColor = MaterialTheme.colorScheme.primary,\n                                            contentColor = MaterialTheme.colorScheme.onPrimary,\n                                        ),\n                                    ) {\n                                        Text(stringResource(R.string.set_as_default))\n                                    }\n                                } else {\n                                    OutlinedButton(\n                                        onClick = {\n                                            coroutineScope.launch {\n                                                context.dataStore.edit { settings ->\n                                                    settings[SleepTimerDefaultKey] = sleepTimerValue\n                                                }\n                                            }\n                                            Toast.makeText(\n                                                context,\n                                                String.format(sleepTimerDefaultSetTemplate, sleepTimerValue.roundToInt()),\n                                                Toast.LENGTH_SHORT,\n                                            ).show()\n                                        },\n                                    ) {\n                                        Text(stringResource(R.string.set_as_default))\n                                    }\n                                }\n\n                                OutlinedButton(\n                                    onClick = {\n                                        showSleepTimerDialog = false\n                                        playerConnection.service.sleepTimer.start(\n                                            minute = -1,\n                                        )\n                                    },\n                                ) {\n                                    Text(stringResource(R.string.end_of_song))\n                                }\n                            }\n                        }\n                    },\n                )\n            }\n        },\n    ) {\n        val queueTitle by playerConnection.queueTitle.collectAsState()\n        val queueWindows by playerConnection.queueWindows.collectAsState()\n        val automix by playerConnection.service.automixItems.collectAsState()\n        val mutableQueueWindows = remember { mutableStateListOf<Timeline.Window>() }\n        val queueLength =\n            remember(queueWindows) {\n                queueWindows.sumOf { it.mediaItem.metadata!!.duration }\n            }\n\n        val coroutineScope = rememberCoroutineScope()\n\n        val headerItems = 1\n        val lazyListState = rememberLazyListState()\n        var dragInfo by remember { mutableStateOf<Pair<Int, Int>?>(null) }\n\n        val currentPlayingUid =\n            remember(currentWindowIndex, queueWindows) {\n                if (currentWindowIndex in queueWindows.indices) {\n                    queueWindows[currentWindowIndex].uid\n                } else {\n                    null\n                }\n            }\n\n        val reorderableState =\n            rememberReorderableLazyListState(\n                lazyListState = lazyListState,\n                scrollThresholdPadding =\n                    WindowInsets.systemBars\n                        .add(\n                            WindowInsets(\n                                top = ListItemHeight,\n                                bottom = ListItemHeight,\n                            ),\n                        ).asPaddingValues(),\n            ) { from, to ->\n                val currentDragInfo = dragInfo\n                dragInfo =\n                    if (currentDragInfo == null) {\n                        from.index to to.index\n                    } else {\n                        currentDragInfo.first to to.index\n                    }\n\n                val safeFrom = (from.index - headerItems).coerceIn(0, mutableQueueWindows.lastIndex)\n                val safeTo = (to.index - headerItems).coerceIn(0, mutableQueueWindows.lastIndex)\n\n                mutableQueueWindows.move(safeFrom, safeTo)\n            }\n\n        LaunchedEffect(reorderableState.isAnyItemDragging) {\n            if (!reorderableState.isAnyItemDragging) {\n                dragInfo?.let { (from, to) ->\n                    val safeFrom = (from - headerItems).coerceIn(0, queueWindows.lastIndex)\n                    val safeTo = (to - headerItems).coerceIn(0, queueWindows.lastIndex)\n\n                    if (!playerConnection.player.shuffleModeEnabled) {\n                        playerConnection.player.moveMediaItem(safeFrom, safeTo)\n                    } else {\n                        playerConnection.player.setShuffleOrder(\n                            DefaultShuffleOrder(\n                                queueWindows\n                                    .map { it.firstPeriodIndex }\n                                    .toMutableList()\n                                    .move(safeFrom, safeTo)\n                                    .toIntArray(),\n                                System.currentTimeMillis(),\n                            ),\n                        )\n                    }\n                    dragInfo = null\n                }\n            }\n        }\n\n        LaunchedEffect(queueWindows) {\n            mutableQueueWindows.apply {\n                clear()\n                addAll(queueWindows)\n            }\n        }\n\n        LaunchedEffect(mutableQueueWindows) {\n            if (currentWindowIndex != -1) {\n                lazyListState.scrollToItem(currentWindowIndex)\n            }\n        }\n\n        Box(\n            modifier =\n                Modifier\n                    .fillMaxSize()\n                    .background(background),\n        ) {\n            LazyColumn(\n                state = lazyListState,\n                contentPadding =\n                    WindowInsets.systemBars\n                        .add(\n                            WindowInsets(\n                                top = ListItemHeight + 8.dp,\n                                bottom = ListItemHeight + 8.dp,\n                            ),\n                        ).asPaddingValues(),\n                modifier = Modifier.nestedScroll(state.preUpPostDownNestedScrollConnection),\n            ) {\n                item(key = \"queue_top_spacer\") {\n                    Spacer(\n                        modifier =\n                            Modifier\n                                .animateContentSize()\n                                .height(if (inSelectMode) 48.dp else 0.dp),\n                    )\n                }\n\n                itemsIndexed(\n                    items = mutableQueueWindows,\n                    key = { _, item -> item.uid.hashCode() },\n                ) { index, window ->\n                    ReorderableItem(\n                        state = reorderableState,\n                        key = window.uid.hashCode(),\n                    ) {\n                        val currentItem by rememberUpdatedState(window)\n                        val isActive = window.uid == currentPlayingUid\n                        val dismissBoxState =\n                            rememberSwipeToDismissBoxState(\n                                positionalThreshold = { totalDistance -> totalDistance },\n                            )\n\n                        var processedDismiss by remember { mutableStateOf(false) }\n                        val removedSongMsg =\n                            stringResource(R.string.removed_song_from_playlist, currentItem.mediaItem.metadata?.title ?: \"\")\n                        val undoStr = stringResource(R.string.undo)\n                        LaunchedEffect(dismissBoxState.currentValue) {\n                            val dv = dismissBoxState.currentValue\n                            if (!processedDismiss && !isListenTogetherGuest && (\n                                    dv == SwipeToDismissBoxValue.StartToEnd ||\n                                        dv == SwipeToDismissBoxValue.EndToStart\n                                )\n                            ) {\n                                processedDismiss = true\n                                playerConnection.player.removeMediaItem(currentItem.firstPeriodIndex)\n                                dismissJob?.cancel()\n                                dismissJob =\n                                    coroutineScope.launch {\n                                        val snackbarResult =\n                                            snackbarHostState.showSnackbar(\n                                                message = removedSongMsg,\n                                                actionLabel = undoStr,\n                                                duration = SnackbarDuration.Short,\n                                            )\n                                        if (snackbarResult == SnackbarResult.ActionPerformed) {\n                                            playerConnection.player.addMediaItem(currentItem.mediaItem)\n                                            playerConnection.player.moveMediaItem(\n                                                mutableQueueWindows.size,\n                                                currentItem.firstPeriodIndex,\n                                            )\n                                        }\n                                    }\n                            }\n                            if (dv == SwipeToDismissBoxValue.Settled) {\n                                processedDismiss = false\n                            }\n                        }\n\n                        val onCheckedChange: (Boolean) -> Unit = {\n                            if (it) {\n                                selection.add(window.mediaItem.mediaId)\n                            } else {\n                                selection.remove(window.mediaItem.mediaId)\n                            }\n                        }\n\n                        val content: @Composable () -> Unit = {\n                            Row(\n                                horizontalArrangement = Arrangement.Center,\n                                modifier = Modifier.animateItem(),\n                            ) {\n                                MediaMetadataListItem(\n                                    mediaMetadata = window.mediaItem.metadata!!,\n                                    isSelected = false,\n                                    isActive = isActive,\n                                    isPlaying = isPlaying && isActive,\n                                    trailingContent = {\n                                        if (inSelectMode) {\n                                            Checkbox(\n                                                checked = window.mediaItem.mediaId in selection,\n                                                onCheckedChange = onCheckedChange,\n                                            )\n                                        } else {\n                                            if (!isListenTogetherGuest) {\n                                                IconButton(\n                                                    onClick = {\n                                                        menuState.show {\n                                                            QueueMenu(\n                                                                mediaMetadata = window.mediaItem.metadata!!,\n                                                                navController = navController,\n                                                                playerBottomSheetState = playerBottomSheetState,\n                                                                onShowDetailsDialog = {\n                                                                    window.mediaItem.mediaId.let {\n                                                                        bottomSheetPageState.show {\n                                                                            ShowMediaInfo(it)\n                                                                        }\n                                                                    }\n                                                                },\n                                                                onDismiss = menuState::dismiss,\n                                                            )\n                                                        }\n                                                    },\n                                                ) {\n                                                    Icon(\n                                                        painter = painterResource(R.drawable.more_vert),\n                                                        contentDescription = null,\n                                                    )\n                                                }\n                                            }\n                                            if (!locked && !isListenTogetherGuest) {\n                                                IconButton(\n                                                    onClick = { },\n                                                    modifier = Modifier.draggableHandle(),\n                                                ) {\n                                                    Icon(\n                                                        painter = painterResource(R.drawable.drag_handle),\n                                                        contentDescription = null,\n                                                    )\n                                                }\n                                            }\n                                        }\n                                    },\n                                    modifier =\n                                        Modifier\n                                            .fillMaxWidth()\n                                            .background(background)\n                                            .combinedClickable(\n                                                onClick = {\n                                                    if (inSelectMode) {\n                                                        onCheckedChange(window.mediaItem.mediaId !in selection)\n                                                    } else if (!isListenTogetherGuest) {\n                                                        if (index == currentWindowIndex) {\n                                                            if (isCasting) {\n                                                                if (castIsPlaying) {\n                                                                    castHandler?.pause()\n                                                                } else {\n                                                                    castHandler?.play()\n                                                                }\n                                                            } else {\n                                                                playerConnection.togglePlayPause()\n                                                            }\n                                                        } else {\n                                                            if (isCasting) {\n                                                                val mediaId = window.mediaItem.mediaId\n                                                                val navigated = castHandler?.navigateToMediaIfInQueue(mediaId) ?: false\n                                                                if (!navigated) {\n                                                                    playerConnection.player.seekToDefaultPosition(window.firstPeriodIndex)\n                                                                }\n                                                            } else {\n                                                                playerConnection.player.seekToDefaultPosition(\n                                                                    window.firstPeriodIndex,\n                                                                )\n                                                                playerConnection.player.playWhenReady = true\n                                                            }\n                                                        }\n                                                    }\n                                                },\n                                                onLongClick = {\n                                                    if (!inSelectMode) {\n                                                        haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                        inSelectMode = true\n                                                        onCheckedChange(true)\n                                                    }\n                                                },\n                                            ),\n                                )\n                            }\n                        }\n\n                        if (locked) {\n                            content()\n                        } else {\n                            SwipeToDismissBox(\n                                state = dismissBoxState,\n                                backgroundContent = {},\n                            ) {\n                                content()\n                            }\n                        }\n                    }\n                }\n\n                if (automix.isNotEmpty()) {\n                    item(key = \"automix_divider\") {\n                        HorizontalDivider(\n                            modifier =\n                                Modifier\n                                    .padding(vertical = 8.dp, horizontal = 4.dp)\n                                    .animateItem(),\n                        )\n\n                        Text(\n                            text = stringResource(R.string.similar_content),\n                            modifier = Modifier.padding(start = 16.dp),\n                        )\n                    }\n\n                    itemsIndexed(\n                        items = automix,\n                        key = { _, it -> it.mediaId },\n                    ) { index, item ->\n                        Row(\n                            horizontalArrangement = Arrangement.Center,\n                        ) {\n                            MediaMetadataListItem(\n                                mediaMetadata = item.metadata!!,\n                                trailingContent = {\n                                    if (!isListenTogetherGuest) {\n                                        IconButton(\n                                            onClick = {\n                                                playerConnection.service.playNextAutomix(\n                                                    item,\n                                                    index,\n                                                )\n                                            },\n                                        ) {\n                                            Icon(\n                                                painter = painterResource(R.drawable.playlist_play),\n                                                contentDescription = null,\n                                            )\n                                        }\n                                        IconButton(\n                                            onClick = {\n                                                playerConnection.service.addToQueueAutomix(\n                                                    item,\n                                                    index,\n                                                )\n                                            },\n                                        ) {\n                                            Icon(\n                                                painter = painterResource(R.drawable.queue_music),\n                                                contentDescription = null,\n                                            )\n                                        }\n                                    }\n                                },\n                                modifier =\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .combinedClickable(\n                                            onClick = {},\n                                            onLongClick = {\n                                                menuState.show {\n                                                    QueueMenu(\n                                                        mediaMetadata = item.metadata!!,\n                                                        navController = navController,\n                                                        playerBottomSheetState = playerBottomSheetState,\n                                                        onShowDetailsDialog = {\n                                                            item.mediaId.let {\n                                                                bottomSheetPageState.show {\n                                                                    ShowMediaInfo(it)\n                                                                }\n                                                            }\n                                                        },\n                                                        onDismiss = menuState::dismiss,\n                                                    )\n                                                }\n                                            },\n                                        ).animateItem(),\n                            )\n                        }\n                    }\n                }\n            }\n        }\n\n        Column(\n            modifier =\n                Modifier\n                    .clickable(\n                        indication = null,\n                        interactionSource = remember { MutableInteractionSource() },\n                    ) { }\n                    .background(\n                        if (pureBlack) {\n                            Color.Black\n                        } else {\n                            MaterialTheme.colorScheme\n                                .secondaryContainer\n                                .copy(alpha = 0.90f)\n                        },\n                    ).windowInsetsPadding(\n                        WindowInsets.systemBars\n                            .only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),\n                    ),\n        ) {\n            Row(\n                horizontalArrangement = Arrangement.spacedBy(6.dp),\n                verticalAlignment = Alignment.CenterVertically,\n                modifier =\n                    Modifier\n                        .height(ListItemHeight)\n                        .padding(horizontal = 12.dp),\n            ) {\n                Text(\n                    text = queueTitle.orEmpty(),\n                    style = MaterialTheme.typography.titleMedium,\n                    maxLines = 2,\n                    overflow = TextOverflow.Ellipsis,\n                    modifier = Modifier.weight(1f),\n                )\n\n                AnimatedVisibility(\n                    visible = !inSelectMode,\n                    enter = fadeIn() + slideInVertically { it },\n                    exit = fadeOut() + slideOutVertically { it },\n                ) {\n                    Row {\n                        IconButton(\n                            onClick = { locked = !locked },\n                            modifier = Modifier.padding(horizontal = 6.dp),\n                        ) {\n                            Icon(\n                                painter = painterResource(if (locked) R.drawable.lock else R.drawable.lock_open),\n                                contentDescription = null,\n                            )\n                        }\n                    }\n                }\n\n                Column(\n                    verticalArrangement = Arrangement.spacedBy(4.dp),\n                    horizontalAlignment = Alignment.End,\n                ) {\n                    Text(\n                        text =\n                            pluralStringResource(\n                                R.plurals.n_song,\n                                queueWindows.size,\n                                queueWindows.size,\n                            ),\n                        style = MaterialTheme.typography.bodyMedium,\n                    )\n\n                    Text(\n                        text = makeTimeString(queueLength * 1000L),\n                        style = MaterialTheme.typography.bodyMedium,\n                    )\n                }\n            }\n\n            AnimatedVisibility(\n                visible = inSelectMode,\n                enter = fadeIn() + expandVertically(),\n                exit = fadeOut() + shrinkVertically(),\n            ) {\n                val selectedSongs =\n                    remember(selection.toList(), mutableQueueWindows) {\n                        mutableQueueWindows\n                            .filter { it.mediaItem.mediaId in selection }\n                            .mapNotNull { it.mediaItem.metadata }\n                    }\n                val selectedItems =\n                    remember(selection.toList(), mutableQueueWindows) {\n                        mutableQueueWindows.filter { it.mediaItem.mediaId in selection }\n                    }\n                val count = selection.size\n                Row(\n                    modifier =\n                        Modifier\n                            .height(48.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    IconButton(\n                        onClick = onExitSelectionMode,\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.close),\n                            contentDescription = null,\n                        )\n                    }\n                    Text(\n                        text = pluralStringResource(R.plurals.n_selected, count, count),\n                        modifier = Modifier.weight(1f),\n                    )\n                    Checkbox(\n                        checked = count == mutableQueueWindows.size && count > 0,\n                        onCheckedChange = {\n                            if (count == mutableQueueWindows.size) {\n                                selection.clear()\n                            } else {\n                                selection.clear()\n                                mutableQueueWindows.forEach {\n                                    selection.add(it.mediaItem.mediaId)\n                                }\n                            }\n                        },\n                    )\n                    IconButton(\n                        enabled = count > 0,\n                        onClick = {\n                            menuState.show {\n                                SelectionMediaMetadataMenu(\n                                    songSelection = selectedSongs,\n                                    onDismiss = menuState::dismiss,\n                                    clearAction = onExitSelectionMode,\n                                    currentItems = selectedItems,\n                                )\n                            }\n                        },\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.more_vert),\n                            contentDescription = null,\n                            tint = LocalContentColor.current,\n                        )\n                    }\n                }\n            }\n            if (pureBlack) {\n                HorizontalDivider()\n            }\n        }\n\n        val shuffleModeEnabled by playerConnection.shuffleModeEnabled.collectAsState()\n\n        Box(\n            modifier =\n                Modifier\n                    .background(\n                        if (pureBlack) {\n                            Color.Black\n                        } else {\n                            MaterialTheme.colorScheme\n                                .secondaryContainer\n                                .copy(alpha = 0.90f)\n                        },\n                    ).fillMaxWidth()\n                    .height(\n                        ListItemHeight +\n                            WindowInsets.systemBars\n                                .asPaddingValues()\n                                .calculateBottomPadding(),\n                    ).align(Alignment.BottomCenter)\n                    .clickable {\n                        state.collapseSoft()\n                    }.windowInsetsPadding(\n                        WindowInsets.systemBars\n                            .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal),\n                    ).padding(12.dp),\n        ) {\n            IconButton(\n                enabled = !isListenTogetherGuest,\n                modifier = Modifier.align(Alignment.CenterStart),\n                onClick = {\n                    coroutineScope\n                        .launch {\n                            lazyListState.animateScrollToItem(\n                                if (playerConnection.player.shuffleModeEnabled) playerConnection.player.currentMediaItemIndex else 0,\n                            )\n                        }.invokeOnCompletion {\n                            playerConnection.player.shuffleModeEnabled =\n                                !playerConnection.player.shuffleModeEnabled\n                        }\n                },\n            ) {\n                val baseAlpha = if (shuffleModeEnabled) 1f else 0.5f\n                val finalAlpha = if (!isListenTogetherGuest) baseAlpha else 0.3f\n                Icon(\n                    painter = painterResource(R.drawable.shuffle),\n                    contentDescription = null,\n                    modifier = Modifier.alpha(finalAlpha),\n                )\n            }\n\n            Icon(\n                painter = painterResource(R.drawable.expand_more),\n                contentDescription = null,\n                modifier = Modifier.align(Alignment.Center),\n            )\n\n            IconButton(\n                enabled = !isListenTogetherGuest,\n                modifier = Modifier.align(Alignment.CenterEnd),\n                onClick = playerConnection.player::toggleRepeatMode,\n            ) {\n                val baseAlpha = if (repeatMode == Player.REPEAT_MODE_OFF) 0.5f else 1f\n                val finalAlpha = if (!isListenTogetherGuest) baseAlpha else 0.3f\n                Icon(\n                    painter =\n                        painterResource(\n                            when (repeatMode) {\n                                Player.REPEAT_MODE_OFF, Player.REPEAT_MODE_ALL -> R.drawable.repeat\n                                Player.REPEAT_MODE_ONE -> R.drawable.repeat_one\n                                else -> throw IllegalStateException()\n                            },\n                        ),\n                    contentDescription = null,\n                    modifier = Modifier.alpha(finalAlpha),\n                )\n            }\n        }\n\n        SnackbarHost(\n            hostState = snackbarHostState,\n            modifier =\n                Modifier\n                    .padding(\n                        bottom =\n                            ListItemHeight +\n                                WindowInsets.systemBars\n                                    .asPaddingValues()\n                                    .calculateBottomPadding(),\n                    ).align(Alignment.BottomCenter),\n        )\n    }\n}\n\n@Composable\nprivate fun PlayerQueueButton(\n    icon: Int,\n    onClick: () -> Unit,\n    isActive: Boolean,\n    enabled: Boolean = true,\n    shape: RoundedCornerShape,\n    modifier: Modifier = Modifier,\n    text: String? = null,\n    textButtonColor: Color,\n    iconButtonColor: Color,\n    iconSize: androidx.compose.ui.unit.Dp,\n    textBackgroundColor: Color,\n    playerBackground: PlayerBackgroundStyle,\n) {\n    val buttonModifier =\n        Modifier\n            .clip(shape)\n            .clickable(enabled = enabled, onClick = onClick)\n\n    val alphaFactor = if (enabled) 1f else 0.35f\n\n    val appliedModifier =\n        if (isActive) {\n            modifier.then(buttonModifier.background(textButtonColor)).alpha(alphaFactor)\n        } else {\n            modifier\n                .then(\n                    buttonModifier.border(\n                        width = 1.dp,\n                        color = textButtonColor.copy(alpha = 0.3f),\n                        shape = shape,\n                    ),\n                ).alpha(alphaFactor)\n        }\n\n    Box(\n        modifier = appliedModifier,\n        contentAlignment = Alignment.Center,\n    ) {\n        if (text != null) {\n            Text(\n                text = text,\n                color = iconButtonColor.copy(alpha = if (enabled) 1f else 0.6f),\n                fontSize = 10.sp,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n                textAlign = TextAlign.Center,\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .basicMarquee(),\n            )\n        } else {\n            val baseTint =\n                if (isActive) {\n                    iconButtonColor\n                } else {\n                    when (playerBackground) {\n                        PlayerBackgroundStyle.BLUR, PlayerBackgroundStyle.GRADIENT -> {\n                            Color.White\n                        }\n\n                        PlayerBackgroundStyle.DEFAULT -> {\n                            MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f)\n                        }\n                    }\n                }\n            val finalTint = if (enabled) baseTint else baseTint.copy(alpha = 0.5f)\n            Icon(\n                painter = painterResource(id = icon),\n                contentDescription = null,\n                modifier = Modifier.size(iconSize),\n                tint = finalTint,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/player/Thumbnail.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.player\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.basicMarquee\nimport androidx.compose.foundation.gestures.detectTapGestures\nimport androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.statusBarsPadding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyHorizontalGrid\nimport androidx.compose.foundation.lazy.grid.items\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.CompositingStrategy\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.media3.common.C\nimport androidx.media3.common.MediaItem\nimport androidx.media3.common.Player\nimport coil3.compose.AsyncImage\nimport coil3.request.CachePolicy\nimport coil3.request.ImageRequest\nimport com.metrolist.music.LocalListenTogetherManager\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.CropAlbumArtKey\nimport com.metrolist.music.constants.HidePlayerThumbnailKey\nimport com.metrolist.music.constants.PlayerBackgroundStyle\nimport com.metrolist.music.constants.PlayerBackgroundStyleKey\nimport com.metrolist.music.constants.PlayerHorizontalPadding\nimport com.metrolist.music.constants.SeekExtraSeconds\nimport com.metrolist.music.constants.SwipeThumbnailKey\nimport com.metrolist.music.constants.ThumbnailCornerRadius\nimport com.metrolist.music.listentogether.RoomRole\nimport com.metrolist.music.ui.component.CastButton\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport kotlinx.coroutines.delay\n\n/**\n * Pre-calculated thumbnail dimensions to avoid repeated calculations during recomposition.\n * All values are computed once and cached.\n */\n@Immutable\ndata class ThumbnailDimensions(\n    val itemWidth: Dp,\n    val containerSize: Dp,\n    val thumbnailSize: Dp,\n    val cornerRadius: Dp\n)\n\n/**\n * Cached media items data to prevent recalculation on every recomposition.\n */\n@Immutable\ndata class MediaItemsData(\n    val items: List<MediaItem>,\n    val currentIndex: Int\n)\n\n/**\n * Calculate thumbnail dimensions once based on container size.\n * This function is marked as @Stable to indicate it produces stable results.\n * In landscape mode, uses the smaller dimension (height) to ensure square thumbnail fits.\n */\n@Stable\nprivate fun calculateThumbnailDimensions(\n    containerWidth: Dp,\n    containerHeight: Dp = containerWidth,\n    horizontalPadding: Dp = PlayerHorizontalPadding,\n    cornerRadius: Dp = ThumbnailCornerRadius,\n    isLandscape: Boolean = false\n): ThumbnailDimensions {\n    // In landscape, use height as the constraining dimension for a square thumbnail\n    val effectiveSize = if (isLandscape) {\n        minOf(containerWidth, containerHeight) - (horizontalPadding * 2)\n    } else {\n        containerWidth - (horizontalPadding * 2)\n    }\n    return ThumbnailDimensions(\n        itemWidth = containerWidth,\n        containerSize = containerWidth,\n        thumbnailSize = effectiveSize,\n        cornerRadius = cornerRadius * 2\n    )\n}\n\n/**\n * Get media items for the thumbnail carousel.\n * Calculates previous, current, and next items based on shuffle mode.\n */\n@Stable\nprivate fun getMediaItems(\n    player: Player,\n    swipeThumbnail: Boolean\n): MediaItemsData {\n    val timeline = player.currentTimeline\n    val currentIndex = player.currentMediaItemIndex\n    val shuffleModeEnabled = player.shuffleModeEnabled\n    \n    val currentMediaItem = try {\n        player.currentMediaItem\n    } catch (e: Exception) { null }\n    \n    val previousMediaItem = if (swipeThumbnail && !timeline.isEmpty) {\n        val previousIndex = timeline.getPreviousWindowIndex(\n            currentIndex,\n            Player.REPEAT_MODE_OFF,\n            shuffleModeEnabled\n        )\n        if (previousIndex != C.INDEX_UNSET) {\n            try { player.getMediaItemAt(previousIndex) } catch (e: Exception) { null }\n        } else null\n    } else null\n\n    val nextMediaItem = if (swipeThumbnail && !timeline.isEmpty) {\n        val nextIndex = timeline.getNextWindowIndex(\n            currentIndex,\n            Player.REPEAT_MODE_OFF,\n            shuffleModeEnabled\n        )\n        if (nextIndex != C.INDEX_UNSET) {\n            try { player.getMediaItemAt(nextIndex) } catch (e: Exception) { null }\n        } else null\n    } else null\n\n    val items = listOfNotNull(previousMediaItem, currentMediaItem, nextMediaItem)\n    val currentMediaIndex = items.indexOf(currentMediaItem)\n    \n    return MediaItemsData(items, currentMediaIndex)\n}\n\n/**\n * Get text color based on player background style.\n * Computed once per background style change.\n */\n@Stable\n@Composable\nprivate fun getTextColor(playerBackground: PlayerBackgroundStyle): Color {\n    return when (playerBackground) {\n        PlayerBackgroundStyle.DEFAULT -> MaterialTheme.colorScheme.onBackground\n        PlayerBackgroundStyle.BLUR -> Color.White\n        PlayerBackgroundStyle.GRADIENT -> Color.White\n    }\n}\n\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nfun Thumbnail(\n    sliderPositionProvider: () -> Long?,\n    modifier: Modifier = Modifier,\n    isPlayerExpanded: () -> Boolean = { true },\n    isLandscape: Boolean = false,\n    isListenTogetherGuest: Boolean = false,\n) {\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val context = LocalContext.current\n    val layoutDirection = LocalLayoutDirection.current\n\n    // Collect states\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n    val error by playerConnection.error.collectAsState()\n    val queueTitle by playerConnection.queueTitle.collectAsState()\n    val canSkipPrevious by playerConnection.canSkipPrevious.collectAsState()\n    val canSkipNext by playerConnection.canSkipNext.collectAsState()\n\n    // Preferences - computed once\n    // Disable swipe for Listen Together guests\n    val swipeThumbnailPref by rememberPreference(SwipeThumbnailKey, true)\n    val swipeThumbnail = swipeThumbnailPref && !isListenTogetherGuest\n    val hidePlayerThumbnail by rememberPreference(HidePlayerThumbnailKey, false)\n    val cropAlbumArt by rememberPreference(CropAlbumArtKey, false)\n    val playerBackground by rememberEnumPreference(\n        key = PlayerBackgroundStyleKey,\n        defaultValue = PlayerBackgroundStyle.DEFAULT\n    )\n    \n    // Pre-calculate text color based on background style\n    val textBackgroundColor = getTextColor(playerBackground)\n    \n    // Grid state\n    val thumbnailLazyGridState = rememberLazyGridState()\n    \n    // Calculate media items data - memoized\n    val mediaItemsData by remember(\n        playerConnection.player.currentMediaItemIndex,\n        playerConnection.player.shuffleModeEnabled,\n        swipeThumbnail,\n        mediaMetadata\n    ) {\n        derivedStateOf {\n            getMediaItems(playerConnection.player, swipeThumbnail)\n        }\n    }\n    \n    val mediaItems = mediaItemsData.items\n    val currentMediaIndex = mediaItemsData.currentIndex\n\n    // Snap behavior - created once per grid state\n    val thumbnailSnapLayoutInfoProvider = remember(thumbnailLazyGridState) {\n        ThumbnailSnapLayoutInfoProvider(\n            lazyGridState = thumbnailLazyGridState,\n            positionInLayout = { layoutSize, itemSize ->\n                (layoutSize / 2f - itemSize / 2f)\n            },\n            velocityThreshold = 500f\n        )\n    }\n\n    // Current item tracking - derived state for efficiency\n    val currentItem by remember { derivedStateOf { thumbnailLazyGridState.firstVisibleItemIndex } }\n    val itemScrollOffset by remember { derivedStateOf { thumbnailLazyGridState.firstVisibleItemScrollOffset } }\n\n    // Handle swipe to change song\n    LaunchedEffect(itemScrollOffset) {\n        if (!thumbnailLazyGridState.isScrollInProgress || !swipeThumbnail || itemScrollOffset != 0 || currentMediaIndex < 0) return@LaunchedEffect\n\n        if (currentItem > currentMediaIndex && canSkipNext) {\n            playerConnection.player.seekToNext()\n        } else if (currentItem < currentMediaIndex && canSkipPrevious) {\n            playerConnection.player.seekToPreviousMediaItem()\n        }\n    }\n\n    // Update position when song changes\n    LaunchedEffect(mediaMetadata, canSkipPrevious, canSkipNext) {\n        val index = maxOf(0, currentMediaIndex)\n        if (index >= 0 && index < mediaItems.size) {\n            try {\n                thumbnailLazyGridState.animateScrollToItem(index)\n            } catch (e: Exception) {\n                thumbnailLazyGridState.scrollToItem(index)\n            }\n        }\n    }\n\n    LaunchedEffect(playerConnection.player.currentMediaItemIndex) {\n        val index = mediaItemsData.currentIndex\n        if (index >= 0 && index != currentItem) {\n            thumbnailLazyGridState.scrollToItem(index)\n        }\n    }\n\n    // Seek effect state\n    var showSeekEffect by remember { mutableStateOf(false) }\n    var seekDirection by remember { mutableStateOf(\"\") }\n\n    Box(\n        modifier = modifier\n            .graphicsLayer {\n                // Use hardware layer for entire Thumbnail to ensure smooth 120Hz animations\n                compositingStrategy = CompositingStrategy.Offscreen\n            }\n    ) {\n        // Error view\n        AnimatedVisibility(\n            visible = error != null,\n            enter = fadeIn(),\n            exit = fadeOut(),\n            modifier = Modifier\n                .padding(32.dp)\n                .align(Alignment.Center),\n        ) {\n            error?.let { playbackError ->\n                PlaybackError(\n                    error = playbackError,\n                    retry = playerConnection.player::prepare,\n                )\n            }\n        }\n\n        // Main thumbnail view\n        AnimatedVisibility(\n            visible = error == null,\n            enter = fadeIn(),\n            exit = fadeOut(),\n            modifier = Modifier\n                .fillMaxSize()\n                .then(if (!isLandscape) Modifier.statusBarsPadding() else Modifier),\n        ) {\n            Column(\n                modifier = Modifier.fillMaxSize(),\n                horizontalAlignment = Alignment.CenterHorizontally,\n                verticalArrangement = if (isLandscape) Arrangement.Center else Arrangement.Top\n            ) {\n                // Now Playing header - hide in landscape mode\n                if (!isLandscape) {\n                    ThumbnailHeader(\n                        queueTitle = queueTitle,\n                        albumTitle = mediaMetadata?.album?.title,\n                        textColor = textBackgroundColor\n                    )\n                }\n                \n                // Thumbnail content\n                BoxWithConstraints(\n                    contentAlignment = Alignment.Center,\n                    modifier = if (isLandscape) {\n                        Modifier.weight(1f, false)\n                    } else {\n                        Modifier.fillMaxSize()\n                    }\n                ) {\n                    // Calculate dimensions once per size change, considering landscape mode\n                    val dimensions = remember(maxWidth, maxHeight, isLandscape) {\n                        calculateThumbnailDimensions(\n                            containerWidth = maxWidth,\n                            containerHeight = maxHeight,\n                            isLandscape = isLandscape\n                        )\n                    }\n\n                    // Remember the onSeek callback to prevent recomposition\n                    val onSeekCallback = remember {\n                        { direction: String, showEffect: Boolean ->\n                            seekDirection = direction\n                            showSeekEffect = showEffect\n                        }\n                    }\n                    \n                    // Derive scroll enabled state to prevent unnecessary recomposition\n                    val isScrollEnabled by remember(swipeThumbnail) {\n                        derivedStateOf { swipeThumbnail && isPlayerExpanded() }\n                    }\n                    \n                    LazyHorizontalGrid(\n                        state = thumbnailLazyGridState,\n                        rows = GridCells.Fixed(1),\n                        flingBehavior = rememberSnapFlingBehavior(thumbnailSnapLayoutInfoProvider),\n                        userScrollEnabled = isScrollEnabled,\n                        modifier = if (isLandscape) {\n                            Modifier.size(dimensions.thumbnailSize + (PlayerHorizontalPadding * 2))\n                        } else {\n                            Modifier.fillMaxSize()\n                        }\n                    ) {\n                        items(\n                            items = mediaItems,\n                            key = { item -> \n                                item.mediaId.ifEmpty { \"unknown_${item.hashCode()}\" }\n                            }\n                        ) { item ->\n                            ThumbnailItem(\n                                item = item,\n                                dimensions = dimensions,\n                                hidePlayerThumbnail = hidePlayerThumbnail,\n                                cropAlbumArt = cropAlbumArt,\n                                textBackgroundColor = textBackgroundColor,\n                                layoutDirection = layoutDirection,\n                                onSeek = onSeekCallback,\n                                playerConnection = playerConnection,\n                                context = context,\n                                isLandscape = isLandscape,\n                                isListenTogetherGuest = isListenTogetherGuest,\n                                currentMediaId = mediaMetadata?.id,\n                                currentMediaThumbnail = mediaMetadata?.thumbnailUrl\n                            )\n                        }\n                    }\n                }\n            }\n        }\n\n        // Seek effect\n        LaunchedEffect(showSeekEffect) {\n            if (showSeekEffect) {\n                delay(1000)\n                showSeekEffect = false\n            }\n        }\n\n        AnimatedVisibility(\n            visible = showSeekEffect,\n            enter = fadeIn(),\n            exit = fadeOut(),\n            modifier = Modifier.align(Alignment.Center)\n        ) {\n            SeekEffectOverlay(seekDirection = seekDirection)\n        }\n    }\n}\n\n/**\n * Header component showing \"Now Playing\" and queue/album title.\n */\n@Composable\nprivate fun ThumbnailHeader(\n    queueTitle: String?,\n    albumTitle: String?,\n    textColor: Color,\n    modifier: Modifier = Modifier\n) {\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val listenTogetherRoleState = listenTogetherManager?.role?.collectAsState(initial = RoomRole.NONE)\n    val isListenTogetherGuest = listenTogetherRoleState?.value == RoomRole.GUEST\n    Box(\n        modifier = modifier\n            .fillMaxWidth()\n            .padding(horizontal = 16.dp, vertical = 8.dp)\n    ) {\n        Column(\n            horizontalAlignment = Alignment.CenterHorizontally,\n            modifier = Modifier\n                .align(Alignment.Center)\n                .padding(horizontal = 48.dp)\n        ) {\n            // Listen Together indicator\n            if (listenTogetherRoleState?.value != RoomRole.NONE) {\n                Text(\n                    text = if (listenTogetherRoleState?.value == RoomRole.HOST) \"Hosting Listen Together\" else \"Listening Together\",\n                    style = MaterialTheme.typography.titleMedium,\n                    color = textColor\n                )\n            } else {\n                Text(\n                    text = stringResource(R.string.now_playing),\n                    style = MaterialTheme.typography.titleMedium,\n                    color = textColor\n                )\n            }\n            val playingFrom = queueTitle ?: albumTitle\n            if (!playingFrom.isNullOrBlank()) {\n                Spacer(modifier = Modifier.height(4.dp))\n                Text(\n                    text = playingFrom,\n                    style = MaterialTheme.typography.titleMedium,\n                    color = textColor.copy(alpha = 0.8f),\n                    maxLines = 1,\n                    modifier = Modifier.basicMarquee()\n                )\n            }\n        }\n    }\n}\n\n/**\n * Individual thumbnail item in the carousel.\n */\n@Composable\nprivate fun ThumbnailItem(\n    item: MediaItem,\n    dimensions: ThumbnailDimensions,\n    hidePlayerThumbnail: Boolean,\n    cropAlbumArt: Boolean,\n    textBackgroundColor: Color,\n    layoutDirection: LayoutDirection,\n    onSeek: (String, Boolean) -> Unit,\n    playerConnection: com.metrolist.music.playback.PlayerConnection,\n    context: android.content.Context,\n    isLandscape: Boolean = false,\n    isListenTogetherGuest: Boolean = false,\n    currentMediaId: String? = null,\n    currentMediaThumbnail: String? = null,\n    modifier: Modifier = Modifier,\n) {\n    val incrementalSeekSkipEnabled by rememberPreference(SeekExtraSeconds, defaultValue = false)\n    var skipMultiplier by remember { mutableIntStateOf(1) }\n    var lastTapTime by remember { mutableLongStateOf(0L) }\n\n    Box(\n        modifier = modifier\n            .then(\n                if (isLandscape) {\n                    Modifier.size(dimensions.thumbnailSize + (PlayerHorizontalPadding * 2))\n                } else {\n                    Modifier\n                        .width(dimensions.itemWidth)\n                        .fillMaxSize()\n                }\n            )\n            .padding(horizontal = PlayerHorizontalPadding)\n            .graphicsLayer {\n                // Render entire thumbnail item on separate hardware layer for smooth animations\n                compositingStrategy = CompositingStrategy.Offscreen\n            }\n            .pointerInput(Unit) {\n                detectTapGestures(\n                    onDoubleTap = { offset ->\n                        if (isListenTogetherGuest) return@detectTapGestures\n\n                        val currentPosition = playerConnection.player.currentPosition\n                        val duration = playerConnection.player.duration\n\n                        val now = System.currentTimeMillis()\n                        if (incrementalSeekSkipEnabled && now - lastTapTime < 1000) {\n                            skipMultiplier++\n                        } else {\n                            skipMultiplier = 1\n                        }\n                        lastTapTime = now\n\n                        val skipAmount = 5000 * skipMultiplier\n\n                        val isLeftSide = (layoutDirection == LayoutDirection.Ltr && offset.x < size.width / 2) ||\n                                (layoutDirection == LayoutDirection.Rtl && offset.x > size.width / 2)\n\n                        if (isLeftSide) {\n                            playerConnection.player.seekTo((currentPosition - skipAmount).coerceAtLeast(0))\n                            onSeek(context.getString(R.string.seek_backward_dynamic, skipAmount / 1000), true)\n                        } else {\n                            playerConnection.player.seekTo((currentPosition + skipAmount).coerceAtMost(duration))\n                            onSeek(context.getString(R.string.seek_forward_dynamic, skipAmount / 1000), true)\n                        }\n                    }\n                )\n            },\n        contentAlignment = Alignment.Center\n    ) {\n        Box(\n            modifier = Modifier\n                .size(dimensions.thumbnailSize)\n                .clip(RoundedCornerShape(dimensions.cornerRadius))\n        ) {\n            if (hidePlayerThumbnail) {\n                HiddenThumbnailPlaceholder(textBackgroundColor = textBackgroundColor)\n            } else {\n                val artworkUriToUse = if (item.mediaId == currentMediaId && !currentMediaThumbnail.isNullOrBlank()) {\n                    currentMediaThumbnail\n                } else {\n                    item.mediaMetadata.artworkUri?.toString()\n                }\n\n                ThumbnailImage(\n                    artworkUri = artworkUriToUse,\n                    cropArtwork = cropAlbumArt\n                )\n            }\n            \n            // Cast button at top-right corner of thumbnail\n            CastButton(\n                modifier = Modifier\n                    .align(Alignment.TopEnd)\n                    .padding(8.dp),\n                tintColor = textBackgroundColor\n            )\n        }\n    }\n}\n\n/**\n * Placeholder shown when thumbnail is hidden.\n */\n@Composable\nprivate fun HiddenThumbnailPlaceholder(\n    textBackgroundColor: Color,\n    modifier: Modifier = Modifier\n) {\n    Box(\n        modifier = modifier\n            .fillMaxSize()\n            .background(MaterialTheme.colorScheme.surfaceVariant),\n        contentAlignment = Alignment.Center\n    ) {\n        Icon(\n            painter = painterResource(R.drawable.small_icon),\n            contentDescription = stringResource(R.string.hide_player_thumbnail),\n            tint = textBackgroundColor.copy(alpha = 0.7f),\n            modifier = Modifier.size(120.dp)\n        )\n    }\n}\n\n/**\n * Actual thumbnail image with caching and hardware layer rendering.\n */\n@Composable\nprivate fun ThumbnailImage(\n    artworkUri: String?,\n    cropArtwork: Boolean,\n    modifier: Modifier = Modifier\n) {\n    Box(\n        modifier = modifier\n            .fillMaxSize()\n            .graphicsLayer {\n                // Use offscreen compositing for hardware acceleration during animations\n                compositingStrategy = CompositingStrategy.Offscreen\n            }\n            .background(MaterialTheme.colorScheme.surfaceVariant)\n    ) {\n        AsyncImage(\n            model = ImageRequest.Builder(LocalContext.current)\n                .data(artworkUri)\n                .memoryCachePolicy(CachePolicy.ENABLED)\n                .diskCachePolicy(CachePolicy.ENABLED)\n                .networkCachePolicy(CachePolicy.ENABLED)\n                .build(),\n            contentDescription = null,\n            contentScale = if (cropArtwork) ContentScale.Crop else ContentScale.Fit,\n            modifier = Modifier.fillMaxSize()\n        )\n    }\n}\n\n/**\n * Seek effect overlay showing seek direction.\n */\n@Composable\nprivate fun SeekEffectOverlay(\n    seekDirection: String,\n    modifier: Modifier = Modifier\n) {\n    Text(\n        text = seekDirection,\n        color = Color.White,\n        fontSize = 16.sp,\n        fontWeight = FontWeight.Bold,\n        textAlign = TextAlign.Center,\n        modifier = modifier\n            .background(Color.Black.copy(alpha = 0.7f), RoundedCornerShape(8.dp))\n            .padding(8.dp)\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/player/ThumbnailSnapUtils.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n * \n * Snap utilities for Thumbnail grid navigation\n * Copyright (C) OuterTune Project - Custom SnapLayoutInfoProvider idea belongs to OuterTune\n */\n\npackage com.metrolist.music.ui.player\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.gestures.Orientation\nimport androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider\nimport androidx.compose.foundation.lazy.grid.LazyGridItemInfo\nimport androidx.compose.foundation.lazy.grid.LazyGridLayoutInfo\nimport androidx.compose.foundation.lazy.grid.LazyGridState\nimport androidx.compose.ui.util.fastForEach\nimport kotlin.math.abs\n\n/**\n * Custom SnapLayoutInfoProvider for horizontal grid snapping behavior.\n * Provides smooth snapping to items based on velocity and position.\n *\n * @param lazyGridState The state of the LazyHorizontalGrid\n * @param positionInLayout Function to calculate the desired snap position\n * @param velocityThreshold Minimum velocity required to trigger directional snap\n */\n@ExperimentalFoundationApi\nfun ThumbnailSnapLayoutInfoProvider(\n    lazyGridState: LazyGridState,\n    positionInLayout: (layoutSize: Float, itemSize: Float) -> Float = { layoutSize, itemSize ->\n        (layoutSize / 2f - itemSize / 2f)\n    },\n    velocityThreshold: Float = 1000f,\n): SnapLayoutInfoProvider = object : SnapLayoutInfoProvider {\n    private val layoutInfo: LazyGridLayoutInfo\n        get() = lazyGridState.layoutInfo\n\n    override fun calculateApproachOffset(velocity: Float, decayOffset: Float): Float = 0f\n    \n    override fun calculateSnapOffset(velocity: Float): Float {\n        val bounds = calculateSnappingOffsetBounds()\n\n        // Only snap when velocity exceeds threshold\n        if (abs(velocity) < velocityThreshold) {\n            return if (abs(bounds.start) < abs(bounds.endInclusive)) {\n                bounds.start\n            } else {\n                bounds.endInclusive\n            }\n        }\n\n        return when {\n            velocity < 0 -> bounds.start\n            velocity > 0 -> bounds.endInclusive\n            else -> 0f\n        }\n    }\n\n    private fun calculateSnappingOffsetBounds(): ClosedFloatingPointRange<Float> {\n        var lowerBoundOffset = Float.NEGATIVE_INFINITY\n        var upperBoundOffset = Float.POSITIVE_INFINITY\n\n        layoutInfo.visibleItemsInfo.fastForEach { item ->\n            val offset = calculateDistanceToDesiredSnapPosition(layoutInfo, item, positionInLayout)\n\n            // Find item that is closest to the center\n            if (offset <= 0 && offset > lowerBoundOffset) {\n                lowerBoundOffset = offset\n            }\n\n            // Find item that is closest to center, but after it\n            if (offset >= 0 && offset < upperBoundOffset) {\n                upperBoundOffset = offset\n            }\n        }\n\n        return lowerBoundOffset.rangeTo(upperBoundOffset)\n    }\n}\n\n/**\n * Calculates the distance from an item's current position to its desired snap position.\n *\n * @param layoutInfo The layout information of the grid\n * @param item The item to calculate distance for\n * @param positionInLayout Function to determine the desired position\n * @return The distance in pixels to the desired snap position\n */\nfun calculateDistanceToDesiredSnapPosition(\n    layoutInfo: LazyGridLayoutInfo,\n    item: LazyGridItemInfo,\n    positionInLayout: (layoutSize: Float, itemSize: Float) -> Float,\n): Float {\n    val containerSize =\n        layoutInfo.singleAxisViewportSize - layoutInfo.beforeContentPadding - layoutInfo.afterContentPadding\n\n    val desiredDistance = positionInLayout(containerSize.toFloat(), item.size.width.toFloat())\n    val itemCurrentPosition = item.offset.x.toFloat()\n\n    return itemCurrentPosition - desiredDistance\n}\n\n/**\n * Extension property to get the viewport size along the scroll axis.\n */\nval LazyGridLayoutInfo.singleAxisViewportSize: Int\n    get() = if (orientation == Orientation.Vertical) viewportSize.height else viewportSize.width\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/AccountScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.GridItemSpan\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.items\nimport androidx.compose.foundation.lazy.grid.itemsIndexed\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport coil3.compose.AsyncImage\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.GridItemSize\nimport com.metrolist.music.constants.GridItemsSizeKey\nimport com.metrolist.music.constants.GridThumbnailHeight\nimport com.metrolist.music.ui.component.ChipsRow\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.YouTubeGridItem\nimport com.metrolist.music.db.entities.PodcastEntity\nimport com.metrolist.music.ui.component.shimmer.GridItemPlaceHolder\nimport com.metrolist.music.ui.component.shimmer.ListItemPlaceHolder\nimport com.metrolist.music.ui.component.shimmer.ShimmerHost\nimport com.metrolist.music.ui.menu.YouTubeAlbumMenu\nimport com.metrolist.music.ui.menu.YouTubeArtistMenu\nimport com.metrolist.music.ui.menu.YouTubePlaylistMenu\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.viewmodels.AccountContentType\nimport com.metrolist.music.viewmodels.AccountViewModel\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)\n@Composable\nfun AccountScreen(\n    navController: NavController,\n    viewModel: AccountViewModel = hiltViewModel(),\n) {\n    val menuState = LocalMenuState.current\n    val haptic = LocalHapticFeedback.current\n\n    val coroutineScope = rememberCoroutineScope()\n\n    val playlists by viewModel.playlists.collectAsState()\n    val albums by viewModel.albums.collectAsState()\n    val artists by viewModel.artists.collectAsState()\n    val sePlaylist by viewModel.sePlaylist.collectAsState()\n    val rdpnPlaylist by viewModel.rdpnPlaylist.collectAsState()\n    val podcastPlaylists by viewModel.podcastPlaylists.collectAsState()\n    val podcastChannels by viewModel.podcastChannels.collectAsState()\n    val selectedContentType by viewModel.selectedContentType.collectAsState()\n    val gridItemSize by rememberEnumPreference(GridItemsSizeKey, GridItemSize.BIG)\n\n    LazyVerticalGrid(\n        columns = GridCells.Adaptive(minSize = GridThumbnailHeight + if (gridItemSize == GridItemSize.BIG) 24.dp else (-24).dp),\n        contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n    ) {\n        item(span = { GridItemSpan(maxLineSpan) }) {\n            ChipsRow(\n                chips = listOf(\n                    AccountContentType.PLAYLISTS to stringResource(R.string.filter_playlists),\n                    AccountContentType.ALBUMS to stringResource(R.string.filter_albums),\n                    AccountContentType.ARTISTS to stringResource(R.string.filter_artists),\n                    AccountContentType.PODCASTS to stringResource(R.string.filter_podcasts),\n                ),\n                currentValue = selectedContentType,\n                onValueUpdate = { viewModel.setSelectedContentType(it) },\n            )\n        }\n\n        when (selectedContentType) {\n            AccountContentType.PLAYLISTS -> {\n                items(\n                    items = playlists.orEmpty().distinctBy { it.id },\n                    key = { it.id },\n                ) { item ->\n                    YouTubeGridItem(\n                        item = item,\n                        fillMaxWidth = true,\n                        modifier = Modifier\n                            .combinedClickable(\n                                onClick = {\n                                    navController.navigate(\"online_playlist/${item.id}\")\n                                },\n                                onLongClick = {\n                                    haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                    menuState.show {\n                                        YouTubePlaylistMenu(\n                                            playlist = item,\n                                            coroutineScope = coroutineScope,\n                                            onDismiss = menuState::dismiss,\n                                        )\n                                    }\n                                },\n                            ),\n                    )\n                }\n\n                if (playlists == null) {\n                    items(8) {\n                        ShimmerHost {\n                            GridItemPlaceHolder(fillMaxWidth = true)\n                        }\n                    }\n                }\n            }\n\n            AccountContentType.ALBUMS -> {\n                items(\n                    items = albums.orEmpty().distinctBy { it.id },\n                    key = { it.id }\n                ) { item ->\n                    YouTubeGridItem(\n                        item = item,\n                        fillMaxWidth = true,\n                        modifier = Modifier\n                            .combinedClickable(\n                                onClick = {\n                                    navController.navigate(\"album/${item.id}\")\n                                },\n                                onLongClick = {\n                                    haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                    menuState.show {\n                                        YouTubeAlbumMenu(\n                                            albumItem = item,\n                                            navController = navController,\n                                            onDismiss = menuState::dismiss\n                                        )\n                                    }\n                                }\n                            )\n                    )\n                }\n\n                if (albums == null) {\n                    items(8) {\n                        ShimmerHost {\n                            GridItemPlaceHolder(fillMaxWidth = true)\n                        }\n                    }\n                }\n            }\n\n            AccountContentType.ARTISTS -> {\n                items(\n                    items = artists.orEmpty().distinctBy { it.id },\n                    key = { it.id }\n                ) { item ->\n                    YouTubeGridItem(\n                        item = item,\n                        fillMaxWidth = true,\n                        modifier = Modifier\n                            .combinedClickable(\n                                onClick = {\n                                    navController.navigate(\"artist/${item.id}\")\n                                },\n                                onLongClick = {\n                                    haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                    menuState.show {\n                                        YouTubeArtistMenu(\n                                            artist = item,\n                                            onDismiss = menuState::dismiss\n                                        )\n                                    }\n                                }\n                            )\n                    )\n                }\n\n                if (artists == null) {\n                    items(8) {\n                        ShimmerHost {\n                            GridItemPlaceHolder(fillMaxWidth = true)\n                        }\n                    }\n                }\n            }\n\n            AccountContentType.PODCASTS -> {\n                // Show RDPN \"New Episodes\" playlist if available\n                rdpnPlaylist?.let { rdpn ->\n                    item(\n                        key = \"rdpn_playlist\",\n                        span = { GridItemSpan(maxLineSpan) },\n                    ) {\n                        SePlaylistAccountItem(\n                            thumbnailUrl = rdpn.thumbnail,\n                            title = stringResource(R.string.new_episodes),\n                            subtitle = stringResource(R.string.auto_playlist),\n                            onClick = { navController.navigate(\"online_playlist/RDPN\") },\n                        )\n                    }\n                }\n\n                // Show SE \"Episodes for Later\" playlist if available\n                sePlaylist?.let { se ->\n                    item(\n                        key = \"se_playlist\",\n                        span = { GridItemSpan(maxLineSpan) },\n                    ) {\n                        SePlaylistAccountItem(\n                            thumbnailUrl = se.thumbnail,\n                            title = stringResource(R.string.episodes_for_later),\n                            subtitle = stringResource(R.string.auto_playlist),\n                            onClick = { navController.navigate(\"online_playlist/SE\") },\n                        )\n                    }\n                }\n\n                // Subscribed podcast shows\n                if (podcastPlaylists.isNotEmpty()) {\n                    item(\n                        key = \"podcasts_header\",\n                        span = { GridItemSpan(maxLineSpan) },\n                    ) {\n                        Text(\n                            text = stringResource(R.string.filter_podcasts),\n                            style = MaterialTheme.typography.titleSmall,\n                            color = MaterialTheme.colorScheme.secondary,\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .padding(horizontal = 16.dp, vertical = 8.dp),\n                        )\n                    }\n                    itemsIndexed(\n                        items = podcastPlaylists,\n                        key = { _, item -> \"podcast_${item.id}\" },\n                        span = { _, _ -> GridItemSpan(maxLineSpan) },\n                    ) { _, podcast ->\n                        PodcastAccountItem(\n                            thumbnailUrl = podcast.thumbnailUrl,\n                            title = podcast.title,\n                            subtitle = podcast.author,\n                            onClick = { navController.navigate(\"online_podcast/${podcast.id}\") },\n                        )\n                    }\n                }\n\n                // Podcast channels\n                if (podcastChannels.isNotEmpty()) {\n                    item(\n                        key = \"channels_header\",\n                        span = { GridItemSpan(maxLineSpan) },\n                    ) {\n                        Text(\n                            text = stringResource(R.string.filter_channels),\n                            style = MaterialTheme.typography.titleSmall,\n                            color = MaterialTheme.colorScheme.secondary,\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .padding(horizontal = 16.dp, vertical = 8.dp),\n                        )\n                    }\n                    itemsIndexed(\n                        items = podcastChannels,\n                        key = { _, item -> \"channel_${item.id}\" },\n                        span = { _, _ -> GridItemSpan(maxLineSpan) },\n                    ) { _, channel ->\n                        PodcastChannelAccountItem(\n                            thumbnailUrl = channel.thumbnail,\n                            name = channel.title,\n                            onClick = { navController.navigate(\"artist/${channel.id}\") },\n                        )\n                    }\n                }\n\n                if (rdpnPlaylist == null && sePlaylist == null && podcastPlaylists.isEmpty() && podcastChannels.isEmpty()) {\n                    items(4, span = { GridItemSpan(maxLineSpan) }) {\n                        ShimmerHost {\n                            ListItemPlaceHolder()\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    TopAppBar(\n        title = { Text(stringResource(R.string.account)) },\n        navigationIcon = {\n            IconButton(\n                onClick = navController::navigateUp,\n                onLongClick = navController::backToMain,\n            ) {\n                Icon(\n                    painterResource(R.drawable.arrow_back),\n                    contentDescription = null,\n                )\n            }\n        },\n    )\n}\n\n@Composable\nprivate fun SePlaylistAccountItem(\n    thumbnailUrl: String?,\n    title: String,\n    subtitle: String,\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = modifier\n            .fillMaxWidth()\n            .clickable(onClick = onClick)\n            .padding(horizontal = 16.dp, vertical = 12.dp),\n    ) {\n        Box(\n            modifier = Modifier\n                .size(56.dp)\n                .clip(RoundedCornerShape(8.dp))\n                .background(MaterialTheme.colorScheme.primaryContainer),\n            contentAlignment = Alignment.Center,\n        ) {\n            if (thumbnailUrl != null) {\n                AsyncImage(\n                    model = thumbnailUrl,\n                    contentDescription = null,\n                    contentScale = ContentScale.Crop,\n                    modifier = Modifier\n                        .size(56.dp)\n                        .clip(RoundedCornerShape(8.dp)),\n                )\n            } else {\n                Icon(\n                    painter = painterResource(R.drawable.queue_music),\n                    contentDescription = null,\n                    tint = MaterialTheme.colorScheme.onPrimaryContainer,\n                    modifier = Modifier.size(28.dp),\n                )\n            }\n        }\n\n        Spacer(Modifier.width(12.dp))\n\n        Column(modifier = Modifier.weight(1f)) {\n            Text(\n                text = title,\n                style = MaterialTheme.typography.bodyLarge,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n            )\n            Text(\n                text = subtitle,\n                style = MaterialTheme.typography.bodyMedium,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n            )\n        }\n\n        Icon(\n            painter = painterResource(R.drawable.navigate_next),\n            contentDescription = null,\n        )\n    }\n}\n\n@Composable\nprivate fun PodcastAccountItem(\n    thumbnailUrl: String?,\n    title: String,\n    subtitle: String?,\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = modifier\n            .fillMaxWidth()\n            .clickable(onClick = onClick)\n            .padding(horizontal = 16.dp, vertical = 8.dp),\n    ) {\n        Box(\n            modifier = Modifier\n                .size(56.dp)\n                .clip(RoundedCornerShape(8.dp))\n                .background(MaterialTheme.colorScheme.primaryContainer),\n            contentAlignment = Alignment.Center,\n        ) {\n            if (thumbnailUrl != null) {\n                AsyncImage(\n                    model = thumbnailUrl,\n                    contentDescription = null,\n                    contentScale = ContentScale.Crop,\n                    modifier = Modifier\n                        .size(56.dp)\n                        .clip(RoundedCornerShape(8.dp)),\n                )\n            } else {\n                Icon(\n                    painter = painterResource(R.drawable.queue_music),\n                    contentDescription = null,\n                    tint = MaterialTheme.colorScheme.onPrimaryContainer,\n                    modifier = Modifier.size(28.dp),\n                )\n            }\n        }\n        Spacer(Modifier.width(12.dp))\n        Column(modifier = Modifier.weight(1f)) {\n            Text(\n                text = title,\n                style = MaterialTheme.typography.bodyLarge,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n            )\n            if (!subtitle.isNullOrBlank()) {\n                Text(\n                    text = subtitle,\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun PodcastChannelAccountItem(\n    thumbnailUrl: String?,\n    name: String,\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = modifier\n            .fillMaxWidth()\n            .clickable(onClick = onClick)\n            .padding(horizontal = 16.dp, vertical = 8.dp),\n    ) {\n        AsyncImage(\n            model = thumbnailUrl,\n            contentDescription = null,\n            contentScale = ContentScale.Crop,\n            modifier = Modifier\n                .size(56.dp)\n                .clip(CircleShape),\n        )\n        Spacer(Modifier.width(12.dp))\n        Text(\n            text = name,\n            style = MaterialTheme.typography.bodyLarge,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n            modifier = Modifier.weight(1f),\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/AlbumScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.ContainedLoadingIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.listSaver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.toMutableStateList\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.pluralStringResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.LinkAnnotation\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.text.withLink\nimport androidx.compose.ui.text.withStyle\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.util.fastForEachIndexed\nimport androidx.compose.ui.util.fastForEachReversed\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.media3.exoplayer.offline.Download\nimport androidx.navigation.NavController\nimport coil3.compose.AsyncImage\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalDownloadUtil\nimport com.metrolist.music.LocalListenTogetherManager\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.HideExplicitKey\nimport com.metrolist.music.constants.HideVideoSongsKey\nimport com.metrolist.music.db.entities.Album\nimport com.metrolist.music.playback.queues.LocalAlbumRadio\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.NavigationTitle\nimport com.metrolist.music.ui.component.SongListItem\nimport com.metrolist.music.ui.component.YouTubeGridItem\nimport com.metrolist.music.ui.menu.AlbumMenu\nimport com.metrolist.music.ui.menu.SelectionSongMenu\nimport com.metrolist.music.ui.menu.SongMenu\nimport com.metrolist.music.ui.menu.YouTubeAlbumMenu\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.makeTimeString\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.viewmodels.AlbumViewModel\n\n@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun AlbumScreen(\n    navController: NavController,\n    viewModel: AlbumViewModel = hiltViewModel(),\n) {\n    val context = LocalContext.current\n    val menuState = LocalMenuState.current\n    val database = LocalDatabase.current\n    val haptic = LocalHapticFeedback.current\n    val coroutineScope = rememberCoroutineScope()\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val isListenTogetherGuest = listenTogetherManager?.let { it.isInRoom && !it.isHost } ?: false\n\n    val scope = rememberCoroutineScope()\n\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n\n    val playlistId by viewModel.playlistId.collectAsState()\n    val albumWithSongs by viewModel.albumWithSongs.collectAsState()\n    val otherVersions by viewModel.otherVersions.collectAsState()\n    val hideExplicit by rememberPreference(key = HideExplicitKey, defaultValue = false)\n    val hideVideoSongs by rememberPreference(key = HideVideoSongsKey, defaultValue = false)\n\n    val filteredSongs =\n        remember(albumWithSongs, hideExplicit, hideVideoSongs) {\n            var songs = albumWithSongs?.songs ?: emptyList()\n            if (hideExplicit) {\n                songs = songs.filter { !it.song.explicit }\n            }\n            if (hideVideoSongs) {\n                songs = songs.filter { !it.song.isVideo }\n            }\n            songs\n        }\n\n    var inSelectMode by rememberSaveable { mutableStateOf(false) }\n    val selection =\n        rememberSaveable(\n            saver =\n                listSaver<MutableList<String>, String>(\n                    save = { it.toList() },\n                    restore = { it.toMutableStateList() },\n                ),\n        ) { mutableStateListOf() }\n    val onExitSelectionMode = {\n        inSelectMode = false\n        selection.clear()\n    }\n    if (inSelectMode) {\n        BackHandler(onBack = onExitSelectionMode)\n    }\n\n    LaunchedEffect(filteredSongs) {\n        selection.fastForEachReversed { songId ->\n            if (filteredSongs.find { it.id == songId } == null) {\n                selection.remove(songId)\n            }\n        }\n    }\n\n    val downloadUtil = LocalDownloadUtil.current\n    var downloadState by remember {\n        mutableIntStateOf(Download.STATE_STOPPED)\n    }\n\n    LaunchedEffect(albumWithSongs) {\n        val songs = albumWithSongs?.songs?.map { it.id }\n        if (songs.isNullOrEmpty()) return@LaunchedEffect\n        downloadUtil.downloads.collect { downloads ->\n            downloadState =\n                if (songs.all { downloads[it]?.state == Download.STATE_COMPLETED }) {\n                    Download.STATE_COMPLETED\n                } else if (songs.all {\n                        downloads[it]?.state == Download.STATE_QUEUED ||\n                            downloads[it]?.state == Download.STATE_DOWNLOADING ||\n                            downloads[it]?.state == Download.STATE_COMPLETED\n                    }\n                ) {\n                    Download.STATE_DOWNLOADING\n                } else {\n                    Download.STATE_STOPPED\n                }\n        }\n    }\n\n    LazyColumn(\n        contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n    ) {\n        val albumWithSongs = albumWithSongs\n        if (albumWithSongs != null && albumWithSongs.songs.isNotEmpty()) {\n            item(key = \"album_header\") {\n                Column(\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(top = 8.dp, bottom = 20.dp),\n                    horizontalAlignment = Alignment.CenterHorizontally,\n                ) {\n                    // Album Thumbnail - Large centered with shadow\n                    Surface(\n                        modifier =\n                            Modifier\n                                .size(240.dp)\n                                .shadow(\n                                    elevation = 24.dp,\n                                    shape = RoundedCornerShape(3.dp),\n                                    spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),\n                                ),\n                        shape = RoundedCornerShape(3.dp),\n                    ) {\n                        AsyncImage(\n                            model = albumWithSongs.album.thumbnailUrl,\n                            contentDescription = null,\n                            contentScale = ContentScale.Crop,\n                            modifier = Modifier.fillMaxSize(),\n                        )\n                    }\n\n                    Spacer(modifier = Modifier.height(20.dp))\n\n                    // Album Name\n                    Text(\n                        text = albumWithSongs.album.title,\n                        style = MaterialTheme.typography.headlineSmall,\n                        fontWeight = FontWeight.Bold,\n                        textAlign = TextAlign.Center,\n                        maxLines = 2,\n                        overflow = TextOverflow.Ellipsis,\n                        modifier = Modifier.padding(horizontal = 32.dp),\n                    )\n\n                    Spacer(modifier = Modifier.height(8.dp))\n\n                    // Artist Names - Below the album name\n                    Text(\n                        buildAnnotatedString {\n                            withStyle(\n                                style =\n                                    MaterialTheme.typography.titleMedium\n                                        .copy(\n                                            fontWeight = FontWeight.Normal,\n                                            color = MaterialTheme.colorScheme.onBackground,\n                                        ).toSpanStyle(),\n                            ) {\n                                albumWithSongs.artists.fastForEachIndexed { index, artist ->\n                                    val link =\n                                        LinkAnnotation.Clickable(artist.id) {\n                                            navController.navigate(\"artist/${artist.id}\")\n                                        }\n                                    withLink(link) {\n                                        append(artist.name)\n                                    }\n                                    if (index != albumWithSongs.artists.lastIndex) {\n                                        append(\", \")\n                                    }\n                                }\n                            }\n                        },\n                        textAlign = TextAlign.Center,\n                    )\n\n                    Spacer(modifier = Modifier.height(12.dp))\n\n                    // Metadata - Year first, then song count • duration\n                    val totalDuration = albumWithSongs.songs.sumOf { it.song.duration }\n                    Column(\n                        horizontalAlignment = Alignment.CenterHorizontally,\n                        verticalArrangement = Arrangement.spacedBy(4.dp),\n                    ) {\n                        // Year\n                        if (albumWithSongs.album.year != null) {\n                            Text(\n                                text = albumWithSongs.album.year.toString(),\n                                style = MaterialTheme.typography.bodyMedium,\n                                color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f),\n                            )\n                        }\n\n                        // Song Count • Duration\n                        Text(\n                            text =\n                                buildString {\n                                    append(\n                                        pluralStringResource(\n                                            R.plurals.n_song,\n                                            albumWithSongs.songs.size,\n                                            albumWithSongs.songs.size,\n                                        ),\n                                    )\n                                    if (totalDuration > 0) {\n                                        append(\" • \")\n                                        append(makeTimeString(totalDuration * 1000L))\n                                    }\n                                },\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f),\n                        )\n                    }\n\n                    Spacer(modifier = Modifier.height(24.dp))\n\n                    // Action Buttons Row\n                    Row(\n                        modifier =\n                            Modifier\n                                .fillMaxWidth()\n                                .padding(horizontal = 24.dp),\n                        horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally),\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        // Like Button - Smaller secondary button\n                        Surface(\n                            onClick = {\n                                database.query {\n                                    update(albumWithSongs.album.toggleLike())\n                                }\n                            },\n                            shape = CircleShape,\n                            color = MaterialTheme.colorScheme.surfaceVariant,\n                            modifier = Modifier.size(48.dp),\n                        ) {\n                            Box(\n                                modifier = Modifier.fillMaxSize(),\n                                contentAlignment = Alignment.Center,\n                            ) {\n                                Icon(\n                                    painter =\n                                        painterResource(\n                                            if (albumWithSongs.album.bookmarkedAt !=\n                                                null\n                                            ) {\n                                                R.drawable.favorite\n                                            } else {\n                                                R.drawable.favorite_border\n                                            },\n                                        ),\n                                    contentDescription = null,\n                                    tint =\n                                        if (albumWithSongs.album.bookmarkedAt != null) {\n                                            MaterialTheme.colorScheme.error\n                                        } else {\n                                            MaterialTheme.colorScheme.onSurfaceVariant\n                                        },\n                                    modifier = Modifier.size(24.dp),\n                                )\n                            }\n                        }\n\n                        // Play Button - Larger primary circular button\n                        Surface(\n                            onClick = {\n                                if (!isListenTogetherGuest) {\n                                    playerConnection.service.getAutomix(playlistId)\n                                    playerConnection.playQueue(\n                                        LocalAlbumRadio(albumWithSongs),\n                                    )\n                                }\n                            },\n                            color = MaterialTheme.colorScheme.primary,\n                            shape = CircleShape,\n                            modifier = Modifier.size(72.dp),\n                        ) {\n                            Box(\n                                contentAlignment = Alignment.Center,\n                                modifier = Modifier.fillMaxSize(),\n                            ) {\n                                Icon(\n                                    painter = painterResource(R.drawable.play),\n                                    contentDescription = stringResource(R.string.play),\n                                    tint = MaterialTheme.colorScheme.onPrimary,\n                                    modifier = Modifier.size(32.dp),\n                                )\n                            }\n                        }\n\n                        // Menu Button - Smaller secondary button\n                        Surface(\n                            onClick = {\n                                menuState.show {\n                                    AlbumMenu(\n                                        originalAlbum =\n                                            Album(\n                                                albumWithSongs.album,\n                                                albumWithSongs.artists,\n                                            ),\n                                        navController = navController,\n                                        onDismiss = menuState::dismiss,\n                                    )\n                                }\n                            },\n                            shape = CircleShape,\n                            color = MaterialTheme.colorScheme.surfaceVariant,\n                            modifier = Modifier.size(48.dp),\n                        ) {\n                            Box(\n                                modifier = Modifier.fillMaxSize(),\n                                contentAlignment = Alignment.Center,\n                            ) {\n                                Icon(\n                                    painter = painterResource(R.drawable.more_vert),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(24.dp),\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n\n            if (filteredSongs.isNotEmpty()) {\n                itemsIndexed(\n                    items = filteredSongs,\n                    key = { _, song -> song.id },\n                ) { index, song ->\n                    val onCheckedChange: (Boolean) -> Unit = {\n                        if (it) {\n                            selection.add(song.id)\n                        } else {\n                            selection.remove(song.id)\n                        }\n                    }\n\n                    SongListItem(\n                        song = song,\n                        albumIndex = index + 1,\n                        isActive = song.id == mediaMetadata?.id,\n                        isPlaying = isPlaying,\n                        showInLibraryIcon = true,\n                        trailingContent = {\n                            if (inSelectMode) {\n                                Checkbox(\n                                    checked = song.id in selection,\n                                    onCheckedChange = onCheckedChange,\n                                )\n                            } else {\n                                IconButton(\n                                    onClick = {\n                                        menuState.show {\n                                            SongMenu(\n                                                originalSong = song,\n                                                navController = navController,\n                                                onDismiss = menuState::dismiss,\n                                            )\n                                        }\n                                    },\n                                ) {\n                                    Icon(\n                                        painter = painterResource(R.drawable.more_vert),\n                                        contentDescription = null,\n                                    )\n                                }\n                            }\n                        },\n                        modifier =\n                            Modifier\n                                .fillMaxWidth()\n                                .animateItem()\n                                .combinedClickable(\n                                    onClick = {\n                                        if (inSelectMode) {\n                                            onCheckedChange(song.id !in selection)\n                                        } else if (!isListenTogetherGuest) {\n                                            if (song.id == mediaMetadata?.id) {\n                                                playerConnection.togglePlayPause()\n                                            } else {\n                                                playerConnection.service.getAutomix(playlistId)\n                                                playerConnection.playQueue(\n                                                    LocalAlbumRadio(albumWithSongs, startIndex = index),\n                                                )\n                                            }\n                                        }\n                                    },\n                                    onLongClick = {\n                                        if (!inSelectMode) {\n                                            haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                            inSelectMode = true\n                                            onCheckedChange(true)\n                                        }\n                                    },\n                                ),\n                    )\n                }\n            }\n\n            if (otherVersions.isNotEmpty()) {\n                item(key = \"other_versions_title\") {\n                    NavigationTitle(\n                        title = stringResource(R.string.other_versions),\n                        modifier = Modifier.animateItem(),\n                    )\n                }\n                item(key = \"other_versions_list\") {\n                    LazyRow(\n                        contentPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues(),\n                    ) {\n                        items(\n                            items = otherVersions.distinctBy { it.id },\n                            key = { it.id },\n                        ) { item ->\n                            YouTubeGridItem(\n                                item = item,\n                                isActive = mediaMetadata?.album?.id == item.id,\n                                isPlaying = isPlaying,\n                                coroutineScope = scope,\n                                modifier =\n                                    Modifier\n                                        .combinedClickable(\n                                            onClick = { navController.navigate(\"album/${item.id}\") },\n                                            onLongClick = {\n                                                haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                menuState.show {\n                                                    YouTubeAlbumMenu(\n                                                        albumItem = item,\n                                                        navController = navController,\n                                                        onDismiss = menuState::dismiss,\n                                                    )\n                                                }\n                                            },\n                                        ).animateItem(),\n                            )\n                        }\n                    }\n                }\n            }\n        } else {\n            item(key = \"loading\") {\n                Box(\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(32.dp),\n                    contentAlignment = Alignment.Center,\n                ) {\n                    ContainedLoadingIndicator()\n                }\n            }\n        }\n    }\n\n    TopAppBar(\n        title = {\n            if (inSelectMode) {\n                Text(pluralStringResource(R.plurals.n_selected, selection.size, selection.size))\n            } else {\n                Text(\n                    text = albumWithSongs?.album?.title.orEmpty(),\n                    style = MaterialTheme.typography.titleLarge,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                )\n            }\n        },\n        navigationIcon = {\n            if (inSelectMode) {\n                IconButton(onClick = onExitSelectionMode) {\n                    Icon(\n                        painter = painterResource(R.drawable.close),\n                        contentDescription = null,\n                    )\n                }\n            } else {\n                IconButton(\n                    onClick = { navController.navigateUp() },\n                    onLongClick = { navController.backToMain() },\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.arrow_back),\n                        contentDescription = null,\n                    )\n                }\n            }\n        },\n        actions = {\n            if (inSelectMode) {\n                Checkbox(\n                    checked = selection.size == filteredSongs.size && selection.isNotEmpty(),\n                    onCheckedChange = {\n                        if (selection.size == filteredSongs.size) {\n                            selection.clear()\n                        } else {\n                            selection.clear()\n                            selection.addAll(filteredSongs.map { it.id })\n                        }\n                    },\n                )\n                IconButton(\n                    enabled = selection.isNotEmpty(),\n                    onClick = {\n                        menuState.show {\n                            SelectionSongMenu(\n                                songSelection =\n                                    selection.mapNotNull { songId ->\n                                        filteredSongs.find { it.id == songId }\n                                    },\n                                onDismiss = menuState::dismiss,\n                                clearAction = onExitSelectionMode,\n                            )\n                        }\n                    },\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.more_vert),\n                        contentDescription = null,\n                    )\n                }\n            }\n        },\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/BrowseScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens\n \n import androidx.compose.foundation.ExperimentalFoundationApi\n import androidx.compose.foundation.combinedClickable\n import androidx.compose.foundation.layout.asPaddingValues\n import androidx.compose.foundation.lazy.grid.GridCells\n import androidx.compose.foundation.lazy.grid.LazyVerticalGrid\n import androidx.compose.foundation.lazy.grid.items\n import androidx.compose.material3.ExperimentalMaterial3Api\n import androidx.compose.material3.Icon\n import androidx.compose.material3.Text\n import androidx.compose.material3.TopAppBar\n import androidx.compose.runtime.Composable\n import androidx.compose.runtime.collectAsState\n import androidx.compose.runtime.getValue\n import androidx.compose.runtime.rememberCoroutineScope\n import androidx.compose.ui.Modifier\n import androidx.compose.ui.res.painterResource\n import androidx.compose.ui.unit.dp\n import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\n import androidx.navigation.NavController\n import com.metrolist.innertube.models.AlbumItem\n import com.metrolist.innertube.models.ArtistItem\n import com.metrolist.innertube.models.PlaylistItem\n import com.metrolist.music.LocalPlayerAwareWindowInsets\n import com.metrolist.music.LocalPlayerConnection\n import com.metrolist.music.R\n import com.metrolist.music.constants.GridItemSize\n import com.metrolist.music.constants.GridItemsSizeKey\n import com.metrolist.music.constants.GridThumbnailHeight\n import com.metrolist.music.ui.component.IconButton\n import com.metrolist.music.ui.component.LocalMenuState\n import com.metrolist.music.ui.component.YouTubeGridItem\n import com.metrolist.music.ui.component.shimmer.GridItemPlaceHolder\n import com.metrolist.music.ui.component.shimmer.ShimmerHost\n import com.metrolist.music.ui.menu.YouTubeAlbumMenu\n import com.metrolist.music.ui.menu.YouTubeArtistMenu\n import com.metrolist.music.ui.menu.YouTubePlaylistMenu\n import com.metrolist.music.ui.utils.backToMain\n import com.metrolist.music.utils.rememberEnumPreference\n import com.metrolist.music.viewmodels.BrowseViewModel\n\n@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)\n @Composable\n fun BrowseScreen(\n    navController: NavController,\n    browseId: String?,\n    viewModel: BrowseViewModel = hiltViewModel(),\n) {\n     val menuState = LocalMenuState.current\n     val playerConnection = LocalPlayerConnection.current ?: return\n     val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n     val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n \n     val title by viewModel.title.collectAsState()\n     val items by viewModel.items.collectAsState()\n \n     val coroutineScope = rememberCoroutineScope()\n     val gridItemSize by rememberEnumPreference(GridItemsSizeKey, GridItemSize.BIG)\n \n     LazyVerticalGrid(\n         columns = GridCells.Adaptive(minSize = GridThumbnailHeight + if (gridItemSize == GridItemSize.BIG) 24.dp else (-24).dp),\n         contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues()\n     ) {\n         items?.let { items ->\n             items(\n                 items = items.distinctBy { it.id },\n                 key = { it.id }\n             ) { item ->\n                 YouTubeGridItem(\n                     item = item,\n                     isPlaying = isPlaying,\n                     fillMaxWidth = true,\n                     coroutineScope = coroutineScope,\n                     modifier = Modifier\n                         .combinedClickable(\n                             onClick = {\n                                 when (item) {\n                                     is AlbumItem -> navController.navigate(\"album/${item.id}\")\n                                     is PlaylistItem -> navController.navigate(\"online_playlist/${item.id}\")\n                                     is ArtistItem -> navController.navigate(\"artist/${item.id}\")\n                                     else -> {\n                                         // Do nothing\n                                     }\n                                 }\n                             },\n                             onLongClick = {\n                                 menuState.show {\n                                     when (item) {\n                                         is AlbumItem ->\n                                             YouTubeAlbumMenu(\n                                                 albumItem = item,\n                                                 navController = navController,\n                                                 onDismiss = menuState::dismiss\n                                             )\n \n                                         is PlaylistItem -> {\n                                             YouTubePlaylistMenu(\n                                                 playlist = item,\n                                                 coroutineScope = coroutineScope,\n                                                 onDismiss = menuState::dismiss\n                                             )\n                                         }\n \n                                         is ArtistItem -> {\n                                             YouTubeArtistMenu(\n                                                 artist = item,\n                                                 onDismiss = menuState::dismiss\n                                             )\n                                         }\n \n                                         else -> {\n                                             // Do nothing\n                                         }\n                                     }\n                                 }\n                             }\n                         )\n                 )\n             }\n \n             if (items.isEmpty()) {\n                 items(8) {\n                     ShimmerHost {\n                         GridItemPlaceHolder(fillMaxWidth = true)\n                     }\n                 }\n             }\n         }\n     }\n \n     TopAppBar(\n         title = { Text(title ?: \"\") },\n         navigationIcon = {\n             IconButton(\n                 onClick = navController::navigateUp,\n                 onLongClick = navController::backToMain\n             ) {\n                 Icon(\n                     painterResource(R.drawable.arrow_back),\n                     contentDescription = null\n                 )\n             }\n         }\n     )\n }\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/ChartsScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyHorizontalGrid\nimport androidx.compose.foundation.lazy.grid.items\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.WatchEndpoint\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.ListItemHeight\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.playback.queues.YouTubeQueue\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.NavigationTitle\nimport com.metrolist.music.ui.component.YouTubeGridItem\nimport com.metrolist.music.ui.component.YouTubeListItem\nimport com.metrolist.music.ui.component.shimmer.GridItemPlaceHolder\nimport com.metrolist.music.ui.component.shimmer.ShimmerHost\nimport com.metrolist.music.ui.component.shimmer.TextPlaceholder\nimport com.metrolist.music.ui.menu.YouTubeSongMenu\nimport com.metrolist.music.ui.utils.SnapLayoutInfoProvider\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.viewmodels.ChartsViewModel\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)\n@Composable\nfun ChartsScreen(\n    navController: NavController,\n    viewModel: ChartsViewModel = hiltViewModel(),\n) {\n    val menuState = LocalMenuState.current\n    val haptic = LocalHapticFeedback.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n\n    val chartsPage by viewModel.chartsPage.collectAsState()\n    val isLoading by viewModel.isLoading.collectAsState()\n\n    val lazyListState = rememberLazyListState()\n    val coroutineScope = rememberCoroutineScope()\n\n    LaunchedEffect(Unit) {\n        if (chartsPage == null) {\n            viewModel.loadCharts()\n        }\n    }\n\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(stringResource(R.string.charts)) },\n                navigationIcon = {\n                    IconButton(\n                        onClick = { navController.navigateUp() },\n                        onLongClick = { navController.backToMain() }\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.arrow_back),\n                            contentDescription = null,\n                        )\n                    }\n                },\n            )\n        }\n    ) { paddingValues ->\n        BoxWithConstraints(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(paddingValues),\n        ) {\n            if (isLoading || chartsPage == null) {\n                ShimmerHost(\n                    modifier = Modifier.fillMaxSize(),\n                ) {\n                    Column(\n                        modifier = Modifier.padding(16.dp)\n                    ) {\n                        TextPlaceholder(\n                            height = 36.dp,\n                            modifier = Modifier\n                                .padding(12.dp)\n                                .fillMaxWidth(0.5f),\n                        )\n                        BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {\n                            val horizontalLazyGridItemWidthFactor = if (maxWidth * 0.475f >= 320.dp) 0.475f else 0.9f\n                            val horizontalLazyGridItemWidth = maxWidth * horizontalLazyGridItemWidthFactor\n\n                            LazyHorizontalGrid(\n                                rows = GridCells.Fixed(4),\n                                contentPadding = PaddingValues(start = 4.dp),\n                                modifier = Modifier\n                                    .fillMaxWidth()\n                                    .height(ListItemHeight * 4),\n                            ) {\n                                items(4) {\n                                    Row(\n                                        modifier = Modifier\n                                            .width(horizontalLazyGridItemWidth)\n                                            .padding(8.dp),\n                                        verticalAlignment = Alignment.CenterVertically,\n                                    ) {\n                                        Box(\n                                            modifier = Modifier\n                                                .size(ListItemHeight - 16.dp)\n                                                .clip(RoundedCornerShape(4.dp))\n                                                .background(MaterialTheme.colorScheme.onSurface),\n                                        )\n                                        Spacer(modifier = Modifier.width(8.dp))\n                                        Column(\n                                            modifier = Modifier.fillMaxHeight(),\n                                            verticalArrangement = Arrangement.Center,\n                                        ) {\n                                            Box(\n                                                modifier = Modifier\n                                                    .height(16.dp)\n                                                    .width(120.dp)\n                                                    .background(MaterialTheme.colorScheme.onSurface),\n                                            )\n                                            Spacer(modifier = Modifier.height(8.dp))\n                                            Box(\n                                                modifier = Modifier\n                                                    .height(12.dp)\n                                                    .width(80.dp)\n                                                    .background(MaterialTheme.colorScheme.onSurface),\n                                            )\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                        TextPlaceholder(\n                            height = 36.dp,\n                            modifier = Modifier\n                                .padding(vertical = 12.dp, horizontal = 12.dp)\n                                .width(250.dp),\n                        )\n                        Row {\n                            repeat(2) {\n                                GridItemPlaceHolder()\n                            }\n                        }\n                    }\n                }\n            } else {\n                LazyColumn(\n                    state = lazyListState,\n                    contentPadding = LocalPlayerAwareWindowInsets.current\n                        .only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom)\n                        .asPaddingValues(),\n                ) {\n                    chartsPage?.sections?.filter { it.title != \"Top music videos\" }?.forEach { section ->\n                        item(key = \"section_title_${section.title}\") {\n                            NavigationTitle(\n                                title = when (section.title) {\n                                    \"Trending\" -> stringResource(R.string.trending)\n                                        else -> section.title.ifEmpty { stringResource(R.string.charts) }\n                                },\n                                modifier = Modifier.animateItem(),\n                            )\n                        }\n                        item(key = \"section_content_${section.title}\") {\n                            BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {\n                                val horizontalLazyGridItemWidthFactor = if (maxWidth * 0.475f >= 320.dp) 0.475f else 0.9f\n                                val horizontalLazyGridItemWidth = maxWidth * horizontalLazyGridItemWidthFactor\n\n                                val lazyGridState = rememberLazyGridState()\n                                val snapLayoutInfoProvider = remember(lazyGridState) {\n                                    SnapLayoutInfoProvider(\n                                        lazyGridState = lazyGridState,\n                                        positionInLayout = { layoutSize, itemSize ->\n                                            (layoutSize * horizontalLazyGridItemWidthFactor / 2f - itemSize / 2f)\n                                        },\n                                    )\n                                }\n\n                                LazyHorizontalGrid(\n                                    state = lazyGridState,\n                                    rows = GridCells.Fixed(4),\n                                    flingBehavior = rememberSnapFlingBehavior(snapLayoutInfoProvider),\n                                    contentPadding = WindowInsets.systemBars\n                                        .only(WindowInsetsSides.Horizontal)\n                                        .asPaddingValues(),\n                                    modifier = Modifier\n                                        .fillMaxWidth()\n                                        .height(ListItemHeight * 4)\n                                        .animateItem(),\n                                ) {\n                                    items(\n                                        items = section.items.filterIsInstance<SongItem>().distinctBy { it.id },\n                                        key = { it.id },\n                                    ) { song ->\n                                        YouTubeListItem(\n                                            item = song,\n                                            isActive = song.id == mediaMetadata?.id,\n                                            isPlaying = isPlaying,\n                                            isSwipeable = false,\n                                            trailingContent = {\n                                                IconButton(\n                                                    onClick = {\n                                                        menuState.show {\n                                                            YouTubeSongMenu(\n                                                                song = song,\n                                                                navController = navController,\n                                                                onDismiss = menuState::dismiss,\n                                                            )\n                                                        }\n                                                    },\n                                                ) {\n                                                    Icon(\n                                                        painter = painterResource(R.drawable.more_vert),\n                                                        contentDescription = null,\n                                                    )\n                                                }\n                                            },\n                                            modifier = Modifier\n                                                .width(horizontalLazyGridItemWidth)\n                                                .combinedClickable(\n                                                    onClick = {\n                                                        if (song.id == mediaMetadata?.id) {\n                                                            playerConnection.togglePlayPause()\n                                                        } else {\n                                                            playerConnection.playQueue(\n                                                                YouTubeQueue(\n                                                                    endpoint = WatchEndpoint(videoId = song.id),\n                                                                    preloadItem = song.toMediaMetadata(),\n                                                                ),\n                                                            )\n                                                        }\n                                                    },\n                                                    onLongClick = {\n                                                        haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                        menuState.show {\n                                                            YouTubeSongMenu(\n                                                                song = song,\n                                                                navController = navController,\n                                                                onDismiss = menuState::dismiss,\n                                                            )\n                                                        }\n                                                    },\n                                                ),\n                                        )\n                                    }\n                                }\n                            }\n                        }\n                    }\n\n                    chartsPage?.sections?.find { it.title == \"Top music videos\" }?.let { topVideosSection ->\n                        item(key = \"top_videos_title\") {\n                            NavigationTitle(\n                                title = stringResource(R.string.top_music_videos),\n                                modifier = Modifier.animateItem(),\n                            )\n                        }\n                        item(key = \"top_videos_content\") {\n                            LazyRow(\n                                contentPadding = WindowInsets.systemBars\n                                    .only(WindowInsetsSides.Horizontal)\n                                    .asPaddingValues(),\n                                modifier = Modifier.animateItem(),\n                            ) {\n                                items(\n                                    items = topVideosSection.items.filterIsInstance<SongItem>().distinctBy { it.id },\n                                    key = { it.id },\n                                ) { video ->\n                                    YouTubeGridItem(\n                                        item = video,\n                                        isActive = video.id == mediaMetadata?.id,\n                                        isPlaying = isPlaying,\n                                        coroutineScope = coroutineScope,\n                                        modifier = Modifier\n                                            .combinedClickable(\n                                                onClick = {\n                                                    if (video.id == mediaMetadata?.id) {\n                                                        playerConnection.togglePlayPause()\n                                                    } else {\n                                                        playerConnection.playQueue(\n                                                            YouTubeQueue(\n                                                                endpoint = WatchEndpoint(videoId = video.id),\n                                                                preloadItem = video.toMediaMetadata(),\n                                                            ),\n                                                        )\n                                                    }\n                                                },\n                                                onLongClick = {\n                                                    haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                    menuState.show {\n                                                        YouTubeSongMenu(\n                                                            song = video,\n                                                            navController = navController,\n                                                            onDismiss = menuState::dismiss,\n                                                        )\n                                                    }\n                                                },\n                                            )\n                                            .animateItem(),\n                                    )\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/CrashActivity.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens\n\nimport android.content.Intent\nimport android.os.Bundle\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.horizontalScroll\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExtendedFloatingActionButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.core.content.FileProvider\nimport com.metrolist.music.R\nimport com.metrolist.music.ui.theme.MetrolistTheme\nimport com.metrolist.music.utils.CrashHandler\nimport java.io.File\nimport java.text.SimpleDateFormat\nimport java.util.Date\nimport java.util.Locale\n\nclass CrashActivity : ComponentActivity() {\n    \n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        enableEdgeToEdge()\n        \n        val crashLog = intent.getStringExtra(CrashHandler.EXTRA_CRASH_LOG) ?: getString(R.string.crash_no_log)\n        \n        setContent {\n            val darkTheme = isSystemInDarkTheme()\n            MetrolistTheme(darkTheme = darkTheme) {\n                CrashScreen(\n                    crashLog = crashLog,\n                    onClose = { finishAffinity() },\n                    onShare = { shareCrashLog(crashLog) }\n                )\n            }\n        }\n    }\n    \n    private fun shareCrashLog(crashLog: String) {\n        try {\n            // Create crash log file\n            val timestamp = SimpleDateFormat(\"yyyyMMdd_HHmmss\", Locale.US).format(Date())\n            val fileName = \"metrolist_crash_$timestamp.txt\"\n            val crashFile = File(cacheDir, fileName)\n            crashFile.writeText(crashLog)\n            \n            // Get URI using FileProvider\n            val uri = FileProvider.getUriForFile(\n                this,\n                \"${packageName}.FileProvider\",\n                crashFile\n            )\n            \n            // Create share intent\n            val shareIntent = Intent(Intent.ACTION_SEND).apply {\n                type = \"text/plain\"\n                putExtra(Intent.EXTRA_STREAM, uri)\n                putExtra(Intent.EXTRA_SUBJECT, getString(R.string.crash_report_subject))\n                addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n            }\n            \n            startActivity(Intent.createChooser(shareIntent, getString(R.string.crash_share_title)))\n        } catch (e: Exception) {\n            // Fallback to simple text share if file sharing fails\n            val shareIntent = Intent(Intent.ACTION_SEND).apply {\n                type = \"text/plain\"\n                putExtra(Intent.EXTRA_TEXT, crashLog)\n                putExtra(Intent.EXTRA_SUBJECT, getString(R.string.crash_report_subject))\n            }\n            startActivity(Intent.createChooser(shareIntent, getString(R.string.crash_share_title)))\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun CrashScreen(\n    crashLog: String,\n    onClose: () -> Unit,\n    onShare: () -> Unit\n) {\n    val context = LocalContext.current\n    \n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { \n                    Text(\n                        text = stringResource(R.string.crash_title),\n                        style = MaterialTheme.typography.headlineSmall\n                    ) \n                },\n                actions = {\n                    IconButton(onClick = onClose) {\n                        Icon(\n                            painter = painterResource(R.drawable.close),\n                            contentDescription = stringResource(R.string.crash_close)\n                        )\n                    }\n                },\n                colors = TopAppBarDefaults.topAppBarColors(\n                    containerColor = MaterialTheme.colorScheme.surface\n                )\n            )\n        },\n        floatingActionButton = {\n            ExtendedFloatingActionButton(\n                onClick = onShare,\n                icon = {\n                    Icon(\n                        painter = painterResource(R.drawable.share),\n                        contentDescription = null\n                    )\n                },\n                text = { Text(stringResource(R.string.crash_share_logs)) },\n                containerColor = MaterialTheme.colorScheme.primaryContainer,\n                contentColor = MaterialTheme.colorScheme.onPrimaryContainer\n            )\n        },\n        containerColor = MaterialTheme.colorScheme.surface\n    ) { paddingValues ->\n        Column(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(paddingValues)\n                .padding(horizontal = 16.dp)\n                .verticalScroll(rememberScrollState())\n        ) {\n            Text(\n                text = stringResource(R.string.crash_description),\n                style = MaterialTheme.typography.bodyLarge,\n                color = MaterialTheme.colorScheme.onSurfaceVariant\n            )\n            \n            Spacer(modifier = Modifier.height(16.dp))\n            \n            // Crash log container\n            Box(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .clip(RoundedCornerShape(16.dp))\n                    .background(MaterialTheme.colorScheme.surfaceContainerHighest)\n                    .padding(16.dp)\n            ) {\n                Text(\n                    text = crashLog,\n                    style = MaterialTheme.typography.bodySmall.copy(\n                        fontFamily = FontFamily.Monospace,\n                        fontSize = 11.sp,\n                        lineHeight = 16.sp\n                    ),\n                    color = MaterialTheme.colorScheme.onSurface,\n                    modifier = Modifier.horizontalScroll(rememberScrollState())\n                )\n            }\n            \n            // Bottom spacing for FAB\n            Spacer(modifier = Modifier.height(88.dp))\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/ExploreScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyHorizontalGrid\nimport androidx.compose.foundation.lazy.grid.items\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport androidx.navigation.compose.currentBackStackEntryAsState\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.WatchEndpoint\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.ListItemHeight\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.playback.queues.YouTubeQueue\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.NavigationTitle\nimport com.metrolist.music.ui.component.YouTubeGridItem\nimport com.metrolist.music.ui.component.YouTubeListItem\nimport com.metrolist.music.ui.component.shimmer.GridItemPlaceHolder\nimport com.metrolist.music.ui.component.shimmer.ShimmerHost\nimport com.metrolist.music.ui.component.shimmer.TextPlaceholder\nimport com.metrolist.music.ui.menu.YouTubeAlbumMenu\nimport com.metrolist.music.ui.menu.YouTubeSongMenu\nimport com.metrolist.music.ui.utils.SnapLayoutInfoProvider\nimport com.metrolist.music.viewmodels.ChartsViewModel\nimport com.metrolist.music.viewmodels.ExploreViewModel\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)\n@Composable\nfun ExploreScreen(\n    navController: NavController,\n    exploreViewModel: ExploreViewModel = hiltViewModel(),\n    chartsViewModel: ChartsViewModel = hiltViewModel(),\n) {\n    val menuState = LocalMenuState.current\n    val haptic = LocalHapticFeedback.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n\n    val explorePage by exploreViewModel.explorePage.collectAsState()\n    val chartsPage by chartsViewModel.chartsPage.collectAsState()\n    val isChartsLoading by chartsViewModel.isLoading.collectAsState()\n\n    val coroutineScope = rememberCoroutineScope()\n    val scrollState = rememberScrollState()\n\n    val backStackEntry by navController.currentBackStackEntryAsState()\n    val scrollToTop by backStackEntry?.savedStateHandle\n        ?.getStateFlow(\"scrollToTop\", false)?.collectAsState() ?: return\n\n    LaunchedEffect(Unit) {\n        if (chartsPage == null) {\n            chartsViewModel.loadCharts()\n        }\n    }\n\n    LaunchedEffect(scrollToTop) {\n        if (scrollToTop) {\n            scrollState.animateScrollTo(0)\n            backStackEntry?.savedStateHandle?.set(\"scrollToTop\", false)\n        }\n    }\n\n    Box(\n        modifier = Modifier.fillMaxSize(),\n    ) {\n        Column(\n            modifier = Modifier.verticalScroll(scrollState),\n        ) {\n            Spacer(\n                Modifier.height(\n                    LocalPlayerAwareWindowInsets.current.asPaddingValues().calculateTopPadding(),\n                ),\n            )\n\n            if (isChartsLoading || chartsPage == null || explorePage == null) {\n                ShimmerHost {\n                    TextPlaceholder(\n                        height = 36.dp,\n                        modifier = Modifier\n                            .padding(12.dp)\n                            .fillMaxWidth(0.5f),\n                    )\n                    BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {\n                        val horizontalLazyGridItemWidthFactor = if (maxWidth * 0.475f >= 320.dp) 0.475f else 0.9f\n                        val horizontalLazyGridItemWidth = maxWidth * horizontalLazyGridItemWidthFactor\n\n                        LazyHorizontalGrid(\n                            rows = GridCells.Fixed(4),\n                            contentPadding = PaddingValues(start = 4.dp),\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .height(ListItemHeight * 4),\n                        ) {\n                            items(4) {\n                                Row(\n                                    modifier = Modifier\n                                        .width(horizontalLazyGridItemWidth)\n                                        .padding(8.dp),\n                                    verticalAlignment = Alignment.CenterVertically,\n                                ) {\n                                    Box(\n                                        modifier = Modifier\n                                            .size(ListItemHeight - 16.dp)\n                                            .clip(RoundedCornerShape(4.dp))\n                                            .background(MaterialTheme.colorScheme.onSurface),\n                                    )\n                                    Spacer(modifier = Modifier.width(8.dp))\n                                    Column(\n                                        modifier = Modifier.fillMaxHeight(),\n                                        verticalArrangement = Arrangement.Center,\n                                    ) {\n                                        Box(\n                                            modifier = Modifier\n                                                .height(16.dp)\n                                                .width(120.dp)\n                                                .background(MaterialTheme.colorScheme.onSurface),\n                                        )\n                                        Spacer(modifier = Modifier.height(8.dp))\n                                        Box(\n                                            modifier = Modifier\n                                                .height(12.dp)\n                                                .width(80.dp)\n                                                .background(MaterialTheme.colorScheme.onSurface),\n                                        )\n                                    }\n                                }\n                            }\n                        }\n                    }\n\n                    TextPlaceholder(\n                        height = 36.dp,\n                        modifier = Modifier\n                            .padding(vertical = 12.dp, horizontal = 12.dp)\n                            .width(250.dp),\n                    )\n                    Row {\n                        repeat(2) {\n                            GridItemPlaceHolder()\n                        }\n                    }\n\n                    TextPlaceholder(\n                        height = 36.dp,\n                        modifier = Modifier\n                            .padding(vertical = 12.dp, horizontal = 12.dp)\n                            .width(250.dp),\n                    )\n                    Row {\n                        repeat(2) {\n                            GridItemPlaceHolder()\n                        }\n                    }\n\n                    TextPlaceholder(\n                        height = 36.dp,\n                        modifier = Modifier\n                            .padding(vertical = 12.dp, horizontal = 12.dp)\n                            .width(250.dp),\n                    )\n                    repeat(4) {\n                        Row {\n                            repeat(2) {\n                                TextPlaceholder(\n                                    height = MoodAndGenresButtonHeight,\n                                    modifier = Modifier\n                                        .padding(horizontal = 6.dp)\n                                        .width(200.dp),\n                                )\n                            }\n                        }\n                    }\n                }\n            } else {\n                chartsPage?.sections?.filter { it.title != \"Top music videos\" }?.forEach { section ->\n                    NavigationTitle(\n                        title = when (section.title) {\n                            \"Trending\" -> stringResource(R.string.trending)\n                            else -> section.title.ifEmpty { stringResource(R.string.charts) }\n                        },\n                    )\n                    BoxWithConstraints(\n                        modifier = Modifier.fillMaxWidth()\n                    ) {\n                        val horizontalLazyGridItemWidthFactor = if (maxWidth * 0.475f >= 320.dp) 0.475f else 0.9f\n                        val horizontalLazyGridItemWidth = maxWidth * horizontalLazyGridItemWidthFactor\n\n                        val lazyGridState = rememberLazyGridState()\n                        val snapLayoutInfoProvider = remember(lazyGridState) {\n                            SnapLayoutInfoProvider(\n                                lazyGridState = lazyGridState,\n                                positionInLayout = { layoutSize, itemSize ->\n                                    (layoutSize * horizontalLazyGridItemWidthFactor / 2f - itemSize / 2f)\n                                },\n                            )\n                        }\n\n                        LazyHorizontalGrid(\n                            state = lazyGridState,\n                            rows = GridCells.Fixed(4),\n                            flingBehavior = rememberSnapFlingBehavior(snapLayoutInfoProvider),\n                            contentPadding = WindowInsets.systemBars\n                                .only(WindowInsetsSides.Horizontal)\n                                .asPaddingValues(),\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .height(ListItemHeight * 4),\n                        ) {\n                            items(\n                                items = section.items.filterIsInstance<SongItem>().distinctBy { it.id },\n                                key = { it.id },\n                            ) { song ->\n                                YouTubeListItem(\n                                    item = song,\n                                    isActive = song.id == mediaMetadata?.id,\n                                    isPlaying = isPlaying,\n                                    isSwipeable = false,\n                                    trailingContent = {\n                                        IconButton(\n                                            onClick = {\n                                                menuState.show {\n                                                    YouTubeSongMenu(\n                                                        song = song,\n                                                        navController = navController,\n                                                        onDismiss = menuState::dismiss,\n                                                    )\n                                                }\n                                            },\n                                        ) {\n                                            Icon(\n                                                painter = painterResource(R.drawable.more_vert),\n                                                contentDescription = null,\n                                            )\n                                        }\n                                    },\n                                    modifier = Modifier\n                                        .width(horizontalLazyGridItemWidth)\n                                        .combinedClickable(\n                                            onClick = {\n                                                if (song.id == mediaMetadata?.id) {\n                                                    playerConnection.togglePlayPause()\n                                                } else {\n                                                    playerConnection.playQueue(\n                                                        YouTubeQueue(\n                                                            endpoint = WatchEndpoint(videoId = song.id),\n                                                            preloadItem = song.toMediaMetadata(),\n                                                        ),\n                                                    )\n                                                }\n                                            },\n                                            onLongClick = {\n                                                haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                menuState.show {\n                                                    YouTubeSongMenu(\n                                                        song = song,\n                                                        navController = navController,\n                                                        onDismiss = menuState::dismiss,\n                                                    )\n                                                }\n                                            },\n                                        ),\n                                )\n                            }\n                        }\n                    }\n                }\n\n                explorePage?.newReleaseAlbums?.let { newReleaseAlbums ->\n                    NavigationTitle(\n                        title = stringResource(R.string.new_release_albums),\n                        onClick = {\n                            navController.navigate(\"new_release\")\n                        },\n                    )\n                    LazyRow(\n                        contentPadding = WindowInsets.systemBars\n                            .only(WindowInsetsSides.Horizontal)\n                            .asPaddingValues(),\n                    ) {\n                        items(\n                            items = newReleaseAlbums.distinctBy { it.id },\n                            key = { it.id },\n                        ) { album ->\n                            YouTubeGridItem(\n                                item = album,\n                                isActive = mediaMetadata?.album?.id == album.id,\n                                isPlaying = isPlaying,\n                                coroutineScope = coroutineScope,\n                                modifier = Modifier\n                                    .combinedClickable(\n                                        onClick = {\n                                            navController.navigate(\"album/${album.id}\")\n                                        },\n                                        onLongClick = {\n                                            haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                            menuState.show {\n                                                YouTubeAlbumMenu(\n                                                    albumItem = album,\n                                                    navController = navController,\n                                                    onDismiss = menuState::dismiss,\n                                                )\n                                            }\n                                        },\n                                    )\n                                    .animateItem(),\n                            )\n                        }\n                    }\n                }\n\n                chartsPage?.sections?.find { it.title == \"Top music videos\" }?.let { topVideosSection ->\n                    NavigationTitle(\n                        title = stringResource(R.string.top_music_videos),\n                    )\n                    LazyRow(\n                        contentPadding = WindowInsets.systemBars\n                            .only(WindowInsetsSides.Horizontal)\n                            .asPaddingValues(),\n                    ) {\n                        items(\n                            items = topVideosSection.items.filterIsInstance<SongItem>().distinctBy { it.id },\n                            key = { it.id },\n                        ) { video ->\n                            YouTubeGridItem(\n                                item = video,\n                                isActive = video.id == mediaMetadata?.id,\n                                isPlaying = isPlaying,\n                                coroutineScope = coroutineScope,\n                                modifier = Modifier\n                                    .combinedClickable(\n                                        onClick = {\n                                            if (video.id == mediaMetadata?.id) {\n                                                playerConnection.togglePlayPause()\n                                            } else {\n                                                playerConnection.playQueue(\n                                                    YouTubeQueue(\n                                                        endpoint = WatchEndpoint(videoId = video.id),\n                                                        preloadItem = video.toMediaMetadata(),\n                                                    ),\n                                                )\n                                            }\n                                        },\n                                        onLongClick = {\n                                            haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                            menuState.show {\n                                                YouTubeSongMenu(\n                                                    song = video,\n                                                    navController = navController,\n                                                    onDismiss = menuState::dismiss,\n                                                )\n                                            }\n                                        },\n                                    )\n                                    .animateItem(),\n                            )\n                        }\n                    }\n                }\n\n                explorePage?.moodAndGenres?.let { moodAndGenres ->\n                    NavigationTitle(\n                        title = stringResource(R.string.mood_and_genres),\n                        onClick = {\n                            navController.navigate(\"mood_and_genres\")\n                        },\n                    )\n                    LazyHorizontalGrid(\n                        rows = GridCells.Fixed(4),\n                        contentPadding = PaddingValues(6.dp),\n                        modifier = Modifier.height((MoodAndGenresButtonHeight + 12.dp) * 4 + 12.dp),\n                    ) {\n                        items(moodAndGenres) {\n                            MoodAndGenresButton(\n                                title = it.title,\n                                onClick = {\n                                    navController.navigate(\"youtube_browse/${it.endpoint.browseId}?params=${it.endpoint.params}\")\n                                },\n                                modifier = Modifier\n                                    .padding(6.dp)\n                                    .width(180.dp),\n                            )\n                        }\n                    }\n                }\n            }\n\n            Spacer(\n                Modifier.height(\n                    LocalPlayerAwareWindowInsets.current.asPaddingValues().calculateBottomPadding()\n                )\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/HistoryScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextField\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.listSaver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.toMutableStateList\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.pluralStringResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.util.fastForEachReversed\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport com.metrolist.innertube.utils.parseCookieString\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.HistorySource\nimport com.metrolist.music.constants.InnerTubeCookieKey\nimport com.metrolist.music.extensions.metadata\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.playback.queues.ListQueue\nimport com.metrolist.music.playback.queues.YouTubeQueue\nimport com.metrolist.music.ui.component.ChipsRow\nimport com.metrolist.music.ui.component.HideOnScrollFAB\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.NavigationTitle\nimport com.metrolist.music.ui.component.SongListItem\nimport com.metrolist.music.ui.component.YouTubeListItem\nimport com.metrolist.music.ui.menu.SelectionMediaMetadataMenu\nimport com.metrolist.music.ui.menu.SongMenu\nimport com.metrolist.music.ui.menu.YouTubeSongMenu\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.viewmodels.DateAgo\nimport com.metrolist.music.viewmodels.HistoryViewModel\nimport java.time.format.DateTimeFormatter\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)\n@Composable\nfun HistoryScreen(\n    navController: NavController,\n    viewModel: HistoryViewModel = hiltViewModel(),\n) {\n    val context = LocalContext.current\n    val database = LocalDatabase.current\n    val menuState = LocalMenuState.current\n    val haptic = LocalHapticFeedback.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n\n    var inSelectMode by rememberSaveable { mutableStateOf(false) }\n    val selection =\n        rememberSaveable(\n            saver =\n                listSaver<MutableList<Long>, Long>(\n                    save = { it.toList() },\n                    restore = { it.toMutableStateList() },\n                ),\n        ) { mutableStateListOf() }\n    val onExitSelectionMode = {\n        inSelectMode = false\n        selection.clear()\n    }\n\n    var isSearching by rememberSaveable { mutableStateOf(false) }\n    var query by rememberSaveable(stateSaver = TextFieldValue.Saver) {\n        mutableStateOf(TextFieldValue())\n    }\n    val focusRequester = remember { FocusRequester() }\n    LaunchedEffect(isSearching) {\n        if (isSearching) {\n            focusRequester.requestFocus()\n        }\n    }\n    if (isSearching) {\n        BackHandler {\n            isSearching = false\n            query = TextFieldValue()\n        }\n    } else if (inSelectMode) {\n        BackHandler(onBack = onExitSelectionMode)\n    }\n\n    val historySource by viewModel.historySource.collectAsState()\n\n    val historyPage by viewModel.historyPage.collectAsState()\n\n    val events by viewModel.events.collectAsState()\n\n    val innerTubeCookie by rememberPreference(InnerTubeCookieKey, \"\")\n    val isLoggedIn =\n        remember(innerTubeCookie) {\n            \"SAPISID\" in parseCookieString(innerTubeCookie)\n        }\n\n    @Composable\n    fun dateAgoToString(dateAgo: DateAgo): String =\n        when (dateAgo) {\n            DateAgo.Today -> stringResource(R.string.today)\n            DateAgo.Yesterday -> stringResource(R.string.yesterday)\n            DateAgo.ThisWeek -> stringResource(R.string.this_week)\n            DateAgo.LastWeek -> stringResource(R.string.last_week)\n            is DateAgo.Other -> dateAgo.date.format(DateTimeFormatter.ofPattern(\"yyyy/MM\"))\n        }\n\n    val filteredEvents =\n        remember(events, query) {\n            if (query.text.isEmpty()) {\n                events\n            } else {\n                events\n                    .mapValues { (_, songs) ->\n                        songs.filter { event ->\n                            event.song.song.title\n                                .contains(query.text, ignoreCase = true) ||\n                                event.song.artists.any {\n                                    it.name.contains(\n                                        query.text,\n                                        ignoreCase = true,\n                                    )\n                                }\n                        }\n                    }.filterValues { it.isNotEmpty() }\n            }\n        }\n\n    val filteredRemoteContent =\n        remember(historyPage, query) {\n            if (query.text.isEmpty()) {\n                historyPage?.sections\n            } else {\n                historyPage\n                    ?.sections\n                    ?.map { section ->\n                        section.copy(\n                            songs =\n                                section.songs.filter { song ->\n                                    song.title.contains(query.text, ignoreCase = true) ||\n                                        song.artists.any { it.name.contains(query.text, ignoreCase = true) }\n                                },\n                        )\n                    }?.filter { it.songs.isNotEmpty() }\n            }\n        }\n\n    val allEvents =\n        remember(filteredEvents) {\n            filteredEvents.values.flatten()\n        }\n\n    LaunchedEffect(allEvents) {\n        selection.fastForEachReversed { eventId ->\n            if (allEvents.find { it.event.id == eventId } == null) {\n                selection.remove(eventId)\n            }\n        }\n    }\n\n    val lazyListState = rememberLazyListState()\n\n    Box(Modifier.fillMaxSize()) {\n        LazyColumn(\n            state = lazyListState,\n            contentPadding =\n                LocalPlayerAwareWindowInsets.current\n                    .only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom)\n                    .asPaddingValues(),\n            modifier =\n                Modifier.windowInsetsPadding(\n                    LocalPlayerAwareWindowInsets.current.only(\n                        WindowInsetsSides.Top,\n                    ),\n                ),\n        ) {\n            item(key = \"chips_row\") {\n                ChipsRow(\n                    chips =\n                        if (isLoggedIn) {\n                            listOf(\n                                HistorySource.LOCAL to stringResource(R.string.local_history),\n                                HistorySource.REMOTE to stringResource(R.string.remote_history),\n                            )\n                        } else {\n                            listOf(HistorySource.LOCAL to stringResource(R.string.local_history))\n                        },\n                    currentValue = historySource,\n                    onValueUpdate = {\n                        viewModel.historySource.value = it\n                        if (it == HistorySource.REMOTE) {\n                            viewModel.fetchRemoteHistory()\n                        }\n                    },\n                )\n            }\n\n            if (historySource == HistorySource.REMOTE && isLoggedIn) {\n                filteredRemoteContent?.forEach { section ->\n                    stickyHeader {\n                        NavigationTitle(\n                            title = section.title,\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .background(MaterialTheme.colorScheme.background),\n                        )\n                    }\n\n                    items(\n                        items = section.songs,\n                        key = { \"${section.title}_${it.id}_${section.songs.indexOf(it)}\" },\n                    ) { song ->\n                        YouTubeListItem(\n                            item = song,\n                            isActive = song.id == mediaMetadata?.id,\n                            isPlaying = isPlaying,\n                            trailingContent = {\n                                IconButton(\n                                    onClick = {\n                                        menuState.show {\n                                            YouTubeSongMenu(\n                                                song = song,\n                                                navController = navController,\n                                                onDismiss = menuState::dismiss,\n                                                onHistoryRemoved = {\n                                                    viewModel.fetchRemoteHistory()\n                                                },\n                                            )\n                                        }\n                                    },\n                                ) {\n                                    Icon(\n                                        painter = painterResource(R.drawable.more_vert),\n                                        contentDescription = null,\n                                    )\n                                }\n                            },\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .combinedClickable(\n                                        onClick = {\n                                            if (song.id == mediaMetadata?.id) {\n                                                playerConnection.togglePlayPause()\n                                            } else {\n                                                playerConnection.playQueue(\n                                                    YouTubeQueue.radio(song.toMediaMetadata()),\n                                                )\n                                            }\n                                        },\n                                        onLongClick = {\n                                            menuState.show {\n                                                YouTubeSongMenu(\n                                                    song = song,\n                                                    navController = navController,\n                                                    onDismiss = menuState::dismiss,\n                                                    onHistoryRemoved = {\n                                                        viewModel.fetchRemoteHistory()\n                                                    },\n                                                )\n                                            }\n                                        },\n                                    ).animateItem(),\n                        )\n                    }\n                }\n            } else {\n                filteredEvents.forEach { (dateAgo, dateEvents) ->\n                    stickyHeader {\n                        NavigationTitle(\n                            title = dateAgoToString(dateAgo),\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .background(MaterialTheme.colorScheme.surface),\n                        )\n                    }\n\n                    itemsIndexed(\n                        items = dateEvents,\n                        key = { index, event -> \"${dateAgo}_${event.event.id}_$index\" },\n                    ) { index, event ->\n                        val onCheckedChange: (Boolean) -> Unit = {\n                            if (it) {\n                                selection.add(event.event.id)\n                            } else {\n                                selection.remove(event.event.id)\n                            }\n                        }\n                        val dateTitle = dateAgoToString(dateAgo)\n\n                        SongListItem(\n                            song = event.song,\n                            isActive = event.song.id == mediaMetadata?.id,\n                            isPlaying = isPlaying,\n                            showInLibraryIcon = true,\n                            trailingContent = {\n                                if (inSelectMode) {\n                                    Checkbox(\n                                        checked = event.event.id in selection,\n                                        onCheckedChange = onCheckedChange,\n                                    )\n                                } else {\n                                    IconButton(\n                                        onClick = {\n                                            menuState.show {\n                                                SongMenu(\n                                                    originalSong = event.song,\n                                                    event = event.event,\n                                                    navController = navController,\n                                                    onDismiss = menuState::dismiss,\n                                                )\n                                            }\n                                        },\n                                    ) {\n                                        Icon(\n                                            painter = painterResource(R.drawable.more_vert),\n                                            contentDescription = null,\n                                        )\n                                    }\n                                }\n                            },\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .combinedClickable(\n                                        onClick = {\n                                            if (inSelectMode) {\n                                                onCheckedChange(event.event.id !in selection)\n                                            } else if (event.song.id == mediaMetadata?.id) {\n                                                playerConnection.togglePlayPause()\n                                            } else {\n                                                playerConnection.playQueue(\n                                                    ListQueue(\n                                                        title = dateTitle,\n                                                        items = dateEvents.map { it.song.toMediaItem() },\n                                                        startIndex = index,\n                                                    ),\n                                                )\n                                            }\n                                        },\n                                        onLongClick = {\n                                            if (!inSelectMode) {\n                                                haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                inSelectMode = true\n                                                onCheckedChange(true)\n                                            }\n                                        },\n                                    ).animateItem(),\n                        )\n                    }\n                }\n            }\n        }\n\n        val historyTitle = stringResource(R.string.history)\n\n        HideOnScrollFAB(\n            visible =\n                if (historySource == HistorySource.REMOTE) {\n                    filteredRemoteContent?.any { it.songs.isNotEmpty() } == true\n                } else {\n                    allEvents.isNotEmpty()\n                },\n            lazyListState = lazyListState,\n            icon = R.drawable.shuffle,\n            onClick = {\n                if (historySource == HistorySource.REMOTE && historyPage != null) {\n                    val songs = filteredRemoteContent?.flatMap { it.songs } ?: emptyList()\n                    if (songs.isNotEmpty()) {\n                        playerConnection.playQueue(\n                            ListQueue(\n                                title = historyTitle,\n                                items = songs.map { it.toMediaItem() }.shuffled(),\n                            ),\n                        )\n                    }\n                } else {\n                    playerConnection.playQueue(\n                        ListQueue(\n                            title = historyTitle,\n                            items = allEvents.map { it.song.toMediaItem() }.shuffled(),\n                        ),\n                    )\n                }\n            },\n        )\n    }\n\n    TopAppBar(\n        title = {\n            if (inSelectMode) {\n                Text(pluralStringResource(R.plurals.n_selected, selection.size, selection.size))\n            } else if (isSearching) {\n                TextField(\n                    value = query,\n                    onValueChange = { query = it },\n                    placeholder = {\n                        Text(\n                            text = stringResource(R.string.search),\n                            style = MaterialTheme.typography.titleLarge,\n                        )\n                    },\n                    singleLine = true,\n                    textStyle = MaterialTheme.typography.titleLarge,\n                    keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),\n                    colors =\n                        TextFieldDefaults.colors(\n                            focusedContainerColor = Color.Transparent,\n                            unfocusedContainerColor = Color.Transparent,\n                            focusedIndicatorColor = Color.Transparent,\n                            unfocusedIndicatorColor = Color.Transparent,\n                            disabledIndicatorColor = Color.Transparent,\n                        ),\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .focusRequester(focusRequester),\n                )\n            } else {\n                Text(stringResource(R.string.history))\n            }\n        },\n        navigationIcon = {\n            if (inSelectMode) {\n                IconButton(onClick = onExitSelectionMode) {\n                    Icon(\n                        painter = painterResource(R.drawable.close),\n                        contentDescription = null,\n                    )\n                }\n            } else {\n                IconButton(\n                    onClick = {\n                        if (isSearching) {\n                            isSearching = false\n                            query = TextFieldValue()\n                        } else {\n                            navController.navigateUp()\n                        }\n                    },\n                    onLongClick = {\n                        if (!isSearching) {\n                            navController.backToMain()\n                        }\n                    },\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.arrow_back),\n                        contentDescription = null,\n                    )\n                }\n            }\n        },\n        actions = {\n            if (inSelectMode) {\n                Checkbox(\n                    checked = selection.size == allEvents.size && selection.isNotEmpty(),\n                    onCheckedChange = {\n                        if (selection.size == allEvents.size) {\n                            selection.clear()\n                        } else {\n                            selection.clear()\n                            selection.addAll(allEvents.map { it.event.id })\n                        }\n                    },\n                )\n                IconButton(\n                    enabled = selection.isNotEmpty(),\n                    onClick = {\n                        menuState.show {\n                            SelectionMediaMetadataMenu(\n                                songSelection =\n                                    selection.mapNotNull { eventId ->\n                                        allEvents\n                                            .find { it.event.id == eventId }\n                                            ?.song\n                                            ?.toMediaItem()\n                                            ?.metadata\n                                    },\n                                onDismiss = menuState::dismiss,\n                                clearAction = onExitSelectionMode,\n                                currentItems = emptyList(),\n                            )\n                        }\n                    },\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.more_vert),\n                        contentDescription = null,\n                    )\n                }\n            } else if (!isSearching) {\n                IconButton(\n                    onClick = { isSearching = true },\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.search),\n                        contentDescription = null,\n                    )\n                }\n            }\n        },\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/HomeScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyHorizontalGrid\nimport androidx.compose.foundation.lazy.grid.items\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.pager.HorizontalPager\nimport androidx.compose.foundation.pager.rememberPagerState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.ContainedLoadingIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.carousel.HorizontalMultiBrowseCarousel\nimport androidx.compose.material3.carousel.rememberCarouselState\nimport androidx.compose.material3.pulltorefresh.PullToRefreshBox\nimport androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator\nimport androidx.compose.material3.pulltorefresh.rememberPullToRefreshState\nimport androidx.compose.material3.surfaceColorAtElevation\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.snapshotFlow\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.Font\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport androidx.navigation.compose.currentBackStackEntryAsState\nimport coil3.compose.AsyncImage\nimport coil3.request.CachePolicy\nimport coil3.request.ImageRequest\nimport coil3.request.crossfade\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.ArtistItem\nimport com.metrolist.innertube.models.EpisodeItem\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.PodcastItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.WatchEndpoint\nimport com.metrolist.innertube.models.YTItem\nimport com.metrolist.innertube.utils.completed\nimport com.metrolist.innertube.utils.parseCookieString\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalListenTogetherManager\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.GridItemSize\nimport com.metrolist.music.constants.GridItemsSizeKey\nimport com.metrolist.music.constants.GridThumbnailHeight\nimport com.metrolist.music.constants.InnerTubeCookieKey\nimport com.metrolist.music.constants.ListItemHeight\nimport com.metrolist.music.constants.ListThumbnailSize\nimport com.metrolist.music.constants.RandomizeHomeOrderKey\nimport com.metrolist.music.constants.SmallGridThumbnailHeight\nimport com.metrolist.music.constants.ThumbnailCornerRadius\nimport com.metrolist.music.db.entities.Album\nimport com.metrolist.music.db.entities.Artist\nimport com.metrolist.music.db.entities.LocalItem\nimport com.metrolist.music.db.entities.Playlist\nimport com.metrolist.music.db.entities.PlaylistEntity\nimport com.metrolist.music.db.entities.PlaylistSongMap\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.playback.queues.ListQueue\nimport com.metrolist.music.playback.queues.LocalAlbumRadio\nimport com.metrolist.music.playback.queues.YouTubeAlbumRadio\nimport com.metrolist.music.playback.queues.YouTubeQueue\nimport com.metrolist.music.ui.component.AlbumGridItem\nimport com.metrolist.music.ui.component.ArtistGridItem\nimport com.metrolist.music.ui.component.ChipsRow\nimport com.metrolist.music.ui.component.HideOnScrollFAB\nimport com.metrolist.music.ui.component.LocalBottomSheetPageState\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.NavigationTitle\nimport com.metrolist.music.ui.component.RandomizeGridItem\nimport com.metrolist.music.ui.component.SongGridItem\nimport com.metrolist.music.ui.component.SongListItem\nimport com.metrolist.music.ui.component.SpeedDialGridItem\nimport com.metrolist.music.ui.component.YouTubeGridItem\nimport com.metrolist.music.ui.component.YouTubeListItem\nimport com.metrolist.music.ui.component.shimmer.GridItemPlaceHolder\nimport com.metrolist.music.ui.component.shimmer.ShimmerHost\nimport com.metrolist.music.ui.component.shimmer.TextPlaceholder\nimport com.metrolist.music.ui.menu.AlbumMenu\nimport com.metrolist.music.ui.menu.ArtistMenu\nimport com.metrolist.music.ui.menu.SongMenu\nimport com.metrolist.music.ui.menu.YouTubeAlbumMenu\nimport com.metrolist.music.ui.menu.YouTubeArtistMenu\nimport com.metrolist.music.ui.menu.YouTubePlaylistMenu\nimport com.metrolist.music.ui.menu.YouTubeSongMenu\nimport com.metrolist.music.ui.utils.SnapLayoutInfoProvider\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.viewmodels.CommunityPlaylistItem\nimport com.metrolist.music.viewmodels.HomeViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport kotlin.math.min\nimport kotlin.random.Random\n\nsealed class HomeSection(\n    val id: String,\n    val baseWeight: Int,\n) {\n    data object SpeedDial : HomeSection(\"speed_dial\", 100)\n\n    data object QuickPicks : HomeSection(\"quick_picks\", 90)\n\n    data object DailyDiscover : HomeSection(\"daily_discover\", 80)\n\n    data object KeepListening : HomeSection(\"keep_listening\", 50)\n\n    data object AccountPlaylists : HomeSection(\"account_playlists\", 40)\n\n    data object ForgottenFavorites : HomeSection(\"forgotten_favorites\", 30)\n\n    data object FromTheCommunity : HomeSection(\"from_the_community\", 20)\n\n    data class SimilarRecommendation(\n        val index: Int,\n    ) : HomeSection(\"similar_recommendation_$index\", 10)\n\n    data class HomePageSection(\n        val index: Int,\n    ) : HomeSection(\"home_page_section_$index\", 10)\n\n    data object MoodAndGenres : HomeSection(\"mood_and_genres\", 5)\n}\n\n@Composable\nfun CommunityPlaylistCard(\n    item: CommunityPlaylistItem,\n    onClick: () -> Unit,\n    onSongClick: (SongItem) -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    val database = LocalDatabase.current\n    val playerConnection = LocalPlayerConnection.current\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val isListenTogetherGuest = listenTogetherManager?.let { it.isInRoom && !it.isHost } ?: false\n    val scope = rememberCoroutineScope()\n    val isDark = isSystemInDarkTheme()\n\n    val containerColor =\n        if (isDark) {\n            MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)\n        } else {\n            MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)\n        }\n\n    val dbPlaylist by database.playlistByBrowseId(item.playlist.id).collectAsState(initial = null)\n    val isBookmarked = dbPlaylist?.playlist?.bookmarkedAt != null\n\n    Card(\n        modifier =\n            modifier\n                .width(320.dp)\n                .height(420.dp),\n        colors =\n            CardDefaults.cardColors(\n                containerColor = containerColor,\n            ),\n        shape = RoundedCornerShape(28.dp),\n        onClick = onClick,\n    ) {\n        Column(\n            modifier = Modifier.fillMaxSize(),\n        ) {\n            Row(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .padding(16.dp),\n                horizontalArrangement = Arrangement.spacedBy(16.dp),\n            ) {\n                // 2x2 Grid of thumbnails\n                Box(\n                    modifier =\n                        Modifier\n                            .size(100.dp)\n                            .clip(RoundedCornerShape(12.dp)),\n                ) {\n                    Column(modifier = Modifier.fillMaxSize()) {\n                        Row(modifier = Modifier.weight(1f)) {\n                            AsyncImage(\n                                model =\n                                    item.songs\n                                        .getOrNull(0)\n                                        ?.thumbnail\n                                        ?.replace(Regex(\"w\\\\d+-h\\\\d+\"), \"w120-h120\"),\n                                contentDescription = null,\n                                contentScale = ContentScale.Crop,\n                                modifier =\n                                    Modifier\n                                        .weight(1f)\n                                        .fillMaxSize(),\n                            )\n                            AsyncImage(\n                                model =\n                                    item.songs\n                                        .getOrNull(1)\n                                        ?.thumbnail\n                                        ?.replace(Regex(\"w\\\\d+-h\\\\d+\"), \"w120-h120\"),\n                                contentDescription = null,\n                                contentScale = ContentScale.Crop,\n                                modifier =\n                                    Modifier\n                                        .weight(1f)\n                                        .fillMaxSize(),\n                            )\n                        }\n                        Row(modifier = Modifier.weight(1f)) {\n                            AsyncImage(\n                                model =\n                                    item.songs\n                                        .getOrNull(2)\n                                        ?.thumbnail\n                                        ?.replace(Regex(\"w\\\\d+-h\\\\d+\"), \"w120-h120\"),\n                                contentDescription = null,\n                                contentScale = ContentScale.Crop,\n                                modifier =\n                                    Modifier\n                                        .weight(1f)\n                                        .fillMaxSize(),\n                            )\n                            AsyncImage(\n                                model =\n                                    item.songs\n                                        .getOrNull(3)\n                                        ?.thumbnail\n                                        ?.replace(Regex(\"w\\\\d+-h\\\\d+\"), \"w120-h120\"),\n                                contentDescription = null,\n                                contentScale = ContentScale.Crop,\n                                modifier =\n                                    Modifier\n                                        .weight(1f)\n                                        .fillMaxSize(),\n                            )\n                        }\n                    }\n                }\n\n                Column(\n                    modifier = Modifier.weight(1f),\n                    verticalArrangement = Arrangement.Center,\n                ) {\n                    Text(\n                        text = item.playlist.title,\n                        style = MaterialTheme.typography.titleMedium,\n                        maxLines = 2,\n                        overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,\n                    )\n                    Spacer(modifier = Modifier.height(4.dp))\n                    Text(\n                        text = item.playlist.author?.name ?: \"\",\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f),\n                        maxLines = 1,\n                    )\n                }\n            }\n\n            Column(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .weight(1f)\n                        .padding(horizontal = 16.dp),\n            ) {\n                item.songs.take(3).forEach { song ->\n                    Row(\n                        modifier =\n                            Modifier\n                                .fillMaxWidth()\n                                .padding(vertical = 4.dp)\n                                .clip(RoundedCornerShape(12.dp))\n                                .combinedClickable(onClick = { onSongClick(song) }),\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.spacedBy(12.dp),\n                    ) {\n                        AsyncImage(\n                            model = song.thumbnail.replace(Regex(\"w\\\\d+-h\\\\d+\"), \"w120-h120\"),\n                            contentDescription = null,\n                            modifier =\n                                Modifier\n                                    .size(56.dp)\n                                    .clip(RoundedCornerShape(12.dp)),\n                            contentScale = ContentScale.Crop,\n                        )\n                        Column(modifier = Modifier.weight(1f)) {\n                            Text(\n                                text = song.title,\n                                style = MaterialTheme.typography.bodyMedium,\n                                maxLines = 1,\n                                overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,\n                            )\n                            Text(\n                                text = song.artists.joinToString(\", \") { it.name },\n                                style = MaterialTheme.typography.bodySmall,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f),\n                                maxLines = 1,\n                                overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,\n                            )\n                        }\n                    }\n                }\n            }\n\n            Row(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 16.dp, vertical = 16.dp),\n                horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),\n            ) {\n                IconButton(\n                    onClick = {\n                        if (!isListenTogetherGuest) {\n                            item.playlist.playEndpoint?.let {\n                                playerConnection?.playQueue(YouTubeQueue(it))\n                            }\n                        }\n                    },\n                    modifier =\n                        Modifier\n                            .size(48.dp)\n                            .background(MaterialTheme.colorScheme.primaryContainer, CircleShape),\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.ic_widget_play),\n                        contentDescription = null,\n                        tint = MaterialTheme.colorScheme.onPrimaryContainer,\n                        modifier = Modifier.size(24.dp),\n                    )\n                }\n\n                IconButton(\n                    onClick = {\n                        if (!isListenTogetherGuest) {\n                            item.playlist.radioEndpoint?.let {\n                                playerConnection?.playQueue(YouTubeQueue(it))\n                            }\n                        }\n                    },\n                    modifier =\n                        Modifier\n                            .size(48.dp)\n                            .background(MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f), CircleShape),\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.radio),\n                        contentDescription = null,\n                        tint = MaterialTheme.colorScheme.onSecondaryContainer,\n                        modifier = Modifier.size(24.dp),\n                    )\n                }\n\n                IconButton(\n                    onClick = {\n                        scope.launch(Dispatchers.IO) {\n                            if (dbPlaylist?.playlist == null) {\n                                database.transaction {\n                                    val playlistEntity =\n                                        PlaylistEntity(\n                                            name = item.playlist.title,\n                                            browseId = item.playlist.id,\n                                            thumbnailUrl = item.playlist.thumbnail,\n                                            remoteSongCount =\n                                                item.playlist.songCountText\n                                                    ?.split(\" \")\n                                                    ?.firstOrNull()\n                                                    ?.toIntOrNull(),\n                                            playEndpointParams = item.playlist.playEndpoint?.params,\n                                            shuffleEndpointParams = item.playlist.shuffleEndpoint?.params,\n                                            radioEndpointParams = item.playlist.radioEndpoint?.params,\n                                        ).toggleLike()\n                                    insert(playlistEntity)\n                                    scope.launch(Dispatchers.IO) {\n                                        item.songs\n                                            .ifEmpty {\n                                                YouTube\n                                                    .playlist(item.playlist.id)\n                                                    .completed()\n                                                    .getOrNull()\n                                                    ?.songs\n                                                    .orEmpty()\n                                            }.map { it.toMediaMetadata() }\n                                            .onEach(::insert)\n                                            .mapIndexed { index, song ->\n                                                PlaylistSongMap(\n                                                    songId = song.id,\n                                                    playlistId = playlistEntity.id,\n                                                    position = index,\n                                                    setVideoId = song.setVideoId,\n                                                )\n                                            }.forEach(::insert)\n                                    }\n                                }\n                            } else {\n                                database.transaction {\n                                    val currentPlaylist = dbPlaylist!!.playlist\n                                    update(currentPlaylist.toggleLike())\n                                }\n                            }\n                        }\n                    },\n                    modifier =\n                        Modifier\n                            .size(48.dp)\n                            .background(MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f), CircleShape),\n                ) {\n                    Icon(\n                        painter = painterResource(if (isBookmarked) R.drawable.library_add_check else R.drawable.library_add),\n                        contentDescription = null,\n                        tint = MaterialTheme.colorScheme.onSecondaryContainer,\n                        modifier = Modifier.size(24.dp),\n                    )\n                }\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)\n@Composable\nfun DailyDiscoverCard(\n    dailyDiscover: com.metrolist.music.viewmodels.DailyDiscoverItem,\n    onClick: () -> Unit,\n    navController: NavController,\n    modifier: Modifier = Modifier,\n) {\n    val database = LocalDatabase.current\n    val playCount by database.getLifetimePlayCount(dailyDiscover.recommendation.id).collectAsState(initial = 0)\n    val menuState = LocalMenuState.current\n    val haptic = LocalHapticFeedback.current\n\n    val song = dailyDiscover.recommendation as? SongItem\n    val playsString = stringResource(R.string.plays)\n\n    Card(\n        modifier =\n            modifier\n                .fillMaxSize()\n                .clip(RoundedCornerShape(28.dp))\n                .combinedClickable(\n                    onClick = onClick,\n                    onLongClick = {\n                        haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                        if (song != null) {\n                            menuState.show {\n                                YouTubeSongMenu(\n                                    song = song,\n                                    navController = navController,\n                                    onDismiss = { menuState.dismiss() },\n                                )\n                            }\n                        }\n                    },\n                ),\n        colors =\n            CardDefaults.cardColors(\n                containerColor = MaterialTheme.colorScheme.surfaceVariant,\n            ),\n        shape = RoundedCornerShape(28.dp),\n    ) {\n        BoxWithConstraints(modifier = Modifier.fillMaxSize()) {\n            AsyncImage(\n                model =\n                    ImageRequest\n                        .Builder(LocalContext.current)\n                        .data(dailyDiscover.recommendation.thumbnail?.replace(Regex(\"w\\\\d+-h\\\\d+\"), \"w544-h544\"))\n                        .crossfade(true)\n                        .build(),\n                contentDescription = null,\n                contentScale = ContentScale.Crop,\n                modifier =\n                    Modifier\n                        .fillMaxSize(),\n            )\n\n            if (maxWidth > 200.dp) {\n                Box(\n                    modifier =\n                        Modifier\n                            .fillMaxSize()\n                            .background(\n                                brush =\n                                    Brush.verticalGradient(\n                                        colors =\n                                            listOf(\n                                                Color.Black.copy(alpha = 0.3f),\n                                                Color.Transparent,\n                                                Color.Black.copy(alpha = 0.6f),\n                                                Color.Black.copy(alpha = 0.9f),\n                                            ),\n                                    ),\n                            ),\n                )\n\n                Column(\n                    modifier =\n                        Modifier\n                            .fillMaxSize()\n                            .padding(24.dp),\n                    verticalArrangement = Arrangement.SpaceBetween,\n                ) {\n                    Column {\n                        Text(\n                            text = dailyDiscover.recommendation.title,\n                            style = MaterialTheme.typography.titleMedium,\n                            color = Color.White,\n                        )\n                        Text(\n                            text =\n                                buildString {\n                                    append((dailyDiscover.recommendation as? SongItem)?.artists?.joinToString(\", \") { it.name } ?: \"\")\n                                    if (playCount > 0) {\n                                        append(\" • $playCount $playsString\")\n                                    }\n                                },\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = Color.White.copy(alpha = 0.7f),\n                        )\n                    }\n\n                    val messages =\n                        listOf(\n                            R.string.daily_discover_sounds_like,\n                            R.string.daily_discover_because_you_listen_to,\n                            R.string.daily_discover_similar_to,\n                            R.string.daily_discover_based_on,\n                            R.string.daily_discover_for_fans_of,\n                        )\n                    val messageRes =\n                        remember(dailyDiscover.seed.id) {\n                            messages[kotlin.math.abs(dailyDiscover.seed.id.hashCode()) % messages.size]\n                        }\n\n                    Text(\n                        text =\n                            stringResource(\n                                messageRes,\n                                \"${dailyDiscover.seed.title} • ${dailyDiscover.seed.artists.joinToString(\", \") { it.name }}\",\n                            ),\n                        style = MaterialTheme.typography.bodySmall,\n                        fontWeight = androidx.compose.ui.text.font.FontWeight.Medium,\n                        color = Color.White.copy(alpha = 0.6f),\n                        maxLines = 1,\n                        overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,\n                    )\n                }\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun HomeScreen(\n    navController: NavController,\n    snackbarHostState: SnackbarHostState,\n    viewModel: HomeViewModel = hiltViewModel(),\n) {\n    val menuState = LocalMenuState.current\n    val bottomSheetPageState = LocalBottomSheetPageState.current\n    val database = LocalDatabase.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val haptic = LocalHapticFeedback.current\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val isListenTogetherGuest = listenTogetherManager?.let { it.isInRoom && !it.isHost } ?: false\n\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n\n    val quickPicks by viewModel.quickPicks.collectAsState()\n    val forgottenFavorites by viewModel.forgottenFavorites.collectAsState()\n    val keepListening by viewModel.keepListening.collectAsState()\n    val similarRecommendations by viewModel.similarRecommendations.collectAsState()\n    val accountPlaylists by viewModel.accountPlaylists.collectAsState()\n    val homePage by viewModel.homePage.collectAsState()\n    val explorePage by viewModel.explorePage.collectAsState()\n    val dailyDiscover by viewModel.dailyDiscover.collectAsState()\n    val communityPlaylists by viewModel.communityPlaylists.collectAsState()\n\n    val allLocalItems by viewModel.allLocalItems.collectAsState()\n    val allYtItems by viewModel.allYtItems.collectAsState()\n    val speedDialItems by viewModel.speedDialItems.collectAsState()\n    val pinnedSpeedDialItems by viewModel.pinnedSpeedDialItems.collectAsState()\n    val selectedChip by viewModel.selectedChip.collectAsState()\n\n    // Official podcast API data\n    val savedPodcastShows by viewModel.savedPodcastShows.collectAsState()\n    val episodesForLater by viewModel.episodesForLater.collectAsState()\n\n    val isLoading: Boolean by viewModel.isLoading.collectAsState()\n    val isMoodAndGenresLoading = isLoading && explorePage?.moodAndGenres == null\n    val isRefreshing by viewModel.isRefreshing.collectAsState()\n    val isRandomizing by viewModel.isRandomizing.collectAsState()\n    val pullRefreshState = rememberPullToRefreshState()\n\n    val quickPicksLazyGridState = rememberLazyGridState()\n    val forgottenFavoritesLazyGridState = rememberLazyGridState()\n\n    val accountName by viewModel.accountName.collectAsState()\n    val accountImageUrl by viewModel.accountImageUrl.collectAsState()\n    val innerTubeCookie by rememberPreference(InnerTubeCookieKey, \"\")\n    val (randomizeHomeOrder) = rememberPreference(RandomizeHomeOrderKey, true)\n\n    val shouldShowWrappedCard by viewModel.showWrappedCard.collectAsState()\n    val wrappedState by viewModel.wrappedManager.state.collectAsState()\n    val isWrappedDataReady = wrappedState.isDataReady\n\n    val isLoggedIn =\n        remember(innerTubeCookie) {\n            \"SAPISID\" in parseCookieString(innerTubeCookie)\n        }\n    val url = if (isLoggedIn) accountImageUrl else null\n\n    // Extract unique podcasts from episodes for \"Podcast Channels\" row\n    // Cache the podcasts to prevent them from disappearing during refresh\n    var cachedPodcasts by remember { mutableStateOf<List<PodcastItem>>(emptyList()) }\n\n    val featuredPodcasts =\n        remember(homePage, selectedChip) {\n            if (selectedChip == null) {\n                cachedPodcasts = emptyList()\n                emptyList()\n            } else {\n                val newPodcasts =\n                    homePage\n                        ?.sections\n                        ?.flatMap { it.items }\n                        ?.filterIsInstance<EpisodeItem>()\n                        ?.mapNotNull { episode ->\n                            episode.podcast?.let { podcast ->\n                                PodcastItem(\n                                    id = podcast.id,\n                                    title = podcast.name,\n                                    author = episode.author,\n                                    episodeCountText = null,\n                                    thumbnail = episode.thumbnail,\n                                    playEndpoint = null,\n                                    shuffleEndpoint = null,\n                                )\n                            }\n                        }?.distinctBy { it.id }\n                        ?.shuffled()\n                        ?.take(10)\n                        ?: emptyList()\n\n                // Only update cache if we got valid data; keep old data during refresh\n                if (newPodcasts.isNotEmpty()) {\n                    cachedPodcasts = newPodcasts\n                }\n                cachedPodcasts\n            }\n        }\n\n    val scope = rememberCoroutineScope()\n    // Track randomization job\n    var randomizeJob by remember { mutableStateOf<kotlinx.coroutines.Job?>(null) }\n\n    val lazylistState = rememberLazyListState()\n    val gridItemSize by rememberEnumPreference(GridItemsSizeKey, GridItemSize.BIG)\n    val currentGridHeight = if (gridItemSize == GridItemSize.BIG) GridThumbnailHeight else SmallGridThumbnailHeight\n    val backStackEntry by navController.currentBackStackEntryAsState()\n    val scrollToTop =\n        backStackEntry?.savedStateHandle?.getStateFlow(\"scrollToTop\", false)?.collectAsState()\n\n    val wrappedDismissed by backStackEntry\n        ?.savedStateHandle\n        ?.getStateFlow(\"wrapped_seen\", false)\n        ?.collectAsState() ?: remember { mutableStateOf(false) }\n\n    var randomSeed by rememberSaveable { mutableLongStateOf(System.currentTimeMillis()) }\n\n    LaunchedEffect(isRefreshing) {\n        if (isRefreshing) {\n            randomSeed = System.currentTimeMillis()\n        }\n    }\n\n    val foundInSettings = stringResource(R.string.found_in_settings_content)\n    LaunchedEffect(wrappedDismissed) {\n        if (wrappedDismissed) {\n            viewModel.markWrappedAsSeen()\n            scope.launch {\n                snackbarHostState.showSnackbar(foundInSettings)\n            }\n            backStackEntry?.savedStateHandle?.set(\"wrapped_seen\", false) // Reset the value\n        }\n    }\n\n    LaunchedEffect(scrollToTop?.value) {\n        if (scrollToTop?.value == true) {\n            lazylistState.animateScrollToItem(0)\n            backStackEntry?.savedStateHandle?.set(\"scrollToTop\", false)\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        snapshotFlow {\n            lazylistState.layoutInfo.visibleItemsInfo\n                .lastOrNull()\n                ?.index\n        }.collect { lastVisibleIndex ->\n            val len = lazylistState.layoutInfo.totalItemsCount\n            if (lastVisibleIndex != null && lastVisibleIndex >= len - 3) {\n                viewModel.loadMoreYouTubeItems(homePage?.continuation)\n            }\n        }\n    }\n\n    if (selectedChip != null) {\n        BackHandler {\n            // if a chip is selected, go back to the normal homepage first\n            viewModel.toggleChip(selectedChip)\n        }\n    }\n\n    val localGridItem: @Composable (LocalItem) -> Unit = {\n        when (it) {\n            is Song -> {\n                SongGridItem(\n                    song = it,\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .combinedClickable(\n                                onClick = {\n                                    if (!isListenTogetherGuest) {\n                                        if (it.id == mediaMetadata?.id) {\n                                            playerConnection.togglePlayPause()\n                                        } else {\n                                            playerConnection.playQueue(\n                                                YouTubeQueue.radio(it.toMediaMetadata()),\n                                            )\n                                        }\n                                    }\n                                },\n                                onLongClick = {\n                                    haptic.performHapticFeedback(\n                                        HapticFeedbackType.LongPress,\n                                    )\n                                    menuState.show {\n                                        SongMenu(\n                                            originalSong = it,\n                                            navController = navController,\n                                            onDismiss = menuState::dismiss,\n                                        )\n                                    }\n                                },\n                            ),\n                    isActive = it.id == mediaMetadata?.id,\n                    isPlaying = isPlaying,\n                )\n            }\n\n            is Album -> {\n                AlbumGridItem(\n                    album = it,\n                    isActive = it.id == mediaMetadata?.album?.id,\n                    isPlaying = isPlaying,\n                    coroutineScope = scope,\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .combinedClickable(\n                                onClick = {\n                                    navController.navigate(\"album/${it.id}\")\n                                },\n                                onLongClick = {\n                                    haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                    menuState.show {\n                                        AlbumMenu(\n                                            originalAlbum = it,\n                                            navController = navController,\n                                            onDismiss = menuState::dismiss,\n                                        )\n                                    }\n                                },\n                            ),\n                )\n            }\n\n            is Artist -> {\n                ArtistGridItem(\n                    artist = it,\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .combinedClickable(\n                                onClick = {\n                                    navController.navigate(\"artist/${it.id}\")\n                                },\n                                onLongClick = {\n                                    haptic.performHapticFeedback(\n                                        HapticFeedbackType.LongPress,\n                                    )\n                                    menuState.show {\n                                        ArtistMenu(\n                                            originalArtist = it,\n                                            coroutineScope = scope,\n                                            onDismiss = menuState::dismiss,\n                                        )\n                                    }\n                                },\n                            ),\n                )\n            }\n\n            is Playlist -> {}\n        }\n    }\n\n    val ytGridItem: @Composable (YTItem) -> Unit = { item ->\n        YouTubeGridItem(\n            item = item,\n            isActive = item.id in listOf(mediaMetadata?.album?.id, mediaMetadata?.id),\n            isPlaying = isPlaying,\n            coroutineScope = scope,\n            thumbnailRatio = 1f,\n            modifier =\n                Modifier\n                    .combinedClickable(\n                        onClick = {\n                            when (item) {\n                                is SongItem -> {\n                                    if (!isListenTogetherGuest) {\n                                        playerConnection.playQueue(\n                                            YouTubeQueue(\n                                                item.endpoint ?: WatchEndpoint(\n                                                    videoId = item.id,\n                                                ),\n                                                item.toMediaMetadata(),\n                                            ),\n                                        )\n                                    }\n                                }\n\n                                is AlbumItem -> {\n                                    navController.navigate(\"album/${item.id}\")\n                                }\n\n                                is ArtistItem -> {\n                                    navController.navigate(\"artist/${item.id}\")\n                                }\n\n                                is PlaylistItem -> {\n                                    navController.navigate(\"online_playlist/${item.id}\")\n                                }\n\n                                is PodcastItem -> {\n                                    navController.navigate(\"online_podcast/${item.id}\")\n                                }\n\n                                is EpisodeItem -> {\n                                    if (!isListenTogetherGuest) {\n                                        playerConnection.playQueue(\n                                            ListQueue(\n                                                title = item.title,\n                                                items = listOf(item.toMediaMetadata().toMediaItem()),\n                                            ),\n                                        )\n                                    }\n                                }\n                            }\n                        },\n                        onLongClick = {\n                            haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                            menuState.show {\n                                when (item) {\n                                    is SongItem -> {\n                                        YouTubeSongMenu(\n                                            song = item,\n                                            navController = navController,\n                                            onDismiss = menuState::dismiss,\n                                        )\n                                    }\n\n                                    is AlbumItem -> {\n                                        YouTubeAlbumMenu(\n                                            albumItem = item,\n                                            navController = navController,\n                                            onDismiss = menuState::dismiss,\n                                        )\n                                    }\n\n                                    is ArtistItem -> {\n                                        YouTubeArtistMenu(\n                                            artist = item,\n                                            onDismiss = menuState::dismiss,\n                                        )\n                                    }\n\n                                    is PlaylistItem -> {\n                                        YouTubePlaylistMenu(\n                                            playlist = item,\n                                            coroutineScope = scope,\n                                            onDismiss = menuState::dismiss,\n                                        )\n                                    }\n\n                                    is PodcastItem -> {\n                                        YouTubePlaylistMenu(\n                                            playlist = item.asPlaylistItem(),\n                                            coroutineScope = scope,\n                                            onDismiss = menuState::dismiss,\n                                        )\n                                    }\n\n                                    is EpisodeItem -> {\n                                        YouTubeSongMenu(\n                                            song = item.asSongItem(),\n                                            navController = navController,\n                                            onDismiss = menuState::dismiss,\n                                        )\n                                    }\n                                }\n                            }\n                        },\n                    ),\n        )\n    }\n\n    val homeSections =\n        remember(\n            randomizeHomeOrder,\n            randomSeed,\n            selectedChip,\n            speedDialItems,\n            quickPicks,\n            dailyDiscover,\n            keepListening,\n            accountPlaylists,\n            forgottenFavorites,\n            communityPlaylists,\n            similarRecommendations,\n            homePage?.sections,\n            explorePage?.moodAndGenres,\n        ) {\n            val list = mutableListOf<HomeSection>()\n            val chipActive = selectedChip != null\n\n            if (!chipActive && speedDialItems.isNotEmpty()) list.add(HomeSection.SpeedDial)\n            if (!chipActive && quickPicks?.isNotEmpty() == true) list.add(HomeSection.QuickPicks)\n            if (!chipActive && communityPlaylists?.isNotEmpty() == true) list.add(HomeSection.FromTheCommunity)\n            if (!chipActive && dailyDiscover?.isNotEmpty() == true) list.add(HomeSection.DailyDiscover)\n            if (!chipActive && keepListening?.isNotEmpty() == true) list.add(HomeSection.KeepListening)\n            if (!chipActive && accountPlaylists?.isNotEmpty() == true) list.add(HomeSection.AccountPlaylists)\n            if (!chipActive && forgottenFavorites?.isNotEmpty() == true) list.add(HomeSection.ForgottenFavorites)\n\n            if (!chipActive) {\n                similarRecommendations?.indices?.forEach { i ->\n                    list.add(HomeSection.SimilarRecommendation(i))\n                }\n            }\n\n            homePage?.sections?.indices?.forEach { i ->\n                list.add(HomeSection.HomePageSection(i))\n            }\n\n            if (explorePage?.moodAndGenres != null) list.add(HomeSection.MoodAndGenres)\n\n            if (randomizeHomeOrder) {\n                list.sortedByDescending { section ->\n                    // Use a stable seed for each section based on the session seed + section ID hash\n                    // This ensures the weight for a specific section remains constant during a session (until refresh)\n                    // even if other sections appear/disappear, preventing jumping.\n                    val sectionRandom = Random(randomSeed + section.id.hashCode())\n\n                    // Flatten the base values to allow for more overlap and variation\n                    // All \"main\" sections start closer together\n                    val base =\n                        when (section) {\n                            HomeSection.SpeedDial,\n                            HomeSection.QuickPicks,\n                            HomeSection.DailyDiscover,\n                            -> 500\n\n                            // Top tier starts equal\n\n                            HomeSection.KeepListening,\n                            HomeSection.AccountPlaylists,\n                            HomeSection.ForgottenFavorites,\n                            HomeSection.FromTheCommunity,\n                            -> 300\n\n                            // Middle tier starts equal\n\n                            else -> 100 // Bottom tier\n                        }\n\n                    val modifier =\n                        when (section) {\n                            // Top tier: High variance to allow shuffling among themselves\n                            // Range: [500-200, 500+400] = [300, 900]\n                            HomeSection.SpeedDial,\n                            HomeSection.QuickPicks,\n                            HomeSection.DailyDiscover,\n                            -> sectionRandom.nextInt(-200, 400)\n\n                            // Middle tier: Can jump up to challenge top tier, or drop lower\n                            // Range: [300-100, 300+400] = [200, 700]\n                            // This allows them to occasionally appear above a \"bad roll\" top tier item\n                            HomeSection.KeepListening,\n                            HomeSection.AccountPlaylists,\n                            HomeSection.ForgottenFavorites,\n                            HomeSection.FromTheCommunity,\n                            -> sectionRandom.nextInt(-100, 400)\n\n                            // Bottom tier: Standard variance\n                            else -> sectionRandom.nextInt(-50, 50)\n                        }\n                    base + modifier\n                }\n            } else {\n                val defaultOrder =\n                    mapOf(\n                        HomeSection.SpeedDial to 100,\n                        HomeSection.QuickPicks to 90,\n                        HomeSection.FromTheCommunity to 80,\n                        HomeSection.DailyDiscover to 70,\n                        HomeSection.KeepListening to 60,\n                        HomeSection.AccountPlaylists to 50,\n                        HomeSection.ForgottenFavorites to 40,\n                        HomeSection.MoodAndGenres to 10,\n                    )\n\n                list.sortedByDescending { section ->\n                    when (section) {\n                        is HomeSection.SimilarRecommendation -> 30 - section.index\n                        is HomeSection.HomePageSection -> 20 - section.index\n                        else -> defaultOrder[section] ?: 0\n                    }\n                }\n            }\n        }\n\n    LaunchedEffect(quickPicks) {\n        quickPicksLazyGridState.scrollToItem(0)\n    }\n\n    LaunchedEffect(forgottenFavorites) {\n        forgottenFavoritesLazyGridState.scrollToItem(0)\n    }\n\n    PullToRefreshBox(\n        state = pullRefreshState,\n        isRefreshing = isRefreshing,\n        onRefresh = viewModel::refresh,\n        indicator = {\n            Indicator(\n                isRefreshing = isRefreshing,\n                state = pullRefreshState,\n                modifier =\n                    Modifier\n                        .align(Alignment.TopCenter)\n                        .padding(LocalPlayerAwareWindowInsets.current.asPaddingValues()),\n            )\n        },\n    ) {\n        BoxWithConstraints(\n            modifier = Modifier.fillMaxSize(),\n            contentAlignment = Alignment.TopStart,\n        ) {\n            val horizontalLazyGridItemWidthFactor = if (maxWidth * 0.475f >= 320.dp) 0.475f else 0.9f\n            val horizontalLazyGridItemWidth = maxWidth * horizontalLazyGridItemWidthFactor\n            val quickPicksSnapLayoutInfoProvider =\n                remember(quickPicksLazyGridState) {\n                    SnapLayoutInfoProvider(\n                        lazyGridState = quickPicksLazyGridState,\n                        positionInLayout = { layoutSize, itemSize ->\n                            (layoutSize * horizontalLazyGridItemWidthFactor / 2f - itemSize / 2f)\n                        },\n                    )\n                }\n            val forgottenFavoritesSnapLayoutInfoProvider =\n                remember(forgottenFavoritesLazyGridState) {\n                    SnapLayoutInfoProvider(\n                        lazyGridState = forgottenFavoritesLazyGridState,\n                        positionInLayout = { layoutSize, itemSize ->\n                            (layoutSize * horizontalLazyGridItemWidthFactor / 2f - itemSize / 2f)\n                        },\n                    )\n                }\n\n            LazyColumn(\n                state = lazylistState,\n                contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n            ) {\n                item {\n                    ChipsRow(\n                        chips = homePage?.chips?.map { it to it.title } ?: emptyList(),\n                        currentValue = selectedChip,\n                        onValueUpdate = {\n                            viewModel.toggleChip(it)\n                        },\n                    )\n                }\n\n                if (isLoading && homePage?.chips.isNullOrEmpty()) {\n                    item(key = \"chips_shimmer\") {\n                        ShimmerHost(showGradient = false) {\n                            LazyRow(\n                                contentPadding =\n                                    WindowInsets.systemBars\n                                        .only(WindowInsetsSides.Horizontal)\n                                        .asPaddingValues(),\n                                horizontalArrangement = Arrangement.spacedBy(8.dp),\n                                modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),\n                            ) {\n                                items(5) {\n                                    TextPlaceholder(\n                                        height = 30.dp,\n                                        shape = RoundedCornerShape(16.dp),\n                                        modifier = Modifier.width(72.dp),\n                                    )\n                                }\n                            }\n                        }\n                    }\n                }\n\n                // Show podcast sections FIRST when podcast chip is selected (fixed at top)\n                if (selectedChip?.title?.contains(\"Podcast\", ignoreCase = true) == true) {\n                    // Show \"Your Shows\" section from official API\n                    if (savedPodcastShows.isNotEmpty()) {\n                        item(key = \"00_your_shows_title\") {\n                            NavigationTitle(\n                                title = stringResource(R.string.your_shows),\n                                onClick = {\n                                    navController.navigate(\"youtube_browse/FEmusic_library_non_music_audio_list\")\n                                },\n                            )\n                        }\n\n                        item(key = \"00_your_shows_list\") {\n                            LazyRow(\n                                contentPadding =\n                                    WindowInsets.systemBars\n                                        .only(WindowInsetsSides.Horizontal)\n                                        .asPaddingValues(),\n                            ) {\n                                items(savedPodcastShows) { podcast ->\n                                    ytGridItem(podcast)\n                                }\n                            }\n                        }\n                    }\n\n                    // Show \"Episodes for Later\" section from official API\n                    if (episodesForLater.isNotEmpty()) {\n                        item(key = \"00_episodes_for_later_title\") {\n                            NavigationTitle(\n                                title = stringResource(R.string.episodes_for_later),\n                                onClick = {\n                                    navController.navigate(\"online_playlist/SE\")\n                                },\n                            )\n                        }\n\n                        item(key = \"00_episodes_for_later_list\") {\n                            LazyRow(\n                                contentPadding =\n                                    WindowInsets.systemBars\n                                        .only(WindowInsetsSides.Horizontal)\n                                        .asPaddingValues(),\n                            ) {\n                                items(episodesForLater) { episode ->\n                                    ytGridItem(episode)\n                                }\n                            }\n                        }\n                    }\n\n                    // Show Podcast Channels row if we have any (extracted from episodes)\n                    // Only show if \"Your Shows\" from official API is empty (to avoid duplicates)\n                    if (featuredPodcasts.isNotEmpty() && savedPodcastShows.isEmpty()) {\n                        item(key = \"0_podcast_channels_title\") {\n                            NavigationTitle(\n                                title = stringResource(R.string.podcast_channels),\n                            )\n                        }\n\n                        item(key = \"0_podcast_channels_list\") {\n                            LazyRow(\n                                contentPadding =\n                                    WindowInsets.systemBars\n                                        .only(WindowInsetsSides.Horizontal)\n                                        .asPaddingValues(),\n                            ) {\n                                items(featuredPodcasts) { podcast ->\n                                    ytGridItem(podcast)\n                                }\n                            }\n                        }\n                    }\n\n                    // Add \"Latest Episodes\" header before episode sections (if we have any sections)\n                    if (homeSections.filterIsInstance<HomeSection.HomePageSection>().isNotEmpty()) {\n                        item(key = \"0_latest_episodes_title\") {\n                            NavigationTitle(\n                                title = stringResource(R.string.latest_episodes),\n                            )\n                        }\n                    }\n\n                    // Render the regular sections from the chip (episodes grouped by category)\n                    // Use key prefix \"1_\" to ensure episodes sort after channels \"0_\"\n                    // Skip sections that duplicate official API sections (Your Shows, Episodes for Later)\n                    homeSections.filterIsInstance<HomeSection.HomePageSection>().forEach { section ->\n                        val sectionData = homePage?.sections?.getOrNull(section.index)\n                        // Skip if this section duplicates an official API section\n                        val skipTitles = listOf(\"your shows\", \"episodes for later\", \"podcast channels\", \"new episodes\")\n                        if (sectionData?.title?.lowercase()?.let { title -> skipTitles.any { title.contains(it) } } == true) {\n                            return@forEach\n                        }\n                        sectionData?.let {\n                            item(key = \"1_chip_section_title_${section.index}\") {\n                                NavigationTitle(\n                                    title = sectionData.title,\n                                    label = sectionData.label,\n                                    thumbnail =\n                                        sectionData.thumbnail?.let { thumbnailUrl ->\n                                            {\n                                                val shape =\n                                                    if (sectionData.endpoint?.isArtistEndpoint == true) {\n                                                        CircleShape\n                                                    } else {\n                                                        RoundedCornerShape(\n                                                            ThumbnailCornerRadius,\n                                                        )\n                                                    }\n                                                AsyncImage(\n                                                    model = thumbnailUrl,\n                                                    contentDescription = null,\n                                                    modifier =\n                                                        Modifier\n                                                            .size(ListThumbnailSize)\n                                                            .clip(shape),\n                                                )\n                                            }\n                                        },\n                                    onClick =\n                                        sectionData.endpoint?.let { endpoint ->\n                                            {\n                                                when {\n                                                    endpoint.browseId == \"FEmusic_moods_and_genres\" -> {\n                                                        navController.navigate(\"mood_and_genres\")\n                                                    }\n\n                                                    endpoint.params != null -> {\n                                                        navController.navigate(\n                                                            \"youtube_browse/${endpoint.browseId}?params=${endpoint.params}\",\n                                                        )\n                                                    }\n\n                                                    else -> {\n                                                        navController.navigate(\"browse/${endpoint.browseId}\")\n                                                    }\n                                                }\n                                            }\n                                        },\n                                    modifier = Modifier.animateItem(),\n                                )\n                            }\n\n                            item(key = \"1_chip_section_list_${section.index}\") {\n                                LazyRow(\n                                    contentPadding =\n                                        WindowInsets.systemBars\n                                            .only(WindowInsetsSides.Horizontal)\n                                            .asPaddingValues(),\n                                ) {\n                                    items(sectionData.items) { item ->\n                                        ytGridItem(item)\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n\n                if (selectedChip == null) {\n                    item(key = \"wrapped_card\") {\n                        AnimatedVisibility(visible = shouldShowWrappedCard) {\n                            Card(\n                                modifier =\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .padding(16.dp),\n                                colors =\n                                    CardDefaults.cardColors(\n                                        containerColor = MaterialTheme.colorScheme.surfaceVariant,\n                                    ),\n                            ) {\n                                Box(\n                                    modifier =\n                                        Modifier\n                                            .fillMaxWidth(),\n                                    contentAlignment = Alignment.Center,\n                                ) {\n                                    if (isWrappedDataReady) {\n                                        val bbhFont =\n                                            try {\n                                                FontFamily(Font(R.font.bbh_bartle_regular))\n                                            } catch (e: Exception) {\n                                                FontFamily.Default\n                                            }\n                                        Column(\n                                            modifier = Modifier.padding(16.dp),\n                                            horizontalAlignment = Alignment.CenterHorizontally,\n                                            verticalArrangement = androidx.compose.foundation.layout.Arrangement.Center,\n                                        ) {\n                                            Text(\n                                                text = stringResource(R.string.wrapped_ready_title),\n                                                style =\n                                                    MaterialTheme.typography.headlineLarge.copy(\n                                                        fontFamily = bbhFont,\n                                                        textAlign = TextAlign.Center,\n                                                    ),\n                                            )\n                                            Spacer(modifier = Modifier.height(8.dp))\n                                            Text(\n                                                text = stringResource(R.string.wrapped_ready_subtitle),\n                                                style =\n                                                    MaterialTheme.typography.bodyLarge.copy(\n                                                        textAlign = TextAlign.Center,\n                                                    ),\n                                            )\n                                            Spacer(modifier = Modifier.height(16.dp))\n                                            Button(onClick = {\n                                                navController.navigate(\"wrapped\")\n                                            }) {\n                                                Text(stringResource(R.string.open))\n                                            }\n                                        }\n                                    } else {\n                                        ContainedLoadingIndicator()\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n\n                homeSections.forEach { section ->\n                    when (section) {\n                        HomeSection.SpeedDial -> {\n                            speedDialItems.takeIf { it.isNotEmpty() }?.let { items ->\n                                item(key = \"speed_dial_title\") {\n                                    NavigationTitle(\n                                        title = stringResource(R.string.speed_dial),\n                                        modifier = Modifier.animateItem(),\n                                    )\n                                }\n\n                                item(key = \"speed_dial_list\") {\n                                    val pagerState = rememberPagerState(pageCount = { (items.size + 8) / 9 })\n                                    val availableWidth = maxWidth - 32.dp\n                                    val itemWidth = availableWidth / 3\n\n                                    Column(\n                                        modifier =\n                                            Modifier\n                                                .fillMaxWidth()\n                                                .animateItem(),\n                                    ) {\n                                        HorizontalPager(\n                                            state = pagerState,\n                                            contentPadding = PaddingValues(horizontal = 16.dp),\n                                            pageSpacing = 16.dp,\n                                            modifier =\n                                                Modifier\n                                                    .fillMaxWidth()\n                                                    .height(itemWidth * 3),\n                                        ) { page ->\n                                            val pageStartIndex = page * 9\n                                            val pageItems = items.drop(pageStartIndex).take(9)\n\n                                            Column(modifier = Modifier.fillMaxSize()) {\n                                                for (row in 0 until 3) {\n                                                    Row(modifier = Modifier.fillMaxWidth()) {\n                                                        for (col in 0 until 3) {\n                                                            val itemIndex = row * 3 + col\n\n                                                            val isRandomizeSlot = (page == 0 && itemIndex == 8)\n\n                                                            if (isRandomizeSlot) {\n                                                                Box(\n                                                                    modifier =\n                                                                        Modifier\n                                                                            .width(itemWidth)\n                                                                            .height(itemWidth)\n                                                                            .padding(4.dp),\n                                                                ) {\n                                                                    RandomizeGridItem(\n                                                                        isLoading = isRandomizing,\n                                                                        onClick = {\n                                                                            if (isRandomizing) {\n                                                                                randomizeJob?.cancel()\n                                                                            } else if (!isListenTogetherGuest) {\n                                                                                randomizeJob =\n                                                                                    scope.launch {\n                                                                                        val randomItem = viewModel.getRandomItem()\n                                                                                        if (randomItem != null) {\n                                                                                            when (randomItem) {\n                                                                                                is SongItem -> {\n                                                                                                    playerConnection.playQueue(\n                                                                                                        YouTubeQueue(\n                                                                                                            randomItem.endpoint\n                                                                                                                ?: WatchEndpoint(\n                                                                                                                    videoId = randomItem.id,\n                                                                                                                ),\n                                                                                                            randomItem.toMediaMetadata(),\n                                                                                                        ),\n                                                                                                    )\n                                                                                                }\n\n                                                                                                is AlbumItem -> {\n                                                                                                    navController.navigate(\n                                                                                                        \"album/${randomItem.id}\",\n                                                                                                    )\n                                                                                                }\n\n                                                                                                is ArtistItem -> {\n                                                                                                    navController.navigate(\n                                                                                                        \"artist/${randomItem.id}\",\n                                                                                                    )\n                                                                                                }\n\n                                                                                                is PlaylistItem -> {\n                                                                                                    navController.navigate(\n                                                                                                        \"online_playlist/${randomItem.id}\",\n                                                                                                    )\n                                                                                                }\n\n                                                                                                is PodcastItem -> {\n                                                                                                    navController.navigate(\n                                                                                                        \"online_podcast/${randomItem.id}\",\n                                                                                                    )\n                                                                                                }\n\n                                                                                                is EpisodeItem -> {\n                                                                                                    playerConnection.playQueue(\n                                                                                                        ListQueue(\n                                                                                                            title = randomItem.title,\n                                                                                                            items =\n                                                                                                                listOf(\n                                                                                                                    randomItem\n                                                                                                                        .toMediaMetadata()\n                                                                                                                        .toMediaItem(),\n                                                                                                                ),\n                                                                                                        ),\n                                                                                                    )\n                                                                                                }\n                                                                                            }\n                                                                                        }\n                                                                                    }\n                                                                            }\n                                                                        },\n                                                                    )\n                                                                }\n                                                            } else if (itemIndex < pageItems.size) {\n                                                                val item = pageItems[itemIndex]\n                                                                val isPinned by database.speedDialDao\n                                                                    .isPinned(\n                                                                        item.id,\n                                                                    ).collectAsState(initial = false)\n\n                                                                Box(\n                                                                    modifier =\n                                                                        Modifier\n                                                                            .width(itemWidth)\n                                                                            .height(itemWidth)\n                                                                            .padding(4.dp),\n                                                                ) {\n                                                                    SpeedDialGridItem(\n                                                                        item = item,\n                                                                        isPinned = isPinned,\n                                                                        isActive =\n                                                                            item.id in listOf(mediaMetadata?.album?.id, mediaMetadata?.id),\n                                                                        isPlaying = isPlaying,\n                                                                        modifier =\n                                                                            Modifier\n                                                                                .fillMaxSize()\n                                                                                .combinedClickable(\n                                                                                    onClick = {\n                                                                                        when (item) {\n                                                                                            is SongItem -> {\n                                                                                                if (!isListenTogetherGuest) {\n                                                                                                    playerConnection.playQueue(\n                                                                                                        YouTubeQueue(\n                                                                                                            item.endpoint\n                                                                                                                ?: WatchEndpoint(\n                                                                                                                    videoId = item.id,\n                                                                                                                ),\n                                                                                                            item.toMediaMetadata(),\n                                                                                                        ),\n                                                                                                    )\n                                                                                                }\n                                                                                            }\n\n                                                                                            is AlbumItem -> {\n                                                                                                navController.navigate(\"album/${item.id}\")\n                                                                                            }\n\n                                                                                            is ArtistItem -> {\n                                                                                                navController.navigate(\"artist/${item.id}\")\n                                                                                            }\n\n                                                                                            is PlaylistItem -> {\n                                                                                                val rawType =\n                                                                                                    pinnedSpeedDialItems\n                                                                                                        .find {\n                                                                                                            it.id ==\n                                                                                                                item.id\n                                                                                                        }?.type\n                                                                                                if (rawType == \"LOCAL_PLAYLIST\") {\n                                                                                                    navController.navigate(\n                                                                                                        \"local_playlist/${item.id}\",\n                                                                                                    )\n                                                                                                } else {\n                                                                                                    navController.navigate(\n                                                                                                        \"online_playlist/${item.id}\",\n                                                                                                    )\n                                                                                                }\n                                                                                            }\n\n                                                                                            is PodcastItem -> {\n                                                                                                navController.navigate(\n                                                                                                    \"online_podcast/${item.id}\",\n                                                                                                )\n                                                                                            }\n\n                                                                                            is EpisodeItem -> {\n                                                                                                if (!isListenTogetherGuest) {\n                                                                                                    playerConnection.playQueue(\n                                                                                                        ListQueue(\n                                                                                                            title = item.title,\n                                                                                                            items =\n                                                                                                                listOf(\n                                                                                                                    item\n                                                                                                                        .toMediaMetadata()\n                                                                                                                        .toMediaItem(),\n                                                                                                                ),\n                                                                                                        ),\n                                                                                                    )\n                                                                                                }\n                                                                                            }\n                                                                                        }\n                                                                                    },\n                                                                                    onLongClick = {\n                                                                                        haptic.performHapticFeedback(\n                                                                                            HapticFeedbackType.LongPress,\n                                                                                        )\n                                                                                        menuState.show {\n                                                                                            when (item) {\n                                                                                                is SongItem -> {\n                                                                                                    YouTubeSongMenu(\n                                                                                                        song = item,\n                                                                                                        navController = navController,\n                                                                                                        onDismiss = menuState::dismiss,\n                                                                                                    )\n                                                                                                }\n\n                                                                                                is AlbumItem -> {\n                                                                                                    YouTubeAlbumMenu(\n                                                                                                        albumItem = item,\n                                                                                                        navController = navController,\n                                                                                                        onDismiss = menuState::dismiss,\n                                                                                                    )\n                                                                                                }\n\n                                                                                                is ArtistItem -> {\n                                                                                                    YouTubeArtistMenu(\n                                                                                                        artist = item,\n                                                                                                        onDismiss = menuState::dismiss,\n                                                                                                    )\n                                                                                                }\n\n                                                                                                is PlaylistItem -> {\n                                                                                                    YouTubePlaylistMenu(\n                                                                                                        playlist = item,\n                                                                                                        coroutineScope = scope,\n                                                                                                        onDismiss = menuState::dismiss,\n                                                                                                    )\n                                                                                                }\n\n                                                                                                is PodcastItem -> {\n                                                                                                    YouTubePlaylistMenu(\n                                                                                                        playlist = item.asPlaylistItem(),\n                                                                                                        coroutineScope = scope,\n                                                                                                        onDismiss = menuState::dismiss,\n                                                                                                    )\n                                                                                                }\n\n                                                                                                is EpisodeItem -> {\n                                                                                                    YouTubeSongMenu(\n                                                                                                        song = item.asSongItem(),\n                                                                                                        navController = navController,\n                                                                                                        onDismiss = menuState::dismiss,\n                                                                                                    )\n                                                                                                }\n                                                                                            }\n                                                                                        }\n                                                                                    },\n                                                                                ),\n                                                                    )\n                                                                }\n                                                            } else {\n                                                                Spacer(modifier = Modifier.width(itemWidth))\n                                                            }\n                                                        }\n                                                    }\n                                                }\n                                            }\n                                        }\n\n                                        if (pagerState.pageCount > 1) {\n                                            Row(\n                                                modifier =\n                                                    Modifier\n                                                        .height(24.dp)\n                                                        .fillMaxWidth(),\n                                                horizontalArrangement = androidx.compose.foundation.layout.Arrangement.Center,\n                                                verticalAlignment = Alignment.CenterVertically,\n                                            ) {\n                                                repeat(pagerState.pageCount) { iteration ->\n                                                    val color =\n                                                        if (pagerState.currentPage == iteration) {\n                                                            MaterialTheme.colorScheme.primary\n                                                        } else {\n                                                            MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)\n                                                        }\n                                                    Box(\n                                                        modifier =\n                                                            Modifier\n                                                                .padding(4.dp)\n                                                                .clip(CircleShape)\n                                                                .background(color)\n                                                                .size(8.dp),\n                                                    )\n                                                }\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                        }\n\n                        HomeSection.QuickPicks -> {\n                            quickPicks?.takeIf { it.isNotEmpty() }?.let { quickPicks ->\n                                item(key = \"quick_picks_title\") {\n                                    val quickPicksTitle = stringResource(R.string.quick_picks)\n                                    NavigationTitle(\n                                        title = quickPicksTitle,\n                                        modifier = Modifier.animateItem(),\n                                        onPlayAllClick =\n                                            if (!isListenTogetherGuest) {\n                                                {\n                                                    playerConnection.playQueue(\n                                                        ListQueue(\n                                                            title = quickPicksTitle,\n                                                            items = quickPicks.distinctBy { it.id }.map { it.toMediaItem() },\n                                                        ),\n                                                    )\n                                                }\n                                            } else {\n                                                null\n                                            },\n                                    )\n                                }\n\n                                item(key = \"quick_picks_list\") {\n                                    LazyHorizontalGrid(\n                                        state = quickPicksLazyGridState,\n                                        rows = GridCells.Fixed(4),\n                                        flingBehavior = rememberSnapFlingBehavior(quickPicksSnapLayoutInfoProvider),\n                                        contentPadding =\n                                            WindowInsets.systemBars\n                                                .only(WindowInsetsSides.Horizontal)\n                                                .asPaddingValues(),\n                                        modifier =\n                                            Modifier\n                                                .fillMaxWidth()\n                                                .height(ListItemHeight * 4)\n                                                .animateItem(),\n                                    ) {\n                                        items(\n                                            items = quickPicks.distinctBy { it.id },\n                                            key = { it.id },\n                                        ) { originalSong ->\n                                            // fetch song from database to keep updated\n                                            val song by database\n                                                .song(originalSong.id)\n                                                .collectAsState(initial = originalSong)\n\n                                            SongListItem(\n                                                song = song!!,\n                                                showInLibraryIcon = true,\n                                                isActive = song!!.id == mediaMetadata?.id,\n                                                isPlaying = isPlaying,\n                                                isSwipeable = false,\n                                                trailingContent = {\n                                                    IconButton(\n                                                        onClick = {\n                                                            menuState.show {\n                                                                SongMenu(\n                                                                    originalSong = song!!,\n                                                                    navController = navController,\n                                                                    onDismiss = menuState::dismiss,\n                                                                )\n                                                            }\n                                                        },\n                                                    ) {\n                                                        Icon(\n                                                            painter = painterResource(R.drawable.more_vert),\n                                                            contentDescription = null,\n                                                        )\n                                                    }\n                                                },\n                                                modifier =\n                                                    Modifier\n                                                        .width(horizontalLazyGridItemWidth)\n                                                        .combinedClickable(\n                                                            onClick = {\n                                                                if (!isListenTogetherGuest) {\n                                                                    if (song!!.id == mediaMetadata?.id) {\n                                                                        playerConnection.togglePlayPause()\n                                                                    } else {\n                                                                        playerConnection.playQueue(\n                                                                            YouTubeQueue.radio(\n                                                                                song!!.toMediaMetadata(),\n                                                                            ),\n                                                                        )\n                                                                    }\n                                                                }\n                                                            },\n                                                            onLongClick = {\n                                                                haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                                menuState.show {\n                                                                    SongMenu(\n                                                                        originalSong = song!!,\n                                                                        navController = navController,\n                                                                        onDismiss = menuState::dismiss,\n                                                                    )\n                                                                }\n                                                            },\n                                                        ),\n                                            )\n                                        }\n                                    }\n                                }\n                            }\n                        }\n\n                        HomeSection.FromTheCommunity -> {\n                            communityPlaylists?.takeIf { it.isNotEmpty() }?.let { playlists ->\n                                item(key = \"community_playlists_title\") {\n                                    NavigationTitle(\n                                        title = stringResource(R.string.from_the_community),\n                                        modifier = Modifier.animateItem(),\n                                    )\n                                }\n\n                                item(key = \"community_playlists_content\") {\n                                    LazyRow(\n                                        contentPadding = PaddingValues(horizontal = 16.dp),\n                                        horizontalArrangement = Arrangement.spacedBy(16.dp),\n                                        modifier = Modifier.animateItem(),\n                                    ) {\n                                        items(playlists) { item ->\n                                            CommunityPlaylistCard(\n                                                item = item,\n                                                onClick = {\n                                                    navController.navigate(\"online_playlist/${item.playlist.id.removePrefix(\"VL\")}\")\n                                                },\n                                                onSongClick = { song ->\n                                                    if (!isListenTogetherGuest) {\n                                                        playerConnection.playQueue(\n                                                            YouTubeQueue(\n                                                                song.endpoint ?: WatchEndpoint(videoId = song.id),\n                                                                song.toMediaMetadata(),\n                                                            ),\n                                                        )\n                                                    }\n                                                },\n                                            )\n                                        }\n                                    }\n                                }\n                            }\n                        }\n\n                        HomeSection.DailyDiscover -> {\n                            dailyDiscover?.takeIf { it.isNotEmpty() }?.let { discoverList ->\n                                item(key = \"daily_discover_content\") {\n                                    Box(\n                                        modifier =\n                                            Modifier\n                                                .fillMaxWidth()\n                                                .height(340.dp)\n                                                .padding(horizontal = 16.dp),\n                                        contentAlignment = Alignment.Center,\n                                    ) {\n                                        val carouselState = rememberCarouselState { discoverList.size }\n                                        HorizontalMultiBrowseCarousel(\n                                            state = carouselState,\n                                            preferredItemWidth = 320.dp,\n                                            itemSpacing = 16.dp,\n                                            modifier =\n                                                Modifier\n                                                    .fillMaxWidth()\n                                                    .height(320.dp),\n                                        ) { i ->\n                                            val item = discoverList[i]\n                                            DailyDiscoverCard(\n                                                dailyDiscover = item,\n                                                onClick = {\n                                                    if (!isListenTogetherGuest) {\n                                                        val song = item.recommendation as? SongItem\n                                                        val mediaMetadata = song?.toMediaMetadata()\n                                                        if (mediaMetadata != null) {\n                                                            playerConnection.playQueue(\n                                                                YouTubeQueue(\n                                                                    song.endpoint ?: WatchEndpoint(videoId = song.id),\n                                                                    mediaMetadata,\n                                                                ),\n                                                            )\n                                                        }\n                                                    }\n                                                },\n                                                navController = navController,\n                                                modifier = Modifier.maskClip(MaterialTheme.shapes.extraLarge),\n                                            )\n                                        }\n                                    }\n                                }\n                            }\n                        }\n\n                        HomeSection.KeepListening -> {\n                            keepListening?.takeIf { it.isNotEmpty() }?.let { keepListening ->\n                                item(key = \"keep_listening_title\") {\n                                    NavigationTitle(\n                                        title = stringResource(R.string.keep_listening),\n                                        modifier = Modifier.animateItem(),\n                                    )\n                                }\n\n                                item(key = \"keep_listening_list\") {\n                                    val rows = if (keepListening.size > 6) 2 else 1\n                                    LazyHorizontalGrid(\n                                        state = rememberLazyGridState(),\n                                        rows = GridCells.Fixed(rows),\n                                        contentPadding =\n                                            WindowInsets.systemBars\n                                                .only(WindowInsetsSides.Horizontal)\n                                                .asPaddingValues(),\n                                        modifier =\n                                            Modifier\n                                                .fillMaxWidth()\n                                                .height(\n                                                    (\n                                                        currentGridHeight +\n                                                            with(LocalDensity.current) {\n                                                                MaterialTheme.typography.bodyLarge.lineHeight\n                                                                    .toDp() * 2 +\n                                                                    MaterialTheme.typography.bodyMedium.lineHeight\n                                                                        .toDp() * 2\n                                                            }\n                                                    ) * rows,\n                                                ).animateItem(),\n                                    ) {\n                                        items(keepListening) {\n                                            localGridItem(it)\n                                        }\n                                    }\n                                }\n                            }\n                        }\n\n                        HomeSection.AccountPlaylists -> {\n                            accountPlaylists?.takeIf { it.isNotEmpty() }?.let { accountPlaylists ->\n                                item(key = \"account_playlists_title\") {\n                                    NavigationTitle(\n                                        label = stringResource(R.string.your_youtube_playlists),\n                                        title = accountName,\n                                        thumbnail = {\n                                            if (url != null) {\n                                                AsyncImage(\n                                                    model =\n                                                        ImageRequest\n                                                            .Builder(LocalContext.current)\n                                                            .data(url)\n                                                            .diskCachePolicy(CachePolicy.ENABLED)\n                                                            .diskCacheKey(url)\n                                                            .crossfade(false)\n                                                            .build(),\n                                                    placeholder = painterResource(id = R.drawable.person),\n                                                    error = painterResource(id = R.drawable.person),\n                                                    contentDescription = null,\n                                                    contentScale = ContentScale.Crop,\n                                                    modifier =\n                                                        Modifier\n                                                            .size(ListThumbnailSize)\n                                                            .clip(CircleShape),\n                                                )\n                                            } else {\n                                                Icon(\n                                                    painter = painterResource(id = R.drawable.person),\n                                                    contentDescription = null,\n                                                    modifier = Modifier.size(ListThumbnailSize),\n                                                )\n                                            }\n                                        },\n                                        onClick = {\n                                            navController.navigate(\"account\")\n                                        },\n                                        modifier = Modifier.animateItem(),\n                                    )\n                                }\n\n                                item(key = \"account_playlists_list\") {\n                                    LazyRow(\n                                        contentPadding =\n                                            WindowInsets.systemBars\n                                                .only(WindowInsetsSides.Horizontal)\n                                                .asPaddingValues(),\n                                        modifier = Modifier.animateItem(),\n                                    ) {\n                                        items(\n                                            items = accountPlaylists.distinctBy { it.id },\n                                            key = { it.id },\n                                        ) { item ->\n                                            ytGridItem(item)\n                                        }\n                                    }\n                                }\n                            }\n                        }\n\n                        HomeSection.ForgottenFavorites -> {\n                            forgottenFavorites?.takeIf { it.isNotEmpty() }?.let { forgottenFavorites ->\n                                item(key = \"forgotten_favorites_title\") {\n                                    val forgottenFavoritesTitle = stringResource(R.string.forgotten_favorites)\n                                    NavigationTitle(\n                                        title = forgottenFavoritesTitle,\n                                        modifier = Modifier.animateItem(),\n                                        onPlayAllClick =\n                                            if (!isListenTogetherGuest) {\n                                                {\n                                                    playerConnection.playQueue(\n                                                        ListQueue(\n                                                            title = forgottenFavoritesTitle,\n                                                            items = forgottenFavorites.distinctBy { it.id }.map { it.toMediaItem() },\n                                                        ),\n                                                    )\n                                                }\n                                            } else {\n                                                null\n                                            },\n                                    )\n                                }\n\n                                item(key = \"forgotten_favorites_list\") {\n                                    // take min in case list size is less than 4\n                                    val rows = min(4, forgottenFavorites.size)\n                                    LazyHorizontalGrid(\n                                        state = forgottenFavoritesLazyGridState,\n                                        rows = GridCells.Fixed(rows),\n                                        contentPadding =\n                                            WindowInsets.systemBars\n                                                .only(WindowInsetsSides.Horizontal)\n                                                .asPaddingValues(),\n                                        flingBehavior =\n                                            rememberSnapFlingBehavior(\n                                                forgottenFavoritesSnapLayoutInfoProvider,\n                                            ),\n                                        modifier =\n                                            Modifier\n                                                .fillMaxWidth()\n                                                .height(ListItemHeight * rows)\n                                                .animateItem(),\n                                    ) {\n                                        items(\n                                            items = forgottenFavorites.distinctBy { it.id },\n                                            key = { it.id },\n                                        ) { originalSong ->\n                                            val song by database\n                                                .song(originalSong.id)\n                                                .collectAsState(initial = originalSong)\n\n                                            SongListItem(\n                                                song = song!!,\n                                                showInLibraryIcon = true,\n                                                isActive = song!!.id == mediaMetadata?.id,\n                                                isPlaying = isPlaying,\n                                                isSwipeable = false,\n                                                trailingContent = {\n                                                    IconButton(\n                                                        onClick = {\n                                                            haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                            menuState.show {\n                                                                SongMenu(\n                                                                    originalSong = song!!,\n                                                                    navController = navController,\n                                                                    onDismiss = menuState::dismiss,\n                                                                )\n                                                            }\n                                                        },\n                                                    ) {\n                                                        Icon(\n                                                            painter = painterResource(R.drawable.more_vert),\n                                                            contentDescription = null,\n                                                        )\n                                                    }\n                                                },\n                                                modifier =\n                                                    Modifier\n                                                        .width(horizontalLazyGridItemWidth)\n                                                        .combinedClickable(\n                                                            onClick = {\n                                                                if (!isListenTogetherGuest) {\n                                                                    if (song!!.id == mediaMetadata?.id) {\n                                                                        playerConnection.togglePlayPause()\n                                                                    } else {\n                                                                        playerConnection.playQueue(\n                                                                            YouTubeQueue.radio(\n                                                                                song!!.toMediaMetadata(),\n                                                                            ),\n                                                                        )\n                                                                    }\n                                                                }\n                                                            },\n                                                            onLongClick = {\n                                                                haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                                menuState.show {\n                                                                    SongMenu(\n                                                                        originalSong = song!!,\n                                                                        navController = navController,\n                                                                        onDismiss = menuState::dismiss,\n                                                                    )\n                                                                }\n                                                            },\n                                                        ),\n                                            )\n                                        }\n                                    }\n                                }\n                            }\n                        }\n\n                        is HomeSection.SimilarRecommendation -> {\n                            val recommendation = similarRecommendations?.getOrNull(section.index)\n                            recommendation?.let {\n                                item(key = \"similar_to_title_${section.index}\") {\n                                    NavigationTitle(\n                                        label = stringResource(R.string.similar_to),\n                                        title = recommendation.title.title,\n                                        thumbnail =\n                                            recommendation.title.thumbnailUrl?.let { thumbnailUrl ->\n                                                {\n                                                    val shape =\n                                                        if (recommendation.title is Artist) {\n                                                            CircleShape\n                                                        } else {\n                                                            RoundedCornerShape(\n                                                                ThumbnailCornerRadius,\n                                                            )\n                                                        }\n                                                    AsyncImage(\n                                                        model = thumbnailUrl,\n                                                        contentDescription = null,\n                                                        modifier =\n                                                            Modifier\n                                                                .size(ListThumbnailSize)\n                                                                .clip(shape),\n                                                    )\n                                                }\n                                            },\n                                        onClick = {\n                                            when (recommendation.title) {\n                                                is Song -> {\n                                                    navController.navigate(\"album/${recommendation.title.album!!.id}\")\n                                                }\n\n                                                is Album -> {\n                                                    navController.navigate(\"album/${recommendation.title.id}\")\n                                                }\n\n                                                is Artist -> {\n                                                    navController.navigate(\"artist/${recommendation.title.id}\")\n                                                }\n\n                                                is Playlist -> {}\n                                            }\n                                        },\n                                        modifier = Modifier.animateItem(),\n                                    )\n                                }\n\n                                item(key = \"similar_to_list_${section.index}\") {\n                                    LazyRow(\n                                        contentPadding =\n                                            WindowInsets.systemBars\n                                                .only(WindowInsetsSides.Horizontal)\n                                                .asPaddingValues(),\n                                        modifier = Modifier.animateItem(),\n                                    ) {\n                                        items(recommendation.items) { item ->\n                                            ytGridItem(item)\n                                        }\n                                    }\n                                }\n                            }\n                        }\n\n                        is HomeSection.HomePageSection -> {\n                            // Skip HomePageSection rendering when podcast chip is selected\n                            // Podcast sections are handled separately with special UI\n                            if (selectedChip?.title?.contains(\"Podcast\", ignoreCase = true) == true) {\n                                return@forEach\n                            }\n                            val sectionData = homePage?.sections?.getOrNull(section.index)\n                            sectionData?.let {\n                                // Check if section contains songs for Play All functionality\n                                val sectionSongs = sectionData.items.filterIsInstance<SongItem>()\n                                val hasPlayableSongs = sectionSongs.isNotEmpty()\n                                // Check if this section contains ONLY songs (like Quick picks, Trending songs)\n                                val isSongsOnlySection =\n                                    sectionData.items.isNotEmpty() &&\n                                        sectionData.items.all { it is SongItem }\n\n                                item(key = \"home_section_title_${section.index}\") {\n                                    NavigationTitle(\n                                        title = sectionData.title,\n                                        label = sectionData.label,\n                                        thumbnail =\n                                            sectionData.thumbnail?.let { thumbnailUrl ->\n                                                {\n                                                    val shape =\n                                                        if (sectionData.endpoint?.isArtistEndpoint == true) {\n                                                            CircleShape\n                                                        } else {\n                                                            RoundedCornerShape(\n                                                                ThumbnailCornerRadius,\n                                                            )\n                                                        }\n                                                    AsyncImage(\n                                                        model = thumbnailUrl,\n                                                        contentDescription = null,\n                                                        modifier =\n                                                            Modifier\n                                                                .size(ListThumbnailSize)\n                                                                .clip(shape),\n                                                    )\n                                                }\n                                            },\n                                        onClick =\n                                            sectionData.endpoint?.let { endpoint ->\n                                                {\n                                                    when {\n                                                        endpoint.browseId == \"FEmusic_moods_and_genres\" -> {\n                                                            navController.navigate(\"mood_and_genres\")\n                                                        }\n\n                                                        // Handle podcast-related browse endpoints\n                                                        endpoint.browseId.startsWith(\"FEmusic_library_non_music_audio\") ||\n                                                            endpoint.browseId.startsWith(\"FEmusic_non_music_audio\") -> {\n                                                            navController.navigate(\"youtube_browse/${endpoint.browseId}\")\n                                                        }\n\n                                                        endpoint.params != null -> {\n                                                            navController.navigate(\n                                                                \"youtube_browse/${endpoint.browseId}?params=${endpoint.params}\",\n                                                            )\n                                                        }\n\n                                                        else -> {\n                                                            navController.navigate(\"browse/${endpoint.browseId}\")\n                                                        }\n                                                    }\n                                                }\n                                            },\n                                        onPlayAllClick =\n                                            if (hasPlayableSongs && !isListenTogetherGuest) {\n                                                {\n                                                    playerConnection.playQueue(\n                                                        ListQueue(\n                                                            title = sectionData.title,\n                                                            items = sectionSongs.map { it.toMediaMetadata().toMediaItem() },\n                                                        ),\n                                                    )\n                                                }\n                                            } else {\n                                                null\n                                            },\n                                        modifier = Modifier.animateItem(),\n                                    )\n                                }\n\n                                if (isSongsOnlySection) {\n                                    // Render songs as a horizontal scrollable list (like Quick picks in YouTube Music)\n                                    item(key = \"home_section_list_${section.index}\") {\n                                        LazyHorizontalGrid(\n                                            state = rememberLazyGridState(),\n                                            rows = GridCells.Fixed(4),\n                                            contentPadding =\n                                                WindowInsets.systemBars\n                                                    .only(WindowInsetsSides.Horizontal)\n                                                    .asPaddingValues(),\n                                            modifier =\n                                                Modifier\n                                                    .fillMaxWidth()\n                                                    .height(ListItemHeight * 4)\n                                                    .animateItem(),\n                                        ) {\n                                            items(\n                                                items = sectionSongs.distinctBy { it.id },\n                                                key = { it.id },\n                                            ) { song ->\n                                                YouTubeListItem(\n                                                    item = song,\n                                                    isActive = song.id == mediaMetadata?.id,\n                                                    isPlaying = isPlaying,\n                                                    isSwipeable = false,\n                                                    trailingContent = {\n                                                        IconButton(\n                                                            onClick = {\n                                                                menuState.show {\n                                                                    YouTubeSongMenu(\n                                                                        song = song,\n                                                                        navController = navController,\n                                                                        onDismiss = menuState::dismiss,\n                                                                    )\n                                                                }\n                                                            },\n                                                        ) {\n                                                            Icon(\n                                                                painter = painterResource(R.drawable.more_vert),\n                                                                contentDescription = null,\n                                                            )\n                                                        }\n                                                    },\n                                                    modifier =\n                                                        Modifier\n                                                            .width(horizontalLazyGridItemWidth)\n                                                            .combinedClickable(\n                                                                onClick = {\n                                                                    when (song) {\n                                                                        is SongItem -> {\n                                                                            if (!isListenTogetherGuest) {\n                                                                                playerConnection.playQueue(\n                                                                                    YouTubeQueue(\n                                                                                        song.endpoint ?: WatchEndpoint(videoId = song.id),\n                                                                                        song.toMediaMetadata(),\n                                                                                    ),\n                                                                                )\n                                                                            }\n                                                                        }\n\n                                                                        // TODO: this will trigger an error in future kotlin releases, make sure it doesnt \n\n                                                                        //is AlbumItem -> {\n                                                                        //    navController.navigate(\"album/${song.id}\")\n                                                                        //}\n\n                                                                        //is ArtistItem -> {\n                                                                        //    navController.navigate(\"artist/${song.id}\")\n                                                                        //}\n\n                                                                        //is PlaylistItem -> {\n                                                                        //    navController.navigate(\n                                                                        //        \"online_playlist/${song.id.removePrefix(\"VL\")}\",\n                                                                        //    )\n                                                                        //}\n\n                                                                        //is PodcastItem -> {\n                                                                        //    navController.navigate(\"online_podcast/${song.id}\")\n                                                                        //}\n\n                                                                        //is EpisodeItem -> {\n                                                                        //    if (!isListenTogetherGuest) {\n                                                                        //        playerConnection.playQueue(\n                                                                        //            ListQueue(\n                                                                        //                title = song.title,\n                                                                        //                items =\n                                                                        //                    listOf(\n                                                                        //                        (song as EpisodeItem)\n                                                                        //                            .toMediaMetadata()\n                                                                        //                            .toMediaItem(),\n                                                                        //                    ),\n                                                                        //            ),\n                                                                        //        )\n                                                                        //    }\n                                                                        //}\n                                                                    }\n                                                                },\n                                                                onLongClick = {\n                                                                    haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                                    menuState.show {\n                                                                        YouTubeSongMenu(\n                                                                            song = song,\n                                                                            navController = navController,\n                                                                            onDismiss = menuState::dismiss,\n                                                                        )\n                                                                    }\n                                                                },\n                                                            ),\n                                                )\n                                            }\n                                        }\n                                    }\n                                } else {\n                                    // Render mixed content as horizontal grid items (albums, playlists, artists, etc.)\n                                    item(key = \"home_section_list_${section.index}\") {\n                                        LazyRow(\n                                            contentPadding =\n                                                WindowInsets.systemBars\n                                                    .only(WindowInsetsSides.Horizontal)\n                                                    .asPaddingValues(),\n                                            modifier = Modifier.animateItem(),\n                                        ) {\n                                            items(sectionData.items) { item ->\n                                                ytGridItem(item)\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                        }\n\n                        HomeSection.MoodAndGenres -> {\n                            // Skip MoodAndGenres when podcast chip is selected\n                            if (selectedChip?.title?.contains(\"Podcast\", ignoreCase = true) == true) {\n                                return@forEach\n                            }\n                            explorePage?.moodAndGenres?.let { moodAndGenres ->\n                                item(key = \"mood_and_genres_title\") {\n                                    NavigationTitle(\n                                        title = stringResource(R.string.mood_and_genres),\n                                        onClick = {\n                                            navController.navigate(\"mood_and_genres\")\n                                        },\n                                        modifier = Modifier.animateItem(),\n                                    )\n                                }\n                                item(key = \"mood_and_genres_list\") {\n                                    LazyHorizontalGrid(\n                                        rows = GridCells.Fixed(4),\n                                        contentPadding = PaddingValues(6.dp),\n                                        modifier =\n                                            Modifier\n                                                .height((MoodAndGenresButtonHeight + 12.dp) * 4 + 12.dp)\n                                                .animateItem(),\n                                    ) {\n                                        items(moodAndGenres) {\n                                            MoodAndGenresButton(\n                                                title = it.title,\n                                                onClick = {\n                                                    navController.navigate(\n                                                        \"youtube_browse/${it.endpoint.browseId}?params=${it.endpoint.params}\",\n                                                    )\n                                                },\n                                                modifier =\n                                                    Modifier\n                                                        .padding(6.dp)\n                                                        .width(180.dp),\n                                            )\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n\n                // Only show shimmer during initial loading, not for pagination\n                if (isLoading && homePage?.sections.isNullOrEmpty()) {\n                    item(key = \"loading_shimmer\") {\n                        ShimmerHost(\n                            modifier = Modifier.animateItem(),\n                        ) {\n                            repeat(2) {\n                                TextPlaceholder(\n                                    height = 36.dp,\n                                    modifier =\n                                        Modifier\n                                            .padding(12.dp)\n                                            .width(250.dp),\n                                )\n                                LazyRow(\n                                    contentPadding =\n                                        WindowInsets.systemBars\n                                            .only(WindowInsetsSides.Horizontal)\n                                            .asPaddingValues(),\n                                ) {\n                                    items(4) {\n                                        GridItemPlaceHolder()\n                                    }\n                                }\n                            }\n\n                            TextPlaceholder(\n                                height = 36.dp,\n                                modifier =\n                                    Modifier\n                                        .padding(vertical = 12.dp, horizontal = 12.dp)\n                                        .width(250.dp),\n                            )\n                            repeat(4) {\n                                Row {\n                                    repeat(2) {\n                                        TextPlaceholder(\n                                            height = MoodAndGenresButtonHeight,\n                                            shape = RoundedCornerShape(6.dp),\n                                            modifier =\n                                                Modifier\n                                                    .padding(horizontal = 12.dp)\n                                                    .width(200.dp),\n                                        )\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n\n            HideOnScrollFAB(\n                visible = allLocalItems.isNotEmpty() || allYtItems.isNotEmpty(),\n                lazyListState = lazylistState,\n                icon = R.drawable.shuffle,\n                onClick = {\n                    if (!isListenTogetherGuest) {\n                        val local =\n                            when {\n                                allLocalItems.isNotEmpty() && allYtItems.isNotEmpty() -> Random.nextFloat() < 0.5\n                                allLocalItems.isNotEmpty() -> true\n                                else -> false\n                            }\n                        scope.launch(Dispatchers.Main) {\n                            if (local) {\n                                when (val luckyItem = allLocalItems.random()) {\n                                    is Song -> {\n                                        playerConnection.playQueue(YouTubeQueue.radio(luckyItem.toMediaMetadata()))\n                                    }\n\n                                    is Album -> {\n                                        val albumWithSongs =\n                                            withContext(Dispatchers.IO) {\n                                                database.albumWithSongs(luckyItem.id).first()\n                                            }\n                                        albumWithSongs?.let {\n                                            playerConnection.playQueue(LocalAlbumRadio(it))\n                                        }\n                                    }\n\n                                    is Artist -> {}\n\n                                    is Playlist -> {}\n                                }\n                            } else {\n                                when (val luckyItem = allYtItems.random()) {\n                                    is SongItem -> {\n                                        playerConnection.playQueue(YouTubeQueue.radio(luckyItem.toMediaMetadata()))\n                                    }\n\n                                    is AlbumItem -> {\n                                        playerConnection.playQueue(YouTubeAlbumRadio(luckyItem.playlistId))\n                                    }\n\n                                    is ArtistItem -> {\n                                        luckyItem.radioEndpoint?.let {\n                                            playerConnection.playQueue(YouTubeQueue(it))\n                                        }\n                                    }\n\n                                    is PlaylistItem -> {\n                                        luckyItem.playEndpoint?.let {\n                                            playerConnection.playQueue(YouTubeQueue(it))\n                                        }\n                                    }\n\n                                    is PodcastItem -> {\n                                        luckyItem.playEndpoint?.let {\n                                            playerConnection.playQueue(YouTubeQueue(it))\n                                        }\n                                    }\n\n                                    is EpisodeItem -> {\n                                        playerConnection.playQueue(\n                                            ListQueue(\n                                                title = luckyItem.title,\n                                                items = listOf(luckyItem.toMediaMetadata().toMediaItem()),\n                                            ),\n                                        )\n                                    }\n                                }\n                            }\n                        }\n                    }\n                },\n                onRecognitionClick = {\n                    navController.navigate(\"recognition\")\n                },\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/ListenTogetherScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens\n\nimport android.content.Context\nimport android.widget.Toast\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.animateContentSize\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.animation.core.spring\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.slideInVertically\nimport androidx.compose.animation.slideOutVertically\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.horizontalScroll\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.imePadding\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.relocation.BringIntoViewRequester\nimport androidx.compose.foundation.relocation.bringIntoViewRequester\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LinearProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedButton\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.OutlinedTextFieldDefaults\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.navigation.NavController\nimport androidx.navigation.compose.currentBackStackEntryAsState\nimport com.metrolist.music.LocalListenTogetherManager\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.AppBarHeight\nimport com.metrolist.music.constants.ListenTogetherInTopBarKey\nimport com.metrolist.music.constants.ListenTogetherUsernameKey\nimport com.metrolist.music.listentogether.ConnectionState\nimport com.metrolist.music.listentogether.JoinRequestPayload\nimport com.metrolist.music.listentogether.ListenTogetherEvent\nimport com.metrolist.music.listentogether.RoomRole\nimport com.metrolist.music.listentogether.SuggestionReceivedPayload\nimport com.metrolist.music.listentogether.UserInfo\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.rememberPreference\nimport kotlinx.coroutines.launch\nimport androidx.compose.material3.IconButton as MaterialIconButton\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)\n@Composable\nfun ListenTogetherScreen(\n    navController: NavController,\n    showTopBar: Boolean = false,\n) {\n    val context = LocalContext.current\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val windowInsets = LocalPlayerAwareWindowInsets.current\n    val joiningRoomTemplate = stringResource(R.string.joining_room)\n\n    if (listenTogetherManager == null) {\n        NotConfiguredContent()\n        return\n    }\n\n    val connectionState by listenTogetherManager.connectionState.collectAsState()\n    val roomState by listenTogetherManager.roomState.collectAsState()\n    val userId by listenTogetherManager.userId.collectAsState()\n    val pendingJoinRequests by listenTogetherManager.pendingJoinRequests.collectAsState()\n    val pendingSuggestions by listenTogetherManager.pendingSuggestions.collectAsState()\n\n    val (listenTogetherInTopBar) = rememberPreference(ListenTogetherInTopBarKey, defaultValue = true)\n    val shouldShowTopBar = showTopBar || listenTogetherInTopBar\n\n    var savedUsername by rememberPreference(ListenTogetherUsernameKey, \"\")\n    var roomCodeInput by rememberSaveable { mutableStateOf(\"\") }\n    var usernameInput by rememberSaveable { mutableStateOf(savedUsername) }\n\n    var isCreatingRoom by rememberSaveable { mutableStateOf(false) }\n    var isJoiningRoom by rememberSaveable { mutableStateOf(false) }\n    var joinErrorMessage by rememberSaveable { mutableStateOf<String?>(null) }\n\n    var selectedUserForMenu by rememberSaveable { mutableStateOf<String?>(null) }\n    var selectedUsername by rememberSaveable { mutableStateOf<String?>(null) }\n\n    val waitingForApprovalText = stringResource(R.string.waiting_for_approval)\n    val invalidRoomCodeText = stringResource(R.string.invalid_room_code)\n    val joinRequestDeniedText = stringResource(R.string.join_request_denied)\n\n    LaunchedEffect(savedUsername) {\n        if (usernameInput.isBlank() && savedUsername.isNotBlank()) {\n            usernameInput = savedUsername\n        }\n    }\n\n    LaunchedEffect(listenTogetherManager) {\n        listenTogetherManager.events.collect { event ->\n            when (event) {\n                is ListenTogetherEvent.JoinRejected -> {\n                    val reason = event.reason\n                    joinErrorMessage =\n                        when {\n                            reason.isNullOrBlank() -> joinRequestDeniedText\n                            reason.contains(\"invalid\", ignoreCase = true) -> invalidRoomCodeText\n                            else -> \"$joinRequestDeniedText: $reason\"\n                        }\n                    isJoiningRoom = false\n                    isCreatingRoom = false\n                }\n\n                is ListenTogetherEvent.JoinApproved -> {\n                    isJoiningRoom = false\n                    joinErrorMessage = null\n                }\n\n                is ListenTogetherEvent.RoomCreated -> {\n                    isCreatingRoom = false\n                    val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager\n                    val clip = android.content.ClipData.newPlainText(\"ListenTogetherRoom\", event.roomCode)\n                    clipboard.setPrimaryClip(clip)\n                }\n\n                else -> {}\n            }\n        }\n    }\n\n    val isInRoom = listenTogetherManager.isInRoom\n    val isHost = roomState?.hostId == userId\n\n    // User action menu dialog\n    if (selectedUserForMenu != null && selectedUsername != null) {\n        UserActionDialog(\n            username = selectedUsername ?: \"\",\n            onKick = {\n                selectedUserForMenu?.let {\n                    listenTogetherManager.kickUser(it, \"Removed by host\")\n                }\n                selectedUserForMenu = null\n                selectedUsername = null\n            },\n            onPermanentKick = {\n                selectedUserForMenu?.let { userId ->\n                    selectedUsername?.let { username ->\n                        listenTogetherManager.blockUser(username)\n                        listenTogetherManager.kickUser(userId, R.string.user_blocked_by_host.toString())\n                    }\n                }\n                selectedUserForMenu = null\n                selectedUsername = null\n            },\n            onTransferOwnership = {\n                selectedUserForMenu?.let {\n                    listenTogetherManager.transferHost(it)\n                }\n                selectedUserForMenu = null\n                selectedUsername = null\n            },\n            onDismiss = {\n                selectedUserForMenu = null\n                selectedUsername = null\n            },\n        )\n    }\n\n    val lazyListState = rememberLazyListState()\n    val coroutineScope = rememberCoroutineScope()\n    val bringIntoViewRequester = remember { BringIntoViewRequester() }\n\n    val backStackEntry by navController.currentBackStackEntryAsState()\n    val scrollToTop = backStackEntry?.savedStateHandle?.getStateFlow(\"scrollToTop\", false)?.collectAsState()\n\n    LaunchedEffect(scrollToTop?.value) {\n        if (scrollToTop?.value == true) {\n            lazyListState.animateScrollToItem(0)\n            backStackEntry?.savedStateHandle?.set(\"scrollToTop\", false)\n        }\n    }\n\n    LazyColumn(\n        state = lazyListState,\n        modifier =\n            Modifier\n                .fillMaxSize()\n                .background(MaterialTheme.colorScheme.background)\n                .imePadding(),\n        contentPadding =\n            PaddingValues(\n                start = 16.dp,\n                end = 16.dp,\n                top = windowInsets.asPaddingValues().calculateTopPadding() + 16.dp,\n                bottom = windowInsets.asPaddingValues().calculateBottomPadding() + 16.dp + AppBarHeight,\n            ),\n        verticalArrangement = Arrangement.spacedBy(16.dp),\n    ) {\n        // Header\n        item {\n            HeaderSection(isInRoom = isInRoom)\n        }\n\n        // Connection status card\n        item {\n            ConnectionStatusCard(\n                connectionState = connectionState,\n                onConnect = { listenTogetherManager.connect() },\n                onDisconnect = { listenTogetherManager.disconnect() },\n                onReconnect = { listenTogetherManager.forceReconnect() },\n            )\n        }\n\n        if (connectionState == ConnectionState.CONNECTED && !isInRoom) {\n            item {\n                Text(\n                    text = stringResource(R.string.listen_together_background_disconnect_note),\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    textAlign = TextAlign.Center,\n                    modifier = Modifier.fillMaxWidth(),\n                )\n            }\n        }\n\n        if (isInRoom) {\n            // Room status card\n            roomState?.let { room ->\n                item {\n                    RoomStatusCard(\n                        roomCode = room.roomCode,\n                        isHost = isHost,\n                        context = context,\n                    )\n                }\n\n                // Connected users\n                val connectedUsers = room.users.filter { it.isConnected }\n                val currentUserIdValue = userId ?: \"\"\n                item {\n                    ConnectedUsersSection(\n                        users = connectedUsers,\n                        isHost = isHost,\n                        currentUserId = currentUserIdValue,\n                        onUserClick = { clickedUserId, username ->\n                            if (isHost && clickedUserId != currentUserIdValue) {\n                                selectedUserForMenu = clickedUserId\n                                selectedUsername = username\n                            }\n                        },\n                    )\n                }\n\n                // Pending join requests (host only)\n                if (isHost && pendingJoinRequests.isNotEmpty()) {\n                    item {\n                        PendingJoinRequestsSection(\n                            requests = pendingJoinRequests,\n                            onApprove = { listenTogetherManager.approveJoin(it) },\n                            onReject = { listenTogetherManager.rejectJoin(it, \"Rejected by host\") },\n                        )\n                    }\n                }\n\n                // Pending suggestions (host only)\n                if (isHost && pendingSuggestions.isNotEmpty()) {\n                    item {\n                        PendingSuggestionsSection(\n                            suggestions = pendingSuggestions,\n                            onApprove = { listenTogetherManager.approveSuggestion(it) },\n                            onReject = { listenTogetherManager.rejectSuggestion(it, \"Rejected by host\") },\n                        )\n                    }\n                }\n\n                // Leave room button\n                item {\n                    Button(\n                        onClick = { listenTogetherManager.leaveRoom() },\n                        modifier = Modifier.fillMaxWidth(),\n                        colors =\n                            ButtonDefaults.buttonColors(\n                                containerColor = MaterialTheme.colorScheme.error,\n                            ),\n                        shape = RoundedCornerShape(16.dp),\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.logout),\n                            contentDescription = null,\n                            modifier = Modifier.size(20.dp),\n                        )\n                        Spacer(Modifier.width(8.dp))\n                        Text(\n                            stringResource(R.string.leave_room),\n                            fontWeight = FontWeight.SemiBold,\n                        )\n                    }\n                }\n            }\n        } else {\n            // Join/Create room section\n            item {\n                JoinCreateRoomSection(\n                    usernameInput = usernameInput,\n                    onUsernameChange = { usernameInput = it },\n                    roomCodeInput = roomCodeInput,\n                    onRoomCodeChange = { roomCodeInput = it },\n                    savedUsername = savedUsername,\n                    isJoiningRoom = isJoiningRoom,\n                    joinErrorMessage = joinErrorMessage,\n                    waitingForApprovalText = waitingForApprovalText,\n                    bringIntoViewRequester = bringIntoViewRequester,\n                    onCreateRoom = {\n                        val username = usernameInput.takeIf { it.isNotBlank() } ?: savedUsername\n                        val finalUsername = username.trim()\n                        if (finalUsername.isNotBlank()) {\n                            savedUsername = finalUsername\n                            Toast.makeText(context, R.string.creating_room, Toast.LENGTH_SHORT).show()\n                            isCreatingRoom = true\n                            isJoiningRoom = false\n                            joinErrorMessage = null\n                            listenTogetherManager.connect()\n                            listenTogetherManager.createRoom(finalUsername)\n                        } else {\n                            Toast.makeText(context, R.string.error_username_empty, Toast.LENGTH_SHORT).show()\n                        }\n                    },\n                    onJoinRoom = {\n                        val username = usernameInput.takeIf { it.isNotBlank() } ?: savedUsername\n                        val finalUsername = username.trim()\n                        if (finalUsername.isNotBlank()) {\n                            savedUsername = finalUsername\n                            Toast\n                                .makeText(\n                                    context,\n                                    String.format(joiningRoomTemplate, roomCodeInput),\n                                    Toast.LENGTH_SHORT,\n                                ).show()\n                            isJoiningRoom = true\n                            isCreatingRoom = false\n                            joinErrorMessage = null\n                            listenTogetherManager.connect()\n                            listenTogetherManager.joinRoom(roomCodeInput, finalUsername)\n                        } else {\n                            Toast.makeText(context, R.string.error_username_empty, Toast.LENGTH_SHORT).show()\n                        }\n                    },\n                    onFieldFocused = {\n                        coroutineScope.launch {\n                            bringIntoViewRequester.bringIntoView()\n                        }\n                    },\n                )\n            }\n        }\n\n        // Settings link\n        item {\n            SettingsLinkCard(\n                onClick = { navController.navigate(\"settings/integrations/listen_together\") },\n            )\n        }\n    }\n\n    if (shouldShowTopBar) {\n        TopAppBar(\n            title = { Text(stringResource(R.string.together)) },\n            navigationIcon = {\n                IconButton(\n                    onClick = navController::navigateUp,\n                    onLongClick = navController::backToMain,\n                ) {\n                    Icon(\n                        painterResource(R.drawable.arrow_back),\n                        contentDescription = null,\n                    )\n                }\n            },\n        )\n    }\n}\n\n@Composable\nprivate fun NotConfiguredContent() {\n    Box(\n        modifier = Modifier.fillMaxSize(),\n        contentAlignment = Alignment.Center,\n    ) {\n        Column(\n            horizontalAlignment = Alignment.CenterHorizontally,\n            modifier = Modifier.padding(24.dp),\n        ) {\n            Icon(\n                painter = painterResource(R.drawable.group),\n                contentDescription = null,\n                tint = MaterialTheme.colorScheme.primary,\n                modifier = Modifier.size(64.dp),\n            )\n            Spacer(modifier = Modifier.height(16.dp))\n            Text(\n                text = stringResource(R.string.listen_together),\n                style = MaterialTheme.typography.headlineMedium,\n                fontWeight = FontWeight.Bold,\n                color = MaterialTheme.colorScheme.primary,\n            )\n            Spacer(modifier = Modifier.height(8.dp))\n            Text(\n                text = stringResource(R.string.listen_together_not_configured),\n                style = MaterialTheme.typography.bodyLarge,\n                textAlign = TextAlign.Center,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun HeaderSection(isInRoom: Boolean = false) {\n    if (isInRoom) return\n\n    Column(\n        modifier = Modifier.fillMaxWidth(),\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n        Box(\n            modifier =\n                Modifier\n                    .size(80.dp)\n                    .clip(CircleShape)\n                    .background(MaterialTheme.colorScheme.primaryContainer),\n            contentAlignment = Alignment.Center,\n        ) {\n            Icon(\n                painter = painterResource(R.drawable.group_outlined),\n                contentDescription = null,\n                tint = MaterialTheme.colorScheme.onPrimaryContainer,\n                modifier = Modifier.size(48.dp),\n            )\n        }\n        Spacer(modifier = Modifier.height(16.dp))\n        Text(\n            text = stringResource(R.string.listen_together),\n            style = MaterialTheme.typography.headlineMedium,\n            fontWeight = FontWeight.Bold,\n            color = MaterialTheme.colorScheme.onBackground,\n        )\n        Spacer(modifier = Modifier.height(4.dp))\n        Text(\n            text = stringResource(R.string.listen_together_description),\n            style = MaterialTheme.typography.bodyMedium,\n            textAlign = TextAlign.Center,\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n        )\n    }\n}\n\n@Composable\nprivate fun ConnectionStatusCard(\n    connectionState: ConnectionState,\n    onConnect: () -> Unit,\n    onDisconnect: () -> Unit,\n    onReconnect: () -> Unit,\n) {\n    Card(\n        modifier =\n            Modifier\n                .fillMaxWidth()\n                .animateContentSize(\n                    animationSpec =\n                        spring(\n                            dampingRatio = Spring.DampingRatioMediumBouncy,\n                            stiffness = Spring.StiffnessLow,\n                        ),\n                ),\n        shape = RoundedCornerShape(20.dp),\n        colors =\n            CardDefaults.cardColors(\n                containerColor =\n                    when (connectionState) {\n                        ConnectionState.CONNECTED -> MaterialTheme.colorScheme.primaryContainer\n                        ConnectionState.CONNECTING, ConnectionState.RECONNECTING -> MaterialTheme.colorScheme.secondaryContainer\n                        ConnectionState.ERROR -> MaterialTheme.colorScheme.errorContainer\n                        ConnectionState.DISCONNECTED -> MaterialTheme.colorScheme.surfaceContainerHigh\n                    },\n            ),\n    ) {\n        Column(\n            modifier = Modifier.padding(16.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n        ) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.Center,\n                modifier = Modifier.fillMaxWidth(),\n            ) {\n                Box(\n                    modifier =\n                        Modifier\n                            .size(10.dp)\n                            .clip(CircleShape)\n                            .background(\n                                color =\n                                    when (connectionState) {\n                                        ConnectionState.CONNECTED -> MaterialTheme.colorScheme.primary\n                                        ConnectionState.CONNECTING, ConnectionState.RECONNECTING -> MaterialTheme.colorScheme.tertiary\n                                        ConnectionState.ERROR -> MaterialTheme.colorScheme.error\n                                        ConnectionState.DISCONNECTED -> MaterialTheme.colorScheme.outline\n                                    },\n                            ),\n                )\n                Spacer(modifier = Modifier.width(10.dp))\n                Text(\n                    text =\n                        when (connectionState) {\n                            ConnectionState.CONNECTED -> stringResource(R.string.listen_together_connected)\n                            ConnectionState.CONNECTING -> stringResource(R.string.listen_together_connecting)\n                            ConnectionState.RECONNECTING -> stringResource(R.string.listen_together_reconnecting)\n                            ConnectionState.ERROR -> stringResource(R.string.listen_together_error)\n                            ConnectionState.DISCONNECTED -> stringResource(R.string.listen_together_disconnected)\n                        },\n                    style = MaterialTheme.typography.titleMedium,\n                    fontWeight = FontWeight.Bold,\n                    color =\n                        when (connectionState) {\n                            ConnectionState.CONNECTED -> MaterialTheme.colorScheme.primary\n                            ConnectionState.CONNECTING, ConnectionState.RECONNECTING -> MaterialTheme.colorScheme.tertiary\n                            ConnectionState.ERROR -> MaterialTheme.colorScheme.error\n                            ConnectionState.DISCONNECTED -> MaterialTheme.colorScheme.onSurfaceVariant\n                        },\n                )\n            }\n\n            if (connectionState == ConnectionState.CONNECTING || connectionState == ConnectionState.RECONNECTING) {\n                Spacer(modifier = Modifier.height(12.dp))\n                LinearProgressIndicator(\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .clip(RoundedCornerShape(8.dp)),\n                    color = MaterialTheme.colorScheme.primary,\n                )\n            }\n\n            Spacer(modifier = Modifier.height(12.dp))\n\n            Row(\n                horizontalArrangement = Arrangement.spacedBy(12.dp),\n                modifier = Modifier.fillMaxWidth(),\n            ) {\n                if (connectionState == ConnectionState.DISCONNECTED || connectionState == ConnectionState.ERROR) {\n                    Button(\n                        onClick = onConnect,\n                        modifier = Modifier.weight(1f),\n                        shape = RoundedCornerShape(12.dp),\n                        colors =\n                            ButtonDefaults.buttonColors(\n                                containerColor = MaterialTheme.colorScheme.primary,\n                            ),\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.link),\n                            contentDescription = null,\n                            modifier = Modifier.size(18.dp),\n                        )\n                        Spacer(Modifier.width(6.dp))\n                        Text(stringResource(R.string.connect), fontWeight = FontWeight.SemiBold)\n                    }\n                } else {\n                    Button(\n                        onClick = onDisconnect,\n                        modifier = Modifier.weight(1f),\n                        shape = RoundedCornerShape(12.dp),\n                        colors =\n                            ButtonDefaults.buttonColors(\n                                containerColor = MaterialTheme.colorScheme.primary,\n                            ),\n                    ) {\n                        Text(stringResource(R.string.disconnect), fontWeight = FontWeight.SemiBold)\n                    }\n                    FilledTonalButton(\n                        onClick = onReconnect,\n                        modifier = Modifier.weight(1f),\n                        shape = RoundedCornerShape(12.dp),\n                    ) {\n                        Text(\"Reconnect\", fontWeight = FontWeight.SemiBold)\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RoomStatusCard(\n    roomCode: String,\n    isHost: Boolean,\n    context: Context,\n) {\n    Card(\n        modifier = Modifier.fillMaxWidth(),\n        shape = RoundedCornerShape(24.dp),\n        colors =\n            CardDefaults.cardColors(\n                containerColor = MaterialTheme.colorScheme.surfaceContainerHighest,\n            ),\n    ) {\n        Column(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(24.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n        ) {\n            Text(\n                text = stringResource(R.string.room_code),\n                style = MaterialTheme.typography.labelLarge,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                textAlign = TextAlign.Center,\n            )\n            Spacer(modifier = Modifier.height(8.dp))\n            Text(\n                text = roomCode,\n                style = MaterialTheme.typography.displaySmall,\n                color = MaterialTheme.colorScheme.primary,\n                fontWeight = FontWeight.Bold,\n                letterSpacing = 6.sp,\n                textAlign = TextAlign.Center,\n            )\n            Spacer(modifier = Modifier.height(4.dp))\n            Text(\n                text =\n                    if (isHost) {\n                        stringResource(R.string.listen_together_you_are_host)\n                    } else {\n                        stringResource(R.string.listen_together_you_are_guest)\n                    },\n                style = MaterialTheme.typography.bodyMedium,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                textAlign = TextAlign.Center,\n            )\n\n            if (isHost) {\n                Spacer(modifier = Modifier.height(16.dp))\n                val inviteLink =\n                    remember(roomCode) {\n                        \"https://metrolist.meowery.eu/listen?code=$roomCode\"\n                    }\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),\n                    modifier = Modifier.fillMaxWidth(),\n                ) {\n                    FilledTonalButton(\n                        onClick = {\n                            val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager\n                            val clip = android.content.ClipData.newPlainText(\"Listen Together Link\", inviteLink)\n                            clipboard.setPrimaryClip(clip)\n                            Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()\n                        },\n                        shape = RoundedCornerShape(12.dp),\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.link),\n                            contentDescription = stringResource(R.string.copy_link),\n                            modifier = Modifier.size(18.dp),\n                        )\n                        Spacer(modifier = Modifier.width(8.dp))\n                        Text(stringResource(R.string.copy_link))\n                    }\n\n                    FilledTonalButton(\n                        onClick = {\n                            val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager\n                            val clip = android.content.ClipData.newPlainText(\"Room Code\", roomCode)\n                            clipboard.setPrimaryClip(clip)\n                            Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()\n                        },\n                        shape = RoundedCornerShape(12.dp),\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.content_copy),\n                            contentDescription = stringResource(R.string.copy_code),\n                            modifier = Modifier.size(18.dp),\n                        )\n                        Spacer(modifier = Modifier.width(8.dp))\n                        Text(stringResource(R.string.copy_code))\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ConnectedUsersSection(\n    users: List<UserInfo>,\n    isHost: Boolean,\n    currentUserId: String,\n    onUserClick: (String, String) -> Unit,\n) {\n    Card(\n        modifier = Modifier.fillMaxWidth(),\n        shape = RoundedCornerShape(20.dp),\n        colors =\n            CardDefaults.cardColors(\n                containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,\n            ),\n    ) {\n        Column(\n            modifier = Modifier.padding(16.dp),\n        ) {\n            Text(\n                text = \"${stringResource(R.string.connected_users)} (${users.size})\",\n                style = MaterialTheme.typography.titleMedium,\n                fontWeight = FontWeight.Bold,\n                color = MaterialTheme.colorScheme.primary,\n            )\n            Spacer(modifier = Modifier.height(12.dp))\n\n            Row(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .horizontalScroll(rememberScrollState()),\n                horizontalArrangement = Arrangement.spacedBy(16.dp),\n            ) {\n                users.forEach { user ->\n                    UserAvatar(\n                        user = user,\n                        isCurrentUser = user.userId == currentUserId,\n                        isClickable = isHost && user.userId != currentUserId,\n                        onClick = { onUserClick(user.userId, user.username) },\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun UserAvatar(\n    user: UserInfo,\n    isCurrentUser: Boolean,\n    isClickable: Boolean,\n    onClick: () -> Unit,\n) {\n    Column(\n        horizontalAlignment = Alignment.CenterHorizontally,\n        modifier =\n            Modifier\n                .width(72.dp)\n                .clickable(enabled = isClickable, onClick = onClick),\n    ) {\n        Box(\n            contentAlignment = Alignment.Center,\n        ) {\n            Surface(\n                modifier = Modifier.size(56.dp),\n                shape = CircleShape,\n                color =\n                    when {\n                        user.isHost -> MaterialTheme.colorScheme.primary\n                        isCurrentUser -> MaterialTheme.colorScheme.secondary\n                        else -> MaterialTheme.colorScheme.surfaceVariant\n                    },\n            ) {\n                Box(\n                    contentAlignment = Alignment.Center,\n                    modifier = Modifier.fillMaxSize(),\n                ) {\n                    Text(\n                        text = user.username.take(1).uppercase(),\n                        style = MaterialTheme.typography.titleLarge,\n                        fontWeight = FontWeight.Bold,\n                        color =\n                            when {\n                                user.isHost -> MaterialTheme.colorScheme.onPrimary\n                                isCurrentUser -> MaterialTheme.colorScheme.onSecondary\n                                else -> MaterialTheme.colorScheme.onSurfaceVariant\n                            },\n                    )\n                }\n            }\n\n            if (user.isHost || isCurrentUser) {\n                Surface(\n                    modifier =\n                        Modifier\n                            .align(Alignment.BottomEnd)\n                            .offset(x = 4.dp, y = 4.dp)\n                            .size(20.dp),\n                    shape = CircleShape,\n                    color = if (user.isHost) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary,\n                ) {\n                    Box(\n                        contentAlignment = Alignment.Center,\n                        modifier = Modifier.fillMaxSize(),\n                    ) {\n                        Icon(\n                            painter =\n                                painterResource(\n                                    if (user.isHost) R.drawable.crown else R.drawable.person,\n                                ),\n                            contentDescription = null,\n                            tint = MaterialTheme.colorScheme.onPrimary,\n                            modifier = Modifier.size(12.dp),\n                        )\n                    }\n                }\n            }\n        }\n\n        Spacer(modifier = Modifier.height(8.dp))\n\n        Text(\n            text = user.username,\n            style = MaterialTheme.typography.labelMedium,\n            fontWeight = if (isCurrentUser) FontWeight.Bold else FontWeight.Medium,\n            color = if (user.isHost) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n            textAlign = TextAlign.Center,\n        )\n\n        if (user.isHost) {\n            Text(\n                text = stringResource(R.string.host_label),\n                style = MaterialTheme.typography.labelSmall,\n                color = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f),\n            )\n        } else if (isCurrentUser) {\n            Text(\n                text = stringResource(R.string.you_label),\n                style = MaterialTheme.typography.labelSmall,\n                color = MaterialTheme.colorScheme.secondary.copy(alpha = 0.8f),\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun PendingJoinRequestsSection(\n    requests: List<JoinRequestPayload>,\n    onApprove: (String) -> Unit,\n    onReject: (String) -> Unit,\n) {\n    Card(\n        modifier = Modifier.fillMaxWidth(),\n        shape = RoundedCornerShape(20.dp),\n        colors =\n            CardDefaults.cardColors(\n                containerColor = MaterialTheme.colorScheme.surfaceContainerHighest,\n            ),\n    ) {\n        Column(\n            modifier = Modifier.padding(16.dp),\n        ) {\n            Text(\n                text = stringResource(R.string.listen_together_join_requests),\n                style = MaterialTheme.typography.titleMedium,\n                fontWeight = FontWeight.Bold,\n                color = MaterialTheme.colorScheme.primary,\n            )\n            Spacer(modifier = Modifier.height(12.dp))\n\n            requests.forEach { request ->\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(vertical = 8.dp),\n                ) {\n                    Surface(\n                        modifier = Modifier.size(40.dp),\n                        shape = CircleShape,\n                        color = MaterialTheme.colorScheme.secondary,\n                    ) {\n                        Box(\n                            contentAlignment = Alignment.Center,\n                            modifier = Modifier.fillMaxSize(),\n                        ) {\n                            Text(\n                                text = request.username.take(1).uppercase(),\n                                style = MaterialTheme.typography.titleMedium,\n                                fontWeight = FontWeight.Bold,\n                                color = MaterialTheme.colorScheme.onSecondary,\n                            )\n                        }\n                    }\n                    Spacer(Modifier.width(12.dp))\n                    Text(\n                        text = request.username,\n                        style = MaterialTheme.typography.bodyLarge,\n                        fontWeight = FontWeight.Medium,\n                        modifier = Modifier.weight(1f),\n                    )\n                    MaterialIconButton(onClick = { onApprove(request.userId) }) {\n                        Icon(\n                            painter = painterResource(R.drawable.check),\n                            contentDescription = stringResource(R.string.approve),\n                            tint = MaterialTheme.colorScheme.primary,\n                            modifier = Modifier.size(24.dp),\n                        )\n                    }\n                    MaterialIconButton(onClick = { onReject(request.userId) }) {\n                        Icon(\n                            painter = painterResource(R.drawable.close),\n                            contentDescription = stringResource(R.string.reject),\n                            tint = MaterialTheme.colorScheme.error,\n                            modifier = Modifier.size(24.dp),\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun PendingSuggestionsSection(\n    suggestions: List<SuggestionReceivedPayload>,\n    onApprove: (String) -> Unit,\n    onReject: (String) -> Unit,\n) {\n    Card(\n        modifier = Modifier.fillMaxWidth(),\n        shape = RoundedCornerShape(20.dp),\n        colors =\n            CardDefaults.cardColors(\n                containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,\n            ),\n    ) {\n        Column(\n            modifier = Modifier.padding(16.dp),\n        ) {\n            Text(\n                text = stringResource(R.string.pending_suggestions),\n                style = MaterialTheme.typography.titleMedium,\n                fontWeight = FontWeight.Bold,\n                color = MaterialTheme.colorScheme.primary,\n            )\n            Spacer(modifier = Modifier.height(12.dp))\n\n            suggestions.forEach { suggestion ->\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(vertical = 8.dp),\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.queue_music),\n                        contentDescription = null,\n                        tint = MaterialTheme.colorScheme.primary,\n                        modifier = Modifier.size(24.dp),\n                    )\n                    Spacer(Modifier.width(12.dp))\n                    Column(modifier = Modifier.weight(1f)) {\n                        Text(\n                            text = suggestion.trackInfo.title,\n                            style = MaterialTheme.typography.bodyMedium,\n                            fontWeight = FontWeight.Medium,\n                            maxLines = 1,\n                            overflow = TextOverflow.Ellipsis,\n                        )\n                        Text(\n                            text = suggestion.fromUsername,\n                            style = MaterialTheme.typography.bodySmall,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                            maxLines = 1,\n                            overflow = TextOverflow.Ellipsis,\n                        )\n                    }\n                    MaterialIconButton(onClick = { onApprove(suggestion.suggestionId) }) {\n                        Icon(\n                            painter = painterResource(R.drawable.check),\n                            contentDescription = stringResource(R.string.approve),\n                            tint = MaterialTheme.colorScheme.primary,\n                            modifier = Modifier.size(24.dp),\n                        )\n                    }\n                    MaterialIconButton(onClick = { onReject(suggestion.suggestionId) }) {\n                        Icon(\n                            painter = painterResource(R.drawable.close),\n                            contentDescription = stringResource(R.string.reject),\n                            tint = MaterialTheme.colorScheme.error,\n                            modifier = Modifier.size(24.dp),\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun JoinCreateRoomSection(\n    usernameInput: String,\n    onUsernameChange: (String) -> Unit,\n    roomCodeInput: String,\n    onRoomCodeChange: (String) -> Unit,\n    savedUsername: String,\n    isJoiningRoom: Boolean,\n    joinErrorMessage: String?,\n    waitingForApprovalText: String,\n    bringIntoViewRequester: BringIntoViewRequester,\n    onCreateRoom: () -> Unit,\n    onJoinRoom: () -> Unit,\n    onFieldFocused: () -> Unit = {},\n) {\n    Card(\n        modifier = Modifier.fillMaxWidth(),\n        shape = RoundedCornerShape(24.dp),\n        colors =\n            CardDefaults.cardColors(\n                containerColor = MaterialTheme.colorScheme.surfaceContainerHighest,\n            ),\n    ) {\n        Column(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(20.dp),\n            verticalArrangement = Arrangement.spacedBy(16.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n        ) {\n            // Username input\n            OutlinedTextField(\n                value = usernameInput,\n                onValueChange = onUsernameChange,\n                label = { Text(stringResource(R.string.username)) },\n                placeholder = { Text(stringResource(R.string.enter_username)) },\n                leadingIcon = {\n                    Icon(\n                        painterResource(R.drawable.person),\n                        null,\n                        tint = MaterialTheme.colorScheme.primary,\n                    )\n                },\n                trailingIcon = {\n                    if (usernameInput.isNotBlank()) {\n                        MaterialIconButton(onClick = { onUsernameChange(\"\") }) {\n                            Icon(painterResource(R.drawable.close), null)\n                        }\n                    }\n                },\n                singleLine = true,\n                shape = RoundedCornerShape(16.dp),\n                colors =\n                    OutlinedTextFieldDefaults.colors(\n                        focusedBorderColor = MaterialTheme.colorScheme.primary,\n                        unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),\n                        focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLow,\n                        unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLow,\n                    ),\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .onFocusChanged { if (it.isFocused) onFieldFocused() },\n            )\n\n            // Room code input\n            OutlinedTextField(\n                value = roomCodeInput,\n                onValueChange = { if (it.length <= 8) onRoomCodeChange(it.uppercase()) },\n                label = { Text(stringResource(R.string.room_code)) },\n                placeholder = { Text(stringResource(R.string.enter_room_code)) },\n                leadingIcon = {\n                    Icon(\n                        painterResource(R.drawable.group),\n                        null,\n                        tint = MaterialTheme.colorScheme.primary,\n                    )\n                },\n                trailingIcon = {\n                    if (roomCodeInput.isNotBlank()) {\n                        MaterialIconButton(onClick = { onRoomCodeChange(\"\") }) {\n                            Icon(painterResource(R.drawable.close), null)\n                        }\n                    }\n                },\n                singleLine = true,\n                shape = RoundedCornerShape(16.dp),\n                colors =\n                    OutlinedTextFieldDefaults.colors(\n                        focusedBorderColor = MaterialTheme.colorScheme.primary,\n                        unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f),\n                        focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLow,\n                        unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLow,\n                    ),\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .bringIntoViewRequester(bringIntoViewRequester)\n                        .onFocusChanged { if (it.isFocused) onFieldFocused() },\n            )\n\n            // Waiting for approval indicator\n            AnimatedVisibility(\n                visible = isJoiningRoom,\n                enter = fadeIn() + slideInVertically(),\n                exit = fadeOut() + slideOutVertically(),\n            ) {\n                Surface(\n                    modifier = Modifier.fillMaxWidth(),\n                    shape = RoundedCornerShape(12.dp),\n                    color = MaterialTheme.colorScheme.primaryContainer,\n                ) {\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.Center,\n                        modifier =\n                            Modifier\n                                .fillMaxWidth()\n                                .padding(16.dp),\n                    ) {\n                        CircularProgressIndicator(\n                            modifier = Modifier.size(20.dp),\n                            strokeWidth = 2.dp,\n                            color = MaterialTheme.colorScheme.primary,\n                        )\n                        Spacer(modifier = Modifier.width(12.dp))\n                        Text(\n                            text = waitingForApprovalText,\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.onPrimaryContainer,\n                            fontWeight = FontWeight.Medium,\n                            textAlign = TextAlign.Center,\n                        )\n                    }\n                }\n            }\n\n            // Error message\n            AnimatedVisibility(\n                visible = joinErrorMessage != null,\n                enter = fadeIn() + slideInVertically(),\n                exit = fadeOut() + slideOutVertically(),\n            ) {\n                Surface(\n                    modifier = Modifier.fillMaxWidth(),\n                    shape = RoundedCornerShape(12.dp),\n                    color = MaterialTheme.colorScheme.errorContainer,\n                ) {\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.Center,\n                        modifier =\n                            Modifier\n                                .fillMaxWidth()\n                                .padding(16.dp),\n                    ) {\n                        Icon(\n                            painterResource(R.drawable.error),\n                            contentDescription = null,\n                            modifier = Modifier.size(20.dp),\n                            tint = MaterialTheme.colorScheme.onErrorContainer,\n                        )\n                        Spacer(modifier = Modifier.width(12.dp))\n                        Text(\n                            text = joinErrorMessage ?: \"\",\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.onErrorContainer,\n                            fontWeight = FontWeight.Medium,\n                            textAlign = TextAlign.Center,\n                        )\n                    }\n                }\n            }\n\n            // Action buttons\n            val hasUsername = usernameInput.trim().isNotBlank() || savedUsername.isNotBlank()\n            val hasRoomCode = roomCodeInput.length == 8\n\n            // Create Room button - visible when username is provided\n            AnimatedVisibility(visible = hasUsername && !hasRoomCode) {\n                Button(\n                    onClick = onCreateRoom,\n                    modifier = Modifier.fillMaxWidth(),\n                    enabled = hasUsername,\n                    shape = RoundedCornerShape(16.dp),\n                    colors =\n                        ButtonDefaults.buttonColors(\n                            containerColor = MaterialTheme.colorScheme.primary,\n                        ),\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.add),\n                        contentDescription = null,\n                        modifier = Modifier.size(20.dp),\n                    )\n                    Spacer(Modifier.width(8.dp))\n                    Text(stringResource(R.string.create_room), fontWeight = FontWeight.SemiBold)\n                }\n            }\n\n            // Join Room button - visible when username and room code are provided\n            AnimatedVisibility(visible = hasUsername && hasRoomCode) {\n                Button(\n                    onClick = onJoinRoom,\n                    modifier = Modifier.fillMaxWidth(),\n                    enabled = hasUsername && hasRoomCode,\n                    shape = RoundedCornerShape(16.dp),\n                    colors =\n                        ButtonDefaults.buttonColors(\n                            containerColor = MaterialTheme.colorScheme.tertiary,\n                        ),\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.login),\n                        contentDescription = null,\n                        modifier = Modifier.size(20.dp),\n                    )\n                    Spacer(Modifier.width(8.dp))\n                    Text(stringResource(R.string.join_room), fontWeight = FontWeight.SemiBold)\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun SettingsLinkCard(onClick: () -> Unit) {\n    Card(\n        modifier =\n            Modifier\n                .fillMaxWidth()\n                .clickable(onClick = onClick),\n        shape = RoundedCornerShape(16.dp),\n        colors =\n            CardDefaults.cardColors(\n                containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,\n            ),\n    ) {\n        Row(\n            modifier = Modifier.padding(16.dp),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            Icon(\n                painter = painterResource(R.drawable.settings),\n                contentDescription = null,\n                tint = MaterialTheme.colorScheme.primary,\n                modifier = Modifier.size(24.dp),\n            )\n            Spacer(Modifier.width(12.dp))\n            Column(modifier = Modifier.weight(1f)) {\n                Text(\n                    text = stringResource(R.string.settings),\n                    style = MaterialTheme.typography.bodyLarge,\n                    fontWeight = FontWeight.Medium,\n                )\n                Text(\n                    text = stringResource(R.string.listen_together_settings_desc),\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n            }\n            Icon(\n                painter = painterResource(R.drawable.arrow_forward),\n                contentDescription = null,\n                tint = MaterialTheme.colorScheme.primary,\n                modifier = Modifier.size(20.dp),\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun UserActionDialog(\n    username: String,\n    onKick: () -> Unit,\n    onPermanentKick: () -> Unit,\n    onTransferOwnership: () -> Unit,\n    onDismiss: () -> Unit,\n) {\n    DefaultDialog(\n        onDismiss = onDismiss,\n        icon = {\n            Icon(\n                painter = painterResource(R.drawable.group),\n                contentDescription = null,\n                modifier = Modifier.size(28.dp),\n            )\n        },\n        title = {\n            Column(horizontalAlignment = Alignment.CenterHorizontally) {\n                Text(\n                    text = stringResource(R.string.manage_user),\n                    fontWeight = FontWeight.Bold,\n                )\n                Text(\n                    text = username,\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n            }\n        },\n        buttons = {\n            TextButton(onClick = onDismiss) {\n                Text(stringResource(android.R.string.cancel))\n            }\n        },\n    ) {\n        Column(\n            verticalArrangement = Arrangement.spacedBy(8.dp),\n        ) {\n            // Kick button\n            Surface(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .clickable(onClick = onKick),\n                shape = RoundedCornerShape(12.dp),\n                color = MaterialTheme.colorScheme.errorContainer,\n            ) {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    modifier = Modifier.padding(16.dp),\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.close),\n                        contentDescription = null,\n                        tint = MaterialTheme.colorScheme.error,\n                        modifier = Modifier.size(24.dp),\n                    )\n                    Spacer(modifier = Modifier.width(16.dp))\n                    Column(modifier = Modifier.weight(1f)) {\n                        Text(\n                            text = stringResource(R.string.kick_user),\n                            style = MaterialTheme.typography.titleMedium,\n                            fontWeight = FontWeight.SemiBold,\n                            color = MaterialTheme.colorScheme.error,\n                        )\n                        Text(\n                            text = stringResource(R.string.kick_user_desc),\n                            style = MaterialTheme.typography.bodySmall,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        )\n                    }\n                }\n            }\n\n            // Permanently kick button\n            Surface(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .clickable(onClick = onPermanentKick),\n                shape = RoundedCornerShape(12.dp),\n                color = MaterialTheme.colorScheme.surfaceVariant,\n            ) {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    modifier = Modifier.padding(16.dp),\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.close),\n                        contentDescription = null,\n                        tint = MaterialTheme.colorScheme.error,\n                        modifier = Modifier.size(24.dp),\n                    )\n                    Spacer(modifier = Modifier.width(16.dp))\n                    Column(modifier = Modifier.weight(1f)) {\n                        Text(\n                            text = stringResource(R.string.permanently_kick_user),\n                            style = MaterialTheme.typography.titleMedium,\n                            fontWeight = FontWeight.SemiBold,\n                        )\n                        Text(\n                            text = stringResource(R.string.permanently_kick_user_desc),\n                            style = MaterialTheme.typography.bodySmall,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        )\n                    }\n                }\n            }\n\n            // Transfer ownership button\n            Surface(\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .clickable(onClick = onTransferOwnership),\n                shape = RoundedCornerShape(12.dp),\n                color = MaterialTheme.colorScheme.primaryContainer,\n            ) {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    modifier = Modifier.padding(16.dp),\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.crown),\n                        contentDescription = null,\n                        tint = MaterialTheme.colorScheme.primary,\n                        modifier = Modifier.size(24.dp),\n                    )\n                    Spacer(modifier = Modifier.width(16.dp))\n                    Column(modifier = Modifier.weight(1f)) {\n                        Text(\n                            text = stringResource(R.string.transfer_ownership),\n                            style = MaterialTheme.typography.titleMedium,\n                            fontWeight = FontWeight.SemiBold,\n                            color = MaterialTheme.colorScheme.primary,\n                        )\n                        Text(\n                            text = stringResource(R.string.transfer_ownership_desc),\n                            style = MaterialTheme.typography.bodySmall,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/LoginScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens\n\nimport android.annotation.SuppressLint\nimport android.content.Intent\nimport android.webkit.CookieManager\nimport android.webkit.JavascriptInterface\nimport android.webkit.WebView\nimport android.webkit.WebViewClient\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.viewinterop.AndroidView\nimport androidx.navigation.NavController\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.AccountChannelHandleKey\nimport com.metrolist.music.constants.AccountEmailKey\nimport com.metrolist.music.constants.AccountNameKey\nimport com.metrolist.music.constants.DataSyncIdKey\nimport com.metrolist.music.constants.InnerTubeCookieKey\nimport com.metrolist.music.constants.VisitorDataKey\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.utils.reportException\nimport kotlinx.coroutines.DelicateCoroutinesApi\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport timber.log.Timber\n\n@SuppressLint(\"SetJavaScriptEnabled\")\n@OptIn(ExperimentalMaterial3Api::class, DelicateCoroutinesApi::class)\n@Composable\nfun LoginScreen(\n    navController: NavController,\n) {\n    val context = LocalContext.current\n    val coroutineScope = rememberCoroutineScope()\n    var visitorData by rememberPreference(VisitorDataKey, \"\")\n    var dataSyncId by rememberPreference(DataSyncIdKey, \"\")\n    var innerTubeCookie by rememberPreference(InnerTubeCookieKey, \"\")\n    var accountName by rememberPreference(AccountNameKey, \"\")\n    var accountEmail by rememberPreference(AccountEmailKey, \"\")\n    var accountChannelHandle by rememberPreference(AccountChannelHandleKey, \"\")\n    var hasCompletedLogin by remember { mutableStateOf(false) }\n\n    var webView: WebView? = null\n\n    AndroidView(\n        modifier = Modifier\n            .windowInsetsPadding(LocalPlayerAwareWindowInsets.current)\n            .fillMaxSize(),\n        factory = { webViewContext ->\n            WebView(webViewContext).apply {\n                webViewClient = object : WebViewClient() {\n                    override fun onPageFinished(view: WebView, url: String?) {\n                        loadUrl(\"javascript:Android.onRetrieveVisitorData(window.yt.config_.VISITOR_DATA)\")\n                        loadUrl(\"javascript:Android.onRetrieveDataSyncId(window.yt.config_.DATASYNC_ID)\")\n\n                        if (url?.startsWith(\"https://music.youtube.com\") == true && !hasCompletedLogin) {\n                            innerTubeCookie = CookieManager.getInstance().getCookie(url)\n                            hasCompletedLogin = true\n\n                            coroutineScope.launch {\n                                // Small delay to ensure preferences are saved\n                                delay(500)\n\n                                // Initialize YouTube object with new authentication data\n                                YouTube.cookie = innerTubeCookie\n                                YouTube.dataSyncId = dataSyncId\n                                YouTube.visitorData = visitorData\n\n                                Timber.d(\"Login: YouTube object initialized, validating...\")\n\n                                YouTube.accountInfo().onSuccess {\n                                    accountName = it.name\n                                    accountEmail = it.email.orEmpty()\n                                    accountChannelHandle = it.channelHandle.orEmpty()\n\n                                    Timber.d(\"Login: Successfully logged in as ${it.name}, restarting app...\")\n\n                                    // Clean up WebView\n                                    webView?.apply {\n                                        stopLoading()\n                                        clearHistory()\n                                        clearCache(true)\n                                        clearFormData()\n                                    }\n\n                                    // Restart app to apply login state throughout\n                                    val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)\n                                    intent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)\n                                    context.startActivity(intent)\n                                    Runtime.getRuntime().exit(0)\n                                }.onFailure {\n                                    Timber.e(it, \"Login: Authentication validation failed\")\n                                    hasCompletedLogin = false // Allow retry\n                                    reportException(it)\n                                }\n                            }\n                        }\n                    }\n                }\n                settings.apply {\n                    javaScriptEnabled = true\n                    setSupportZoom(true)\n                    builtInZoomControls = true\n                    displayZoomControls = false\n                }\n                addJavascriptInterface(object {\n                    @JavascriptInterface\n                    fun onRetrieveVisitorData(newVisitorData: String?) {\n                        if (newVisitorData != null) {\n                            visitorData = newVisitorData\n                        }\n                    }\n                    @JavascriptInterface\n                    fun onRetrieveDataSyncId(newDataSyncId: String?) {\n                        if (newDataSyncId != null) {\n                            dataSyncId = newDataSyncId.substringBefore(\"||\")\n                        }\n                    }\n                }, \"Android\")\n                webView = this\n                loadUrl(\"https://accounts.google.com/ServiceLogin?continue=https%3A%2F%2Fmusic.youtube.com\")\n            }\n        }\n    )\n\n    TopAppBar(\n        title = { Text(stringResource(R.string.login)) },\n        navigationIcon = {\n            IconButton(\n                onClick = navController::navigateUp,\n                onLongClick = navController::backToMain\n            ) {\n                Icon(\n                    painterResource(R.drawable.arrow_back),\n                    contentDescription = null\n                )\n            }\n        }\n    )\n\n    BackHandler(enabled = webView?.canGoBack() == true) {\n        webView?.goBack()\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/MoodAndGenresScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens\n\nimport android.content.res.Configuration.ORIENTATION_LANDSCAPE\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.NavigationTitle\nimport com.metrolist.music.ui.component.shimmer.ListItemPlaceHolder\nimport com.metrolist.music.ui.component.shimmer.ShimmerHost\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.viewmodels.MoodAndGenresViewModel\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun MoodAndGenresScreen(\n    navController: NavController,\n    viewModel: MoodAndGenresViewModel = hiltViewModel(),\n) {\n    val localConfiguration = LocalConfiguration.current\n    val itemsPerRow = if (localConfiguration.orientation == ORIENTATION_LANDSCAPE) 3 else 2\n\n    val moodAndGenresList by viewModel.moodAndGenres.collectAsState()\n\n    LazyColumn(\n        contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n    ) {\n        if (moodAndGenresList == null) {\n            item(key = \"mood_and_genres_shimmer\") {\n                ShimmerHost(\n                    modifier = Modifier.animateItem()\n                ) {\n                    repeat(8) {\n                        ListItemPlaceHolder()\n                    }\n                }\n            }\n        }\n\n        moodAndGenresList?.forEachIndexed { index, moodAndGenres ->\n            item(key = \"mood_and_genres_section_$index\") {\n                Column(\n                    modifier = Modifier\n                        .animateItem()\n                        .padding(horizontal = 6.dp),\n                ) {\n                    NavigationTitle(\n                        title = moodAndGenres.title,\n                    )\n                    moodAndGenres.items.chunked(itemsPerRow).forEach { row ->\n                        Row {\n                            row.forEach {\n                                MoodAndGenresButton(\n                                    title = it.title,\n                                    onClick = {\n                                        navController.navigate(\"youtube_browse/${it.endpoint.browseId}?params=${it.endpoint.params}\")\n                                    },\n                                    modifier =\n                                    Modifier\n                                        .weight(1f)\n                                        .padding(6.dp),\n                                )\n                            }\n\n                            repeat(itemsPerRow - row.size) {\n                                Spacer(Modifier.weight(1f))\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    TopAppBar(\n        title = { Text(stringResource(R.string.mood_and_genres)) },\n        navigationIcon = {\n            IconButton(\n                onClick = navController::navigateUp,\n                onLongClick = navController::backToMain,\n            ) {\n                Icon(\n                    painterResource(R.drawable.arrow_back),\n                    contentDescription = null,\n                )\n            }\n        },\n    )\n}\n\n@Composable\nfun MoodAndGenresButton(\n    title: String,\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    Box(\n        contentAlignment = Alignment.CenterStart,\n        modifier =\n        modifier\n            .height(MoodAndGenresButtonHeight)\n            .clip(RoundedCornerShape(6.dp))\n            .background(MaterialTheme.colorScheme.surfaceContainer)\n            .clickable(onClick = onClick)\n            .padding(horizontal = 12.dp),\n    ) {\n        Text(\n            text = title,\n            style = MaterialTheme.typography.labelLarge,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n        )\n    }\n}\n\nval MoodAndGenresButtonHeight = 48.dp\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/NavigationBuilder.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens\n\nimport android.app.Activity\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.slideInHorizontally\nimport androidx.compose.animation.slideOutHorizontally\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.TopAppBarScrollBehavior\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.navigation.NavGraphBuilder\nimport androidx.navigation.NavHostController\nimport androidx.navigation.NavType\nimport androidx.navigation.compose.composable\nimport androidx.navigation.compose.dialog\nimport androidx.navigation.navArgument\nimport com.metrolist.music.constants.DarkModeKey\nimport com.metrolist.music.constants.PureBlackKey\nimport com.metrolist.music.ui.screens.artist.ArtistAlbumsScreen\nimport com.metrolist.music.ui.screens.artist.ArtistItemsScreen\nimport com.metrolist.music.ui.screens.artist.ArtistScreen\nimport com.metrolist.music.ui.screens.artist.ArtistSongsScreen\nimport com.metrolist.music.ui.screens.equalizer.EqScreen\nimport com.metrolist.music.ui.screens.library.LibraryScreen\nimport com.metrolist.music.ui.screens.playlist.AutoPlaylistScreen\nimport com.metrolist.music.ui.screens.playlist.CachePlaylistScreen\nimport com.metrolist.music.ui.screens.playlist.LocalPlaylistScreen\nimport com.metrolist.music.ui.screens.playlist.OnlinePlaylistScreen\nimport com.metrolist.music.ui.screens.playlist.TopPlaylistScreen\nimport com.metrolist.music.ui.screens.podcast.OnlinePodcastScreen\nimport com.metrolist.music.ui.screens.recognition.RecognitionHistoryScreen\nimport com.metrolist.music.ui.screens.recognition.RecognitionScreen\nimport com.metrolist.music.ui.screens.search.OnlineSearchResult\nimport com.metrolist.music.ui.screens.search.SearchScreen\nimport com.metrolist.music.ui.screens.settings.AboutScreen\nimport com.metrolist.music.ui.screens.settings.AiSettings\nimport com.metrolist.music.ui.screens.settings.AndroidAutoSettings\nimport com.metrolist.music.ui.screens.settings.AppearanceSettings\nimport com.metrolist.music.ui.screens.settings.BackupAndRestore\nimport com.metrolist.music.ui.screens.settings.ContentSettings\nimport com.metrolist.music.ui.screens.settings.DarkMode\nimport com.metrolist.music.ui.screens.settings.DiscordLoginScreen\nimport com.metrolist.music.ui.screens.settings.PlayerSettings\nimport com.metrolist.music.ui.screens.settings.PrivacySettings\nimport com.metrolist.music.ui.screens.settings.RomanizationSettings\nimport com.metrolist.music.ui.screens.settings.SettingsScreen\nimport com.metrolist.music.ui.screens.settings.StorageSettings\nimport com.metrolist.music.ui.screens.settings.ThemeScreen\nimport com.metrolist.music.ui.screens.settings.UpdaterScreen\nimport com.metrolist.music.ui.screens.settings.integrations.DiscordSettings\nimport com.metrolist.music.ui.screens.settings.integrations.IntegrationScreen\nimport com.metrolist.music.ui.screens.settings.integrations.LastFMSettings\nimport com.metrolist.music.ui.screens.settings.integrations.ListenTogetherSettings\nimport com.metrolist.music.ui.screens.wrapped.WrappedScreen\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\n\n@OptIn(ExperimentalMaterial3Api::class)\nfun NavGraphBuilder.navigationBuilder(\n    navController: NavHostController,\n    scrollBehavior: TopAppBarScrollBehavior,\n    latestVersionName: String,\n    activity: Activity,\n    snackbarHostState: SnackbarHostState,\n) {\n    composable(Screens.Home.route) {\n        HomeScreen(navController = navController, snackbarHostState = snackbarHostState)\n    }\n\n    composable(Screens.Search.route) {\n        val pureBlackEnabled by rememberPreference(PureBlackKey, defaultValue = false)\n        val darkTheme by rememberEnumPreference(DarkModeKey, defaultValue = DarkMode.AUTO)\n        val isSystemInDarkTheme = isSystemInDarkTheme()\n        val useDarkTheme =\n            remember(darkTheme, isSystemInDarkTheme) {\n                if (darkTheme == DarkMode.AUTO) isSystemInDarkTheme else darkTheme == DarkMode.ON\n            }\n        val pureBlack =\n            remember(pureBlackEnabled, useDarkTheme) {\n                pureBlackEnabled && useDarkTheme\n            }\n        SearchScreen(\n            navController = navController,\n            pureBlack = pureBlack,\n        )\n    }\n\n    composable(Screens.Library.route) {\n        LibraryScreen(navController)\n    }\n\n    composable(Screens.ListenTogether.route) {\n        ListenTogetherScreen(navController, showTopBar = false)\n    }\n\n    composable(\n        route = \"listen_together_from_topbar\",\n    ) {\n        ListenTogetherScreen(navController, showTopBar = true)\n    }\n\n    composable(\"history\") {\n        HistoryScreen(navController)\n    }\n\n    composable(\"stats\") {\n        StatsScreen(navController)\n    }\n\n    composable(\"mood_and_genres\") {\n        MoodAndGenresScreen(navController)\n    }\n\n    composable(\"account\") {\n        AccountScreen(navController)\n    }\n\n    composable(\"new_release\") {\n        NewReleaseScreen(navController)\n    }\n\n    composable(\"charts_screen\") {\n        ChartsScreen(navController)\n    }\n\n    composable(\n        route = \"browse/{browseId}\",\n        arguments =\n            listOf(\n                navArgument(\"browseId\") {\n                    type = NavType.StringType\n                },\n            ),\n    ) {\n        BrowseScreen(\n            navController,\n            it.arguments?.getString(\"browseId\"),\n        )\n    }\n\n    composable(\n        route = \"search/{query}\",\n        arguments =\n            listOf(\n                navArgument(\"query\") {\n                    type = NavType.StringType\n                },\n            ),\n        enterTransition = {\n            fadeIn(tween(250))\n        },\n        exitTransition = {\n            if (targetState.destination.route?.startsWith(\"search/\") == true) {\n                fadeOut(tween(200))\n            } else {\n                fadeOut(tween(200)) + slideOutHorizontally { -it / 2 }\n            }\n        },\n        popEnterTransition = {\n            if (initialState.destination.route?.startsWith(\"search/\") == true) {\n                fadeIn(tween(250))\n            } else {\n                fadeIn(tween(250)) + slideInHorizontally { -it / 2 }\n            }\n        },\n        popExitTransition = {\n            fadeOut(tween(200))\n        },\n    ) {\n        OnlineSearchResult(navController)\n    }\n\n    composable(\n        route = \"album/{albumId}\",\n        arguments =\n            listOf(\n                navArgument(\"albumId\") {\n                    type = NavType.StringType\n                },\n            ),\n    ) {\n        AlbumScreen(navController)\n    }\n\n    composable(\n        route = \"artist/{artistId}?isPodcastChannel={isPodcastChannel}\",\n        arguments =\n            listOf(\n                navArgument(\"artistId\") {\n                    type = NavType.StringType\n                },\n                navArgument(\"isPodcastChannel\") {\n                    type = NavType.BoolType\n                    defaultValue = false\n                },\n            ),\n    ) {\n        ArtistScreen(navController)\n    }\n\n    composable(\n        route = \"artist/{artistId}/songs\",\n        arguments =\n            listOf(\n                navArgument(\"artistId\") {\n                    type = NavType.StringType\n                },\n            ),\n    ) {\n        ArtistSongsScreen(navController)\n    }\n\n    composable(\n        route = \"artist/{artistId}/albums\",\n        arguments =\n            listOf(\n                navArgument(\"artistId\") {\n                    type = NavType.StringType\n                },\n            ),\n    ) {\n        ArtistAlbumsScreen(navController, scrollBehavior)\n    }\n\n    composable(\n        route = \"artist/{artistId}/items?browseId={browseId}?params={params}\",\n        arguments =\n            listOf(\n                navArgument(\"artistId\") {\n                    type = NavType.StringType\n                },\n                navArgument(\"browseId\") {\n                    type = NavType.StringType\n                    nullable = true\n                },\n                navArgument(\"params\") {\n                    type = NavType.StringType\n                    nullable = true\n                },\n            ),\n    ) {\n        ArtistItemsScreen(navController)\n    }\n\n    composable(\n        route = \"online_playlist/{playlistId}\",\n        arguments =\n            listOf(\n                navArgument(\"playlistId\") {\n                    type = NavType.StringType\n                },\n            ),\n    ) {\n        OnlinePlaylistScreen(navController)\n    }\n\n    composable(\n        route = \"online_podcast/{podcastId}\",\n        arguments =\n            listOf(\n                navArgument(\"podcastId\") {\n                    type = NavType.StringType\n                },\n            ),\n    ) {\n        OnlinePodcastScreen(navController, scrollBehavior)\n    }\n\n    composable(\n        route = \"local_playlist/{playlistId}\",\n        arguments =\n            listOf(\n                navArgument(\"playlistId\") {\n                    type = NavType.StringType\n                },\n            ),\n    ) {\n        LocalPlaylistScreen(navController)\n    }\n\n    composable(\n        route = \"auto_playlist/{playlist}\",\n        arguments =\n            listOf(\n                navArgument(\"playlist\") {\n                    type = NavType.StringType\n                },\n            ),\n    ) {\n        AutoPlaylistScreen(navController)\n    }\n\n    composable(\n        route = \"cache_playlist/{playlist}\",\n        arguments =\n            listOf(\n                navArgument(\"playlist\") {\n                    type = NavType.StringType\n                },\n            ),\n    ) {\n        CachePlaylistScreen(navController)\n    }\n\n    composable(\n        route = \"top_playlist/{top}\",\n        arguments =\n            listOf(\n                navArgument(\"top\") {\n                    type = NavType.StringType\n                },\n            ),\n    ) {\n        TopPlaylistScreen(navController)\n    }\n\n    composable(\n        route = \"youtube_browse/{browseId}?params={params}\",\n        arguments =\n            listOf(\n                navArgument(\"browseId\") {\n                    type = NavType.StringType\n                    nullable = true\n                },\n                navArgument(\"params\") {\n                    type = NavType.StringType\n                    nullable = true\n                },\n            ),\n    ) {\n        YouTubeBrowseScreen(navController)\n    }\n\n    composable(\"settings\") {\n        SettingsScreen(navController, latestVersionName)\n    }\n\n    composable(\"settings/appearance\") {\n        AppearanceSettings(navController, activity, snackbarHostState)\n    }\n\n    composable(\"settings/appearance/theme\") {\n        ThemeScreen(navController)\n    }\n\n    composable(\"settings/content\") {\n        ContentSettings(navController)\n    }\n\n    composable(\"settings/content/romanization\") {\n        RomanizationSettings(navController)\n    }\n\n    composable(\"settings/ai\") {\n        AiSettings(navController)\n    }\n\n    composable(\"settings/player\") {\n        PlayerSettings(navController)\n    }\n\n    composable(\"settings/storage\") {\n        StorageSettings(navController)\n    }\n\n    composable(\"settings/privacy\") {\n        PrivacySettings(navController)\n    }\n\n    composable(\"settings/backup_restore\") {\n        BackupAndRestore(navController)\n    }\n\n    composable(\"settings/integrations\") {\n        IntegrationScreen(navController)\n    }\n\n    composable(\"settings/integrations/discord\") {\n        DiscordSettings(navController, snackbarHostState)\n    }\n\n    composable(\"settings/integrations/lastfm\") {\n        LastFMSettings(navController)\n    }\n\n    composable(route = \"settings/integrations/listen_together\") {\n        ListenTogetherSettings(navController)\n    }\n\n    composable(\"settings/discord/login\") {\n        DiscordLoginScreen(navController)\n    }\n\n    composable(\"settings/updater\") {\n        UpdaterScreen(navController)\n    }\n\n    composable(\"settings/about\") {\n        AboutScreen(navController, scrollBehavior)\n    }\n\n    composable(\"login\") {\n        LoginScreen(navController)\n    }\n\n    composable(\"wrapped\") {\n        WrappedScreen(navController)\n    }\n\n    dialog(\"equalizer\") {\n        EqScreen()\n    }\n\n    composable(\n        route = \"recognition?autoStart={autoStart}\",\n        arguments =\n            listOf(\n                navArgument(\"autoStart\") {\n                    type = NavType.BoolType\n                    defaultValue = false\n                },\n            ),\n    ) {\n        RecognitionScreen(navController, it.arguments?.getBoolean(\"autoStart\") ?: false)\n    }\n\n    composable(\"recognition_history\") {\n        RecognitionHistoryScreen(navController)\n    }\n    composable(\"settings/android_auto\") {\n        AndroidAutoSettings(navController, scrollBehavior)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/NewReleaseScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.items\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.GridItemSize\nimport com.metrolist.music.constants.GridItemsSizeKey\nimport com.metrolist.music.constants.GridThumbnailHeight\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.YouTubeGridItem\nimport com.metrolist.music.ui.component.shimmer.GridItemPlaceHolder\nimport com.metrolist.music.ui.component.shimmer.ShimmerHost\nimport com.metrolist.music.ui.menu.YouTubeAlbumMenu\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.viewmodels.NewReleaseViewModel\n\n@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun NewReleaseScreen(\n    navController: NavController,\n    viewModel: NewReleaseViewModel = hiltViewModel(),\n) {\n    val menuState = LocalMenuState.current\n    val haptic = LocalHapticFeedback.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n\n    val newReleaseAlbums by viewModel.newReleaseAlbums.collectAsState()\n\n    val coroutineScope = rememberCoroutineScope()\n    val gridItemSize by rememberEnumPreference(GridItemsSizeKey, GridItemSize.BIG)\n\n    LazyVerticalGrid(\n        columns = GridCells.Adaptive(minSize = GridThumbnailHeight + if (gridItemSize == GridItemSize.BIG) 24.dp else (-24).dp),\n        contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n    ) {\n        items(\n            items = newReleaseAlbums.distinctBy { it.id },\n            key = { it.id },\n        ) { album ->\n            YouTubeGridItem(\n                item = album,\n                isActive = mediaMetadata?.album?.id == album.id,\n                isPlaying = isPlaying,\n                fillMaxWidth = true,\n                coroutineScope = coroutineScope,\n                modifier =\n                Modifier\n                    .combinedClickable(\n                        onClick = {\n                            navController.navigate(\"album/${album.id}\")\n                        },\n                        onLongClick = {\n                            haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                            menuState.show {\n                                YouTubeAlbumMenu(\n                                    albumItem = album,\n                                    navController = navController,\n                                    onDismiss = menuState::dismiss,\n                                )\n                            }\n                        },\n                    ),\n            )\n        }\n\n        if (newReleaseAlbums.isEmpty()) {\n            items(8) {\n                ShimmerHost {\n                    GridItemPlaceHolder(fillMaxWidth = true)\n                }\n            }\n        }\n    }\n\n    TopAppBar(\n        title = { Text(stringResource(R.string.new_release_albums)) },\n        navigationIcon = {\n            IconButton(\n                onClick = navController::navigateUp,\n                onLongClick = navController::backToMain,\n            ) {\n                Icon(\n                    painterResource(R.drawable.arrow_back),\n                    contentDescription = null,\n                )\n            }\n        },\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/Screens.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens\n\nimport androidx.annotation.DrawableRes\nimport androidx.annotation.StringRes\nimport androidx.compose.runtime.Immutable\nimport com.metrolist.music.R\n\n@Immutable\nsealed class Screens(\n    @StringRes val titleId: Int,\n    @DrawableRes val iconIdInactive: Int,\n    @DrawableRes val iconIdActive: Int,\n    val route: String,\n) {\n    object Home : Screens(\n        titleId = R.string.home,\n        iconIdInactive = R.drawable.home_outlined,\n        iconIdActive = R.drawable.home_filled,\n        route = \"home\"\n    )\n\n    object Search : Screens(\n        titleId = R.string.search,\n        iconIdInactive = R.drawable.search,\n        iconIdActive = R.drawable.search,\n        route = \"search_input\"\n    )\n\n    object ListenTogether : Screens(\n        titleId = R.string.together,\n        iconIdInactive = R.drawable.group_outlined,\n        iconIdActive = R.drawable.group_filled,\n        route = \"listen_together\"\n    )\n\n    object Library : Screens(\n        titleId = R.string.filter_library,\n        iconIdInactive = R.drawable.library_music_outlined,\n        iconIdActive = R.drawable.library_music_filled,\n        route = \"library\"\n    )\n\n    companion object {\n        val MainScreens = listOf(Home, Search, ListenTogether, Library)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/StatsScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextField\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.listSaver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.toMutableStateList\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.pluralStringResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport com.metrolist.innertube.models.Artist\nimport com.metrolist.innertube.models.WatchEndpoint\nimport com.metrolist.innertube.utils.parseCookieString\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.CONTENT_TYPE_ARTIST\nimport com.metrolist.music.constants.InnerTubeCookieKey\nimport com.metrolist.music.constants.StatPeriod\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.playback.queues.ListQueue\nimport com.metrolist.music.playback.queues.YouTubeQueue\nimport com.metrolist.music.ui.component.ArtistListItem\nimport com.metrolist.music.ui.component.ChoiceChipsRow\nimport com.metrolist.music.ui.component.EmptyPlaceholder\nimport com.metrolist.music.ui.component.HideOnScrollFAB\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.LocalAlbumsGrid\nimport com.metrolist.music.ui.component.LocalArtistsGrid\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.LocalSongsGrid\nimport com.metrolist.music.ui.component.NavigationTitle\nimport com.metrolist.music.ui.component.TimeTransfer\nimport com.metrolist.music.ui.component.PlaylistGridItem\nimport com.metrolist.music.ui.menu.AlbumMenu\nimport com.metrolist.music.ui.menu.ArtistMenu\nimport com.metrolist.music.ui.menu.SongMenu\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.joinByBullet\nimport com.metrolist.music.utils.makeTimeString\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.viewmodels.StatsViewModel\nimport java.time.LocalDateTime\nimport java.time.format.DateTimeFormatter\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)\n@Composable\nfun StatsScreen(\n    navController: NavController,\n    viewModel: StatsViewModel = hiltViewModel(),\n) {\n    val sArtists = viewModel.selectedArtists // SnapshotStateList<Artist>\n\n// Helper actions:\n    val toggleArtistSelection: (Artist) -> Unit = { artist ->\n        if (sArtists.any { it.id == artist.id }) {\n            sArtists.removeAll { it.id == artist.id }\n        } else {\n            sArtists.add(artist)\n        }\n    }\n\n    val clearArtistSelection: () -> Unit = {\n        sArtists.clear()\n    }\n    val menuState = LocalMenuState.current\n    val haptic = LocalHapticFeedback.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n    val context = LocalContext.current\n\n    var inSelectMode by rememberSaveable { mutableStateOf(false) }\n    val selection = rememberSaveable(\n        saver = listSaver<MutableList<Long>, Long>(\n            save = { it.toList() },\n            restore = { it.toMutableStateList() }\n        )\n    ) { mutableStateListOf() }\n    val onExitSelectionMode = {\n        inSelectMode = false\n        selection.clear()\n    }\n\n    var isSearching by rememberSaveable { mutableStateOf(false) }\n    var query by rememberSaveable(stateSaver = TextFieldValue.Saver) {\n        mutableStateOf(TextFieldValue())\n    }\n    val focusRequester = remember { FocusRequester() }\n    LaunchedEffect(isSearching) {\n        if (isSearching) {\n            focusRequester.requestFocus()\n        }\n    }\n    if (isSearching) {\n        BackHandler {\n            isSearching = false\n            query = TextFieldValue()\n        }\n    } else if (inSelectMode) {\n        BackHandler(onBack = onExitSelectionMode)\n    }\n\n    val indexChips by viewModel.indexChips.collectAsState()\n    val mostPlayedSongs by viewModel.mostPlayedSongs.collectAsState()\n    val mostPlayedSongsStats by viewModel.filteredSongs.collectAsState()\n    val mostPlayedArtists by viewModel.filteredArtists.collectAsState()\n    val mostPlayedAlbums by viewModel.filteredAlbums.collectAsState()\n    val allArtists by viewModel.mostPlayedArtists.collectAsState()\n    val firstEvent by viewModel.firstEvent.collectAsState()\n    val weeklyMostPlaylist by viewModel.weeklyMostPlaylist.collectAsState()\n    val monthlyMostPlaylist by viewModel.monthlyMostPlaylist.collectAsState()\n    val recapPlaylists by viewModel.recapPlaylists.collectAsState()\n    val currentDate = LocalDateTime.now()\n    val orderedMostPlayedSongs = remember(mostPlayedSongsStats, mostPlayedSongs) {\n        val songsById = mostPlayedSongs.associateBy { it.song.id }\n        mostPlayedSongsStats.mapNotNull { statsSong -> songsById[statsSong.id] }\n    }\n    val mostPeriodPlaylists = listOfNotNull(weeklyMostPlaylist, monthlyMostPlaylist)\n    val (innerTubeCookie) = rememberPreference(InnerTubeCookieKey, \"\")\n    val isLoggedIn =\n        remember(innerTubeCookie) {\n            \"SAPISID\" in parseCookieString(innerTubeCookie)\n        }\n    val visibleStatsPlaylists =\n        remember(mostPeriodPlaylists, recapPlaylists, isLoggedIn) {\n            if (isLoggedIn) {\n                (mostPeriodPlaylists + recapPlaylists).distinctBy { it.id }\n            } else {\n                mostPeriodPlaylists\n            }\n        }\n\n    val coroutineScope = rememberCoroutineScope()\n    val lazyListState = rememberLazyListState()\n    val selectedOption by viewModel.selectedOption.collectAsState()\n\n    var showTimeTransfer by rememberSaveable { mutableStateOf(false) }\n    var prevOptionOrdinal by rememberSaveable { mutableStateOf<OptionStats?>(null) }\n    var prevIndexChips by rememberSaveable { mutableStateOf<Int?>(null) }\n\n    LaunchedEffect(showTimeTransfer) {\n        if (showTimeTransfer) {\n            if (prevOptionOrdinal == null) prevOptionOrdinal = selectedOption\n            if (prevIndexChips == null) prevIndexChips = indexChips\n            viewModel.selectedOption.value = OptionStats.CONTINUOUS // \"throughout time\" in your VM\n            viewModel.indexChips.value = StatPeriod.ALL.ordinal // optional: ensure it’s actually “now -> throughout time”\n        }\n    }\n\n    if (showTimeTransfer) {\n        TimeTransfer(\n            onDismiss = {\n                            showTimeTransfer = false\n                            prevOptionOrdinal?.let { viewModel.selectedOption.value = it }\n                            prevIndexChips?.let { viewModel.indexChips.value = it }\n\n                            // Clear snapshots for the next open\n                            prevOptionOrdinal = null\n                            prevIndexChips = null\n                        },\n        )\n    }\n\n    LaunchedEffect(Unit) {\n        viewModel.syncMostPlaylistsIfNeeded()\n    }\n\n    val weeklyDates =\n        if (currentDate != null && firstEvent != null) {\n            generateSequence(currentDate) { it.minusWeeks(1) }\n                .takeWhile { it.isAfter(firstEvent?.event?.timestamp?.minusWeeks(1)) }\n                .mapIndexed { index, date ->\n                    val endDate = date.plusWeeks(1).minusDays(1).coerceAtMost(currentDate)\n                    val formatter = DateTimeFormatter.ofPattern(\"dd MMM\")\n\n                    val startDateFormatted = formatter.format(date)\n                    val endDateFormatted = formatter.format(endDate)\n\n                    val startMonth = date.month\n                    val endMonth = endDate.month\n                    val startYear = date.year\n                    val endYear = endDate.year\n\n                    val text =\n                        when {\n                            startYear != currentDate.year -> \"$startDateFormatted, $startYear - $endDateFormatted, $endYear\"\n                            startMonth != endMonth -> \"$startDateFormatted - $endDateFormatted\"\n                            else -> \"${date.dayOfMonth} - $endDateFormatted\"\n                        }\n                    Pair(index, text)\n                }.toList()\n        } else {\n            emptyList()\n        }\n\n    val monthlyDates =\n        if (currentDate != null && firstEvent != null) {\n            generateSequence(\n                currentDate.plusMonths(1).withDayOfMonth(1).minusDays(1),\n            ) { it.minusMonths(1) }\n                .takeWhile {\n                    it.isAfter(\n                        firstEvent\n                            ?.event\n                            ?.timestamp\n                            ?.withDayOfMonth(1),\n                    )\n                }.mapIndexed { index, date ->\n                    val formatter = DateTimeFormatter.ofPattern(\"MMM\")\n                    val formattedDate = formatter.format(date)\n                    val text =\n                        if (date.year != currentDate.year) {\n                            \"$formattedDate ${date.year}\"\n                        } else {\n                            formattedDate\n                        }\n                    Pair(index, text)\n                }.toList()\n        } else {\n            emptyList()\n        }\n\n    val yearlyDates =\n        if (currentDate != null && firstEvent != null) {\n            generateSequence(\n                currentDate\n                    .plusYears(1)\n                    .withDayOfYear(1)\n                    .minusDays(1),\n            ) { it.minusYears(1) }\n                .takeWhile {\n                    it.isAfter(\n                        firstEvent\n                            ?.event\n                            ?.timestamp,\n                    )\n                }.mapIndexed { index, date ->\n                    Pair(index, \"${date.year}\")\n                }.toList()\n        } else {\n            emptyList()\n        }\n\n    Box(modifier = Modifier.fillMaxSize()) {\n        LazyColumn(\n            state = lazyListState,\n            contentPadding =\n                LocalPlayerAwareWindowInsets.current\n                    .only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom)\n                    .asPaddingValues(),\n            modifier =\n                Modifier.windowInsetsPadding(\n                    LocalPlayerAwareWindowInsets.current.only(WindowInsetsSides.Top),\n                ),\n        ) {\n            val filteredArtists = allArtists.map { artistWrapper ->\n                Artist(\n                    id = artistWrapper.artist.id,\n                    name = artistWrapper.artist.name,\n                )\n            }.filter { artist ->\n                artist.name.contains(query.text, ignoreCase = true)\n            }\n\n            item(key = \"choice_chips\") {\n                ChoiceChipsRow(\n                    chips =\n                        when (selectedOption) {\n                            OptionStats.WEEKS -> weeklyDates\n                            OptionStats.MONTHS -> monthlyDates\n                            OptionStats.YEARS -> yearlyDates\n                            OptionStats.CONTINUOUS -> {\n                                listOf(\n                                    StatPeriod.WEEK_1.ordinal to pluralStringResource(\n                                        R.plurals.n_week,\n                                        1,\n                                        1\n                                    ),\n                                    StatPeriod.MONTH_1.ordinal to pluralStringResource(\n                                        R.plurals.n_month,\n                                        1,\n                                        1\n                                    ),\n                                    StatPeriod.MONTH_3.ordinal to pluralStringResource(\n                                        R.plurals.n_month,\n                                        3,\n                                        3\n                                    ),\n                                    StatPeriod.MONTH_6.ordinal to pluralStringResource(\n                                        R.plurals.n_month,\n                                        6,\n                                        6\n                                    ),\n                                    StatPeriod.YEAR_1.ordinal to pluralStringResource(\n                                        R.plurals.n_year,\n                                        1,\n                                        1\n                                    ),\n                                    StatPeriod.ALL.ordinal to stringResource(R.string.filter_all),\n                                )\n                            }\n                        },\n                    options =\n                        listOf(\n                            OptionStats.CONTINUOUS to stringResource(id = R.string.continuous),\n                            OptionStats.WEEKS to stringResource(R.string.weeks),\n                            OptionStats.MONTHS to stringResource(R.string.months),\n                            OptionStats.YEARS to stringResource(R.string.years),\n                        ),\n                    selectedOption = selectedOption,\n                    onSelectionChange = {\n                        viewModel.selectedOption.value = it\n                        viewModel.indexChips.value = 0\n                    },\n                    currentValue = indexChips,\n                    onValueUpdate = { viewModel.indexChips.value = it },\n                )\n            }\n\n            if (visibleStatsPlaylists.isNotEmpty() && !isSearching && sArtists.isEmpty()) {\n                item(key = \"mostPeriodPlaylistsTitle\") {\n                    NavigationTitle(\n                        title =\n                            pluralStringResource(\n                                R.plurals.n_playlist,\n                                visibleStatsPlaylists.size,\n                                visibleStatsPlaylists.size,\n                            ),\n                        modifier = Modifier.animateItem(),\n                    )\n                }\n\n                item(key = \"mostPeriodPlaylists\") {\n                    LazyRow(\n                        contentPadding = PaddingValues(horizontal = 4.dp),\n                        modifier = Modifier.animateItem(),\n                    ) {\n                        itemsIndexed(\n                            items = visibleStatsPlaylists,\n                            key = { _, playlist -> playlist.id },\n                        ) { _, playlist ->\n                            PlaylistGridItem(\n                                playlist = playlist,\n                                autoPlaylist = true,\n                                modifier =\n                                    Modifier\n                                        .combinedClickable(\n                                            onClick = {\n                                                navController.navigate(\"local_playlist/${playlist.id}\")\n                                            },\n                                        ).animateItem(),\n                            )\n                        }\n                    }\n                }\n            }\n\n\n            if (!isSearching) {\n                item(key = \"mostPlayedSongs\") {\n                    NavigationTitle(\n                        title = \"${mostPlayedSongsStats.size} ${stringResource(id = R.string.songs)}\",\n                        onPlayAllClick =\n                            if (orderedMostPlayedSongs.isNotEmpty()) {\n                                {\n                                    playerConnection.playQueue(\n                                        ListQueue(\n                                            title = context.getString(R.string.most_played_songs),\n                                            items = orderedMostPlayedSongs.map { it.toMediaMetadata().toMediaItem() },\n                                        )\n                                    )\n                                }\n                            } else {\n                                null\n                            },\n                        modifier = Modifier.animateItem(),\n                    )\n\n                    LazyRow(\n                        modifier = Modifier.animateItem(),\n                    ) {\n                        itemsIndexed(\n                            items = mostPlayedSongsStats,\n                            key = { _, song -> song.id },\n                        ) { index, song ->\n                            LocalSongsGrid(\n                                title = \"${index + 1}. ${song.title}\",\n                                subtitle =\n                                    joinByBullet(\n                                        pluralStringResource(\n                                            R.plurals.n_time,\n                                            song.songCountListened,\n                                            song.songCountListened,\n                                        ),\n                                        makeTimeString(song.timeListened),\n                                    ),\n                                thumbnailUrl = song.thumbnailUrl,\n                                isActive = song.id == mediaMetadata?.id,\n                                isPlaying = isPlaying,\n                                modifier =\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .combinedClickable(\n                                            onClick = {\n                                                if (song.id == mediaMetadata?.id) {\n                                                    playerConnection.togglePlayPause()\n                                                } else {\n                                                    val targetSong = mostPlayedSongs.find { it.id == song.id }\n                                                    if (targetSong != null) {\n                                                        playerConnection.playQueue(\n                                                            YouTubeQueue(\n                                                                endpoint = WatchEndpoint(song.id),\n                                                                preloadItem = targetSong.toMediaMetadata(),\n                                                            ),\n                                                        )\n                                                    }\n                                                }\n                                            },\n                                            onLongClick = {\n                                                val targetSong = mostPlayedSongs.find { it.id == song.id }\n                                                if (targetSong != null) {\n                                                    haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                    menuState.show {\n                                                        SongMenu(\n                                                            originalSong = targetSong,\n                                                            navController = navController,\n                                                            onDismiss = menuState::dismiss,\n                                                        )\n                                                    }\n                                                }\n                                            },\n                                        )\n                                        .animateItem(),\n                            )\n                        }\n                    }\n                }\n            }\n\n            if (!isSearching) {\n                item(key = \"mostPlayedArtists\") {\n                    NavigationTitle(\n                        title = \"${mostPlayedArtists.size} ${stringResource(id = R.string.artists)}\",\n                        modifier = Modifier.animateItem(),\n                    )\n\n                    LazyRow(\n                        modifier = Modifier.animateItem(),\n                    ) {\n                        itemsIndexed(\n                            items = mostPlayedArtists,\n                            key = { _, artist -> artist.id },\n                        ) { index, artist ->\n                            LocalArtistsGrid(\n                                title = \"${index + 1}. ${artist.artist.name}\",\n                                subtitle =\n                                    joinByBullet(\n                                        pluralStringResource(\n                                            R.plurals.n_time,\n                                            artist.songCount,\n                                            artist.songCount,\n                                        ),\n                                        makeTimeString(artist.timeListened?.toLong()),\n                                    ),\n                                thumbnailUrl = artist.artist.thumbnailUrl,\n                                modifier =\n                                    Modifier\n                                        .combinedClickable(\n                                            onClick = {\n                                                navController.navigate(\"artist/${artist.id}\")\n                                            },\n                                            onLongClick = {\n                                                haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                menuState.show {\n                                                    ArtistMenu(\n                                                        originalArtist = artist,\n                                                        coroutineScope = coroutineScope,\n                                                        onDismiss = menuState::dismiss,\n                                                    )\n                                                }\n                                            },\n                                        ).animateItem(),\n                            )\n                        }\n                    }\n                }\n            }\n\n            if (!isSearching) {\n                item(key = \"mostPlayedAlbums\") {\n                    NavigationTitle(\n                        title = \"${mostPlayedAlbums.size} ${stringResource(id = R.string.albums)}\",\n                        modifier = Modifier.animateItem(),\n                    )\n\n                if (mostPlayedAlbums.isNotEmpty()) {\n                    LazyRow(\n                        modifier = Modifier.animateItem(),\n                    ) {\n                        itemsIndexed(\n                            items = mostPlayedAlbums,\n                            key = { _, album -> album.id },\n                        ) { index, album ->\n                            LocalAlbumsGrid(\n                                title = \"${index + 1}. ${album.album.title}\",\n                                subtitle =\n                                    joinByBullet(\n                                        pluralStringResource(\n                                            R.plurals.n_time,\n                                            album.songCountListened ?: 0,\n                                            album.songCountListened ?: 0,\n                                        ),\n                                        makeTimeString(album.timeListened),\n                                    ),\n                                thumbnailUrl = album.album.thumbnailUrl,\n                                isActive = album.id == mediaMetadata?.album?.id,\n                                isPlaying = isPlaying,\n                                modifier =\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .combinedClickable(\n                                            onClick = {\n                                                navController.navigate(\"album/${album.id}\")\n                                            },\n                                            onLongClick = {\n                                                haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                menuState.show {\n                                                    AlbumMenu(\n                                                        originalAlbum = album,\n                                                        navController = navController,\n                                                        onDismiss = menuState::dismiss,\n                                                    )\n                                                }\n                                            },\n                                        ).animateItem(),\n                            )\n                        }\n                    }\n                }\n            }\n            }\n\n            if (isSearching) {\n                items(\n                    items = allArtists.filter { artist ->\n                        artist.artist.name.contains(query.text, ignoreCase = true)\n                    },\n                    key = { it.id },\n                    contentType = { CONTENT_TYPE_ARTIST },\n                ) { artist ->\n                    val uiArtist = Artist(name = artist.artist.name, id = artist.id)\n                    val isChecked = sArtists.any { it.id == uiArtist.id }\n                    Row( // Use a row to arrange the checkbox and ArtistListItem horizontally\n                        verticalAlignment = Alignment.CenterVertically,\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .clickable {\n                                toggleArtistSelection(uiArtist)\n                            }\n                            .padding(8.dp)\n                    ) {\n                        ArtistListItem(\n                            artist = artist,\n                            modifier = Modifier.weight(1f) // Allow ArtistListItem to take remaining space\n                        )\n\n                        Checkbox(\n\n                            checked = sArtists.contains(Artist(name = artist.artist.name, id = artist.id)), // Get the current checked state\n                            onCheckedChange = {\n                                toggleArtistSelection(uiArtist)\n                            }\n                        )\n                    }\n                }\n            }\n            if (query.text.isNotEmpty() && filteredArtists.isEmpty()) {\n                item(key = \"no_result\") {\n                    EmptyPlaceholder(\n                        icon = R.drawable.search,\n                        text = stringResource(R.string.no_results_found),\n                    )\n                }\n            }\n        }\n        // FAB to shuffle most played songs\n        if (mostPlayedSongsStats.isNotEmpty() && !isSearching) {\n            HideOnScrollFAB(\n                visible = true,\n                lazyListState = lazyListState,\n                icon = R.drawable.shuffle,\n                onClick = {\n                    playerConnection.playQueue(\n                        ListQueue(\n                            title = context.getString(R.string.most_played_songs),\n                            items = orderedMostPlayedSongs.map { it.toMediaMetadata().toMediaItem() }.shuffled(),\n                        ),\n                    )\n                },\n            )\n        }\n    }\n\n\n\n    TopAppBar(\n        title = {\n            if (inSelectMode) {\n                Text(pluralStringResource(R.plurals.n_selected, selection.size, selection.size))\n            } else if (isSearching) {\n                Row {\n                    TextField(\n                        value = query,\n                        onValueChange = { query = it },\n                        placeholder = {\n                            Text(\n                                text = stringResource(R.string.search),\n                                style = MaterialTheme.typography.titleLarge\n                            )\n                        },\n                        singleLine = true,\n                        textStyle = MaterialTheme.typography.titleLarge,\n                        keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),\n                        colors = TextFieldDefaults.colors(\n                            focusedContainerColor = Color.Transparent,\n                            unfocusedContainerColor = Color.Transparent,\n                            focusedIndicatorColor = Color.Transparent,\n                            unfocusedIndicatorColor = Color.Transparent,\n                            disabledIndicatorColor = Color.Transparent,\n                        ),\n                        modifier = Modifier\n                            .weight(1f)\n                            .focusRequester(focusRequester)\n                    )\n\n                    if (sArtists.isNotEmpty()) {\n                        androidx.compose.material3.IconButton(onClick = clearArtistSelection) {\n                            Icon(\n                                painter = painterResource(R.drawable.close),\n                                contentDescription = \"Clear Artists\",\n                                tint = MaterialTheme.colorScheme.onSurface\n                            )\n                        }\n                    }\n                }\n\n            } else {\n                Text(stringResource(R.string.stats))\n            }\n        },\n        navigationIcon = {\n            if (inSelectMode) {\n                androidx.compose.material3.IconButton(onClick = onExitSelectionMode) {\n                    Icon(\n                        painter = painterResource(R.drawable.close),\n                        contentDescription = \"Select Button\",\n                    )\n                }\n            } else {\n                IconButton(\n                    onClick = {\n                        if (isSearching) {\n                            isSearching = false\n                            query = TextFieldValue()\n                        } else {\n                            navController.navigateUp()\n                        }\n                    },\n                    onLongClick = {\n                        if (!isSearching) {\n                            navController.backToMain()\n                        }\n                    }\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.arrow_back),\n                        contentDescription = \"Back Button\"\n                    )\n                }\n            }\n        },\n        actions = {\n            if (inSelectMode) {\n                Checkbox(\n                    checked = true,\n                    onCheckedChange = {\n                    }\n                )\n                androidx.compose.material3.IconButton(\n                    enabled = selection.isNotEmpty(),\n                    onClick = {\n                    }\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.more_vert),\n                        contentDescription = \"More Button\"\n                    )\n                }\n            } else if (!isSearching) {\n                androidx.compose.material3.IconButton(\n                    onClick = { isSearching = true }\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.search),\n                        contentDescription = \"Search Button\"\n                    )\n                }\n                IconButton(\n                    onClick = {showTimeTransfer = true},\n                    onLongClick = {showTimeTransfer = true},\n                ) {\n                    Icon(\n                        painterResource(R.drawable.sync),\n                        contentDescription = \"Time Transfer\",\n                    )\n                }\n            }\n        }\n    )\n}\n\n\nenum class OptionStats { WEEKS, MONTHS, YEARS, CONTINUOUS }\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/YouTubeBrowseScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.items\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.ArtistItem\nimport com.metrolist.innertube.models.EpisodeItem\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.PodcastItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.GridItemSize\nimport com.metrolist.music.constants.GridItemsSizeKey\nimport com.metrolist.music.constants.GridThumbnailHeight\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.playback.queues.YouTubeQueue\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.YouTubeGridItem\nimport com.metrolist.music.ui.component.shimmer.GridItemPlaceHolder\nimport com.metrolist.music.ui.component.shimmer.ShimmerHost\nimport com.metrolist.music.ui.menu.YouTubeAlbumMenu\nimport com.metrolist.music.ui.menu.YouTubeArtistMenu\nimport com.metrolist.music.ui.menu.YouTubePlaylistMenu\nimport com.metrolist.music.ui.menu.YouTubeSongMenu\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.viewmodels.YouTubeBrowseViewModel\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)\n@Composable\nfun YouTubeBrowseScreen(\n    navController: NavController,\n    viewModel: YouTubeBrowseViewModel = hiltViewModel(),\n) {\n    val menuState = LocalMenuState.current\n    val haptic = LocalHapticFeedback.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n\n    val browseResult by viewModel.result.collectAsState()\n\n    val coroutineScope = rememberCoroutineScope()\n    val gridItemSize by rememberEnumPreference(GridItemsSizeKey, GridItemSize.BIG)\n\n    val allItems = browseResult?.items?.flatMap { it.items } ?: emptyList()\n\n    LazyVerticalGrid(\n        columns = GridCells.Adaptive(minSize = GridThumbnailHeight + if (gridItemSize == GridItemSize.BIG) 24.dp else (-24).dp),\n        contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n    ) {\n        if (browseResult == null) {\n            items(8) {\n                ShimmerHost {\n                    GridItemPlaceHolder(fillMaxWidth = true)\n                }\n            }\n        }\n\n        items(\n            items = allItems.distinctBy { it.id },\n            key = { it.id }\n        ) { item ->\n            YouTubeGridItem(\n                item = item,\n                isActive = when (item) {\n                    is SongItem -> mediaMetadata?.id == item.id\n                    is AlbumItem -> mediaMetadata?.album?.id == item.id\n                    else -> false\n                },\n                isPlaying = isPlaying,\n                fillMaxWidth = true,\n                coroutineScope = coroutineScope,\n                modifier = Modifier\n                    .combinedClickable(\n                        onClick = {\n                            when (item) {\n                                is SongItem -> {\n                                    if (item.id == mediaMetadata?.id) {\n                                        playerConnection.togglePlayPause()\n                                    } else {\n                                        playerConnection.playQueue(\n                                            YouTubeQueue.radio(item.toMediaMetadata())\n                                        )\n                                    }\n                                }\n                                is AlbumItem -> navController.navigate(\"album/${item.id}\")\n                                is ArtistItem -> navController.navigate(\"artist/${item.id}\")\n                                is PlaylistItem -> navController.navigate(\"online_playlist/${item.id}\")\n                                is PodcastItem -> navController.navigate(\"online_podcast/${item.id}\")\n                                is EpisodeItem -> {\n                                    if (item.id == mediaMetadata?.id) {\n                                        playerConnection.togglePlayPause()\n                                    } else {\n                                        playerConnection.playQueue(\n                                            YouTubeQueue.radio(item.toMediaMetadata())\n                                        )\n                                    }\n                                }\n                            }\n                        },\n                        onLongClick = {\n                            haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                            menuState.show {\n                                when (item) {\n                                    is SongItem ->\n                                        YouTubeSongMenu(\n                                            song = item,\n                                            navController = navController,\n                                            onDismiss = menuState::dismiss,\n                                        )\n                                    is AlbumItem ->\n                                        YouTubeAlbumMenu(\n                                            albumItem = item,\n                                            navController = navController,\n                                            onDismiss = menuState::dismiss,\n                                        )\n                                    is ArtistItem ->\n                                        YouTubeArtistMenu(\n                                            artist = item,\n                                            onDismiss = menuState::dismiss,\n                                        )\n                                    is PlaylistItem ->\n                                        YouTubePlaylistMenu(\n                                            playlist = item,\n                                            coroutineScope = coroutineScope,\n                                            onDismiss = menuState::dismiss,\n                                        )\n                                    is PodcastItem ->\n                                        YouTubePlaylistMenu(\n                                            playlist = item.asPlaylistItem(),\n                                            coroutineScope = coroutineScope,\n                                            onDismiss = menuState::dismiss,\n                                        )\n                                    is EpisodeItem ->\n                                        YouTubeSongMenu(\n                                            song = item.asSongItem(),\n                                            navController = navController,\n                                            onDismiss = menuState::dismiss,\n                                        )\n                                }\n                            }\n                        }\n                    )\n                    .animateItem()\n            )\n        }\n    }\n\n    TopAppBar(\n        title = { Text(browseResult?.title.orEmpty()) },\n        navigationIcon = {\n            IconButton(\n                onClick = navController::navigateUp,\n                onLongClick = navController::backToMain\n            ) {\n                Icon(\n                    painterResource(R.drawable.arrow_back),\n                    contentDescription = null\n                )\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/artist/ArtistAlbumsScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.artist\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.GridItemSpan\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.items\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarScrollBehavior\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.listSaver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.toMutableStateList\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.pluralStringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.CONTENT_TYPE_ALBUM\nimport com.metrolist.music.constants.CONTENT_TYPE_HEADER\nimport com.metrolist.music.constants.GridItemSize\nimport com.metrolist.music.constants.GridItemsSizeKey\nimport com.metrolist.music.constants.GridThumbnailHeight\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.LibraryAlbumGridItem\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.viewmodels.ArtistAlbumsViewModel\n\n@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun ArtistAlbumsScreen(\n    navController: NavController,\n    scrollBehavior: TopAppBarScrollBehavior,\n    viewModel: ArtistAlbumsViewModel = hiltViewModel(),\n) {\n    val menuState = LocalMenuState.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n\n    val artist by viewModel.artist.collectAsState()\n    val albums by viewModel.albums.collectAsState()\n\n    val coroutineScope = rememberCoroutineScope()\n    val lazyGridState = rememberLazyGridState()\n    val gridItemSize by rememberEnumPreference(GridItemsSizeKey, GridItemSize.BIG)\n\n    var inSelectMode by rememberSaveable { mutableStateOf(false) }\n    val selection = rememberSaveable(\n        saver = listSaver<MutableList<String>, String>(\n            save = { it.toList() },\n            restore = { it.toMutableStateList() }\n        )\n    ) { mutableStateListOf() }\n    val onExitSelectionMode = {\n        inSelectMode = false\n        selection.clear()\n    }\n    if (inSelectMode) {\n        BackHandler(onBack = onExitSelectionMode)\n    }\n\n    val snackbarHostState = remember { SnackbarHostState() }\n\n    Box(\n        modifier = Modifier.fillMaxSize()\n    ) {\n        LazyVerticalGrid(\n            state = lazyGridState,\n            columns = GridCells.Adaptive(minSize = GridThumbnailHeight + if (gridItemSize == GridItemSize.BIG) 24.dp else (-24).dp),\n            contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues()\n        ) {\n            item(\n                key = \"header\",\n                span = { GridItemSpan(maxLineSpan) },\n                contentType = CONTENT_TYPE_HEADER\n            ) {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    modifier = Modifier.padding(horizontal = 16.dp)\n                ) {\n                    Spacer(Modifier.weight(1f))\n\n                    Text(\n                        text = pluralStringResource(R.plurals.n_album, albums.size, albums.size),\n                        style = MaterialTheme.typography.titleSmall,\n                        color = MaterialTheme.colorScheme.secondary\n                    )\n                }\n            }\n\n            items(\n                items = albums.distinctBy { it.id },\n                key = { it.id },\n                contentType = { CONTENT_TYPE_ALBUM }\n            ) { album ->\n                LibraryAlbumGridItem(\n                    navController = navController,\n                    menuState = menuState,\n                    coroutineScope = coroutineScope,\n                    album = album,\n                    isActive = album.id == mediaMetadata?.album?.id,\n                    isPlaying = isPlaying,\n                    modifier = Modifier.animateItem()\n                )\n            }\n        }\n\n        TopAppBar(\n            title = { Text(artist?.artist?.name.orEmpty()) },\n            navigationIcon = {\n                IconButton(\n                    onClick = navController::navigateUp,\n                    onLongClick = navController::backToMain\n                ) {\n                    Icon(\n                        painter = painterResource(id = R.drawable.arrow_back),\n                        contentDescription = null\n                    )\n                }\n            },\n            scrollBehavior = scrollBehavior\n        )\n\n        SnackbarHost(\n            hostState = snackbarHostState,\n            modifier = Modifier\n                .windowInsetsPadding(LocalPlayerAwareWindowInsets.current)\n                .align(Alignment.BottomCenter)\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/artist/ArtistItemsScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.artist\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.items\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.snapshotFlow\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.ArtistItem\nimport com.metrolist.innertube.models.EpisodeItem\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.PodcastItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.WatchEndpoint\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.GridItemSize\nimport com.metrolist.music.constants.GridItemsSizeKey\nimport com.metrolist.music.constants.GridThumbnailHeight\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.playback.queues.YouTubeQueue\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.YouTubeGridItem\nimport com.metrolist.music.ui.component.YouTubeListItem\nimport com.metrolist.music.ui.component.shimmer.GridItemPlaceHolder\nimport com.metrolist.music.ui.component.shimmer.ListItemPlaceHolder\nimport com.metrolist.music.ui.component.shimmer.ShimmerHost\nimport com.metrolist.music.ui.menu.YouTubeAlbumMenu\nimport com.metrolist.music.ui.menu.YouTubeArtistMenu\nimport com.metrolist.music.ui.menu.YouTubePlaylistMenu\nimport com.metrolist.music.ui.menu.YouTubeSongMenu\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.viewmodels.ArtistItemsViewModel\n\n@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun ArtistItemsScreen(\n    navController: NavController,\n    viewModel: ArtistItemsViewModel = hiltViewModel(),\n) {\n    val menuState = LocalMenuState.current\n    val haptic = LocalHapticFeedback.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n\n    val lazyListState = rememberLazyListState()\n    val lazyGridState = rememberLazyGridState()\n    val coroutineScope = rememberCoroutineScope()\n    val gridItemSize by rememberEnumPreference(GridItemsSizeKey, GridItemSize.BIG)\n\n    val title by viewModel.title.collectAsState()\n    val itemsPage by viewModel.itemsPage.collectAsState()\n\n    LaunchedEffect(lazyListState) {\n        snapshotFlow {\n            lazyListState.layoutInfo.visibleItemsInfo.any { it.key == \"loading\" }\n        }.collect { shouldLoadMore ->\n            if (!shouldLoadMore) return@collect\n            viewModel.loadMore()\n        }\n    }\n\n    LaunchedEffect(lazyGridState) {\n        snapshotFlow {\n            lazyGridState.layoutInfo.visibleItemsInfo.any { it.key == \"loading\" }\n        }.collect { shouldLoadMore ->\n            if (!shouldLoadMore) return@collect\n            viewModel.loadMore()\n        }\n    }\n\n    if (itemsPage == null) {\n        ShimmerHost(\n            modifier = Modifier.windowInsetsPadding(LocalPlayerAwareWindowInsets.current),\n        ) {\n            repeat(8) {\n                ListItemPlaceHolder()\n            }\n        }\n    }\n\n    if (itemsPage?.items?.firstOrNull() is SongItem) {\n        LazyColumn(\n            state = lazyListState,\n            contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n        ) {\n            items(\n                items = itemsPage?.items.orEmpty().distinctBy { it.id },\n                key = { it.id },\n            ) { item ->\n                YouTubeListItem(\n                    item = item,\n                    isActive =\n                    when (item) {\n                        is SongItem -> mediaMetadata?.id == item.id\n                        is AlbumItem -> mediaMetadata?.album?.id == item.id\n                        else -> false\n                    },\n                    isPlaying = isPlaying,\n                    trailingContent = {\n                        IconButton(\n                            onClick = {\n                                menuState.show {\n                                    when (item) {\n                                        is SongItem ->\n                                            YouTubeSongMenu(\n                                                song = item,\n                                                navController = navController,\n                                                onDismiss = menuState::dismiss,\n                                            )\n\n                                        is AlbumItem ->\n                                            YouTubeAlbumMenu(\n                                                albumItem = item,\n                                                navController = navController,\n                                                onDismiss = menuState::dismiss,\n                                            )\n\n                                        is ArtistItem ->\n                                            YouTubeArtistMenu(\n                                                artist = item,\n                                                onDismiss = menuState::dismiss,\n                                            )\n\n                                        is PlaylistItem ->\n                                            YouTubePlaylistMenu(\n                                                playlist = item,\n                                                coroutineScope = coroutineScope,\n                                                onDismiss = menuState::dismiss,\n                                            )\n\n                                        is PodcastItem ->\n                                            YouTubePlaylistMenu(\n                                                playlist = item.asPlaylistItem(),\n                                                coroutineScope = coroutineScope,\n                                                onDismiss = menuState::dismiss,\n                                            )\n\n                                        is EpisodeItem ->\n                                            YouTubeSongMenu(\n                                                song = item.asSongItem(),\n                                                navController = navController,\n                                                onDismiss = menuState::dismiss,\n                                            )\n                                    }\n                                }\n                            },\n                        ) {\n                            Icon(\n                                painter = painterResource(R.drawable.more_vert),\n                                contentDescription = null,\n                            )\n                        }\n                    },\n                    modifier =\n                    Modifier\n                        .clickable {\n                            when (item) {\n                                is SongItem -> {\n                                    if (item.id == mediaMetadata?.id) {\n                                        playerConnection.togglePlayPause()\n                                    } else {\n                                        playerConnection.playQueue(\n                                            YouTubeQueue(\n                                                item.endpoint ?: WatchEndpoint(videoId = item.id),\n                                                item.toMediaMetadata()\n                                            ),\n                                        )\n                                    }\n                                }\n\n                                is AlbumItem -> navController.navigate(\"album/${item.id}\")\n                                is ArtistItem -> navController.navigate(\"artist/${item.id}\")\n                                is PlaylistItem -> navController.navigate(\"online_playlist/${item.id}\")\n                                is PodcastItem -> navController.navigate(\"online_podcast/${item.id}\")\n                                is EpisodeItem -> {\n                                    if (item.id == mediaMetadata?.id) {\n                                        playerConnection.togglePlayPause()\n                                    } else {\n                                        playerConnection.playQueue(\n                                            YouTubeQueue(\n                                                item.endpoint ?: WatchEndpoint(videoId = item.id),\n                                                item.toMediaMetadata()\n                                            ),\n                                        )\n                                    }\n                                }\n                            }\n                        },\n                )\n            }\n\n            if (itemsPage?.continuation != null) {\n                item(key = \"loading\") {\n                    ShimmerHost {\n                        repeat(3) {\n                            ListItemPlaceHolder()\n                        }\n                    }\n                }\n            }\n        }\n    } else {\n        LazyVerticalGrid(\n            state = lazyGridState,\n            columns = GridCells.Adaptive(minSize = GridThumbnailHeight + if (gridItemSize == GridItemSize.BIG) 24.dp else (-24).dp),\n            contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues()\n        ) {\n            items(\n                items = itemsPage?.items.orEmpty().distinctBy { it.id },\n                key = { it.id }\n            ) { item ->\n                YouTubeGridItem(\n                    item = item,\n                    isActive = when (item) {\n                        is SongItem -> mediaMetadata?.id == item.id\n                        is AlbumItem -> mediaMetadata?.album?.id == item.id\n                        else -> false\n                    },\n                    isPlaying = isPlaying,\n                    fillMaxWidth = true,\n                    coroutineScope = coroutineScope,\n                    modifier = Modifier\n                        .combinedClickable(\n                            onClick = {\n                                when (item) {\n                                    is SongItem -> playerConnection.playQueue(\n                                        YouTubeQueue(\n                                            item.endpoint ?: WatchEndpoint(videoId = item.id),\n                                            item.toMediaMetadata()\n                                        )\n                                    )\n\n                                    is AlbumItem -> navController.navigate(\"album/${item.id}\")\n                                    is ArtistItem -> navController.navigate(\"artist/${item.id}\")\n                                    is PlaylistItem -> navController.navigate(\"online_playlist/${item.id}\")\n                                    is PodcastItem -> navController.navigate(\"online_podcast/${item.id}\")\n                                    is EpisodeItem -> playerConnection.playQueue(\n                                        YouTubeQueue(\n                                            item.endpoint ?: WatchEndpoint(videoId = item.id),\n                                            item.toMediaMetadata()\n                                        )\n                                    )\n                                }\n                            },\n                            onLongClick = {\n                                haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                menuState.show {\n                                    when (item) {\n                                        is SongItem -> YouTubeSongMenu(\n                                            song = item,\n                                            navController = navController,\n                                            onDismiss = menuState::dismiss\n                                        )\n\n                                        is AlbumItem -> YouTubeAlbumMenu(\n                                            albumItem = item,\n                                            navController = navController,\n                                            onDismiss = menuState::dismiss\n                                        )\n\n                                        is ArtistItem -> YouTubeArtistMenu(\n                                            artist = item,\n                                            onDismiss = menuState::dismiss\n                                        )\n\n                                        is PlaylistItem -> YouTubePlaylistMenu(\n                                            playlist = item,\n                                            coroutineScope = coroutineScope,\n                                            onDismiss = menuState::dismiss\n                                        )\n\n                                        is PodcastItem -> YouTubePlaylistMenu(\n                                            playlist = item.asPlaylistItem(),\n                                            coroutineScope = coroutineScope,\n                                            onDismiss = menuState::dismiss\n                                        )\n\n                                        is EpisodeItem -> YouTubeSongMenu(\n                                            song = item.asSongItem(),\n                                            navController = navController,\n                                            onDismiss = menuState::dismiss\n                                        )\n                                    }\n                                }\n                            }\n                        )\n                        .animateItem()\n                )\n            }\n\n            if (itemsPage?.continuation != null) {\n                item(key = \"loading\") {\n                    ShimmerHost(Modifier.animateItem()) {\n                        GridItemPlaceHolder(fillMaxWidth = true)\n                    }\n                }\n            }\n        }\n    }\n\n    TopAppBar(\n        title = { Text(title) },\n        navigationIcon = {\n            IconButton(\n                onClick = navController::navigateUp,\n                onLongClick = navController::backToMain,\n            ) {\n                Icon(\n                    painterResource(R.drawable.arrow_back),\n                    contentDescription = null,\n                )\n            }\n        },\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/artist/ArtistScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.artist\n\nimport android.content.ClipData\nimport android.content.ClipboardManager\nimport android.content.Context\nimport android.widget.Toast\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedButton\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.platform.LocalResources\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.compose.ui.util.fastForEach\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport coil3.compose.AsyncImage\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.ArtistItem\nimport com.metrolist.innertube.models.EpisodeItem\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.PodcastItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.WatchEndpoint\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalListenTogetherManager\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.AppBarHeight\nimport com.metrolist.music.constants.HideExplicitKey\nimport com.metrolist.music.constants.ShowArtistDescriptionKey\nimport com.metrolist.music.constants.ShowArtistSubscriberCountKey\nimport com.metrolist.music.constants.ShowMonthlyListenersKey\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.playback.queues.ListQueue\nimport com.metrolist.music.playback.queues.YouTubeQueue\nimport com.metrolist.music.ui.component.AlbumGridItem\nimport com.metrolist.music.ui.component.ExpandableText\nimport com.metrolist.music.ui.component.HideOnScrollFAB\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.LinkSegment\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.NavigationTitle\nimport com.metrolist.music.ui.component.SongListItem\nimport com.metrolist.music.ui.component.YouTubeGridItem\nimport com.metrolist.music.ui.component.YouTubeListItem\nimport com.metrolist.music.ui.component.shimmer.ButtonPlaceholder\nimport com.metrolist.music.ui.component.shimmer.ListItemPlaceHolder\nimport com.metrolist.music.ui.component.shimmer.ShimmerHost\nimport com.metrolist.music.ui.component.shimmer.TextPlaceholder\nimport com.metrolist.music.ui.menu.AlbumMenu\nimport com.metrolist.music.ui.menu.SongMenu\nimport com.metrolist.music.ui.menu.YouTubeAlbumMenu\nimport com.metrolist.music.ui.menu.YouTubeArtistMenu\nimport com.metrolist.music.ui.menu.YouTubePlaylistMenu\nimport com.metrolist.music.ui.menu.YouTubeSongMenu\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.ui.utils.fadingEdge\nimport com.metrolist.music.ui.utils.isScrollingUp\nimport com.metrolist.music.ui.utils.resize\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.viewmodels.ArtistViewModel\nimport com.valentinilk.shimmer.shimmer\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\n\n@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun ArtistScreen(\n    navController: NavController,\n    viewModel: ArtistViewModel = hiltViewModel(),\n) {\n    val context = LocalContext.current\n    val database = LocalDatabase.current\n    val menuState = LocalMenuState.current\n    val haptic = LocalHapticFeedback.current\n    val coroutineScope = rememberCoroutineScope()\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val isGuest = listenTogetherManager?.isInRoom == true && !listenTogetherManager.isHost\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n    val artistPage = viewModel.artistPage\n    val libraryArtist by viewModel.libraryArtist.collectAsState()\n    val librarySongs by viewModel.librarySongs.collectAsState()\n    val libraryAlbums by viewModel.libraryAlbums.collectAsState()\n    val isChannelSubscribed by viewModel.isChannelSubscribed.collectAsState()\n    val hideExplicit by rememberPreference(key = HideExplicitKey, defaultValue = false)\n    val showArtistDescription by rememberPreference(key = ShowArtistDescriptionKey, defaultValue = true)\n    val showArtistSubscriberCount by rememberPreference(key = ShowArtistSubscriberCountKey, defaultValue = true)\n    val showMonthlyListeners by rememberPreference(key = ShowMonthlyListenersKey, defaultValue = true)\n\n    val lazyListState = rememberLazyListState()\n    val snackbarHostState = remember { SnackbarHostState() }\n    var showLocal by rememberSaveable { mutableStateOf(false) }\n    val density = LocalDensity.current\n\n    // Calculate the offset value outside of the offset lambda\n    val systemBarsTopPadding = WindowInsets.systemBars.asPaddingValues().calculateTopPadding()\n    val headerOffset =\n        with(density) {\n            -(systemBarsTopPadding + AppBarHeight).roundToPx()\n        }\n\n    val transparentAppBar by remember {\n        derivedStateOf {\n            lazyListState.firstVisibleItemIndex == 0 && lazyListState.firstVisibleItemScrollOffset < 100\n        }\n    }\n\n    LaunchedEffect(libraryArtist) {\n        // always show local page for local artists. Show local page remote artist when offline\n        showLocal = libraryArtist?.artist?.isLocal == true\n    }\n\n    Box(\n        modifier = Modifier.fillMaxSize(),\n    ) {\n        LazyColumn(\n            state = lazyListState,\n            contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n        ) {\n            if (artistPage == null && !showLocal) {\n                item(key = \"shimmer\") {\n                    ShimmerHost(\n                        modifier =\n                            Modifier\n                                .offset {\n                                    IntOffset(x = 0, y = headerOffset)\n                                },\n                    ) {\n                        // Artist Image Placeholder\n                        Box(\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .aspectRatio(1.1f),\n                        ) {\n                            Spacer(\n                                modifier =\n                                    Modifier\n                                        .fillMaxSize()\n                                        .shimmer()\n                                        .background(MaterialTheme.colorScheme.onSurface)\n                                        .fadingEdge(\n                                            top = systemBarsTopPadding + AppBarHeight,\n                                            bottom = 200.dp,\n                                        ),\n                            )\n                        }\n                        // Artist Name and Controls Section\n                        Column(\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .padding(16.dp),\n                        ) {\n                            // Artist Name Placeholder\n                            TextPlaceholder(\n                                height = 36.dp,\n                                modifier =\n                                    Modifier\n                                        .fillMaxWidth(0.7f)\n                                        .padding(bottom = 16.dp),\n                            )\n\n                            // Buttons Row Placeholder\n                            Row(\n                                modifier = Modifier.fillMaxWidth(),\n                                verticalAlignment = Alignment.CenterVertically,\n                            ) {\n                                // Subscribe Button Placeholder\n                                ButtonPlaceholder(\n                                    modifier =\n                                        Modifier\n                                            .width(120.dp)\n                                            .height(40.dp),\n                                )\n\n                                Spacer(modifier = Modifier.weight(1f))\n\n                                // Right side buttons\n                                Row(\n                                    horizontalArrangement = Arrangement.spacedBy(16.dp),\n                                    verticalAlignment = Alignment.CenterVertically,\n                                ) {\n                                    // Radio Button Placeholder\n                                    ButtonPlaceholder(\n                                        modifier =\n                                            Modifier\n                                                .width(100.dp)\n                                                .height(40.dp),\n                                    )\n\n                                    // Shuffle Button Placeholder\n                                    Box(\n                                        modifier =\n                                            Modifier\n                                                .size(48.dp)\n                                                .shimmer()\n                                                .background(\n                                                    MaterialTheme.colorScheme.onSurface,\n                                                    RoundedCornerShape(24.dp),\n                                                ),\n                                    )\n                                }\n                            }\n                        }\n                        // Songs List Placeholder\n                        repeat(6) {\n                            ListItemPlaceHolder()\n                        }\n                    }\n                }\n            } else {\n                item(key = \"header\") {\n                    val thumbnail = artistPage?.artist?.thumbnail ?: libraryArtist?.artist?.thumbnailUrl\n                    val artistName = artistPage?.artist?.title ?: libraryArtist?.artist?.name\n\n                    Box {\n                        // Artist Image with offset\n                        if (thumbnail != null) {\n                            Box(\n                                modifier =\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .aspectRatio(1f)\n                                        .offset {\n                                            IntOffset(x = 0, y = headerOffset)\n                                        },\n                            ) {\n                                AsyncImage(\n                                    model = thumbnail.resize(1200, 1200),\n                                    contentDescription = null,\n                                    modifier =\n                                        Modifier\n                                            .fillMaxWidth()\n                                            .align(Alignment.TopCenter)\n                                            .fadingEdge(\n                                                bottom = 200.dp,\n                                            ),\n                                )\n                            }\n                        }\n\n                        // Artist Name and Controls Section - positioned at bottom of image\n                        Column(\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .padding(\n                                        top =\n                                            if (thumbnail != null) {\n                                                // Position content at the bottom part of the image\n                                                // Using screen width to calculate aspect ratio height minus overlap\n                                                LocalResources.current.displayMetrics.widthPixels.let { screenWidth ->\n                                                    with(density) {\n                                                        ((screenWidth / 1.2f) - 144).toDp()\n                                                    }\n                                                }\n                                            } else {\n                                                16.dp\n                                            },\n                                    ),\n                        ) {\n                            Column(\n                                modifier =\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .padding(horizontal = 16.dp),\n                            ) {\n                                // Artist Name\n                                Text(\n                                    text = artistName ?: \"Unknown\",\n                                    style = MaterialTheme.typography.headlineLarge,\n                                    fontWeight = FontWeight.Bold,\n                                    maxLines = 1,\n                                    overflow = TextOverflow.Ellipsis,\n                                    fontSize = 32.sp,\n                                    modifier = Modifier.padding(bottom = 16.dp),\n                                )\n\n                                // Buttons Row\n                                Row(\n                                    modifier = Modifier.fillMaxWidth(),\n                                    verticalAlignment = Alignment.CenterVertically,\n                                ) {\n                                    // Subscribe Button\n                                    OutlinedButton(\n                                        onClick = {\n                                            viewModel.toggleChannelSubscription()\n                                        },\n                                        colors =\n                                            ButtonDefaults.outlinedButtonColors(\n                                                containerColor =\n                                                    if (isChannelSubscribed) {\n                                                        MaterialTheme.colorScheme.surface\n                                                    } else {\n                                                        Color.Transparent\n                                                    },\n                                            ),\n                                        shape = RoundedCornerShape(50),\n                                        modifier = Modifier.height(40.dp),\n                                    ) {\n                                        Text(\n                                            text = stringResource(if (isChannelSubscribed) R.string.subscribed else R.string.subscribe),\n                                            fontSize = 14.sp,\n                                            color = if (!isChannelSubscribed) MaterialTheme.colorScheme.error else LocalContentColor.current,\n                                        )\n                                    }\n\n                                    Spacer(modifier = Modifier.weight(1f))\n\n                                    Row(\n                                        horizontalArrangement = Arrangement.spacedBy(16.dp),\n                                        verticalAlignment = Alignment.CenterVertically,\n                                    ) {\n                                        // Radio Button\n                                        if (!showLocal && !isGuest) {\n                                            artistPage?.artist?.radioEndpoint?.let { radioEndpoint ->\n                                                OutlinedButton(\n                                                    onClick = {\n                                                        playerConnection.playQueue(YouTubeQueue(radioEndpoint))\n                                                    },\n                                                    shape = RoundedCornerShape(50),\n                                                    modifier = Modifier.height(40.dp),\n                                                ) {\n                                                    Icon(\n                                                        painter = painterResource(R.drawable.radio),\n                                                        contentDescription = null,\n                                                        modifier = Modifier.size(20.dp),\n                                                    )\n                                                    Spacer(modifier = Modifier.width(8.dp))\n                                                    Text(\n                                                        text = stringResource(R.string.radio),\n                                                        fontSize = 14.sp,\n                                                    )\n                                                }\n                                            }\n                                        }\n\n                                        // Shuffle Button\n                                        if (!showLocal && !isGuest) {\n                                            artistPage?.artist?.shuffleEndpoint?.let { shuffleEndpoint ->\n                                                IconButton(\n                                                    onClick = {\n                                                        playerConnection.playQueue(YouTubeQueue(shuffleEndpoint))\n                                                    },\n                                                    modifier =\n                                                        Modifier\n                                                            .size(48.dp)\n                                                            .background(\n                                                                MaterialTheme.colorScheme.primary,\n                                                                RoundedCornerShape(24.dp),\n                                                            ),\n                                                ) {\n                                                    Icon(\n                                                        painter = painterResource(R.drawable.shuffle),\n                                                        contentDescription = \"Shuffle\",\n                                                        tint = MaterialTheme.colorScheme.onPrimary,\n                                                        modifier = Modifier.size(20.dp),\n                                                    )\n                                                }\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                            Spacer(modifier = Modifier.height(16.dp))\n                        }\n                    }\n                }\n\n                // About Artist Section\n                if (!showLocal && (showArtistDescription || showArtistSubscriberCount || showMonthlyListeners)) {\n                    val description = artistPage?.description\n                    val descriptionRuns = artistPage?.descriptionRuns\n                    val subscriberCount = artistPage?.subscriberCountText\n                    val monthlyListeners = artistPage?.monthlyListenerCount\n\n                    if ((showArtistDescription && !description.isNullOrEmpty()) ||\n                        (showArtistSubscriberCount && !subscriberCount.isNullOrEmpty()) ||\n                        (showMonthlyListeners && !monthlyListeners.isNullOrEmpty())\n                    ) {\n                        item(key = \"about_artist\") {\n                            Column(\n                                modifier =\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .padding(horizontal = 16.dp)\n                                        .padding(bottom = 16.dp)\n                                        .animateItem(),\n                            ) {\n                                if (showArtistDescription && (!description.isNullOrEmpty() || !descriptionRuns.isNullOrEmpty())) {\n                                    Text(\n                                        text = stringResource(R.string.about_artist),\n                                        style = MaterialTheme.typography.titleMedium,\n                                        fontWeight = FontWeight.Bold,\n                                        modifier = Modifier.padding(bottom = 8.dp),\n                                    )\n                                }\n\n                                if (showArtistSubscriberCount && !subscriberCount.isNullOrEmpty()) {\n                                    Text(\n                                        text = subscriberCount,\n                                        style = MaterialTheme.typography.bodyMedium,\n                                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                                        modifier = Modifier.padding(bottom = 4.dp),\n                                    )\n                                }\n\n                                if (showMonthlyListeners && !monthlyListeners.isNullOrEmpty()) {\n                                    Text(\n                                        text = monthlyListeners,\n                                        style = MaterialTheme.typography.bodyMedium,\n                                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                                        modifier =\n                                            Modifier.padding(\n                                                bottom =\n                                                    if (showArtistDescription &&\n                                                        !description.isNullOrEmpty()\n                                                    ) {\n                                                        8.dp\n                                                    } else {\n                                                        0.dp\n                                                    },\n                                            ),\n                                    )\n                                }\n\n                                if (showArtistDescription && (!description.isNullOrEmpty() || !descriptionRuns.isNullOrEmpty())) {\n                                    ExpandableText(\n                                        text = description.orEmpty(),\n                                        runs =\n                                            descriptionRuns?.map {\n                                                LinkSegment(\n                                                    text = it.text,\n                                                    url = it.navigationEndpoint?.urlEndpoint?.url,\n                                                )\n                                            },\n                                        collapsedMaxLines = 3,\n                                    )\n                                }\n                            }\n                        }\n                    }\n                }\n\n                if (showLocal) {\n                    if (librarySongs.isNotEmpty()) {\n                        item(key = \"local_songs_title\") {\n                            NavigationTitle(\n                                title = stringResource(R.string.songs),\n                                modifier = Modifier.animateItem(),\n                                onClick = {\n                                    navController.navigate(\"artist/${viewModel.artistId}/songs\")\n                                },\n                            )\n                        }\n\n                        val filteredLibrarySongs =\n                            if (hideExplicit) {\n                                librarySongs.filter { !it.song.explicit }\n                            } else {\n                                librarySongs\n                            }\n                        itemsIndexed(\n                            items = filteredLibrarySongs,\n                            key = { index, item -> \"local_song_${item.id}_$index\" },\n                        ) { index, song ->\n                            SongListItem(\n                                song = song,\n                                showInLibraryIcon = true,\n                                isActive = song.id == mediaMetadata?.id,\n                                isPlaying = isPlaying,\n                                trailingContent = {\n                                    IconButton(\n                                        onClick = {\n                                            menuState.show {\n                                                SongMenu(\n                                                    originalSong = song,\n                                                    navController = navController,\n                                                    onDismiss = menuState::dismiss,\n                                                )\n                                            }\n                                        },\n                                    ) {\n                                        Icon(\n                                            painter = painterResource(R.drawable.more_vert),\n                                            contentDescription = null,\n                                        )\n                                    }\n                                },\n                                modifier =\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .combinedClickable(\n                                            onClick = {\n                                                if (!isGuest) {\n                                                    if (song.id == mediaMetadata?.id) {\n                                                        playerConnection.togglePlayPause()\n                                                    } else {\n                                                        playerConnection.playQueue(\n                                                            ListQueue(\n                                                                title = libraryArtist?.artist?.name ?: \"Unknown Artist\",\n                                                                items = librarySongs.map { it.toMediaItem() },\n                                                                startIndex = index,\n                                                            ),\n                                                        )\n                                                    }\n                                                }\n                                            },\n                                            onLongClick = {\n                                                haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                menuState.show {\n                                                    SongMenu(\n                                                        originalSong = song,\n                                                        navController = navController,\n                                                        onDismiss = menuState::dismiss,\n                                                    )\n                                                }\n                                            },\n                                        ).animateItem(),\n                            )\n                        }\n                    }\n\n                    if (libraryAlbums.isNotEmpty()) {\n                        item(key = \"local_albums_title\") {\n                            NavigationTitle(\n                                title = stringResource(R.string.albums),\n                                modifier = Modifier.animateItem(),\n                                onClick = {\n                                    navController.navigate(\"artist/${viewModel.artistId}/albums\")\n                                },\n                            )\n                        }\n\n                        item(key = \"local_albums_list\") {\n                            val filteredLibraryAlbums =\n                                if (hideExplicit) {\n                                    libraryAlbums.filter { !it.album.explicit }\n                                } else {\n                                    libraryAlbums\n                                }\n                            LazyRow(\n                                contentPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues(),\n                            ) {\n                                items(\n                                    items = filteredLibraryAlbums,\n                                    key = { \"local_album_${it.id}_${filteredLibraryAlbums.indexOf(it)}\" },\n                                ) { album ->\n                                    AlbumGridItem(\n                                        album = album,\n                                        isActive = mediaMetadata?.album?.id == album.id,\n                                        isPlaying = isPlaying,\n                                        coroutineScope = coroutineScope,\n                                        modifier =\n                                            Modifier\n                                                .combinedClickable(\n                                                    onClick = {\n                                                        navController.navigate(\"album/${album.id}\")\n                                                    },\n                                                    onLongClick = {\n                                                        haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                        menuState.show {\n                                                            AlbumMenu(\n                                                                originalAlbum = album,\n                                                                navController = navController,\n                                                                onDismiss = menuState::dismiss,\n                                                            )\n                                                        }\n                                                    },\n                                                ).animateItem(),\n                                    )\n                                }\n                            }\n                        }\n                    }\n                } else {\n                    artistPage?.sections?.fastForEach { section ->\n                        if (section.items.isNotEmpty()) {\n                            item(key = \"section_${section.title}\") {\n                                NavigationTitle(\n                                    title = section.title,\n                                    modifier = Modifier.animateItem(),\n                                    onClick =\n                                        section.moreEndpoint?.let {\n                                            {\n                                                navController.navigate(\n                                                    \"artist/${viewModel.artistId}/items?browseId=${it.browseId}?params=${it.params}\",\n                                                )\n                                            }\n                                        },\n                                )\n                            }\n                        }\n\n                        if ((section.items.firstOrNull() as? SongItem)?.album != null) {\n                            items(\n                                items = section.items.distinctBy { it.id },\n                                key = { \"youtube_song_${it.id}\" },\n                            ) { song ->\n                                YouTubeListItem(\n                                    item = song as SongItem,\n                                    isActive = mediaMetadata?.id == song.id,\n                                    isPlaying = isPlaying,\n                                    trailingContent = {\n                                        IconButton(\n                                            onClick = {\n                                                menuState.show {\n                                                    YouTubeSongMenu(\n                                                        song = song,\n                                                        navController = navController,\n                                                        onDismiss = menuState::dismiss,\n                                                    )\n                                                }\n                                            },\n                                        ) {\n                                            Icon(\n                                                painter = painterResource(R.drawable.more_vert),\n                                                contentDescription = null,\n                                            )\n                                        }\n                                    },\n                                    modifier =\n                                        Modifier\n                                            .combinedClickable(\n                                                onClick = {\n                                                    if (!isGuest) {\n                                                        if (song.id == mediaMetadata?.id) {\n                                                            playerConnection.togglePlayPause()\n                                                        } else {\n                                                            playerConnection.playQueue(\n                                                                YouTubeQueue(\n                                                                    WatchEndpoint(videoId = song.id),\n                                                                    song.toMediaMetadata(),\n                                                                ),\n                                                            )\n                                                        }\n                                                    }\n                                                },\n                                                onLongClick = {\n                                                    haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                    menuState.show {\n                                                        YouTubeSongMenu(\n                                                            song = song,\n                                                            navController = navController,\n                                                            onDismiss = menuState::dismiss,\n                                                        )\n                                                    }\n                                                },\n                                            ).animateItem(),\n                                )\n                            }\n                        } else {\n                            item(key = \"section_list_${section.title}\") {\n                                LazyRow(\n                                    contentPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues(),\n                                ) {\n                                    items(\n                                        items = section.items.distinctBy { it.id },\n                                        key = { \"youtube_album_${it.id}\" },\n                                    ) { item ->\n                                        YouTubeGridItem(\n                                            item = item,\n                                            isActive =\n                                                when (item) {\n                                                    is SongItem -> mediaMetadata?.id == item.id\n                                                    is AlbumItem -> mediaMetadata?.album?.id == item.id\n                                                    else -> false\n                                                },\n                                            isPlaying = isPlaying,\n                                            coroutineScope = coroutineScope,\n                                            thumbnailRatio = 1f, // Use square thumbnails for all items in horizontal scroll\n                                            modifier =\n                                                Modifier\n                                                    .combinedClickable(\n                                                        onClick = {\n                                                            when (item) {\n                                                                is SongItem -> {\n                                                                    if (!isGuest) {\n                                                                        playerConnection.playQueue(\n                                                                            YouTubeQueue(\n                                                                                WatchEndpoint(videoId = item.id),\n                                                                                item.toMediaMetadata(),\n                                                                            ),\n                                                                        )\n                                                                    }\n                                                                }\n\n                                                                is AlbumItem -> {\n                                                                    navController.navigate(\"album/${item.id}\")\n                                                                }\n\n                                                                is ArtistItem -> {\n                                                                    navController.navigate(\"artist/${item.id}\")\n                                                                }\n\n                                                                is PlaylistItem -> {\n                                                                    navController.navigate(\"online_playlist/${item.id}\")\n                                                                }\n\n                                                                is PodcastItem -> {\n                                                                    navController.navigate(\"online_podcast/${item.id}\")\n                                                                }\n\n                                                                is EpisodeItem -> {\n                                                                    if (!isGuest) {\n                                                                        playerConnection.playQueue(\n                                                                            YouTubeQueue(\n                                                                                WatchEndpoint(videoId = item.id),\n                                                                                item.toMediaMetadata(),\n                                                                            ),\n                                                                        )\n                                                                    }\n                                                                }\n                                                            }\n                                                        },\n                                                        onLongClick = {\n                                                            haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                            menuState.show {\n                                                                when (item) {\n                                                                    is SongItem -> {\n                                                                        YouTubeSongMenu(\n                                                                            song = item,\n                                                                            navController = navController,\n                                                                            onDismiss = menuState::dismiss,\n                                                                        )\n                                                                    }\n\n                                                                    is AlbumItem -> {\n                                                                        YouTubeAlbumMenu(\n                                                                            albumItem = item,\n                                                                            navController = navController,\n                                                                            onDismiss = menuState::dismiss,\n                                                                        )\n                                                                    }\n\n                                                                    is ArtistItem -> {\n                                                                        YouTubeArtistMenu(\n                                                                            artist = item,\n                                                                            onDismiss = menuState::dismiss,\n                                                                        )\n                                                                    }\n\n                                                                    is PlaylistItem -> {\n                                                                        YouTubePlaylistMenu(\n                                                                            playlist = item,\n                                                                            coroutineScope = coroutineScope,\n                                                                            onDismiss = menuState::dismiss,\n                                                                        )\n                                                                    }\n\n                                                                    is PodcastItem -> {\n                                                                        YouTubePlaylistMenu(\n                                                                            playlist = item.asPlaylistItem(),\n                                                                            coroutineScope = coroutineScope,\n                                                                            onDismiss = menuState::dismiss,\n                                                                        )\n                                                                    }\n\n                                                                    is EpisodeItem -> {\n                                                                        YouTubeSongMenu(\n                                                                            song = item.asSongItem(),\n                                                                            navController = navController,\n                                                                            onDismiss = menuState::dismiss,\n                                                                        )\n                                                                    }\n                                                                }\n                                                            }\n                                                        },\n                                                    ).animateItem(),\n                                        )\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        val isScrollingUp = lazyListState.isScrollingUp()\n        val showLocalFab = librarySongs.isNotEmpty() && libraryArtist?.artist?.isLocal != true\n\n        // Library/Local Toggle FAB\n        HideOnScrollFAB(\n            visible = showLocalFab,\n            lazyListState = lazyListState,\n            icon = if (showLocal) R.drawable.language else R.drawable.library_music,\n            onClick = {\n                showLocal = showLocal.not()\n                if (!showLocal && artistPage == null) viewModel.fetchArtistsFromYTM()\n            },\n        )\n\n        // Play All FAB (Stacked above Library/Local FAB if visible)\n        val canPlayAll =\n            !isGuest && (\n                (showLocal && librarySongs.isNotEmpty()) ||\n                    (\n                        !showLocal && artistPage?.sections?.any {\n                            (it.items.firstOrNull() as? SongItem)?.album != null\n                        } == true\n                    )\n            )\n\n        if (canPlayAll) {\n            androidx.compose.animation.AnimatedVisibility(\n                visible = isScrollingUp,\n                enter = androidx.compose.animation.slideInVertically { it * 2 },\n                exit = androidx.compose.animation.slideOutVertically { it * 2 },\n                modifier =\n                    Modifier\n                        .align(Alignment.BottomEnd)\n                        .windowInsetsPadding(\n                            LocalPlayerAwareWindowInsets.current\n                                .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal),\n                        )\n                        // Add padding to position it above the other FAB (56dp height + 16dp padding + 8dp spacing)\n                        // If the other FAB is visible.\n                        .padding(bottom = if (showLocalFab) 64.dp else 0.dp),\n            ) {\n                val onPlayAllClick: () -> Unit = {\n                    if (!isGuest) {\n                        if (showLocal) {\n                            if (librarySongs.isNotEmpty()) {\n                                playerConnection.playQueue(\n                                    ListQueue(\n                                        title = libraryArtist?.artist?.name ?: \"Unknown Artist\",\n                                        items = librarySongs.map { it.toMediaItem() },\n                                    ),\n                                )\n                            }\n                        } else if (artistPage != null) {\n                            val songSection =\n                                artistPage.sections.find { section ->\n                                    (section.items.firstOrNull() as? SongItem)?.album != null\n                                }\n\n                            val moreEndpoint = songSection?.moreEndpoint\n                            if (moreEndpoint != null) {\n                                coroutineScope.launch(kotlinx.coroutines.Dispatchers.IO) {\n                                    val result = YouTube.artistItems(moreEndpoint).getOrNull()\n                                    withContext(kotlinx.coroutines.Dispatchers.Main) {\n                                        if (result != null && result.items.isNotEmpty()) {\n                                            val songs = result.items.filterIsInstance<SongItem>().map { it.toMediaItem() }\n                                            playerConnection.playQueue(\n                                                ListQueue(\n                                                    title = artistPage.artist.title,\n                                                    items = songs,\n                                                ),\n                                            )\n                                        } else {\n                                            // Fallback to loaded items\n                                            val songs = songSection.items.filterIsInstance<SongItem>().map { it.toMediaItem() }\n                                            if (songs.isNotEmpty()) {\n                                                playerConnection.playQueue(\n                                                    ListQueue(\n                                                        title = artistPage.artist.title,\n                                                        items = songs,\n                                                    ),\n                                                )\n                                            }\n                                        }\n                                    }\n                                }\n                            } else if (songSection != null) {\n                                // Use loaded items if no more endpoint\n                                val songs = songSection.items.filterIsInstance<SongItem>().map { it.toMediaItem() }\n                                playerConnection.playQueue(\n                                    ListQueue(\n                                        title = artistPage.artist.title,\n                                        items = songs,\n                                    ),\n                                )\n                            } else {\n                                // Fallback to shuffle endpoint (stripped) if no song section found\n                                val shuffleEndpoint = artistPage.artist.shuffleEndpoint\n                                if (shuffleEndpoint != null) {\n                                    val endpoint =\n                                        if (shuffleEndpoint.playlistId != null) {\n                                            WatchEndpoint(\n                                                playlistId = shuffleEndpoint.playlistId,\n                                                params = null, // Remove shuffle params to play in order\n                                                videoId = null, // Ensure videoId is null to start from beginning of playlist\n                                            )\n                                        } else {\n                                            shuffleEndpoint\n                                        }\n                                    playerConnection.playQueue(YouTubeQueue(endpoint))\n                                }\n                            }\n                        }\n                    }\n                }\n\n                if (showLocalFab) {\n                    androidx.compose.material3.SmallFloatingActionButton(\n                        modifier = Modifier.padding(16.dp).offset(x = (-4).dp), // Align center with standard FAB (56dp vs 48dp)\n                        onClick = onPlayAllClick,\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.play),\n                            contentDescription = \"Play All\",\n                        )\n                    }\n                } else {\n                    androidx.compose.material3.FloatingActionButton(\n                        modifier = Modifier.padding(16.dp),\n                        onClick = onPlayAllClick,\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.play),\n                            contentDescription = \"Play All\",\n                            modifier = Modifier.size(32.dp),\n                        )\n                    }\n                }\n            }\n        }\n\n        SnackbarHost(\n            hostState = snackbarHostState,\n            modifier =\n                Modifier\n                    .windowInsetsPadding(LocalPlayerAwareWindowInsets.current)\n                    .align(Alignment.BottomCenter),\n        )\n    }\n\n    TopAppBar(\n        title = { if (!transparentAppBar) Text(artistPage?.artist?.title.orEmpty()) },\n        navigationIcon = {\n            IconButton(\n                onClick = navController::navigateUp,\n                onLongClick = navController::backToMain,\n            ) {\n                Icon(\n                    painterResource(R.drawable.arrow_back),\n                    contentDescription = null,\n                )\n            }\n        },\n        actions = {\n            IconButton(\n                onClick = {\n                    viewModel.artistPage?.artist?.shareLink?.let { link ->\n                        val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager\n                        val clip = ClipData.newPlainText(\"Artist Link\", link)\n                        clipboard.setPrimaryClip(clip)\n                        Toast.makeText(context, R.string.link_copied, Toast.LENGTH_SHORT).show()\n                    }\n                },\n            ) {\n                Icon(\n                    painterResource(R.drawable.link),\n                    contentDescription = null,\n                )\n            }\n        },\n        colors =\n            if (transparentAppBar) {\n                TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent)\n            } else {\n                TopAppBarDefaults.topAppBarColors()\n            },\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/artist/ArtistSongsScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.artist\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.pluralStringResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.ArtistSongSortDescendingKey\nimport com.metrolist.music.constants.ArtistSongSortType\nimport com.metrolist.music.constants.ArtistSongSortTypeKey\nimport com.metrolist.music.constants.CONTENT_TYPE_HEADER\nimport com.metrolist.music.constants.HideExplicitKey\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.playback.queues.ListQueue\nimport com.metrolist.music.ui.component.HideOnScrollFAB\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.SongListItem\nimport com.metrolist.music.ui.component.SortHeader\nimport com.metrolist.music.ui.menu.SongMenu\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.viewmodels.ArtistSongsViewModel\n\n@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun ArtistSongsScreen(\n    navController: NavController,\n    viewModel: ArtistSongsViewModel = hiltViewModel(),\n) {\n    val menuState = LocalMenuState.current\n    val queueAllSongsStr = stringResource(R.string.queue_all_songs)\n    val haptic = LocalHapticFeedback.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n\n    val (sortType, onSortTypeChange) =\n        rememberEnumPreference(\n            ArtistSongSortTypeKey,\n            ArtistSongSortType.CREATE_DATE,\n        )\n    val (sortDescending, onSortDescendingChange) =\n        rememberPreference(\n            ArtistSongSortDescendingKey,\n            true,\n        )\n    val hideExplicit by rememberPreference(key = HideExplicitKey, defaultValue = false)\n    val artist by viewModel.artist.collectAsState()\n    val songs by viewModel.songs.collectAsState()\n    val lazyListState = rememberLazyListState()\n\n    Box(\n        modifier = Modifier.fillMaxSize(),\n    ) {\n        LazyColumn(\n            state = lazyListState,\n            contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n        ) {\n            item(\n                key = \"header\",\n                contentType = CONTENT_TYPE_HEADER,\n            ) {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    modifier = Modifier.padding(horizontal = 16.dp),\n                ) {\n                    SortHeader(\n                        sortType = sortType,\n                        sortDescending = sortDescending,\n                        onSortTypeChange = onSortTypeChange,\n                        onSortDescendingChange = onSortDescendingChange,\n                        sortTypeText = { sortType ->\n                            when (sortType) {\n                                ArtistSongSortType.CREATE_DATE -> R.string.sort_by_create_date\n                                ArtistSongSortType.NAME -> R.string.sort_by_name\n                                ArtistSongSortType.PLAY_TIME -> R.string.sort_by_play_time\n                            }\n                        },\n                    )\n\n                    Spacer(Modifier.weight(1f))\n\n                    Text(\n                        text = pluralStringResource(R.plurals.n_song, songs.size, songs.size),\n                        style = MaterialTheme.typography.titleSmall,\n                        color = MaterialTheme.colorScheme.secondary,\n                    )\n                }\n            }\n\n            itemsIndexed(\n                items = songs,\n                key = { _, item -> item.id },\n            ) { index, song ->\n                SongListItem(\n                    song = song,\n                    showInLibraryIcon = true,\n                    isActive = song.id == mediaMetadata?.id,\n                    isPlaying = isPlaying,\n                    trailingContent = {\n                        IconButton(\n                            onClick = {\n                                menuState.show {\n                                    SongMenu(\n                                        originalSong = song,\n                                        navController = navController,\n                                        onDismiss = menuState::dismiss,\n                                    )\n                                }\n                            },\n                        ) {\n                            Icon(\n                                painter = painterResource(R.drawable.more_vert),\n                                contentDescription = null,\n                            )\n                        }\n                    },\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .combinedClickable(\n                                onClick = {\n                                    if (song.id == mediaMetadata?.id) {\n                                        playerConnection.togglePlayPause()\n                                    } else {\n                                        playerConnection.playQueue(\n                                            ListQueue(\n                                                title = queueAllSongsStr,\n                                                items = songs.map { it.toMediaItem() },\n                                                startIndex = index,\n                                            ),\n                                        )\n                                    }\n                                },\n                                onLongClick = {\n                                    haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                    menuState.show {\n                                        SongMenu(\n                                            originalSong = song,\n                                            navController = navController,\n                                            onDismiss = menuState::dismiss,\n                                        )\n                                    }\n                                },\n                            ).animateItem(),\n                )\n            }\n        }\n\n        TopAppBar(\n            title = { Text(artist?.artist?.name.orEmpty()) },\n            navigationIcon = {\n                IconButton(\n                    onClick = navController::navigateUp,\n                    onLongClick = navController::backToMain,\n                ) {\n                    Icon(\n                        painterResource(R.drawable.arrow_back),\n                        contentDescription = null,\n                    )\n                }\n            },\n        )\n\n        HideOnScrollFAB(\n            lazyListState = lazyListState,\n            icon = R.drawable.shuffle,\n            onClick = {\n                playerConnection.playQueue(\n                    ListQueue(\n                        title = artist?.artist?.name,\n                        items = songs.shuffled().map { it.toMediaItem() },\n                    ),\n                )\n            },\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/equalizer/EQState.kt",
    "content": "package com.metrolist.music.ui.screens.equalizer\n\nimport com.metrolist.music.eq.data.SavedEQProfile\n\n/**\n * UI State for EQ Screen\n */\ndata class EQState(\n    val profiles: List<SavedEQProfile> = emptyList(),\n    val activeProfileId: String? = null,\n    val importStatus: String? = null,\n    val error: String? = null\n)"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/equalizer/EQViewModel.kt",
    "content": "package com.metrolist.music.ui.screens.equalizer\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.music.eq.EqualizerService\nimport com.metrolist.music.eq.data.EQProfileRepository\nimport com.metrolist.music.eq.data.ParametricEQParser\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport java.io.InputStream\nimport javax.inject.Inject\n\n/**\n * ViewModel for EQ Screen\n * Manages EQ profiles and applies them to the EqualizerService\n */\n@HiltViewModel\nclass EQViewModel @Inject constructor(\n    private val eqProfileRepository: EQProfileRepository,\n    private val equalizerService: EqualizerService\n) : ViewModel() {\n\n    private val _state = MutableStateFlow(EQState())\n    val state: StateFlow<EQState> = _state.asStateFlow()\n\n    init {\n        loadProfiles()\n    }\n\n    /**\n     * Load all saved EQ profiles (sorted: AutoEQ first, then custom)\n     */\n    private fun loadProfiles() {\n        // Observe profiles changes\n        viewModelScope.launch {\n            eqProfileRepository.profiles.collect { _ ->\n                val sortedProfiles = eqProfileRepository.getSortedProfiles()\n                _state.update {\n                    it.copy(profiles = sortedProfiles)\n                }\n            }\n        }\n\n        // Observe active profile changes separately\n        viewModelScope.launch {\n            eqProfileRepository.activeProfile.collect { activeProfile ->\n                _state.update {\n                    it.copy(activeProfileId = activeProfile?.id)\n                }\n            }\n        }\n    }\n\n    /**\n     * Select and apply an EQ profile\n     * Pass null to disable EQ\n     */\n    fun selectProfile(profileId: String?) {\n        viewModelScope.launch {\n            if (profileId == null) {\n                // Disable EQ\n                equalizerService.disable()\n                eqProfileRepository.setActiveProfile(null)\n            } else {\n                // Apply the selected profile\n                val profile = _state.value.profiles.find { it.id == profileId }\n                if (profile != null) {\n                    val result = equalizerService.applyProfile(profile)\n                    result.onSuccess {\n                        eqProfileRepository.setActiveProfile(profileId)\n                    }.onFailure { e ->\n                        _state.update { it.copy(error = e.message ?: \"Unknown error\") }\n                    }\n                }\n            }\n        }\n    }\n\n    /**\n     * Clear error message\n     */\n    fun clearError() {\n        _state.update { it.copy(error = null) }\n    }\n\n    /**\n     * Delete an EQ profile\n     */\n    fun deleteProfile(profileId: String) {\n        viewModelScope.launch {\n            eqProfileRepository.deleteProfile(profileId)\n        }\n    }\n\n    /**\n     * Import a custom EQ profile from a file\n     */\n    fun importCustomProfile(\n        fileName: String,\n        inputStream: InputStream,\n        onSuccess: () -> Unit,\n        onError: (Exception) -> Unit\n    ) {\n        viewModelScope.launch {\n            try {\n                // Read the file content\n                val content = inputStream.bufferedReader().use { it.readText() }\n                inputStream.close()\n\n                // Parse the ParametricEQ format\n                val parametricEQ = ParametricEQParser.parseText(content)\n\n                // Validate the parsed EQ\n                val validationErrors = ParametricEQParser.validate(parametricEQ)\n                if (validationErrors.isNotEmpty()) {\n                    onError(Exception(\"Invalid EQ file: ${validationErrors.first()}\"))\n                    return@launch\n                }\n\n                // Extract profile name from file name (remove .txt extension)\n                val profileName = fileName.removeSuffix(\".txt\")\n\n                // Import the profile\n                eqProfileRepository.importCustomProfile(profileName, parametricEQ)\n\n                _state.update { it.copy(importStatus = \"Successfully imported $profileName\") }\n                onSuccess()\n            } catch (e: Exception) {\n                onError(Exception(\"Failed to import EQ profile: ${e.message}\"))\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/equalizer/EqScreen.kt",
    "content": "package com.metrolist.music.ui.screens.equalizer\n\nimport android.annotation.SuppressLint\nimport android.content.Intent\nimport android.media.audiofx.AudioEffect\nimport android.media.session.PlaybackState\nimport android.provider.OpenableColumns\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.ListItem\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedButton\nimport androidx.compose.material3.RadioButton\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.pluralStringResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.lifecycle.compose.collectAsStateWithLifecycle\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.eq.data.SavedEQProfile\nimport timber.log.Timber\n\n/**\n * EQ Screen - Manage and select EQ profiles\n */\n@SuppressLint(\"LocalContextGetResourceValueCall\")\n@Composable\nfun EqScreen(\n    viewModel: EQViewModel = hiltViewModel(),\n    playbackState: PlaybackState? = null\n) {\n    val state by viewModel.state.collectAsStateWithLifecycle()\n    val context = LocalContext.current\n    val playerConnection = LocalPlayerConnection.current\n\n    var showError by remember { mutableStateOf<String?>(null) }\n\n    // Activity result launcher for system equalizer\n    val activityResultLauncher = rememberLauncherForActivityResult(\n        ActivityResultContracts.StartActivityForResult()\n    ) { }\n\n    // File picker for custom EQ import\n    val filePickerLauncher = rememberLauncherForActivityResult(\n        contract = ActivityResultContracts.GetContent()\n    ) { uri ->\n        if (uri != null) {\n            try {\n                val contentResolver = context.contentResolver\n\n                // Extract file name from URI\n                var fileName = \"custom_eq.txt\"\n                contentResolver.query(uri, null, null, null, null)?.use { cursor ->\n                    if (cursor.moveToFirst()) {\n                        val displayNameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)\n                        if (displayNameIndex >= 0) {\n                            val name = cursor.getString(displayNameIndex)\n                            if (!name.isNullOrBlank()) {\n                                fileName = name\n                            }\n                        }\n                    }\n                }\n\n                val inputStream = contentResolver.openInputStream(uri)\n\n                if (inputStream != null) {\n                    viewModel.importCustomProfile(\n                        fileName = fileName,\n                        inputStream = inputStream,\n                        onSuccess = {\n                            Timber.d(\"Custom EQ profile imported successfully: $fileName\")\n                        },\n                        onError = { error ->\n                            Timber.d(\"Error: Unable to import Custom EQ profile: $fileName\")\n                            showError = context.getString(R.string.import_error_title) + \": \" + error.message\n                        })\n                } else {\n                    showError = context.getString(R.string.error_file_read)\n                }\n            } catch (e: Exception) {\n                showError = context.getString(R.string.error_file_open, e.message)\n            }\n        }\n    }\n\n    EqScreenContent(\n        profiles = state.profiles,\n        activeProfileId = state.activeProfileId,\n        onProfileSelected = { viewModel.selectProfile(it) },\n        onImportCustomEQ = {\n            // Launch file picker for .txt files\n            filePickerLauncher.launch(\"text/plain\")\n        },\n        onOpenSystemEqualizer = {\n            playerConnection?.let { connection ->\n                val intent = Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply {\n                    putExtra(\n                        AudioEffect.EXTRA_AUDIO_SESSION,\n                        connection.player.audioSessionId\n                    )\n                    putExtra(\n                        AudioEffect.EXTRA_PACKAGE_NAME,\n                        context.packageName\n                    )\n                    putExtra(\n                        AudioEffect.EXTRA_CONTENT_TYPE,\n                        AudioEffect.CONTENT_TYPE_MUSIC\n                    )\n                }\n                if (intent.resolveActivity(context.packageManager) != null) {\n                    activityResultLauncher.launch(intent)\n                }\n            }\n        },\n        onDeleteProfile = { viewModel.deleteProfile(it) }\n    )\n\n    // Error dialog\n    if (showError != null) {\n        AlertDialog(\n            onDismissRequest = { showError = null },\n            title = {\n                Text(stringResource(R.string.import_error_title))\n            },\n            text = {\n                Text(showError ?: \"\")\n            },\n            confirmButton = {\n                TextButton(onClick = { showError = null }) {\n                    Text(stringResource(android.R.string.ok))\n                }\n            }\n        )\n    }\n\n    // Error dialog for apply failure\n    if (state.error != null) {\n        AlertDialog(\n            onDismissRequest = { viewModel.clearError() },\n            title = {\n                Text(stringResource(R.string.error_title))\n            },\n            text = {\n                Text(stringResource(R.string.error_eq_apply_failed, state.error ?: \"\"))\n            },\n            confirmButton = {\n                TextButton(onClick = { viewModel.clearError() }) {\n                    Text(stringResource(android.R.string.ok))\n                }\n            }\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun EqScreenContent(\n    profiles: List<SavedEQProfile>,\n    activeProfileId: String?,\n    onProfileSelected: (String?) -> Unit,\n    onImportCustomEQ: () -> Unit,\n    onOpenSystemEqualizer: () -> Unit,\n    onDeleteProfile: (String) -> Unit\n) {\n    Surface(\n        shape = MaterialTheme.shapes.extraLarge,\n        color = MaterialTheme.colorScheme.surfaceContainerHigh,\n        tonalElevation = 6.dp,\n        modifier = Modifier\n            .fillMaxWidth(0.9f)\n            .heightIn(max = 600.dp)\n            .padding(vertical = 24.dp) // Optional extra padding if desired, but dialog handles it.\n    ) {\n        Column {\n            // Header\n            Row(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = 24.dp, vertical = 16.dp),\n                verticalAlignment = Alignment.CenterVertically,\n                horizontalArrangement = Arrangement.SpaceBetween\n            ) {\n                Column {\n                    Text(\n                        text = stringResource(R.string.equalizer_header),\n                        style = MaterialTheme.typography.headlineSmall\n                    )\n                    Text(\n                        text = pluralStringResource(\n                            id = R.plurals.profiles_count,\n                            count = profiles.size,\n                            profiles.size\n                        ),\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant\n                    )\n                }\n                Row {\n                    IconButton(onClick = onImportCustomEQ) {\n                        Icon(\n                            painter = painterResource(R.drawable.add),\n                            contentDescription = stringResource(R.string.import_profile)\n                        )\n                    }\n                    IconButton(onClick = onOpenSystemEqualizer) {\n                        Icon(\n                            painter = painterResource(R.drawable.equalizer),\n                            contentDescription = stringResource(R.string.system_equalizer)\n                        )\n                    }\n                }\n            }\n\n            // Profile list\n            LazyColumn(\n                modifier = Modifier.fillMaxWidth(),\n                contentPadding = PaddingValues(bottom = 16.dp)\n            ) {\n                // \"No Equalization\" option (always first)\n                item {\n                    NoEqualizationItem(\n                        isSelected = activeProfileId == null,\n                        onSelected = { onProfileSelected(null) }\n                    )\n                }\n\n                // Custom profiles only\n                val customProfiles = profiles.filter { it.isCustom }\n\n                if (customProfiles.isNotEmpty()) {\n                    items(customProfiles) { profile ->\n                        EQProfileItem(\n                            profile = profile,\n                            isSelected = activeProfileId == profile.id,\n                            onSelected = { onProfileSelected(profile.id) },\n                            onDelete = { onDeleteProfile(profile.id) }\n                        )\n                    }\n                }\n\n                // Empty state\n                if (customProfiles.isEmpty()) {\n                    item {\n                        Box(\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .padding(32.dp),\n                            contentAlignment = Alignment.Center\n                        ) {\n                            Column(\n                                horizontalAlignment = Alignment.CenterHorizontally\n                            ) {\n                                Icon(\n                                    painter = painterResource(R.drawable.equalizer),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(48.dp),\n                                    tint = MaterialTheme.colorScheme.onSurfaceVariant\n                                )\n                                Spacer(modifier = Modifier.height(16.dp))\n                                Text(\n                                    text = stringResource(R.string.no_profiles),\n                                    style = MaterialTheme.typography.titleMedium,\n                                    color = MaterialTheme.colorScheme.onSurfaceVariant\n                                )\n                                Spacer(modifier = Modifier.height(8.dp))\n                                Button(onClick = onImportCustomEQ) {\n                                    Text(stringResource(R.string.import_profile))\n                                }\n                                Spacer(modifier = Modifier.height(8.dp))\n                                OutlinedButton(onClick = onOpenSystemEqualizer) {\n                                    Text(stringResource(R.string.system_equalizer))\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n// --- HELPER COMPOSABLES ---\n\n@Composable\nprivate fun NoEqualizationItem(\n    isSelected: Boolean,\n    onSelected: () -> Unit\n) {\n    ListItem(\n        headlineContent = {\n            Text(\n                stringResource(R.string.eq_disabled),\n                fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal\n            )\n        },\n        leadingContent = {\n            RadioButton(\n                selected = isSelected,\n                onClick = onSelected\n            )\n        },\n        modifier = Modifier\n            .clickable(onClick = onSelected)\n            .padding(horizontal = 8.dp) // align with design\n    )\n}\n\n@Composable\nprivate fun EQProfileItem(\n    profile: SavedEQProfile,\n    isSelected: Boolean,\n    onSelected: () -> Unit,\n    onDelete: () -> Unit\n) {\n    var showDeleteDialog by remember { mutableStateOf(false) }\n\n    ListItem(\n        headlineContent = {\n            Text(\n                text = profile.deviceModel,\n                fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal\n            )\n        },\n        supportingContent = {\n            Text(\n                pluralStringResource(\n                    id = R.plurals.band_count,\n                    count = profile.bands.size,\n                    profile.bands.size\n                )\n            )\n        },\n        leadingContent = {\n            RadioButton(\n                selected = isSelected,\n                onClick = onSelected\n            )\n        },\n        trailingContent = {\n            IconButton(onClick = { showDeleteDialog = true }) {\n                Icon(\n                    painter = painterResource(R.drawable.delete),\n                    contentDescription = stringResource(R.string.delete_profile_desc),\n                    tint = MaterialTheme.colorScheme.error\n                )\n            }\n        },\n        modifier = Modifier\n            .clickable(onClick = onSelected)\n            .padding(horizontal = 8.dp)\n    )\n\n    // Delete confirmation dialog\n    if (showDeleteDialog) {\n        AlertDialog(\n            onDismissRequest = { showDeleteDialog = false },\n            title = { Text(stringResource(R.string.delete_profile_desc)) },\n            text = {\n                Text(\n                    stringResource(R.string.delete_profile_confirmation, profile.name)\n                )\n            },\n            confirmButton = {\n                TextButton(\n                    onClick = {\n                        onDelete()\n                        showDeleteDialog = false\n                    }\n                ) {\n                    Text(stringResource(android.R.string.ok))\n                }\n            },\n            dismissButton = {\n                TextButton(onClick = { showDeleteDialog = false }) {\n                    Text(stringResource(android.R.string.cancel))\n                }\n            }\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/library/LibraryAlbumsScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.library\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.GridItemSpan\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.items\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.FilterChip\nimport androidx.compose.material3.FilterChipDefaults\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.pluralStringResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport androidx.navigation.compose.currentBackStackEntryAsState\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.AlbumFilter\nimport com.metrolist.music.constants.AlbumFilterKey\nimport com.metrolist.music.constants.AlbumSortDescendingKey\nimport com.metrolist.music.constants.AlbumSortType\nimport com.metrolist.music.constants.AlbumSortTypeKey\nimport com.metrolist.music.constants.AlbumViewTypeKey\nimport com.metrolist.music.constants.CONTENT_TYPE_ALBUM\nimport com.metrolist.music.constants.CONTENT_TYPE_HEADER\nimport com.metrolist.music.constants.GridItemSize\nimport com.metrolist.music.constants.GridItemsSizeKey\nimport com.metrolist.music.constants.GridThumbnailHeight\nimport com.metrolist.music.constants.HideExplicitKey\nimport com.metrolist.music.constants.LibraryViewType\nimport com.metrolist.music.constants.YtmSyncKey\nimport com.metrolist.music.ui.component.ChipsRow\nimport com.metrolist.music.ui.component.EmptyPlaceholder\nimport com.metrolist.music.ui.component.LibraryAlbumGridItem\nimport com.metrolist.music.ui.component.LibraryAlbumListItem\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.SortHeader\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.viewmodels.LibraryAlbumsViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\n@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun LibraryAlbumsScreen(\n    navController: NavController,\n    onDeselect: () -> Unit,\n    viewModel: LibraryAlbumsViewModel = hiltViewModel(),\n) {\n    val menuState = LocalMenuState.current\n    val haptic = LocalHapticFeedback.current\n    val coroutineScope = rememberCoroutineScope()\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n\n    var viewType by rememberEnumPreference(AlbumViewTypeKey, LibraryViewType.GRID)\n    var filter by rememberEnumPreference(AlbumFilterKey, AlbumFilter.LIKED)\n    val (sortType, onSortTypeChange) = rememberEnumPreference(\n        AlbumSortTypeKey,\n        AlbumSortType.CREATE_DATE\n    )\n    val (sortDescending, onSortDescendingChange) = rememberPreference(AlbumSortDescendingKey, true)\n    val gridItemSize by rememberEnumPreference(GridItemsSizeKey, GridItemSize.BIG)\n\n    val (ytmSync) = rememberPreference(YtmSyncKey, true)\n    val hideExplicit by rememberPreference(key = HideExplicitKey, defaultValue = false)\n\n    val filterContent = @Composable {\n        Row {\n            Spacer(Modifier.width(12.dp))\n            FilterChip(\n                label = { Text(stringResource(R.string.albums)) },\n                selected = true,\n                colors = FilterChipDefaults.filterChipColors(containerColor = MaterialTheme.colorScheme.surface),\n                onClick = onDeselect,\n                shape = RoundedCornerShape(16.dp),\n                leadingIcon = {\n                    Icon(painter = painterResource(R.drawable.close), contentDescription = \"\")\n                },\n            )\n            ChipsRow(\n                chips =\n                listOf(\n                    AlbumFilter.LIKED to stringResource(R.string.filter_liked),\n                    AlbumFilter.LIBRARY to stringResource(R.string.filter_library),\n                    AlbumFilter.UPLOADED to stringResource(R.string.filter_uploaded)\n                ),\n                currentValue = filter,\n                onValueUpdate = {\n                    filter = it\n                },\n                modifier = Modifier.weight(1f),\n            )\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        if (ytmSync) {\n            withContext(Dispatchers.IO) {\n                viewModel.sync()\n            }\n        }\n    }\n\n    val albums by viewModel.allAlbums.collectAsState()\n\n    val lazyListState = rememberLazyListState()\n    val lazyGridState = rememberLazyGridState()\n    val backStackEntry by navController.currentBackStackEntryAsState()\n    val scrollToTop =\n        backStackEntry?.savedStateHandle?.getStateFlow(\"scrollToTop\", false)?.collectAsState()\n\n    LaunchedEffect(scrollToTop?.value) {\n        if (scrollToTop?.value == true) {\n            when (viewType) {\n                LibraryViewType.LIST -> lazyListState.animateScrollToItem(0)\n                LibraryViewType.GRID -> lazyGridState.animateScrollToItem(0)\n            }\n            backStackEntry?.savedStateHandle?.set(\"scrollToTop\", false)\n        }\n    }\n\n    val headerContent = @Composable {\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n            modifier = Modifier.padding(start = 16.dp),\n        ) {\n            SortHeader(\n                sortType = sortType,\n                sortDescending = sortDescending,\n                onSortTypeChange = onSortTypeChange,\n                onSortDescendingChange = onSortDescendingChange,\n                sortTypeText = { sortType ->\n                    when (sortType) {\n                        AlbumSortType.CREATE_DATE -> R.string.sort_by_create_date\n                        AlbumSortType.NAME -> R.string.sort_by_name\n                        AlbumSortType.ARTIST -> R.string.sort_by_artist\n                        AlbumSortType.YEAR -> R.string.sort_by_year\n                        AlbumSortType.SONG_COUNT -> R.string.sort_by_song_count\n                        AlbumSortType.LENGTH -> R.string.sort_by_length\n                        AlbumSortType.PLAY_TIME -> R.string.sort_by_play_time\n                    }\n                },\n            )\n\n            Spacer(Modifier.weight(1f))\n\n            Text(\n                text = pluralStringResource(R.plurals.n_album, albums.size, albums.size),\n                style = MaterialTheme.typography.titleSmall,\n                color = MaterialTheme.colorScheme.secondary,\n            )\n\n            IconButton(\n                onClick = {\n                    viewType = viewType.toggle()\n                },\n                modifier = Modifier.padding(start = 6.dp, end = 6.dp),\n            ) {\n                Icon(\n                    painter =\n                    painterResource(\n                        when (viewType) {\n                            LibraryViewType.LIST -> R.drawable.list\n                            LibraryViewType.GRID -> R.drawable.grid_view\n                        },\n                    ),\n                    contentDescription = null,\n                )\n            }\n        }\n    }\n\n    Box(\n        modifier = Modifier.fillMaxSize(),\n    ) {\n        when (viewType) {\n            LibraryViewType.LIST ->\n                LazyColumn(\n                    state = lazyListState,\n                    contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n                ) {\n                    item(\n                        key = \"filter\",\n                        contentType = CONTENT_TYPE_HEADER,\n                    ) {\n                        filterContent()\n                    }\n\n                    item(\n                        key = \"header\",\n                        contentType = CONTENT_TYPE_HEADER,\n                    ) {\n                        headerContent()\n                    }\n\n                    albums.let { albums ->\n                        if (albums.isEmpty()) {\n                            item(key = \"empty_placeholder\") {\n                                EmptyPlaceholder(\n                                    icon = R.drawable.album,\n                                    text = stringResource(R.string.library_album_empty),\n                                    modifier = Modifier.animateItem()\n                                )\n                            }\n                        }\n\n                        val filteredAlbumsForList = if (hideExplicit) {\n                            albums.filter { !it.album.explicit }\n                        } else {\n                            albums\n                        }\n                        items(\n                            items = filteredAlbumsForList.distinctBy { it.id },\n                            key = { it.id },\n                            contentType = { CONTENT_TYPE_ALBUM },\n                        ) { album ->\n                            LibraryAlbumListItem(\n                                navController = navController,\n                                menuState = menuState,\n                                album = album,\n                                isActive = album.id == mediaMetadata?.album?.id,\n                                isPlaying = isPlaying,\n                                modifier = Modifier\n                                    .animateItem()\n                            )\n                        }\n                    }\n                }\n\n            LibraryViewType.GRID ->\n                LazyVerticalGrid(\n                    state = lazyGridState,\n                    columns =\n                    GridCells.Adaptive(\n                        minSize = GridThumbnailHeight + if (gridItemSize == GridItemSize.BIG) 24.dp else (-24).dp,\n                    ),\n                    contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n                ) {\n                    item(\n                        key = \"filter\",\n                        span = { GridItemSpan(maxLineSpan) },\n                        contentType = CONTENT_TYPE_HEADER,\n                    ) {\n                        filterContent()\n                    }\n\n                    item(\n                        key = \"header\",\n                        span = { GridItemSpan(maxLineSpan) },\n                        contentType = CONTENT_TYPE_HEADER,\n                    ) {\n                        headerContent()\n                    }\n\n                    albums.let { albums ->\n                        if (albums.isEmpty()) {\n                            item(span = { GridItemSpan(maxLineSpan) }) {\n                                EmptyPlaceholder(\n                                    icon = R.drawable.album,\n                                    text = stringResource(R.string.library_album_empty),\n                                    modifier = Modifier.animateItem()\n                                )\n                            }\n                        }\n\n                        val filteredAlbumsForGrid = if (hideExplicit) {\n                            albums.filter { !it.album.explicit }\n                        } else {\n                            albums\n                        }\n                        items(\n                            items = filteredAlbumsForGrid.distinctBy { it.id },\n                            key = { it.id },\n                            contentType = { CONTENT_TYPE_ALBUM },\n                        ) { album ->\n                            LibraryAlbumGridItem(\n                                navController = navController,\n                                menuState = menuState,\n                                coroutineScope = coroutineScope,\n                                album = album,\n                                isActive = album.id == mediaMetadata?.album?.id,\n                                isPlaying = isPlaying,\n                                modifier = Modifier\n                                    .animateItem()\n                            )\n                        }\n                    }\n                }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/library/LibraryArtistsScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.library\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.GridItemSpan\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.items\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.FilterChip\nimport androidx.compose.material3.FilterChipDefaults\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.pluralStringResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport androidx.navigation.compose.currentBackStackEntryAsState\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.ArtistFilter\nimport com.metrolist.music.constants.ArtistFilterKey\nimport com.metrolist.music.constants.ArtistSortDescendingKey\nimport com.metrolist.music.constants.ArtistSortType\nimport com.metrolist.music.constants.ArtistSortTypeKey\nimport com.metrolist.music.constants.ArtistViewTypeKey\nimport com.metrolist.music.constants.CONTENT_TYPE_ARTIST\nimport com.metrolist.music.constants.CONTENT_TYPE_HEADER\nimport com.metrolist.music.constants.GridItemSize\nimport com.metrolist.music.constants.GridItemsSizeKey\nimport com.metrolist.music.constants.GridThumbnailHeight\nimport com.metrolist.music.constants.LibraryViewType\nimport com.metrolist.music.constants.YtmSyncKey\nimport com.metrolist.music.ui.component.ChipsRow\nimport com.metrolist.music.ui.component.EmptyPlaceholder\nimport com.metrolist.music.ui.component.LibraryArtistGridItem\nimport com.metrolist.music.ui.component.LibraryArtistListItem\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.SortHeader\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.viewmodels.LibraryArtistsViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\n@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun LibraryArtistsScreen(\n    navController: NavController,\n    onDeselect: () -> Unit,\n    viewModel: LibraryArtistsViewModel = hiltViewModel(),\n) {\n    val menuState = LocalMenuState.current\n    val haptic = LocalHapticFeedback.current\n    val coroutineScope = rememberCoroutineScope()\n    var viewType by rememberEnumPreference(ArtistViewTypeKey, LibraryViewType.GRID)\n\n    var filter by rememberEnumPreference(ArtistFilterKey, ArtistFilter.LIKED)\n    val (sortType, onSortTypeChange) = rememberEnumPreference(\n        ArtistSortTypeKey,\n        ArtistSortType.CREATE_DATE\n    )\n    val (sortDescending, onSortDescendingChange) = rememberPreference(ArtistSortDescendingKey, true)\n    val gridItemSize by rememberEnumPreference(GridItemsSizeKey, GridItemSize.BIG)\n    val (ytmSync) = rememberPreference(YtmSyncKey, true)\n\n    val filterContent = @Composable {\n        Row {\n            Spacer(Modifier.width(12.dp))\n            FilterChip(\n                label = { Text(stringResource(R.string.artists)) },\n                selected = true,\n                colors = FilterChipDefaults.filterChipColors(containerColor = MaterialTheme.colorScheme.surface),\n                onClick = onDeselect,\n                shape = RoundedCornerShape(16.dp),\n                leadingIcon = {\n                    Icon(painter = painterResource(R.drawable.close), contentDescription = \"\")\n                },\n            )\n            ChipsRow(\n                chips =\n                listOf(\n                    ArtistFilter.LIKED to stringResource(R.string.filter_liked),\n                    ArtistFilter.LIBRARY to stringResource(R.string.filter_library)\n                ),\n                currentValue = filter,\n                onValueUpdate = {\n                    filter = it\n                },\n                modifier = Modifier.weight(1f),\n            )\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        if (ytmSync) {\n            withContext(Dispatchers.IO) {\n                viewModel.sync()\n            }\n        }\n    }\n\n    val artists by viewModel.allArtists.collectAsState()\n\n    val lazyListState = rememberLazyListState()\n    val lazyGridState = rememberLazyGridState()\n    val backStackEntry by navController.currentBackStackEntryAsState()\n    val scrollToTop =\n        backStackEntry?.savedStateHandle?.getStateFlow(\"scrollToTop\", false)?.collectAsState()\n\n    LaunchedEffect(scrollToTop?.value) {\n        if (scrollToTop?.value == true) {\n            when (viewType) {\n                LibraryViewType.LIST -> lazyListState.animateScrollToItem(0)\n                LibraryViewType.GRID -> lazyGridState.animateScrollToItem(0)\n            }\n            backStackEntry?.savedStateHandle?.set(\"scrollToTop\", false)\n        }\n    }\n\n    val headerContent = @Composable {\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n            modifier = Modifier.padding(start = 16.dp),\n        ) {\n            SortHeader(\n                sortType = sortType,\n                sortDescending = sortDescending,\n                onSortTypeChange = onSortTypeChange,\n                onSortDescendingChange = onSortDescendingChange,\n                sortTypeText = { sortType ->\n                    when (sortType) {\n                        ArtistSortType.CREATE_DATE -> R.string.sort_by_create_date\n                        ArtistSortType.NAME -> R.string.sort_by_name\n                        ArtistSortType.SONG_COUNT -> R.string.sort_by_song_count\n                        ArtistSortType.PLAY_TIME -> R.string.sort_by_play_time\n                    }\n                },\n            )\n\n            Spacer(Modifier.weight(1f))\n\n            Text(\n                text = pluralStringResource(\n                    R.plurals.n_artist,\n                    artists.size,\n                    artists.size\n                ),\n                style = MaterialTheme.typography.titleSmall,\n                color = MaterialTheme.colorScheme.secondary,\n            )\n\n            IconButton(\n                onClick = {\n                    viewType = viewType.toggle()\n                },\n                modifier = Modifier.padding(start = 6.dp, end = 6.dp),\n            ) {\n                Icon(\n                    painter =\n                    painterResource(\n                        when (viewType) {\n                            LibraryViewType.LIST -> R.drawable.list\n                            LibraryViewType.GRID -> R.drawable.grid_view\n                        },\n                    ),\n                    contentDescription = null,\n                )\n            }\n        }\n    }\n\n    Box(\n        modifier = Modifier.fillMaxSize(),\n    ) {\n        when (viewType) {\n            LibraryViewType.LIST ->\n                LazyColumn(\n                    state = lazyListState,\n                    contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n                ) {\n                    item(\n                        key = \"filter\",\n                        contentType = CONTENT_TYPE_HEADER,\n                    ) {\n                        filterContent()\n                    }\n\n                    item(\n                        key = \"header\",\n                        contentType = CONTENT_TYPE_HEADER,\n                    ) {\n                        headerContent()\n                    }\n\n                    artists.let { artists ->\n                        if (artists.isEmpty()) {\n                            item(key = \"empty_placeholder\") {\n                                EmptyPlaceholder(\n                                    icon = R.drawable.artist,\n                                    text = stringResource(R.string.library_artist_empty),\n                                    modifier = Modifier.animateItem()\n                                )\n                            }\n                        }\n\n                        items(\n                            items = artists.distinctBy { it.id },\n                            key = { it.id },\n                            contentType = { CONTENT_TYPE_ARTIST },\n                        ) { artist ->\n                            LibraryArtistListItem(\n                                navController = navController,\n                                menuState = menuState,\n                                coroutineScope = coroutineScope,\n                                modifier = Modifier.animateItem(),\n                                artist = artist\n                            )\n                        }\n                    }\n                }\n\n            LibraryViewType.GRID ->\n                LazyVerticalGrid(\n                    state = lazyGridState,\n                    columns =\n                    GridCells.Adaptive(\n                        minSize = GridThumbnailHeight + if (gridItemSize == GridItemSize.BIG) 24.dp else (-24).dp,\n                    ),\n                    contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n                ) {\n                    item(\n                        key = \"filter\",\n                        span = { GridItemSpan(maxLineSpan) },\n                        contentType = CONTENT_TYPE_HEADER,\n                    ) {\n                        filterContent()\n                    }\n\n                    item(\n                        key = \"header\",\n                        span = { GridItemSpan(maxLineSpan) },\n                        contentType = CONTENT_TYPE_HEADER,\n                    ) {\n                        headerContent()\n                    }\n\n                    artists.let { artists ->\n                        if (artists.isEmpty()) {\n                            item(span = { GridItemSpan(maxLineSpan) }) {\n                                EmptyPlaceholder(\n                                    icon = R.drawable.artist,\n                                    text = stringResource(R.string.library_artist_empty),\n                                    modifier = Modifier.animateItem()\n                                )\n                            }\n                        }\n\n                        items(\n                            items = artists.distinctBy { it.id },\n                            key = { it.id },\n                            contentType = { CONTENT_TYPE_ARTIST },\n                        ) { artist ->\n                            LibraryArtistGridItem(\n                                navController = navController,\n                                menuState = menuState,\n                                coroutineScope = coroutineScope,\n                                modifier = Modifier.animateItem(),\n                                artist = artist\n                            )\n                        }\n                    }\n                }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/library/LibraryMixScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.library\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.GridItemSpan\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.items\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator\nimport androidx.compose.material3.pulltorefresh.pullToRefresh\nimport androidx.compose.material3.pulltorefresh.rememberPullToRefreshState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport androidx.navigation.compose.currentBackStackEntryAsState\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.AlbumViewTypeKey\nimport com.metrolist.music.constants.CONTENT_TYPE_HEADER\nimport com.metrolist.music.constants.CONTENT_TYPE_PLAYLIST\nimport com.metrolist.music.constants.GridItemSize\nimport com.metrolist.music.constants.GridItemsSizeKey\nimport com.metrolist.music.constants.GridThumbnailHeight\nimport com.metrolist.music.constants.LibraryViewType\nimport com.metrolist.music.constants.MixSortDescendingKey\nimport com.metrolist.music.constants.MixSortType\nimport com.metrolist.music.constants.MixSortTypeKey\nimport com.metrolist.music.constants.MixSortTypeKey\nimport com.metrolist.music.constants.ShowCachedPlaylistKey\nimport com.metrolist.music.constants.ShowDownloadedPlaylistKey\nimport com.metrolist.music.constants.ShowLikedPlaylistKey\nimport com.metrolist.music.constants.ShowTopPlaylistKey\nimport com.metrolist.music.constants.ShowUploadedPlaylistKey\nimport com.metrolist.music.constants.YtmSyncKey\nimport com.metrolist.music.db.entities.Album\nimport com.metrolist.music.db.entities.Artist\nimport com.metrolist.music.db.entities.Playlist\nimport com.metrolist.music.db.entities.PlaylistEntity\nimport com.metrolist.music.extensions.reversed\nimport com.metrolist.music.ui.component.AlbumGridItem\nimport com.metrolist.music.ui.component.AlbumListItem\nimport com.metrolist.music.ui.component.ArtistGridItem\nimport com.metrolist.music.ui.component.ArtistListItem\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.PlaylistGridItem\nimport com.metrolist.music.ui.component.PlaylistListItem\nimport com.metrolist.music.ui.component.SortHeader\nimport com.metrolist.music.ui.menu.AlbumMenu\nimport com.metrolist.music.ui.menu.ArtistMenu\nimport com.metrolist.music.ui.menu.PlaylistMenu\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.viewmodels.LibraryMixViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport java.text.Collator\nimport java.time.LocalDateTime\nimport java.util.Locale\nimport java.util.UUID\nimport androidx.compose.ui.platform.LocalLocale\n\n@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun LibraryMixScreen(\n    navController: NavController,\n    filterContent: @Composable () -> Unit,\n    viewModel: LibraryMixViewModel = hiltViewModel(),\n) {\n    val menuState = LocalMenuState.current\n    val haptic = LocalHapticFeedback.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n\n    var viewType by rememberEnumPreference(AlbumViewTypeKey, LibraryViewType.GRID)\n    val (sortType, onSortTypeChange) = rememberEnumPreference(\n        MixSortTypeKey,\n        MixSortType.CREATE_DATE\n    )\n    val (sortDescending, onSortDescendingChange) = rememberPreference(MixSortDescendingKey, true)\n    val gridItemSize by rememberEnumPreference(GridItemsSizeKey, GridItemSize.BIG)\n\n    val (ytmSync) = rememberPreference(YtmSyncKey, true)\n\n    val topSize by viewModel.topValue.collectAsState(initial = 50)\n    val likedPlaylist =\n        Playlist(\n            playlist = PlaylistEntity(\n                id = UUID.randomUUID().toString(),\n                name = stringResource(R.string.liked)\n            ),\n            songCount = 0,\n            songThumbnails = emptyList(),\n        )\n\n    val downloadPlaylist =\n        Playlist(\n            playlist = PlaylistEntity(\n                id = UUID.randomUUID().toString(),\n                name = stringResource(R.string.offline)\n            ),\n            songCount = 0,\n            songThumbnails = emptyList(),\n        )\n\n    val topPlaylist =\n        Playlist(\n            playlist = PlaylistEntity(\n                id = UUID.randomUUID().toString(),\n                name = stringResource(R.string.my_top) + \" $topSize\"\n            ),\n            songCount = 0,\n            songThumbnails = emptyList(),\n        )\n\n    val uploadedPlaylist =\n        Playlist(\n            playlist = PlaylistEntity(\n                id = UUID.randomUUID().toString(),\n                name = stringResource(R.string.uploaded_playlist)\n            ),\n            songCount = 0,\n            songThumbnails = emptyList(),\n        )\n\n    val cachedPlaylist =\n        Playlist(\n            playlist = PlaylistEntity(\n                id = UUID.randomUUID().toString(),\n                name = stringResource(R.string.cached_playlist)\n            ),\n            songCount = 0,\n            songThumbnails = emptyList(),\n        )\n\n    val (showLiked) = rememberPreference(ShowLikedPlaylistKey, true)\n    val (showDownloaded) = rememberPreference(ShowDownloadedPlaylistKey, true)\n    val (showTop) = rememberPreference(ShowTopPlaylistKey, true)\n    val (showUploaded) = rememberPreference(ShowUploadedPlaylistKey, true)\n    val (showCached) = rememberPreference(ShowCachedPlaylistKey, true)\n\n    val albums = viewModel.albums.collectAsState()\n    val artist = viewModel.artists.collectAsState()\n    val playlist = viewModel.playlists.collectAsState()\n\n    var allItems = albums.value + artist.value + playlist.value\n    val collator = Collator.getInstance(LocalLocale.current.platformLocale)\n    collator.strength = Collator.PRIMARY\n    allItems =\n        when (sortType) {\n            MixSortType.CREATE_DATE ->\n                allItems.sortedBy { item ->\n                    when (item) {\n                        is Album -> item.album.bookmarkedAt\n                        is Artist -> item.artist.bookmarkedAt\n                        is Playlist -> item.playlist.createdAt\n                        else -> LocalDateTime.now()\n                    }\n                }\n\n            MixSortType.NAME ->\n                allItems.sortedWith(\n                    compareBy(collator) { item ->\n                        when (item) {\n                            is Album -> item.album.title\n                            is Artist -> item.artist.name\n                            is Playlist -> item.playlist.name\n                            else -> \"\"\n                        }\n                    },\n                )\n\n            MixSortType.LAST_UPDATED ->\n                allItems.sortedBy { item ->\n                    when (item) {\n                        is Album -> item.album.lastUpdateTime\n                        is Artist -> item.artist.lastUpdateTime\n                        is Playlist -> item.playlist.lastUpdateTime\n                        else -> LocalDateTime.now()\n                    }\n                }\n        }.reversed(sortDescending)\n\n    val coroutineScope = rememberCoroutineScope()\n\n    val lazyListState = rememberLazyListState()\n    val lazyGridState = rememberLazyGridState()\n    val backStackEntry by navController.currentBackStackEntryAsState()\n    val scrollToTop =\n        backStackEntry?.savedStateHandle?.getStateFlow(\"scrollToTop\", false)?.collectAsState()\n\n    LaunchedEffect(scrollToTop?.value) {\n        if (scrollToTop?.value == true) {\n            when (viewType) {\n                LibraryViewType.LIST -> lazyListState.animateScrollToItem(0)\n                LibraryViewType.GRID -> lazyGridState.animateScrollToItem(0)\n            }\n            backStackEntry?.savedStateHandle?.set(\"scrollToTop\", false)\n        }\n    }\n\n    LaunchedEffect(Unit) {\n         if (ytmSync) {\n             withContext(Dispatchers.IO) {\n                 viewModel.syncAllLibrary()\n             }\n         }\n    }\n\n    val headerContent = @Composable {\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n            modifier = Modifier.padding(start = 16.dp),\n        ) {\n            SortHeader(\n                sortType = sortType,\n                sortDescending = sortDescending,\n                onSortTypeChange = onSortTypeChange,\n                onSortDescendingChange = onSortDescendingChange,\n                sortTypeText = { sortType ->\n                    when (sortType) {\n                        MixSortType.CREATE_DATE -> R.string.sort_by_create_date\n                        MixSortType.LAST_UPDATED -> R.string.sort_by_last_updated\n                        MixSortType.NAME -> R.string.sort_by_name\n                    }\n                },\n            )\n\n            Spacer(Modifier.weight(1f))\n\n            IconButton(\n                onClick = {\n                    viewType = viewType.toggle()\n                },\n                modifier = Modifier.padding(start = 6.dp, end = 6.dp),\n            ) {\n                Icon(\n                    painter =\n                    painterResource(\n                        when (viewType) {\n                            LibraryViewType.LIST -> R.drawable.list\n                            LibraryViewType.GRID -> R.drawable.grid_view\n                        },\n                    ),\n                    contentDescription = null,\n                )\n            }\n        }\n    }\n\n    val isRefreshing by viewModel.isRefreshing.collectAsState()\n    val pullRefreshState = rememberPullToRefreshState()\n\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .pullToRefresh(\n                state = pullRefreshState,\n                isRefreshing = isRefreshing,\n                onRefresh = viewModel::refresh\n            ),\n    ) {\n        when (viewType) {\n            LibraryViewType.LIST ->\n                LazyColumn(\n                    state = lazyListState,\n                    contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n                ) {\n                    item(\n                        key = \"filter\",\n                        contentType = CONTENT_TYPE_HEADER,\n                    ) {\n                        filterContent()\n                    }\n\n                    item(\n                        key = \"header\",\n                        contentType = CONTENT_TYPE_HEADER,\n                    ) {\n                        headerContent()\n                    }\n\n                    if (showLiked) {\n                        item(\n                            key = \"likedPlaylist\",\n                            contentType = { CONTENT_TYPE_PLAYLIST },\n                        ) {\n                            PlaylistListItem(\n                                playlist = likedPlaylist,\n                                autoPlaylist = true,\n                                modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .clickable {\n                                        navController.navigate(\"auto_playlist/liked\")\n                                    }\n                                    .animateItem(),\n                            )\n                        }\n                    }\n\n                    if (showDownloaded) {\n                        item(\n                            key = \"downloadedPlaylist\",\n                            contentType = { CONTENT_TYPE_PLAYLIST },\n                        ) {\n                            PlaylistListItem(\n                                playlist = downloadPlaylist,\n                                autoPlaylist = true,\n                                modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .clickable {\n                                        navController.navigate(\"auto_playlist/downloaded\")\n                                    }\n                                    .animateItem(),\n                            )\n                        }\n                    }\n\n                    if (showCached) {\n                        item(\n                            key = \"cachedPlaylist\",\n                            contentType = { CONTENT_TYPE_PLAYLIST },\n                        ) {\n                            PlaylistListItem(\n                                playlist = cachedPlaylist,\n                                autoPlaylist = true,\n                                modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .clickable {\n                                        navController.navigate(\"cache_playlist/cached\")\n                                    }\n                                    .animateItem(),\n                            )\n                        }\n                    }\n\n                    if (showTop) {\n                        item(\n                            key = \"TopPlaylist\",\n                            contentType = { CONTENT_TYPE_PLAYLIST },\n                        ) {\n                            PlaylistListItem(\n                                playlist = topPlaylist,\n                                autoPlaylist = true,\n                                modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .clickable {\n                                        navController.navigate(\"top_playlist/$topSize\")\n                                    }\n                                    .animateItem(),\n                            )\n                        }\n                    }\n\n                    if (showUploaded) {\n                        item(\n                            key = \"uploadedPlaylist\",\n                            contentType = { CONTENT_TYPE_PLAYLIST },\n                        ) {\n                            PlaylistListItem(\n                                playlist = uploadedPlaylist,\n                                autoPlaylist = true,\n                                modifier =\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .clickable {\n                                            navController.navigate(\"auto_playlist/uploaded\")\n                                        }\n                                        .animateItem(),\n                            )\n                        }\n                    }\n\n                    items(\n                        items = allItems.distinctBy { it.id },\n                        key = { it.id },\n                        contentType = { CONTENT_TYPE_PLAYLIST },\n                    ) { item ->\n                        when (item) {\n                            is Playlist -> {\n                                PlaylistListItem(\n                                    playlist = item,\n                                    trailingContent = {\n                                        IconButton(\n                                            onClick = {\n                                                menuState.show {\n                                                    PlaylistMenu(\n                                                        playlist = item,\n                                                        coroutineScope = coroutineScope,\n                                                        onDismiss = menuState::dismiss,\n                                                    )\n                                                }\n                                            },\n                                        ) {\n                                            Icon(\n                                                painter = painterResource(R.drawable.more_vert),\n                                                contentDescription = null,\n                                            )\n                                        }\n                                    },\n                                    modifier =\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .combinedClickable(\n                                            onClick = {\n                                                if (!item.playlist.isEditable && item.songCount == 0 && item.playlist.browseId != null)\n                                                    navController.navigate(\"online_playlist/${item.playlist.browseId}\")\n                                                else\n                                                    navController.navigate(\"local_playlist/${item.id}\")\n                                            },\n                                            onLongClick = {\n                                                haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                menuState.show {\n                                                    PlaylistMenu(\n                                                        playlist = item,\n                                                        coroutineScope = coroutineScope,\n                                                        onDismiss = menuState::dismiss,\n                                                    )\n                                                }\n                                            },\n                                        )\n                                        .animateItem(),\n                                )\n                            }\n\n                            is Artist -> {\n                                ArtistListItem(\n                                    artist = item,\n                                    trailingContent = {\n                                        IconButton(\n                                            onClick = {\n                                                menuState.show {\n                                                    ArtistMenu(\n                                                        originalArtist = item,\n                                                        coroutineScope = coroutineScope,\n                                                        onDismiss = menuState::dismiss,\n                                                    )\n                                                }\n                                            },\n                                        ) {\n                                            Icon(\n                                                painter = painterResource(R.drawable.more_vert),\n                                                contentDescription = null,\n                                            )\n                                        }\n                                    },\n                                    modifier =\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .combinedClickable(\n                                            onClick = {\n                                                navController.navigate(\"artist/${item.id}\")\n                                            },\n                                            onLongClick = {\n                                                haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                menuState.show {\n                                                    ArtistMenu(\n                                                        originalArtist = item,\n                                                        coroutineScope = coroutineScope,\n                                                        onDismiss = menuState::dismiss,\n                                                    )\n                                                }\n                                            },\n                                        )\n                                        .animateItem(),\n                                )\n                            }\n\n                            is Album -> {\n                                AlbumListItem(\n                                    album = item,\n                                    isActive = item.id == mediaMetadata?.album?.id,\n                                    isPlaying = isPlaying,\n                                    trailingContent = {\n                                        IconButton(\n                                            onClick = {\n                                                menuState.show {\n                                                    AlbumMenu(\n                                                        originalAlbum = item,\n                                                        navController = navController,\n                                                        onDismiss = menuState::dismiss,\n                                                    )\n                                                }\n                                            },\n                                        ) {\n                                            Icon(\n                                                painter = painterResource(R.drawable.more_vert),\n                                                contentDescription = null,\n                                            )\n                                        }\n                                    },\n                                    modifier =\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .combinedClickable(\n                                            onClick = {\n                                                navController.navigate(\"album/${item.id}\")\n                                            },\n                                            onLongClick = {\n                                                haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                menuState.show {\n                                                    AlbumMenu(\n                                                        originalAlbum = item,\n                                                        navController = navController,\n                                                        onDismiss = menuState::dismiss,\n                                                    )\n                                                }\n                                            },\n                                        )\n                                        .animateItem(),\n                                )\n                            }\n\n                            else -> {}\n                        }\n                    }\n                }\n\n            LibraryViewType.GRID ->\n                LazyVerticalGrid(\n                    state = lazyGridState,\n                    columns =\n                    GridCells.Adaptive(\n                        minSize = GridThumbnailHeight + if (gridItemSize == GridItemSize.BIG) 24.dp else (-24).dp,\n                    ),\n                    contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n                ) {\n                    item(\n                        key = \"filter\",\n                        span = { GridItemSpan(maxLineSpan) },\n                        contentType = CONTENT_TYPE_HEADER,\n                    ) {\n                        filterContent()\n                    }\n\n                    item(\n                        key = \"header\",\n                        span = { GridItemSpan(maxLineSpan) },\n                        contentType = CONTENT_TYPE_HEADER,\n                    ) {\n                        headerContent()\n                    }\n\n                    if (showLiked) {\n                        item(\n                            key = \"likedPlaylist\",\n                            contentType = { CONTENT_TYPE_PLAYLIST },\n                        ) {\n                            PlaylistGridItem(\n                                playlist = likedPlaylist,\n                                fillMaxWidth = true,\n                                autoPlaylist = true,\n                                modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .combinedClickable(\n                                        onClick = {\n                                            navController.navigate(\"auto_playlist/liked\")\n                                        },\n                                    )\n                                    .animateItem(),\n                            )\n                        }\n                    }\n\n                    if (showDownloaded) {\n                        item(\n                            key = \"downloadedPlaylist\",\n                            contentType = { CONTENT_TYPE_PLAYLIST },\n                        ) {\n                            PlaylistGridItem(\n                                playlist = downloadPlaylist,\n                                fillMaxWidth = true,\n                                autoPlaylist = true,\n                                modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .combinedClickable(\n                                        onClick = {\n                                            navController.navigate(\"auto_playlist/downloaded\")\n                                        },\n                                    )\n                                    .animateItem(),\n                            )\n                        }\n                    }\n\n                    if (showCached) {\n                        item(\n                            key = \"cachedPlaylist\",\n                            contentType = { CONTENT_TYPE_PLAYLIST },\n                        ) {\n                            PlaylistGridItem(\n                                playlist = cachedPlaylist,\n                                fillMaxWidth = true,\n                                autoPlaylist = true,\n                                modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .combinedClickable(\n                                        onClick = {\n                                            navController.navigate(\"cache_playlist/cached\")\n                                        },\n                                    )\n                                    .animateItem(),\n                            )\n                        }\n                    }\n\n                    if (showTop) {\n                        item(\n                            key = \"TopPlaylist\",\n                            contentType = { CONTENT_TYPE_PLAYLIST },\n                        ) {\n                            PlaylistGridItem(\n                                playlist = topPlaylist,\n                                fillMaxWidth = true,\n                                autoPlaylist = true,\n                                modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .combinedClickable(\n                                        onClick = {\n                                            navController.navigate(\"top_playlist/$topSize\")\n                                        },\n                                    )\n                                    .animateItem(),\n                            )\n                        }\n                    }\n\n                    if (showUploaded) {\n                        item(\n                            key = \"uploadedPlaylist\",\n                            contentType = { CONTENT_TYPE_PLAYLIST },\n                        ) {\n                            PlaylistGridItem(\n                                playlist = uploadedPlaylist,\n                                fillMaxWidth = true,\n                                autoPlaylist = true,\n                                modifier =\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .clickable {\n                                            navController.navigate(\"auto_playlist/uploaded\")\n                                        }\n                                        .animateItem(),\n                            )\n                        }\n                    }\n\n                    items(\n                        items = allItems.distinctBy { it.id },\n                        key = { it.id },\n                        contentType = { CONTENT_TYPE_PLAYLIST },\n                    ) { item ->\n                        when (item) {\n                            is Playlist -> {\n                                PlaylistGridItem(\n                                    playlist = item,\n                                    fillMaxWidth = true,\n                                    modifier =\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .combinedClickable(\n                                            onClick = {\n                                                if (!item.playlist.isEditable && item.songCount == 0 && item.playlist.browseId != null)\n                                                    navController.navigate(\"online_playlist/${item.playlist.browseId}\")\n                                                else\n                                                    navController.navigate(\"local_playlist/${item.id}\")\n                                            },\n                                            onLongClick = {\n                                                haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                menuState.show {\n                                                    PlaylistMenu(\n                                                        playlist = item,\n                                                        coroutineScope = coroutineScope,\n                                                        onDismiss = menuState::dismiss,\n                                                    )\n                                                }\n                                            },\n                                        )\n                                        .animateItem(),\n                                )\n                            }\n\n                            is Artist -> {\n                                ArtistGridItem(\n                                    artist = item,\n                                    fillMaxWidth = true,\n                                    modifier =\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .combinedClickable(\n                                            onClick = {\n                                                navController.navigate(\"artist/${item.id}\")\n                                            },\n                                            onLongClick = {\n                                                haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                menuState.show {\n                                                    ArtistMenu(\n                                                        originalArtist = item,\n                                                        coroutineScope = coroutineScope,\n                                                        onDismiss = menuState::dismiss,\n                                                    )\n                                                }\n                                            },\n                                        )\n                                        .animateItem(),\n                                )\n                            }\n\n                            is Album -> {\n                                AlbumGridItem(\n                                    album = item,\n                                    isActive = item.id == mediaMetadata?.album?.id,\n                                    isPlaying = isPlaying,\n                                    coroutineScope = coroutineScope,\n                                    fillMaxWidth = true,\n                                    modifier =\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .combinedClickable(\n                                            onClick = {\n                                                navController.navigate(\"album/${item.id}\")\n                                            },\n                                            onLongClick = {\n                                                haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                menuState.show {\n                                                    AlbumMenu(\n                                                        originalAlbum = item,\n                                                        navController = navController,\n                                                        onDismiss = menuState::dismiss,\n                                                    )\n                                                }\n                                            },\n                                        )\n                                        .animateItem(),\n                                )\n                            }\n\n                            else -> {}\n                        }\n                    }\n                }\n        }\n\n        Indicator(\n            isRefreshing = isRefreshing,\n            state = pullRefreshState,\n            modifier = Modifier\n                .align(Alignment.TopCenter)\n                .padding(LocalPlayerAwareWindowInsets.current.asPaddingValues()),\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/library/LibraryPlaylistsScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.library\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.grid.GridCells\nimport androidx.compose.foundation.lazy.grid.GridItemSpan\nimport androidx.compose.foundation.lazy.grid.LazyVerticalGrid\nimport androidx.compose.foundation.lazy.grid.items\nimport androidx.compose.foundation.lazy.grid.rememberLazyGridState\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.pluralStringResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport androidx.navigation.compose.currentBackStackEntryAsState\nimport com.metrolist.innertube.utils.parseCookieString\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.CONTENT_TYPE_HEADER\nimport com.metrolist.music.constants.CONTENT_TYPE_PLAYLIST\nimport com.metrolist.music.constants.GridItemSize\nimport com.metrolist.music.constants.GridItemsSizeKey\nimport com.metrolist.music.constants.GridThumbnailHeight\nimport com.metrolist.music.constants.InnerTubeCookieKey\nimport com.metrolist.music.constants.LibraryViewType\nimport com.metrolist.music.constants.PlaylistSortDescendingKey\nimport com.metrolist.music.constants.PlaylistSortType\nimport com.metrolist.music.constants.PlaylistSortTypeKey\nimport com.metrolist.music.constants.PlaylistViewTypeKey\nimport com.metrolist.music.constants.ShowCachedPlaylistKey\nimport com.metrolist.music.constants.ShowDownloadedPlaylistKey\nimport com.metrolist.music.constants.ShowLikedPlaylistKey\nimport com.metrolist.music.constants.ShowTopPlaylistKey\nimport com.metrolist.music.constants.ShowUploadedPlaylistKey\nimport com.metrolist.music.constants.YtmSyncKey\nimport com.metrolist.music.db.entities.Playlist\nimport com.metrolist.music.db.entities.PlaylistEntity\nimport com.metrolist.music.ui.component.CreatePlaylistDialog\nimport com.metrolist.music.ui.component.HideOnScrollFAB\nimport com.metrolist.music.ui.component.LibraryPlaylistGridItem\nimport com.metrolist.music.ui.component.LibraryPlaylistListItem\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.PlaylistGridItem\nimport com.metrolist.music.ui.component.PlaylistListItem\nimport com.metrolist.music.ui.component.SortHeader\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.viewmodels.LibraryPlaylistsViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport java.util.UUID\n\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nfun LibraryPlaylistsScreen(\n    navController: NavController,\n    filterContent: @Composable () -> Unit,\n    viewModel: LibraryPlaylistsViewModel = hiltViewModel(),\n    initialTextFieldValue: String? = null,\n    allowSyncing: Boolean = true,\n) {\n    val menuState = LocalMenuState.current\n    val haptic = LocalHapticFeedback.current\n\n    val coroutineScope = rememberCoroutineScope()\n\n    var viewType by rememberEnumPreference(PlaylistViewTypeKey, LibraryViewType.GRID)\n    val (sortType, onSortTypeChange) = rememberEnumPreference(\n        PlaylistSortTypeKey,\n        PlaylistSortType.CREATE_DATE\n    )\n    val (sortDescending, onSortDescendingChange) = rememberPreference(\n        PlaylistSortDescendingKey,\n        true\n    )\n    val gridItemSize by rememberEnumPreference(GridItemsSizeKey, GridItemSize.BIG)\n\n    val playlists by viewModel.allPlaylists.collectAsState()\n\n    val topSize by viewModel.topValue.collectAsState(initial = 50)\n\n    val likedPlaylist =\n        Playlist(\n            playlist = PlaylistEntity(\n                id = UUID.randomUUID().toString(),\n                name = stringResource(R.string.liked)\n            ),\n            songCount = 0,\n            songThumbnails = emptyList(),\n        )\n\n    val downloadPlaylist =\n        Playlist(\n            playlist = PlaylistEntity(\n                id = UUID.randomUUID().toString(),\n                name = stringResource(R.string.offline)\n            ),\n            songCount = 0,\n            songThumbnails = emptyList(),\n        )\n\n    val topPlaylist =\n        Playlist(\n            playlist = PlaylistEntity(\n                id = UUID.randomUUID().toString(),\n                name = stringResource(R.string.my_top) + \" $topSize\"\n            ),\n            songCount = 0,\n            songThumbnails = emptyList(),\n        )\n\n\n    val uploadedPlaylist =\n        Playlist(\n            playlist = PlaylistEntity(\n                id = UUID.randomUUID().toString(),\n                name = stringResource(R.string.uploaded_playlist)\n            ),\n            songCount = 0,\n            songThumbnails = emptyList(),\n        )\n\n    val cachedPlaylist =\n        Playlist(\n            playlist = PlaylistEntity(\n                id = UUID.randomUUID().toString(),\n                name = stringResource(R.string.cached_playlist)\n            ),\n            songCount = 0,\n            songThumbnails = emptyList(),\n        )\n\n    val (showLiked) = rememberPreference(ShowLikedPlaylistKey, true)\n    val (showDownloaded) = rememberPreference(ShowDownloadedPlaylistKey, true)\n    val (showTop) = rememberPreference(ShowTopPlaylistKey, true)\n    val (showUploaded) = rememberPreference(ShowUploadedPlaylistKey, true)\n    val (showCached) = rememberPreference(ShowCachedPlaylistKey, true)\n\n    val lazyListState = rememberLazyListState()\n    val lazyGridState = rememberLazyGridState()\n\n    val backStackEntry by navController.currentBackStackEntryAsState()\n    val scrollToTop =\n        backStackEntry?.savedStateHandle?.getStateFlow(\"scrollToTop\", false)?.collectAsState()\n\n    val (innerTubeCookie) = rememberPreference(InnerTubeCookieKey, \"\")\n    val isLoggedIn = remember(innerTubeCookie) {\n        \"SAPISID\" in parseCookieString(innerTubeCookie)\n    }\n\n    val (ytmSync) = rememberPreference(YtmSyncKey, true)\n\n    LaunchedEffect(Unit) {\n        if (ytmSync) {\n            withContext(Dispatchers.IO) {\n                viewModel.sync()\n            }\n        }\n    }\n\n    LaunchedEffect(scrollToTop?.value) {\n        if (scrollToTop?.value == true) {\n            when (viewType) {\n                LibraryViewType.LIST -> lazyListState.animateScrollToItem(0)\n                LibraryViewType.GRID -> lazyGridState.animateScrollToItem(0)\n            }\n            backStackEntry?.savedStateHandle?.set(\"scrollToTop\", false)\n        }\n    }\n\n    var showCreatePlaylistDialog by rememberSaveable { mutableStateOf(false) }\n\n    if (showCreatePlaylistDialog) {\n        CreatePlaylistDialog(\n            onDismiss = { showCreatePlaylistDialog = false },\n            initialTextFieldValue = initialTextFieldValue,\n            allowSyncing = allowSyncing,\n            onPlaylistCreated = { playlistId ->\n                showCreatePlaylistDialog = false\n                navController.navigate(\"local_playlist/$playlistId\")\n            }\n        )\n    }\n\n    val headerContent = @Composable {\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n            modifier = Modifier.padding(start = 16.dp),\n        ) {\n            SortHeader(\n                sortType = sortType,\n                sortDescending = sortDescending,\n                onSortTypeChange = onSortTypeChange,\n                onSortDescendingChange = onSortDescendingChange,\n                sortTypeText = { sortType ->\n                    when (sortType) {\n                        PlaylistSortType.CREATE_DATE -> R.string.sort_by_create_date\n                        PlaylistSortType.NAME -> R.string.sort_by_name\n                        PlaylistSortType.SONG_COUNT -> R.string.sort_by_song_count\n                        PlaylistSortType.LAST_UPDATED -> R.string.sort_by_last_updated\n                    }\n                },\n            )\n\n            Spacer(Modifier.weight(1f))\n\n            Text(\n                text = pluralStringResource(\n                    R.plurals.n_playlist,\n                    playlists.size,\n                    playlists.size\n                ),\n                style = MaterialTheme.typography.titleSmall,\n                color = MaterialTheme.colorScheme.secondary,\n            )\n\n            IconButton(\n                onClick = {\n                    viewType = viewType.toggle()\n                },\n                modifier = Modifier.padding(start = 6.dp, end = 6.dp),\n            ) {\n                Icon(\n                    painter =\n                    painterResource(\n                        when (viewType) {\n                            LibraryViewType.LIST -> R.drawable.list\n                            LibraryViewType.GRID -> R.drawable.grid_view\n                        },\n                    ),\n                    contentDescription = null,\n                )\n            }\n        }\n    }\n\n    Box(\n        modifier = Modifier.fillMaxSize(),\n    ) {\n        when (viewType) {\n            LibraryViewType.LIST -> {\n                LazyColumn(\n                    state = lazyListState,\n                    contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n                ) {\n                    item(\n                        key = \"filter\",\n                        contentType = CONTENT_TYPE_HEADER,\n                    ) {\n                        filterContent()\n                    }\n\n                    item(\n                        key = \"header\",\n                        contentType = CONTENT_TYPE_HEADER,\n                    ) {\n                        headerContent()\n                    }\n\n                    if (showLiked) {\n                        item(\n                            key = \"likedPlaylist\",\n                            contentType = { CONTENT_TYPE_PLAYLIST },\n                        ) {\n                            PlaylistListItem(\n                                playlist = likedPlaylist,\n                                autoPlaylist = true,\n                                modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .clickable {\n                                        navController.navigate(\"auto_playlist/liked\")\n                                    }\n                                    .animateItem(),\n                            )\n                        }\n                    }\n\n                    if (showDownloaded) {\n                        item(\n                            key = \"downloadedPlaylist\",\n                            contentType = { CONTENT_TYPE_PLAYLIST },\n                        ) {\n                            PlaylistListItem(\n                                playlist = downloadPlaylist,\n                                autoPlaylist = true,\n                                modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .clickable {\n                                        navController.navigate(\"auto_playlist/downloaded\")\n                                    }\n                                    .animateItem(),\n                            )\n                        }\n                    }\n\n                    if (showCached) {\n                        item(\n                            key = \"cachedPlaylist\",\n                            contentType = { CONTENT_TYPE_PLAYLIST },\n                        ) {\n                            PlaylistListItem(\n                                playlist = cachedPlaylist,\n                                autoPlaylist = true,\n                                modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .clickable {\n                                        navController.navigate(\"cache_playlist/cached\")\n                                    }\n                                    .animateItem(),\n                            )\n                        }\n                    }\n\n                    if (showTop) {\n                        item(\n                            key = \"TopPlaylist\",\n                            contentType = { CONTENT_TYPE_PLAYLIST },\n                        ) {\n                            PlaylistListItem(\n                                playlist = topPlaylist,\n                                autoPlaylist = true,\n                                modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .clickable {\n                                        navController.navigate(\"top_playlist/$topSize\")\n                                    }\n                                    .animateItem(),\n                            )\n                        }\n                    }\n\n\n                    if (showUploaded) {\n                        item(\n                            key = \"uploadedPlaylist\",\n                            contentType = { CONTENT_TYPE_PLAYLIST },\n                        ) {\n                            PlaylistListItem(\n                                playlist = uploadedPlaylist,\n                                autoPlaylist = true,\n                                modifier =\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .clickable {\n                                            navController.navigate(\"auto_playlist/uploaded\")\n                                        }\n                                        .animateItem(),\n                            )\n                        }\n                    }\n\n                    playlists.let { playlists ->\n                        if (playlists.isEmpty()) {\n                            item(key = \"empty_placeholder\") {\n                            }\n                        }\n\n                        items(\n                            items = playlists.distinctBy { it.id },\n                            key = { it.id },\n                            contentType = { CONTENT_TYPE_PLAYLIST },\n                        ) { playlist ->\n                            LibraryPlaylistListItem(\n                                navController = navController,\n                                menuState = menuState,\n                                coroutineScope = coroutineScope,\n                                playlist = playlist,\n                                modifier = Modifier.animateItem()\n                            )\n                        }\n                    }\n                }\n\n                HideOnScrollFAB(\n                    lazyListState = lazyListState,\n                    icon = R.drawable.add,\n                    onClick = {\n                        showCreatePlaylistDialog = true\n                    },\n                )\n            }\n\n            LibraryViewType.GRID -> {\n                LazyVerticalGrid(\n                    state = lazyGridState,\n                    columns =\n                    GridCells.Adaptive(\n                        minSize = GridThumbnailHeight + if (gridItemSize == GridItemSize.BIG) 24.dp else (-24).dp,\n                    ),\n                    contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n                ) {\n                    item(\n                        key = \"filter\",\n                        span = { GridItemSpan(maxLineSpan) },\n                        contentType = CONTENT_TYPE_HEADER,\n                    ) {\n                        filterContent()\n                    }\n\n                    item(\n                        key = \"header\",\n                        span = { GridItemSpan(maxLineSpan) },\n                        contentType = CONTENT_TYPE_HEADER,\n                    ) {\n                        headerContent()\n                    }\n\n                    if (showLiked) {\n                        item(\n                            key = \"likedPlaylist\",\n                            contentType = { CONTENT_TYPE_PLAYLIST },\n                        ) {\n                            PlaylistGridItem(\n                                playlist = likedPlaylist,\n                                fillMaxWidth = true,\n                                autoPlaylist = true,\n                                modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .combinedClickable(\n                                        onClick = {\n                                            navController.navigate(\"auto_playlist/liked\")\n                                        },\n                                    )\n                                    .animateItem(),\n                            )\n                        }\n                    }\n\n                    if (showDownloaded) {\n                        item(\n                            key = \"downloadedPlaylist\",\n                            contentType = { CONTENT_TYPE_PLAYLIST },\n                        ) {\n                            PlaylistGridItem(\n                                playlist = downloadPlaylist,\n                                fillMaxWidth = true,\n                                autoPlaylist = true,\n                                modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .combinedClickable(\n                                        onClick = {\n                                            navController.navigate(\"auto_playlist/downloaded\")\n                                        },\n                                    )\n                                    .animateItem(),\n                            )\n                        }\n                    }\n\n                    if (showCached) {\n                        item(\n                            key = \"cachedPlaylist\",\n                            contentType = { CONTENT_TYPE_PLAYLIST },\n                        ) {\n                            PlaylistGridItem(\n                                playlist = cachedPlaylist,\n                                fillMaxWidth = true,\n                                autoPlaylist = true,\n                                modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .combinedClickable(\n                                        onClick = {\n                                            navController.navigate(\"cache_playlist/cached\")\n                                        },\n                                    )\n                                    .animateItem(),\n                            )\n                        }\n                    }\n\n                    if (showTop) {\n                        item(\n                            key = \"TopPlaylist\",\n                            contentType = { CONTENT_TYPE_PLAYLIST },\n                        ) {\n                            PlaylistGridItem(\n                                playlist = topPlaylist,\n                                fillMaxWidth = true,\n                                autoPlaylist = true,\n                                modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .combinedClickable(\n                                        onClick = {\n                                            navController.navigate(\"top_playlist/$topSize\")\n                                        },\n                                    )\n                                    .animateItem(),\n                            )\n                        }\n                    }\n\n                    if (showUploaded) {\n                        item(\n                            key = \"uploadedPlaylist\",\n                            contentType = { CONTENT_TYPE_PLAYLIST },\n                        ) {\n                            PlaylistGridItem(\n                                playlist = uploadedPlaylist,\n                                fillMaxWidth = true,\n                                autoPlaylist = true,\n                                modifier =\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .clickable {\n                                            navController.navigate(\"auto_playlist/uploaded\")\n                                        }\n                                        .animateItem(),\n                            )\n                        }\n                    }\n\n                    playlists.let { playlists ->\n                        if (playlists.isEmpty()) {\n                            item(span = { GridItemSpan(maxLineSpan) }) {\n                            }\n                        }\n\n                        items(\n                            items = playlists.distinctBy { it.id },\n                            key = { it.id },\n                            contentType = { CONTENT_TYPE_PLAYLIST },\n                        ) { playlist ->\n                            LibraryPlaylistGridItem(\n                                navController = navController,\n                                menuState = menuState,\n                                coroutineScope = coroutineScope,\n                                playlist = playlist,\n                                modifier = Modifier.animateItem()\n                            )\n                        }\n                    }\n                }\n\n                HideOnScrollFAB(\n                    lazyListState = lazyGridState,\n                    icon = R.drawable.add,\n                    onClick = {\n                        showCreatePlaylistDialog = true\n                    },\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/library/LibraryPodcastsScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.library\n\nimport android.content.Intent\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.FilterChip\nimport androidx.compose.material3.FilterChipDefaults\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator\nimport androidx.compose.material3.pulltorefresh.pullToRefresh\nimport androidx.compose.material3.pulltorefresh.rememberPullToRefreshState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.pluralStringResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.LifecycleEventObserver\nimport androidx.lifecycle.compose.LocalLifecycleOwner\nimport androidx.navigation.NavController\nimport androidx.navigation.compose.currentBackStackEntryAsState\nimport coil3.compose.AsyncImage\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.LocalSyncUtils\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.CONTENT_TYPE_HEADER\nimport com.metrolist.music.constants.CONTENT_TYPE_SONG\nimport com.metrolist.music.constants.PodcastFilter\nimport com.metrolist.music.constants.PodcastFilterKey\nimport com.metrolist.music.constants.SongSortDescendingKey\nimport com.metrolist.music.constants.SongSortType\nimport com.metrolist.music.constants.SongSortTypeKey\nimport com.metrolist.music.constants.ThumbnailCornerRadius\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.db.entities.PodcastEntity\nimport com.metrolist.music.db.entities.SpeedDialItem\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.playback.queues.ListQueue\nimport com.metrolist.music.ui.component.ChipsRow\nimport com.metrolist.music.ui.component.HideOnScrollFAB\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.Material3MenuGroup\nimport com.metrolist.music.ui.component.Material3MenuItemData\nimport com.metrolist.music.ui.component.SongListItem\nimport com.metrolist.music.ui.component.SortHeader\nimport com.metrolist.music.ui.menu.SongMenu\nimport com.metrolist.music.utils.joinByBullet\nimport com.metrolist.music.utils.makeTimeString\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.viewmodels.LibraryPodcastsViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\n\n@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun LibraryPodcastsScreen(\n    navController: NavController,\n    onDeselect: () -> Unit,\n    viewModel: LibraryPodcastsViewModel = hiltViewModel(),\n) {\n    val downloadedEpisodesStr = stringResource(R.string.downloaded_episodes)\n    val database = LocalDatabase.current\n    val menuState = LocalMenuState.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n\n    var podcastFilter by rememberEnumPreference(PodcastFilterKey, PodcastFilter.EPISODES)\n\n    val (sortType, onSortTypeChange) =\n        rememberEnumPreference(\n            SongSortTypeKey,\n            SongSortType.CREATE_DATE,\n        )\n    val (sortDescending, onSortDescendingChange) = rememberPreference(SongSortDescendingKey, true)\n\n    val subscribedChannels by viewModel.subscribedChannels.collectAsState()\n    val downloadedEpisodes by viewModel.downloadedEpisodes.collectAsState()\n    val savedEpisodes by viewModel.savedEpisodes.collectAsState()\n    val sePlaylist by viewModel.sePlaylist.collectAsState()\n    val podcastChannels by viewModel.podcastChannels.collectAsState()\n    val rdpnPlaylist by viewModel.rdpnPlaylist.collectAsState()\n\n    // Refresh channels when screen becomes visible (ON_RESUME)\n    val lifecycleOwner = LocalLifecycleOwner.current\n    DisposableEffect(lifecycleOwner) {\n        val observer =\n            LifecycleEventObserver { _, event ->\n                if (event == Lifecycle.Event.ON_RESUME) {\n                    viewModel.refreshChannels()\n                }\n            }\n        lifecycleOwner.lifecycle.addObserver(observer)\n        onDispose {\n            lifecycleOwner.lifecycle.removeObserver(observer)\n        }\n    }\n\n    val lazyListState = rememberLazyListState()\n\n    val backStackEntry by navController.currentBackStackEntryAsState()\n    val scrollToTop =\n        backStackEntry?.savedStateHandle?.getStateFlow(\"scrollToTop\", false)?.collectAsState()\n\n    LaunchedEffect(scrollToTop?.value) {\n        if (scrollToTop?.value == true) {\n            lazyListState.animateScrollToItem(0)\n            backStackEntry?.savedStateHandle?.set(\"scrollToTop\", false)\n        }\n    }\n\n    var isRefreshing by remember { mutableStateOf(false) }\n    val coroutineScope = rememberCoroutineScope()\n    val pullToRefreshState = rememberPullToRefreshState()\n\n    Box(\n        modifier =\n            Modifier\n                .fillMaxSize()\n                .pullToRefresh(\n                    state = pullToRefreshState,\n                    isRefreshing = isRefreshing,\n                    onRefresh = {\n                        if (!isRefreshing) {\n                            isRefreshing = true\n                            coroutineScope.launch {\n                                viewModel.refreshAll()\n                                isRefreshing = false\n                            }\n                        }\n                    },\n                ),\n    ) {\n        // Chip row header — same pattern as LibrarySongsScreen\n        val chipsHeader = @Composable {\n            Row {\n                Spacer(Modifier.width(12.dp))\n                FilterChip(\n                    label = { Text(stringResource(R.string.filter_podcasts)) },\n                    selected = true,\n                    colors =\n                        FilterChipDefaults.filterChipColors(\n                            containerColor = MaterialTheme.colorScheme.surface,\n                        ),\n                    onClick = onDeselect,\n                    shape = RoundedCornerShape(16.dp),\n                    border = null,\n                    leadingIcon = {\n                        Icon(\n                            painter = painterResource(R.drawable.close),\n                            contentDescription = null,\n                        )\n                    },\n                )\n                ChipsRow(\n                    chips =\n                        listOf(\n                            PodcastFilter.EPISODES to stringResource(R.string.filter_episodes),\n                            PodcastFilter.CHANNELS to stringResource(R.string.filter_channels),\n                            PodcastFilter.DOWNLOADED to stringResource(R.string.filter_downloaded),\n                        ),\n                    currentValue = podcastFilter,\n                    onValueUpdate = { podcastFilter = it },\n                    modifier = Modifier.weight(1f),\n                )\n            }\n        }\n\n        when (podcastFilter) {\n            // ── EPISODES FOR LATER tab ────────────────────────────────────\n            PodcastFilter.EPISODES -> {\n                LazyColumn(\n                    state = lazyListState,\n                    contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n                ) {\n                    item(key = \"filter\", contentType = CONTENT_TYPE_HEADER) {\n                        chipsHeader()\n                    }\n\n                    // RDPN \"New Episodes\" auto-playlist card\n                    item(key = \"rdpn_playlist\", contentType = CONTENT_TYPE_HEADER) {\n                        AutoPlaylistCard(\n                            title = stringResource(R.string.new_episodes),\n                            thumbnailUrl = rdpnPlaylist?.thumbnail,\n                            episodeCount = rdpnPlaylist?.songCountText,\n                            onClick = { navController.navigate(\"online_playlist/RDPN\") },\n                        )\n                    }\n\n                    // Episodes for Later - card/folder (works both logged in and out)\n                    item(key = \"episodes_for_later\", contentType = CONTENT_TYPE_HEADER) {\n                        AutoPlaylistCard(\n                            title = stringResource(R.string.episodes_for_later),\n                            thumbnailUrl = sePlaylist?.thumbnail ?: savedEpisodes.firstOrNull()?.song?.thumbnailUrl,\n                            episodeCount =\n                                sePlaylist?.songCountText ?: if (savedEpisodes.isNotEmpty()) {\n                                    pluralStringResource(R.plurals.n_episode, savedEpisodes.size, savedEpisodes.size)\n                                } else {\n                                    null\n                                },\n                            onClick = { navController.navigate(\"online_playlist/SE\") },\n                        )\n                    }\n\n                    // Saved podcast shows (episode playlists) from YT Music library\n                    itemsIndexed(\n                        items = subscribedChannels,\n                        key = { _, item -> item.id },\n                        contentType = { _, _ -> CONTENT_TYPE_SONG },\n                    ) { _, podcast ->\n                        PodcastEpisodePlaylistItem(\n                            podcast = podcast,\n                            onClick = { navController.navigate(\"online_podcast/${podcast.id}\") },\n                            onMenuClick = {\n                                menuState.show {\n                                    PodcastEpisodePlaylistMenu(\n                                        podcast = podcast,\n                                        database = database,\n                                        onDismiss = menuState::dismiss,\n                                    )\n                                }\n                            },\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .animateItem(),\n                        )\n                    }\n                }\n            }\n\n            // ── CHANNELS tab — podcast host artist pages from YT Music ───\n            PodcastFilter.CHANNELS -> {\n                LazyColumn(\n                    state = lazyListState,\n                    contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n                ) {\n                    item(key = \"filter\", contentType = CONTENT_TYPE_HEADER) {\n                        chipsHeader()\n                    }\n\n                    item(key = \"channels_count\", contentType = CONTENT_TYPE_HEADER) {\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .padding(horizontal = 16.dp, vertical = 4.dp),\n                        ) {\n                            Text(\n                                text =\n                                    pluralStringResource(\n                                        R.plurals.n_channel,\n                                        podcastChannels.size,\n                                        podcastChannels.size,\n                                    ),\n                                style = MaterialTheme.typography.titleSmall,\n                                color = MaterialTheme.colorScheme.secondary,\n                            )\n                        }\n                    }\n\n                    itemsIndexed(\n                        items = podcastChannels,\n                        key = { _, item -> item.id },\n                        contentType = { _, _ -> CONTENT_TYPE_SONG },\n                    ) { _, channel ->\n                        PodcastArtistChannelItem(\n                            thumbnailUrl = channel.thumbnail,\n                            channelName = channel.title,\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .clickable {\n                                        navController.navigate(\"artist/${channel.id}\")\n                                    }.animateItem(),\n                        )\n                    }\n\n                    if (podcastChannels.isEmpty()) {\n                        item(key = \"empty\") {\n                            Box(\n                                modifier =\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .padding(vertical = 48.dp),\n                                contentAlignment = Alignment.Center,\n                            ) {\n                                Text(\n                                    text = stringResource(R.string.no_subscribed_channels),\n                                    style = MaterialTheme.typography.bodyMedium,\n                                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n\n            // ── DOWNLOADED tab ────────────────────────────────────────────\n            PodcastFilter.DOWNLOADED -> {\n                LazyColumn(\n                    state = lazyListState,\n                    contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n                ) {\n                    item(key = \"filter\", contentType = CONTENT_TYPE_HEADER) {\n                        chipsHeader()\n                    }\n\n                    item(key = \"sort_header\", contentType = CONTENT_TYPE_HEADER) {\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            modifier = Modifier.padding(horizontal = 16.dp),\n                        ) {\n                            SortHeader(\n                                sortType = sortType,\n                                sortDescending = sortDescending,\n                                onSortTypeChange = onSortTypeChange,\n                                onSortDescendingChange = onSortDescendingChange,\n                                sortTypeText = { st ->\n                                    when (st) {\n                                        SongSortType.CREATE_DATE -> R.string.sort_by_create_date\n                                        SongSortType.NAME -> R.string.sort_by_name\n                                        SongSortType.ARTIST -> R.string.sort_by_artist\n                                        SongSortType.PLAY_TIME -> R.string.sort_by_play_time\n                                    }\n                                },\n                            )\n                            Spacer(Modifier.weight(1f))\n                            Text(\n                                text =\n                                    pluralStringResource(\n                                        R.plurals.n_episode,\n                                        downloadedEpisodes.size,\n                                        downloadedEpisodes.size,\n                                    ),\n                                style = MaterialTheme.typography.titleSmall,\n                                color = MaterialTheme.colorScheme.secondary,\n                            )\n                        }\n                    }\n\n                    itemsIndexed(\n                        items = downloadedEpisodes,\n                        key = { _, item -> item.song.id },\n                        contentType = { _, _ -> CONTENT_TYPE_SONG },\n                    ) { index, episode ->\n                        // Always show channel name: use artists if available,\n                        // else fall back to song.albumName (podcast show title stored during sync)\n                        val channelName =\n                            episode.artists\n                                .joinToString { it.name }\n                                .ifEmpty { episode.song.albumName ?: \"\" }\n                        val subtitle =\n                            joinByBullet(\n                                channelName,\n                                makeTimeString(episode.song.duration.toLong() * 1000L),\n                            )\n                        SongListItem(\n                            song = episode,\n                            showInLibraryIcon = false,\n                            isActive = episode.id == mediaMetadata?.id,\n                            isPlaying = isPlaying,\n                            showLikedIcon = false,\n                            showDownloadIcon = true,\n                            subtitleOverride = subtitle.ifEmpty { null },\n                            trailingContent = {\n                                IconButton(\n                                    onClick = {\n                                        menuState.show {\n                                            SongMenu(\n                                                originalSong = episode,\n                                                navController = navController,\n                                                onDismiss = menuState::dismiss,\n                                            )\n                                        }\n                                    },\n                                ) {\n                                    Icon(\n                                        painter = painterResource(R.drawable.more_vert),\n                                        contentDescription = null,\n                                    )\n                                }\n                            },\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .clickable {\n                                        if (episode.id == mediaMetadata?.id) {\n                                            playerConnection.togglePlayPause()\n                                        } else {\n                                            playerConnection.playQueue(\n                                                ListQueue(\n                                                    title = downloadedEpisodesStr,\n                                                    items = downloadedEpisodes.map { it.toMediaItem() },\n                                                    startIndex = index,\n                                                ),\n                                            )\n                                        }\n                                    }.animateItem(),\n                        )\n                    }\n\n                    if (downloadedEpisodes.isEmpty()) {\n                        item(key = \"empty\") {\n                            Box(\n                                modifier =\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .padding(vertical = 48.dp),\n                                contentAlignment = Alignment.Center,\n                            ) {\n                                Text(\n                                    text = stringResource(R.string.no_downloaded_episodes),\n                                    style = MaterialTheme.typography.bodyMedium,\n                                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                                )\n                            }\n                        }\n                    }\n                }\n\n                HideOnScrollFAB(\n                    visible = downloadedEpisodes.isNotEmpty(),\n                    lazyListState = lazyListState,\n                    icon = R.drawable.shuffle,\n                    onClick = {\n                        playerConnection.playQueue(\n                            ListQueue(\n                                title = downloadedEpisodesStr,\n                                items = downloadedEpisodes.shuffled().map { it.toMediaItem() },\n                            ),\n                        )\n                    },\n                )\n            }\n        }\n\n        Indicator(\n            isRefreshing = isRefreshing,\n            state = pullToRefreshState,\n            modifier =\n                Modifier\n                    .align(Alignment.TopCenter)\n                    .padding(LocalPlayerAwareWindowInsets.current.asPaddingValues()),\n        )\n    }\n}\n\n/** Auto-playlist card — mirrors YT Music design. Used for both SE and RDPN playlists. */\n@Composable\nprivate fun AutoPlaylistCard(\n    title: String,\n    thumbnailUrl: String?,\n    episodeCount: String?,\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier =\n            modifier\n                .fillMaxWidth()\n                .clickable(onClick = onClick)\n                .padding(horizontal = 16.dp, vertical = 10.dp),\n    ) {\n        Box(\n            modifier =\n                Modifier\n                    .size(56.dp)\n                    .clip(RoundedCornerShape(ThumbnailCornerRadius))\n                    .background(MaterialTheme.colorScheme.primaryContainer),\n            contentAlignment = Alignment.Center,\n        ) {\n            if (thumbnailUrl != null) {\n                AsyncImage(\n                    model = thumbnailUrl,\n                    contentDescription = null,\n                    contentScale = ContentScale.Crop,\n                    modifier =\n                        Modifier\n                            .size(56.dp)\n                            .clip(RoundedCornerShape(ThumbnailCornerRadius)),\n                )\n            } else {\n                Icon(\n                    painter = painterResource(R.drawable.queue_music),\n                    contentDescription = null,\n                    tint = MaterialTheme.colorScheme.onPrimaryContainer,\n                    modifier = Modifier.size(28.dp),\n                )\n            }\n        }\n\n        Spacer(Modifier.width(12.dp))\n\n        Column(modifier = Modifier.weight(1f)) {\n            Text(\n                text = title,\n                style = MaterialTheme.typography.bodyLarge,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n            )\n            Text(\n                text =\n                    buildString {\n                        append(stringResource(R.string.auto_playlist))\n                        if (!episodeCount.isNullOrBlank()) {\n                            append(\" • \")\n                            append(episodeCount)\n                        }\n                    },\n                style = MaterialTheme.typography.bodyMedium,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n            )\n        }\n    }\n}\n\n/** Episode playlist row shown in the Episodes tab — represents a saved podcast show */\n@Composable\nprivate fun PodcastEpisodePlaylistItem(\n    podcast: PodcastEntity,\n    onClick: () -> Unit,\n    onMenuClick: () -> Unit,\n    modifier: Modifier = Modifier,\n) {\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier =\n            modifier\n                .clickable(onClick = onClick)\n                .padding(horizontal = 16.dp, vertical = 8.dp),\n    ) {\n        Box(\n            modifier =\n                Modifier\n                    .size(56.dp)\n                    .clip(RoundedCornerShape(ThumbnailCornerRadius))\n                    .background(MaterialTheme.colorScheme.primaryContainer),\n            contentAlignment = Alignment.Center,\n        ) {\n            if (podcast.thumbnailUrl != null) {\n                AsyncImage(\n                    model = podcast.thumbnailUrl,\n                    contentDescription = null,\n                    contentScale = ContentScale.Crop,\n                    modifier =\n                        Modifier\n                            .size(56.dp)\n                            .clip(RoundedCornerShape(ThumbnailCornerRadius)),\n                )\n            } else {\n                Icon(\n                    painter = painterResource(R.drawable.queue_music),\n                    contentDescription = null,\n                    tint = MaterialTheme.colorScheme.onPrimaryContainer,\n                    modifier = Modifier.size(28.dp),\n                )\n            }\n        }\n\n        Spacer(Modifier.width(12.dp))\n\n        Column(modifier = Modifier.weight(1f)) {\n            Text(\n                text = podcast.title,\n                style = MaterialTheme.typography.bodyLarge,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n            )\n            if (!podcast.author.isNullOrBlank()) {\n                Text(\n                    text = podcast.author,\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis,\n                )\n            }\n        }\n\n        IconButton(onClick = onMenuClick) {\n            Icon(\n                painter = painterResource(R.drawable.more_vert),\n                contentDescription = null,\n            )\n        }\n    }\n}\n\n/** Menu shown when tapping the three-dot icon on an episode playlist */\n@Composable\nprivate fun PodcastEpisodePlaylistMenu(\n    podcast: PodcastEntity,\n    database: MusicDatabase,\n    onDismiss: () -> Unit,\n) {\n    val context = LocalContext.current\n    val coroutineScope = rememberCoroutineScope()\n    val syncUtils = LocalSyncUtils.current\n    val isPinned by database.speedDialDao.isPinned(podcast.id).collectAsState(initial = false)\n\n    val playlistId = podcast.id.removePrefix(\"MPSP\")\n    val shareUrl = \"https://music.youtube.com/playlist?list=$playlistId\"\n\n    Spacer(Modifier.height(12.dp))\n    Material3MenuGroup(\n        items =\n            listOf(\n                Material3MenuItemData(\n                    title = { Text(text = stringResource(R.string.remove_from_library)) },\n                    icon = {\n                        Icon(\n                            painter = painterResource(R.drawable.delete),\n                            contentDescription = null,\n                        )\n                    },\n                    onClick = {\n                        coroutineScope.launch(Dispatchers.IO) {\n                            // Update local database\n                            database.query {\n                                update(podcast.copy(bookmarkedAt = null))\n                            }\n                            // Sync with YouTube (unsave podcast only, don't unsubscribe channel)\n                            syncUtils.savePodcast(podcast.id, false)\n                        }\n                        onDismiss()\n                    },\n                ),\n                Material3MenuItemData(\n                    title = { Text(text = stringResource(R.string.share)) },\n                    icon = {\n                        Icon(\n                            painter = painterResource(R.drawable.share),\n                            contentDescription = null,\n                        )\n                    },\n                    onClick = {\n                        val intent =\n                            Intent().apply {\n                                action = Intent.ACTION_SEND\n                                type = \"text/plain\"\n                                putExtra(Intent.EXTRA_TEXT, shareUrl)\n                            }\n                        context.startActivity(Intent.createChooser(intent, null))\n                        onDismiss()\n                    },\n                ),\n                Material3MenuItemData(\n                    title = {\n                        Text(\n                            text =\n                                stringResource(\n                                    if (isPinned) {\n                                        R.string.unpin_from_speed_dial\n                                    } else {\n                                        R.string.pin_to_speed_dial\n                                    },\n                                ),\n                        )\n                    },\n                    icon = {\n                        Icon(\n                            painter =\n                                painterResource(\n                                    if (isPinned) R.drawable.remove else R.drawable.add,\n                                ),\n                            contentDescription = null,\n                        )\n                    },\n                    onClick = {\n                        coroutineScope.launch(Dispatchers.IO) {\n                            if (isPinned) {\n                                database.speedDialDao.delete(podcast.id)\n                            } else {\n                                database.speedDialDao.insert(\n                                    SpeedDialItem(\n                                        id = podcast.id,\n                                        title = podcast.title,\n                                        subtitle = podcast.author,\n                                        thumbnailUrl = podcast.thumbnailUrl,\n                                        type = \"PLAYLIST\",\n                                    ),\n                                )\n                            }\n                        }\n                        onDismiss()\n                    },\n                ),\n            ),\n    )\n    Spacer(Modifier.height(12.dp))\n}\n\n/** Artist/channel page item shown in the Channels tab */\n@Composable\nprivate fun PodcastArtistChannelItem(\n    thumbnailUrl: String?,\n    channelName: String,\n    modifier: Modifier = Modifier,\n) {\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = modifier.padding(horizontal = 16.dp, vertical = 8.dp),\n    ) {\n        AsyncImage(\n            model = thumbnailUrl,\n            contentDescription = null,\n            contentScale = ContentScale.Crop,\n            modifier =\n                Modifier\n                    .size(56.dp)\n                    .clip(CircleShape),\n        )\n\n        Spacer(Modifier.width(12.dp))\n\n        Text(\n            text = channelName,\n            style = MaterialTheme.typography.bodyLarge,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n            modifier = Modifier.weight(1f),\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/library/LibraryScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.library\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.navigation.NavController\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.ChipSortTypeKey\nimport com.metrolist.music.constants.LibraryFilter\nimport com.metrolist.music.ui.component.ChipsRow\nimport com.metrolist.music.utils.rememberEnumPreference\n\n@Composable\nfun LibraryScreen(navController: NavController) {\n    var filterType by rememberEnumPreference(ChipSortTypeKey, LibraryFilter.LIBRARY)\n\n    val filterContent = @Composable {\n        Row {\n            ChipsRow(\n                chips = listOf(\n                    LibraryFilter.PLAYLISTS to stringResource(R.string.filter_playlists),\n                    LibraryFilter.SONGS to stringResource(R.string.filter_songs),\n                    LibraryFilter.ALBUMS to stringResource(R.string.filter_albums),\n                    LibraryFilter.ARTISTS to stringResource(R.string.filter_artists),\n                    LibraryFilter.PODCASTS to stringResource(R.string.filter_podcasts),\n                ),\n                currentValue = filterType,\n                onValueUpdate = {\n                    filterType = if (filterType == it) LibraryFilter.LIBRARY else it\n                },\n                modifier = Modifier.weight(1f),\n            )\n        }\n    }\n\n    Box(modifier = Modifier.fillMaxSize()) {\n        when (filterType) {\n            LibraryFilter.LIBRARY -> LibraryMixScreen(navController, filterContent)\n            LibraryFilter.PLAYLISTS -> LibraryPlaylistsScreen(navController, filterContent)\n            LibraryFilter.SONGS -> LibrarySongsScreen(\n                navController,\n                { filterType = LibraryFilter.LIBRARY },\n            )\n            LibraryFilter.ALBUMS -> LibraryAlbumsScreen(\n                navController,\n                { filterType = LibraryFilter.LIBRARY },\n            )\n            LibraryFilter.ARTISTS -> LibraryArtistsScreen(\n                navController,\n                { filterType = LibraryFilter.LIBRARY },\n            )\n            LibraryFilter.PODCASTS -> LibraryPodcastsScreen(\n                navController,\n                { filterType = LibraryFilter.LIBRARY },\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/library/LibrarySongsScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.library\n\nimport android.net.Uri\nimport android.widget.Toast\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.FilterChip\nimport androidx.compose.material3.FilterChipDefaults\nimport androidx.compose.material3.FloatingActionButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LinearProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.pluralStringResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport androidx.navigation.compose.currentBackStackEntryAsState\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.CONTENT_TYPE_HEADER\nimport com.metrolist.music.constants.CONTENT_TYPE_SONG\nimport com.metrolist.music.constants.HideExplicitKey\nimport com.metrolist.music.constants.SongFilter\nimport com.metrolist.music.constants.SongFilterKey\nimport com.metrolist.music.constants.SongSortDescendingKey\nimport com.metrolist.music.constants.SongSortType\nimport com.metrolist.music.constants.SongSortTypeKey\nimport com.metrolist.music.constants.YtmSyncKey\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.playback.queues.ListQueue\nimport com.metrolist.music.ui.component.ChipsRow\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.HideOnScrollFAB\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.SongListItem\nimport com.metrolist.music.ui.component.SortHeader\nimport com.metrolist.music.ui.menu.SongMenu\nimport com.metrolist.music.ui.utils.isScrollingUp\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.viewmodels.LibrarySongsViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\n\n@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun LibrarySongsScreen(\n    navController: NavController,\n    onDeselect: () -> Unit,\n    viewModel: LibrarySongsViewModel = hiltViewModel(),\n) {\n    val context = LocalContext.current\n    val menuState = LocalMenuState.current\n    val uploadUnsupportedFormatStr = stringResource(R.string.upload_unsupported_format)\n    val uploadFileTooLargeStr = stringResource(R.string.upload_file_too_large)\n    val uploadFailedStr = stringResource(R.string.upload_failed)\n    val uploadCompleteStr = stringResource(R.string.upload_complete)\n    val queueAllSongsStr = stringResource(R.string.queue_all_songs)\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n    val scope = rememberCoroutineScope()\n\n    val (sortType, onSortTypeChange) =\n        rememberEnumPreference(\n            SongSortTypeKey,\n            SongSortType.CREATE_DATE,\n        )\n    val (sortDescending, onSortDescendingChange) = rememberPreference(SongSortDescendingKey, true)\n\n    val (ytmSync) = rememberPreference(YtmSyncKey, true)\n    val hideExplicit by rememberPreference(key = HideExplicitKey, defaultValue = false)\n\n    val songs by viewModel.allSongs.collectAsState()\n\n    var filter by rememberEnumPreference(SongFilterKey, SongFilter.LIKED)\n\n    // Upload state\n    var showUploadDialog by remember { mutableStateOf(false) }\n    var uploadProgress by remember { mutableFloatStateOf(0f) }\n    var currentUploadIndex by remember { mutableIntStateOf(0) }\n    var totalUploads by remember { mutableIntStateOf(0) }\n    var currentFileName by remember { mutableStateOf(\"\") }\n    var isUploading by remember { mutableStateOf(false) }\n    var uploadJob by remember { mutableStateOf<kotlinx.coroutines.Job?>(null) }\n\n    val filePickerLauncher =\n        rememberLauncherForActivityResult(\n            contract = ActivityResultContracts.OpenMultipleDocuments(),\n        ) { uris: List<Uri> ->\n            if (uris.isNotEmpty()) {\n                uploadJob =\n                    scope.launch {\n                        isUploading = true\n                        showUploadDialog = true\n                        totalUploads = uris.size\n                        var successCount = 0\n\n                        uris.forEachIndexed { index, uri ->\n                            currentUploadIndex = index + 1\n                            uploadProgress = 0f\n\n                            try {\n                                val fileName = uri.lastPathSegment?.substringAfterLast('/') ?: \"unknown\"\n                                currentFileName = fileName\n                                val extension = fileName.substringAfterLast('.', \"\").lowercase()\n\n                                if (extension !in YouTube.SUPPORTED_UPLOAD_TYPES) {\n                                    withContext(Dispatchers.Main) {\n                                        Toast\n                                            .makeText(\n                                                context,\n                                                uploadUnsupportedFormatStr,\n                                                Toast.LENGTH_SHORT,\n                                            ).show()\n                                    }\n                                    return@forEachIndexed\n                                }\n\n                                val inputStream = context.contentResolver.openInputStream(uri)\n                                val data = inputStream?.readBytes()\n                                inputStream?.close()\n\n                                if (data == null) return@forEachIndexed\n\n                                if (data.size > YouTube.MAX_UPLOAD_SIZE) {\n                                    withContext(Dispatchers.Main) {\n                                        Toast\n                                            .makeText(\n                                                context,\n                                                uploadFileTooLargeStr,\n                                                Toast.LENGTH_SHORT,\n                                            ).show()\n                                    }\n                                    return@forEachIndexed\n                                }\n\n                                val result =\n                                    YouTube.uploadSong(\n                                        filename = fileName,\n                                        data = data,\n                                        onProgress = { progress ->\n                                            uploadProgress = progress\n                                        },\n                                    )\n\n                                if (result.isSuccess && result.getOrDefault(false)) {\n                                    successCount++\n                                }\n                            } catch (e: Exception) {\n                                withContext(Dispatchers.Main) {\n                                    Toast\n                                        .makeText(\n                                            context,\n                                            uploadFailedStr + \": ${e.message}\",\n                                            Toast.LENGTH_SHORT,\n                                        ).show()\n                                }\n                            }\n                        }\n\n                        isUploading = false\n\n                        if (successCount > 0) {\n                            // Show completion briefly\n                            uploadProgress = 1f\n                            currentFileName = uploadCompleteStr\n                            kotlinx.coroutines.delay(1000)\n\n                            // Show toast on main thread\n                            withContext(Dispatchers.Main) {\n                                Toast\n                                    .makeText(\n                                        context,\n                                        uploadCompleteStr,\n                                        Toast.LENGTH_SHORT,\n                                    ).show()\n                            }\n\n                            showUploadDialog = false\n\n                            // Refresh uploaded songs\n                            viewModel.syncUploadedSongs()\n                        } else {\n                            showUploadDialog = false\n                        }\n                    }\n            }\n        }\n\n    LaunchedEffect(Unit) {\n        if (ytmSync) {\n            when (filter) {\n                SongFilter.LIKED -> viewModel.syncLikedSongs()\n                SongFilter.LIBRARY -> viewModel.syncLibrarySongs()\n                SongFilter.UPLOADED -> viewModel.syncUploadedSongs()\n                else -> return@LaunchedEffect\n            }\n        }\n    }\n\n    val lazyListState = rememberLazyListState()\n\n    val backStackEntry by navController.currentBackStackEntryAsState()\n    val scrollToTop =\n        backStackEntry?.savedStateHandle?.getStateFlow(\"scrollToTop\", false)?.collectAsState()\n\n    LaunchedEffect(scrollToTop?.value) {\n        if (scrollToTop?.value == true) {\n            lazyListState.animateScrollToItem(0)\n            backStackEntry?.savedStateHandle?.set(\"scrollToTop\", false)\n        }\n    }\n\n    val filteredSongs =\n        if (hideExplicit) {\n            songs.filter { !it.song.explicit }\n        } else {\n            songs\n        }\n\n    // Upload progress dialog\n    if (showUploadDialog) {\n        DefaultDialog(\n            onDismiss = {\n                if (isUploading) {\n                    uploadJob?.cancel()\n                    isUploading = false\n                }\n                showUploadDialog = false\n            },\n            icon = {\n                Icon(\n                    painter = painterResource(R.drawable.upload),\n                    contentDescription = null,\n                )\n            },\n            title = { Text(stringResource(R.string.uploading)) },\n            buttons = {\n                TextButton(\n                    onClick = {\n                        if (isUploading) {\n                            uploadJob?.cancel()\n                            isUploading = false\n                        }\n                        showUploadDialog = false\n                    },\n                ) {\n                    Text(stringResource(R.string.cancel))\n                }\n            },\n        ) {\n            Text(\n                text = stringResource(R.string.upload_progress, currentUploadIndex, totalUploads),\n                style = MaterialTheme.typography.bodyMedium,\n            )\n            Spacer(modifier = Modifier.height(8.dp))\n            Text(\n                text = currentFileName,\n                style = MaterialTheme.typography.bodySmall,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n            )\n            Spacer(modifier = Modifier.height(16.dp))\n            LinearProgressIndicator(\n                progress = { uploadProgress },\n                modifier = Modifier.fillMaxWidth(),\n            )\n        }\n    }\n\n    Box(\n        modifier = Modifier.fillMaxSize(),\n    ) {\n        LazyColumn(\n            state = lazyListState,\n            contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n        ) {\n            item(\n                key = \"filter\",\n                contentType = CONTENT_TYPE_HEADER,\n            ) {\n                Row {\n                    Spacer(Modifier.width(12.dp))\n                    FilterChip(\n                        label = { Text(stringResource(R.string.songs)) },\n                        selected = true,\n                        colors = FilterChipDefaults.filterChipColors(containerColor = MaterialTheme.colorScheme.surface),\n                        onClick = onDeselect,\n                        shape = RoundedCornerShape(16.dp),\n                        leadingIcon = {\n                            Icon(\n                                painter = painterResource(R.drawable.close),\n                                contentDescription = \"\",\n                            )\n                        },\n                    )\n                    ChipsRow(\n                        chips =\n                            listOf(\n                                SongFilter.LIKED to stringResource(R.string.filter_liked),\n                                SongFilter.LIBRARY to stringResource(R.string.filter_library),\n                                SongFilter.UPLOADED to stringResource(R.string.filter_uploaded),\n                                SongFilter.DOWNLOADED to stringResource(R.string.filter_downloaded),\n                            ),\n                        currentValue = filter,\n                        onValueUpdate = {\n                            filter = it\n                        },\n                        modifier = Modifier.weight(1f),\n                    )\n                }\n            }\n\n            item(\n                key = \"header\",\n                contentType = CONTENT_TYPE_HEADER,\n            ) {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    modifier = Modifier.padding(horizontal = 16.dp),\n                ) {\n                    SortHeader(\n                        sortType = sortType,\n                        sortDescending = sortDescending,\n                        onSortTypeChange = onSortTypeChange,\n                        onSortDescendingChange = onSortDescendingChange,\n                        sortTypeText = { sortType ->\n                            when (sortType) {\n                                SongSortType.CREATE_DATE -> R.string.sort_by_create_date\n                                SongSortType.NAME -> R.string.sort_by_name\n                                SongSortType.ARTIST -> R.string.sort_by_artist\n                                SongSortType.PLAY_TIME -> R.string.sort_by_play_time\n                            }\n                        },\n                    )\n\n                    Spacer(Modifier.weight(1f))\n\n                    Text(\n                        text =\n                            pluralStringResource(\n                                R.plurals.n_song,\n                                filteredSongs.size,\n                                filteredSongs.size,\n                            ),\n                        style = MaterialTheme.typography.titleSmall,\n                        color = MaterialTheme.colorScheme.secondary,\n                    )\n                }\n            }\n\n            itemsIndexed(\n                items = filteredSongs,\n                key = { _, item -> item.song.id },\n                contentType = { _, _ -> CONTENT_TYPE_SONG },\n            ) { index, song ->\n                SongListItem(\n                    song = song,\n                    showInLibraryIcon = true,\n                    isActive = song.id == mediaMetadata?.id,\n                    isPlaying = isPlaying,\n                    showLikedIcon = true,\n                    showDownloadIcon = filter != SongFilter.DOWNLOADED,\n                    trailingContent = {\n                        IconButton(\n                            onClick = {\n                                menuState.show {\n                                    SongMenu(\n                                        originalSong = song,\n                                        navController = navController,\n                                        onDismiss = menuState::dismiss,\n                                    )\n                                }\n                            },\n                        ) {\n                            Icon(\n                                painter = painterResource(R.drawable.more_vert),\n                                contentDescription = null,\n                            )\n                        }\n                    },\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .clickable {\n                                if (song.id == mediaMetadata?.id) {\n                                    playerConnection.togglePlayPause()\n                                } else {\n                                    playerConnection.playQueue(\n                                        ListQueue(\n                                            title = queueAllSongsStr,\n                                            items = filteredSongs.map { it.toMediaItem() },\n                                            startIndex = index,\n                                        ),\n                                    )\n                                }\n                            }.animateItem(),\n                )\n            }\n        }\n\n        // Show upload FAB when on UPLOADED filter, shuffle FAB otherwise\n        HideOnScrollFAB(\n            visible = if (filter == SongFilter.UPLOADED) true else filteredSongs.isNotEmpty(),\n            lazyListState = lazyListState,\n            icon = if (filter == SongFilter.UPLOADED) R.drawable.upload else R.drawable.shuffle,\n            onClick = {\n                if (filter == SongFilter.UPLOADED) {\n                    filePickerLauncher.launch(\n                        arrayOf(\n                            \"audio/mpeg\",\n                            \"audio/mp4\",\n                            \"audio/x-m4a\",\n                            \"audio/flac\",\n                            \"audio/ogg\",\n                            \"audio/x-ms-wma\",\n                        ),\n                    )\n                } else {\n                    playerConnection.playQueue(\n                        ListQueue(\n                            title = queueAllSongsStr,\n                            items = filteredSongs.shuffled().map { it.toMediaItem() },\n                        ),\n                    )\n                }\n            },\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/playlist/AutoPlaylistScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.playlist\n\nimport android.net.Uri\nimport android.widget.Toast\nimport androidx.activity.compose.BackHandler\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.ime\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.union\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.FloatingActionButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LinearProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TextField\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator\nimport androidx.compose.material3.pulltorefresh.pullToRefresh\nimport androidx.compose.material3.pulltorefresh.rememberPullToRefreshState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.listSaver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.toMutableStateList\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalFocusManager\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.pluralStringResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.util.fastForEachReversed\nimport androidx.compose.ui.util.fastSumBy\nimport androidx.core.net.toUri\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.media3.exoplayer.offline.Download\nimport androidx.media3.exoplayer.offline.DownloadRequest\nimport androidx.media3.exoplayer.offline.DownloadService\nimport androidx.navigation.NavController\nimport coil3.compose.AsyncImage\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.music.LocalDownloadUtil\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.HideExplicitKey\nimport com.metrolist.music.constants.SongSortDescendingKey\nimport com.metrolist.music.constants.SongSortType\nimport com.metrolist.music.constants.SongSortTypeKey\nimport com.metrolist.music.constants.YtmSyncKey\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.playback.ExoDownloadService\nimport com.metrolist.music.playback.queues.ListQueue\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.DraggableScrollbar\nimport com.metrolist.music.ui.component.EmptyPlaceholder\nimport com.metrolist.music.ui.component.HideOnScrollFAB\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.SongListItem\nimport com.metrolist.music.ui.component.SortHeader\nimport com.metrolist.music.ui.menu.AutoPlaylistMenu\nimport com.metrolist.music.ui.menu.SelectionSongMenu\nimport com.metrolist.music.ui.menu.SongMenu\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.ui.utils.isScrollingUp\nimport com.metrolist.music.utils.makeTimeString\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.viewmodels.AutoPlaylistViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\n\n@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun AutoPlaylistScreen(\n    navController: NavController,\n    viewModel: AutoPlaylistViewModel = hiltViewModel(),\n) {\n    val context = LocalContext.current\n    val menuState = LocalMenuState.current\n    val haptic = LocalHapticFeedback.current\n    val uploadUnsupportedFormatStr = stringResource(R.string.upload_unsupported_format)\n    val uploadFileTooLargeStr = stringResource(R.string.upload_file_too_large)\n    val uploadFailedStr = stringResource(R.string.upload_failed)\n    val uploadCompleteStr = stringResource(R.string.upload_complete)\n    val focusManager = LocalFocusManager.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n    val playlist =\n        when (viewModel.playlist) {\n            \"liked\" -> stringResource(R.string.liked)\n            \"uploaded\" -> stringResource(R.string.uploaded_playlist)\n            else -> stringResource(R.string.offline)\n        }\n\n    val songs by viewModel.likedSongs.collectAsState(null)\n    val mutableSongs =\n        remember {\n            mutableStateListOf<Song>()\n        }\n\n    var isSearching by remember { mutableStateOf(false) }\n    var query by remember { mutableStateOf(TextFieldValue()) }\n    val focusRequester = remember { FocusRequester() }\n\n    LaunchedEffect(isSearching) {\n        if (isSearching) {\n            focusRequester.requestFocus()\n        }\n    }\n\n    val hideExplicit by rememberPreference(key = HideExplicitKey, defaultValue = false)\n\n    val (ytmSync) = rememberPreference(YtmSyncKey, true)\n\n    val likeLength =\n        remember(songs) {\n            songs?.fastSumBy { it.song.duration } ?: 0\n        }\n\n    val playlistId = viewModel.playlist\n    val playlistType =\n        when (playlistId) {\n            \"liked\" -> PlaylistType.LIKE\n            \"downloaded\" -> PlaylistType.DOWNLOAD\n            \"uploaded\" -> PlaylistType.UPLOADED\n            else -> PlaylistType.OTHER\n        }\n\n    var inSelectMode by rememberSaveable { mutableStateOf(false) }\n    val selection =\n        rememberSaveable(\n            saver =\n                listSaver<MutableList<String>, String>(\n                    save = { it.toList() },\n                    restore = { it.toMutableStateList() },\n                ),\n        ) { mutableStateListOf() }\n    var selectionAnchorSongId by rememberSaveable { mutableStateOf<String?>(null) }\n    val onExitSelectionMode = {\n        inSelectMode = false\n        selection.clear()\n        selectionAnchorSongId = null\n    }\n\n    if (isSearching) {\n        BackHandler {\n            isSearching = false\n            query = TextFieldValue()\n        }\n    } else if (inSelectMode) {\n        BackHandler(onBack = onExitSelectionMode)\n    }\n\n    val (sortType, onSortTypeChange) =\n        rememberEnumPreference(\n            SongSortTypeKey,\n            SongSortType.CREATE_DATE,\n        )\n    val (sortDescending, onSortDescendingChange) = rememberPreference(SongSortDescendingKey, true)\n\n    val downloadUtil = LocalDownloadUtil.current\n    var downloadState by remember {\n        mutableIntStateOf(Download.STATE_STOPPED)\n    }\n\n    val scope = rememberCoroutineScope()\n\n    // Upload state\n    var showUploadDialog by remember { mutableStateOf(false) }\n    var uploadProgress by remember { mutableFloatStateOf(0f) }\n    var currentUploadIndex by remember { mutableIntStateOf(0) }\n    var totalUploads by remember { mutableIntStateOf(0) }\n    var currentFileName by remember { mutableStateOf(\"\") }\n    var isUploading by remember { mutableStateOf(false) }\n    var uploadJob by remember { mutableStateOf<kotlinx.coroutines.Job?>(null) }\n\n    val filePickerLauncher =\n        rememberLauncherForActivityResult(\n            contract = ActivityResultContracts.OpenMultipleDocuments(),\n        ) { uris: List<Uri> ->\n            if (uris.isNotEmpty()) {\n                uploadJob =\n                    scope.launch {\n                        isUploading = true\n                        showUploadDialog = true\n                        totalUploads = uris.size\n                        var successCount = 0\n\n                        uris.forEachIndexed { index, uri ->\n                            currentUploadIndex = index + 1\n                            uploadProgress = 0f\n\n                            try {\n                                val fileName = uri.lastPathSegment?.substringAfterLast('/') ?: \"unknown\"\n                                currentFileName = fileName\n                                val extension = fileName.substringAfterLast('.', \"\").lowercase()\n\n                                if (extension !in YouTube.SUPPORTED_UPLOAD_TYPES) {\n                                    withContext(Dispatchers.Main) {\n                                        Toast\n                                            .makeText(\n                                                context,\n                                                uploadUnsupportedFormatStr,\n                                                Toast.LENGTH_SHORT,\n                                            ).show()\n                                    }\n                                    return@forEachIndexed\n                                }\n\n                                val inputStream = context.contentResolver.openInputStream(uri)\n                                val data = inputStream?.readBytes()\n                                inputStream?.close()\n\n                                if (data == null) return@forEachIndexed\n\n                                if (data.size > YouTube.MAX_UPLOAD_SIZE) {\n                                    withContext(Dispatchers.Main) {\n                                        Toast\n                                            .makeText(\n                                                context,\n                                                uploadFileTooLargeStr,\n                                                Toast.LENGTH_SHORT,\n                                            ).show()\n                                    }\n                                    return@forEachIndexed\n                                }\n\n                                val result =\n                                    YouTube.uploadSong(\n                                        filename = fileName,\n                                        data = data,\n                                        onProgress = { progress ->\n                                            uploadProgress = progress\n                                        },\n                                    )\n\n                                if (result.isSuccess && result.getOrDefault(false)) {\n                                    successCount++\n                                }\n                            } catch (e: Exception) {\n                                withContext(Dispatchers.Main) {\n                                    Toast\n                                        .makeText(\n                                            context,\n                                            uploadFailedStr + \": ${e.message}\",\n                                            Toast.LENGTH_SHORT,\n                                        ).show()\n                                }\n                            }\n                        }\n\n                        isUploading = false\n\n                        if (successCount > 0) {\n                            // Show completion briefly\n                            uploadProgress = 1f\n                            currentFileName = uploadCompleteStr\n                            kotlinx.coroutines.delay(1000)\n\n                            // Show toast on main thread\n                            withContext(Dispatchers.Main) {\n                                Toast\n                                    .makeText(\n                                        context,\n                                        uploadCompleteStr,\n                                        Toast.LENGTH_SHORT,\n                                    ).show()\n                            }\n\n                            showUploadDialog = false\n\n                            // Refresh uploaded songs\n                            viewModel.syncUploadedSongs()\n                        } else {\n                            showUploadDialog = false\n                        }\n                    }\n            }\n        }\n\n    LaunchedEffect(Unit) {\n        println(\"[UPLOAD_DEBUG] AutoPlaylistScreen LaunchedEffect: playlistId=$playlistId, playlistType=$playlistType, ytmSync=$ytmSync\")\n        if (ytmSync) {\n            withContext(Dispatchers.IO) {\n                if (playlistType == PlaylistType.LIKE) {\n                    println(\"[UPLOAD_DEBUG] AutoPlaylistScreen: Calling syncLikedSongs()\")\n                    viewModel.syncLikedSongs()\n                }\n                if (playlistType == PlaylistType.UPLOADED) {\n                    println(\"[UPLOAD_DEBUG] AutoPlaylistScreen: Calling syncUploadedSongs()\")\n                    viewModel.syncUploadedSongs()\n                }\n            }\n        } else {\n            println(\"[UPLOAD_DEBUG] AutoPlaylistScreen: ytmSync is false, not syncing\")\n        }\n    }\n\n    LaunchedEffect(songs) {\n        mutableSongs.apply {\n            clear()\n            songs?.let { addAll(it) }\n        }\n        if (songs?.isEmpty() == true) return@LaunchedEffect\n        downloadUtil.downloads.collect { downloads ->\n            downloadState =\n                if (songs?.all { downloads[it.song.id]?.state == Download.STATE_COMPLETED } == true) {\n                    Download.STATE_COMPLETED\n                } else if (songs?.all {\n                        downloads[it.song.id]?.state == Download.STATE_QUEUED ||\n                            downloads[it.song.id]?.state == Download.STATE_DOWNLOADING ||\n                            downloads[it.song.id]?.state == Download.STATE_COMPLETED\n                    } == true\n                ) {\n                    Download.STATE_DOWNLOADING\n                } else {\n                    Download.STATE_STOPPED\n                }\n        }\n    }\n\n    var showRemoveDownloadDialog by remember {\n        mutableStateOf(false)\n    }\n\n    if (showRemoveDownloadDialog) {\n        DefaultDialog(\n            onDismiss = { showRemoveDownloadDialog = false },\n            content = {\n                Text(\n                    text = stringResource(R.string.remove_download_playlist_confirm, playlist),\n                    style = MaterialTheme.typography.bodyLarge,\n                    modifier = Modifier.padding(horizontal = 18.dp),\n                )\n            },\n            buttons = {\n                TextButton(\n                    onClick = { showRemoveDownloadDialog = false },\n                ) {\n                    Text(text = stringResource(android.R.string.cancel))\n                }\n\n                TextButton(\n                    onClick = {\n                        showRemoveDownloadDialog = false\n                        songs!!.forEach { song ->\n                            DownloadService.sendRemoveDownload(\n                                context,\n                                ExoDownloadService::class.java,\n                                song.song.id,\n                                false,\n                            )\n                        }\n                    },\n                ) {\n                    Text(text = stringResource(android.R.string.ok))\n                }\n            },\n        )\n    }\n\n    // Upload progress dialog\n    if (showUploadDialog) {\n        DefaultDialog(\n            onDismiss = {\n                if (isUploading) {\n                    uploadJob?.cancel()\n                    isUploading = false\n                }\n                showUploadDialog = false\n            },\n            icon = {\n                Icon(\n                    painter = painterResource(R.drawable.upload),\n                    contentDescription = null,\n                )\n            },\n            title = { Text(stringResource(R.string.uploading)) },\n            buttons = {\n                TextButton(\n                    onClick = {\n                        if (isUploading) {\n                            uploadJob?.cancel()\n                            isUploading = false\n                        }\n                        showUploadDialog = false\n                    },\n                ) {\n                    Text(stringResource(R.string.cancel))\n                }\n            },\n        ) {\n            Text(\n                text = stringResource(R.string.upload_progress, currentUploadIndex, totalUploads),\n                style = MaterialTheme.typography.bodyMedium,\n            )\n            Spacer(modifier = Modifier.height(8.dp))\n            Text(\n                text = currentFileName,\n                style = MaterialTheme.typography.bodySmall,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n            )\n            Spacer(modifier = Modifier.height(16.dp))\n            LinearProgressIndicator(\n                progress = { uploadProgress },\n                modifier = Modifier.fillMaxWidth(),\n            )\n        }\n    }\n\n    val filteredSongs =\n        remember(songs, query) {\n            if (query.text.isEmpty()) {\n                songs ?: emptyList()\n            } else {\n                songs?.filter { song ->\n                    song.song.title.contains(query.text, true) ||\n                        song.artists.any { it.name.contains(query.text, true) }\n                } ?: emptyList()\n            }\n        }\n\n    LaunchedEffect(filteredSongs) {\n        selection.fastForEachReversed { songId ->\n            if (filteredSongs.find { it.id == songId } == null) {\n                selection.remove(songId)\n            }\n        }\n\n        if (selectionAnchorSongId != null && filteredSongs.none { it.id == selectionAnchorSongId }) {\n            selectionAnchorSongId = filteredSongs.firstOrNull { it.id in selection }?.id\n        }\n    }\n\n    val state = rememberLazyListState()\n\n    val isRefreshing by viewModel.isRefreshing.collectAsState()\n    val pullRefreshState = rememberPullToRefreshState()\n    val canRefresh = playlistType == PlaylistType.LIKE || playlistType == PlaylistType.UPLOADED\n\n    Box(\n        modifier =\n            Modifier\n                .fillMaxSize()\n                .then(\n                    if (canRefresh) {\n                        Modifier.pullToRefresh(\n                            state = pullRefreshState,\n                            isRefreshing = isRefreshing,\n                            onRefresh = viewModel::refresh,\n                        )\n                    } else {\n                        Modifier\n                    },\n                ),\n    ) {\n        LazyColumn(\n            state = state,\n            contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n        ) {\n            if (songs != null) {\n                if (songs!!.isEmpty()) {\n                    item(key = \"empty_placeholder\") {\n                        EmptyPlaceholder(\n                            icon = R.drawable.music_note,\n                            text = stringResource(R.string.playlist_is_empty),\n                        )\n                    }\n                } else {\n                    if (!isSearching) {\n                        item(key = \"playlist_header\") {\n                            AutoPlaylistHeader(\n                                name = playlist,\n                                songs = songs!!,\n                                likeLength = likeLength,\n                                downloadState = downloadState,\n                                onShowRemoveDownloadDialog = { showRemoveDownloadDialog = true },\n                                menuState = menuState,\n                                modifier = Modifier.animateItem(),\n                            )\n                        }\n                    }\n\n                    item(key = \"songs_header\") {\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            modifier = Modifier.padding(start = 16.dp),\n                        ) {\n                            SortHeader(\n                                sortType = sortType,\n                                sortDescending = sortDescending,\n                                onSortTypeChange = onSortTypeChange,\n                                onSortDescendingChange = onSortDescendingChange,\n                                sortTypeText = { sortType ->\n                                    when (sortType) {\n                                        SongSortType.CREATE_DATE -> R.string.sort_by_create_date\n                                        SongSortType.NAME -> R.string.sort_by_name\n                                        SongSortType.ARTIST -> R.string.sort_by_artist\n                                        SongSortType.PLAY_TIME -> R.string.sort_by_play_time\n                                    }\n                                },\n                                modifier = Modifier.weight(1f),\n                            )\n                        }\n                    }\n                }\n\n                if (filteredSongs.isNotEmpty()) {\n                    itemsIndexed(\n                        items = filteredSongs,\n                        key = { _, song -> song.id },\n                    ) { index, song ->\n                        val onCheckedChange: (Boolean) -> Unit = {\n                            if (it) {\n                                selection.add(song.id)\n                            } else {\n                                selection.remove(song.id)\n                            }\n                        }\n\n                        SongListItem(\n                            song = song,\n                            isActive = song.song.id == mediaMetadata?.id,\n                            isPlaying = isPlaying,\n                            showInLibraryIcon = true,\n                            trailingContent = {\n                                if (inSelectMode) {\n                                    Checkbox(\n                                        checked = song.id in selection,\n                                        onCheckedChange = onCheckedChange,\n                                    )\n                                } else {\n                                    IconButton(\n                                        onClick = {\n                                            menuState.show {\n                                                SongMenu(\n                                                    originalSong = song,\n                                                    navController = navController,\n                                                    onDismiss = menuState::dismiss,\n                                                )\n                                            }\n                                        },\n                                    ) {\n                                        Icon(\n                                            painter = painterResource(R.drawable.more_vert),\n                                            contentDescription = null,\n                                        )\n                                    }\n                                }\n                            },\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .combinedClickable(\n                                        onClick = {\n                                            if (inSelectMode) {\n                                                onCheckedChange(song.id !in selection)\n                                            } else if (song.song.id == mediaMetadata?.id) {\n                                                playerConnection.togglePlayPause()\n                                            } else {\n                                                playerConnection.playQueue(\n                                                    ListQueue(\n                                                        title = playlist,\n                                                        items = songs!!.map { it.toMediaItem() },\n                                                        startIndex = songs!!.indexOfFirst { it.id == song.id },\n                                                    ),\n                                                )\n                                            }\n                                        },\n                                        onLongClick = {\n                                            if (!inSelectMode) {\n                                                haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                inSelectMode = true\n                                                onCheckedChange(true)\n                                                selectionAnchorSongId = song.id\n                                            } else {\n                                                val anchorIndex =\n                                                    selectionAnchorSongId?.let { anchorSongId ->\n                                                        filteredSongs.indexOfFirst { it.id == anchorSongId }\n                                                    } ?: -1\n\n                                                if (anchorIndex == -1) {\n                                                    onCheckedChange(true)\n                                                    selectionAnchorSongId = song.id\n                                                } else {\n                                                    val range = if (anchorIndex <= index) anchorIndex..index else index..anchorIndex\n                                                    for (rangeIndex in range) {\n                                                        val rangeSongId = filteredSongs[rangeIndex].id\n                                                        if (rangeSongId !in selection) {\n                                                            selection.add(rangeSongId)\n                                                        }\n                                                    }\n                                                }\n                                            }\n                                        },\n                                    ).animateItem(),\n                        )\n                    }\n                }\n            }\n        }\n\n        DraggableScrollbar(\n            modifier =\n                Modifier\n                    .padding(\n                        LocalPlayerAwareWindowInsets.current\n                            .union(WindowInsets.ime)\n                            .asPaddingValues(),\n                    ).align(Alignment.CenterEnd),\n            scrollState = state,\n            headerItems = 2,\n        )\n\n        if (canRefresh) {\n            Indicator(\n                isRefreshing = isRefreshing,\n                state = pullRefreshState,\n                modifier =\n                    Modifier\n                        .align(Alignment.TopCenter)\n                        .padding(LocalPlayerAwareWindowInsets.current.asPaddingValues()),\n            )\n        }\n\n        // Upload FAB for uploaded playlist - positioned above mini player\n        if (playlistType == PlaylistType.UPLOADED) {\n            androidx.compose.animation.AnimatedVisibility(\n                visible = state.isScrollingUp(),\n                enter = androidx.compose.animation.slideInVertically { it },\n                exit = androidx.compose.animation.slideOutVertically { it },\n                modifier =\n                    Modifier\n                        .align(Alignment.BottomEnd)\n                        .windowInsetsPadding(\n                            LocalPlayerAwareWindowInsets.current\n                                .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal),\n                        ).padding(16.dp),\n            ) {\n                FloatingActionButton(\n                    onClick = {\n                        filePickerLauncher.launch(\n                            arrayOf(\n                                \"audio/mpeg\",\n                                \"audio/mp4\",\n                                \"audio/x-m4a\",\n                                \"audio/flac\",\n                                \"audio/ogg\",\n                                \"audio/x-ms-wma\",\n                            ),\n                        )\n                    },\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.upload),\n                        contentDescription = stringResource(R.string.upload_songs),\n                    )\n                }\n            }\n        }\n\n        TopAppBar(\n            title = {\n                when {\n                    inSelectMode -> {\n                        Text(\n                            text = pluralStringResource(R.plurals.n_song, selection.size, selection.size),\n                            style = MaterialTheme.typography.titleLarge,\n                        )\n                    }\n\n                    isSearching -> {\n                        TextField(\n                            value = query,\n                            onValueChange = { query = it },\n                            placeholder = {\n                                Text(\n                                    text = stringResource(R.string.search),\n                                    style = MaterialTheme.typography.titleLarge,\n                                )\n                            },\n                            singleLine = true,\n                            textStyle = MaterialTheme.typography.titleLarge,\n                            keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),\n                            colors =\n                                TextFieldDefaults.colors(\n                                    focusedContainerColor = Color.Transparent,\n                                    unfocusedContainerColor = Color.Transparent,\n                                    focusedIndicatorColor = Color.Transparent,\n                                    unfocusedIndicatorColor = Color.Transparent,\n                                    disabledIndicatorColor = Color.Transparent,\n                                ),\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .focusRequester(focusRequester),\n                        )\n                    }\n\n                    else -> {\n                        Text(\n                            text = playlist,\n                            style = MaterialTheme.typography.titleLarge,\n                        )\n                    }\n                }\n            },\n            navigationIcon = {\n                IconButton(\n                    onClick = {\n                        when {\n                            isSearching -> {\n                                isSearching = false\n                                query = TextFieldValue()\n                                focusManager.clearFocus()\n                            }\n\n                            inSelectMode -> {\n                                onExitSelectionMode()\n                            }\n\n                            else -> {\n                                navController.navigateUp()\n                            }\n                        }\n                    },\n                    onLongClick = {\n                        if (!isSearching && !inSelectMode) {\n                            navController.backToMain()\n                        }\n                    },\n                ) {\n                    Icon(\n                        painter =\n                            painterResource(\n                                if (inSelectMode) R.drawable.close else R.drawable.arrow_back,\n                            ),\n                        contentDescription = null,\n                    )\n                }\n            },\n            actions = {\n                if (inSelectMode) {\n                    Checkbox(\n                        checked = selection.size == filteredSongs.size && selection.isNotEmpty(),\n                        onCheckedChange = {\n                            if (selection.size == filteredSongs.size) {\n                                selection.clear()\n                            } else {\n                                selection.clear()\n                                selection.addAll(filteredSongs.map { it.id })\n                            }\n                        },\n                    )\n                    IconButton(\n                        enabled = selection.isNotEmpty(),\n                        onClick = {\n                            menuState.show {\n                                SelectionSongMenu(\n                                    songSelection = filteredSongs.filter { it.id in selection },\n                                    onDismiss = menuState::dismiss,\n                                    clearAction = onExitSelectionMode,\n                                    isUploadedPlaylist = playlistType == PlaylistType.UPLOADED,\n                                )\n                            }\n                        },\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.more_vert),\n                            contentDescription = null,\n                        )\n                    }\n                } else if (!isSearching) {\n                    IconButton(\n                        onClick = { isSearching = true },\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.search),\n                            contentDescription = null,\n                        )\n                    }\n                }\n            },\n        )\n    }\n}\n\n@Composable\nprivate fun AutoPlaylistHeader(\n    name: String,\n    songs: List<Song>,\n    likeLength: Int,\n    downloadState: Int,\n    onShowRemoveDownloadDialog: () -> Unit,\n    menuState: com.metrolist.music.ui.component.MenuState,\n    modifier: Modifier = Modifier,\n) {\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val context = LocalContext.current\n\n    Column(\n        modifier =\n            modifier\n                .fillMaxWidth()\n                .padding(top = 8.dp, bottom = 20.dp),\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n        // Playlist Thumbnail - Large centered with shadow\n        Box(\n            modifier = Modifier.padding(top = 8.dp, bottom = 20.dp),\n        ) {\n            androidx.compose.material3.Surface(\n                modifier =\n                    Modifier\n                        .size(240.dp)\n                        .shadow(\n                            elevation = 24.dp,\n                            shape = RoundedCornerShape(3.dp),\n                            spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),\n                        ),\n                shape = RoundedCornerShape(3.dp),\n            ) {\n                AsyncImage(\n                    model = songs[0].song.thumbnailUrl,\n                    contentDescription = null,\n                    contentScale = androidx.compose.ui.layout.ContentScale.Crop,\n                    modifier = Modifier.fillMaxSize(),\n                )\n            }\n        }\n\n        // Playlist Name\n        Text(\n            text = name,\n            style = MaterialTheme.typography.headlineSmall,\n            fontWeight = FontWeight.Bold,\n            textAlign = androidx.compose.ui.text.style.TextAlign.Center,\n            maxLines = 2,\n            overflow = TextOverflow.Ellipsis,\n            modifier = Modifier.padding(horizontal = 32.dp),\n        )\n\n        Spacer(modifier = Modifier.height(12.dp))\n\n        // Metadata - Song Count • Duration\n        Text(\n            text =\n                buildString {\n                    append(pluralStringResource(R.plurals.n_song, songs.size, songs.size))\n                    if (likeLength > 0) {\n                        append(\" • \")\n                        append(makeTimeString(likeLength * 1000L))\n                    }\n                },\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f),\n        )\n\n        Spacer(modifier = Modifier.height(24.dp))\n\n        // Action Buttons Row\n        Row(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = 24.dp),\n            horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            // Shuffle Button - Smaller secondary button\n            androidx.compose.material3.Surface(\n                onClick = {\n                    playerConnection.playQueue(\n                        ListQueue(\n                            title = name,\n                            items = songs.shuffled().map { it.toMediaItem() },\n                        ),\n                    )\n                },\n                shape = androidx.compose.foundation.shape.CircleShape,\n                color = MaterialTheme.colorScheme.surfaceVariant,\n                modifier = Modifier.size(48.dp),\n            ) {\n                Box(\n                    modifier = Modifier.fillMaxSize(),\n                    contentAlignment = Alignment.Center,\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.shuffle),\n                        contentDescription = stringResource(R.string.shuffle),\n                        modifier = Modifier.size(24.dp),\n                    )\n                }\n            }\n\n            // Play Button - Larger primary circular button\n            Surface(\n                onClick = {\n                    playerConnection.playQueue(\n                        ListQueue(\n                            title = name,\n                            items = songs.map { it.toMediaItem() },\n                        ),\n                    )\n                },\n                color = MaterialTheme.colorScheme.primary,\n                shape = androidx.compose.foundation.shape.CircleShape,\n                modifier = Modifier.size(72.dp),\n            ) {\n                Box(\n                    contentAlignment = Alignment.Center,\n                    modifier = Modifier.fillMaxSize(),\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.play),\n                        contentDescription = stringResource(R.string.play),\n                        tint = MaterialTheme.colorScheme.onPrimary,\n                        modifier = Modifier.size(32.dp),\n                    )\n                }\n            }\n\n            // Menu Button - Smaller secondary button\n            Surface(\n                onClick = {\n                    menuState.show {\n                        AutoPlaylistMenu(\n                            downloadState = downloadState,\n                            onQueue = {\n                                playerConnection.addToQueue(\n                                    songs.map { it.toMediaItem() },\n                                )\n                            },\n                            onDownload = {\n                                when (downloadState) {\n                                    Download.STATE_COMPLETED -> {\n                                        onShowRemoveDownloadDialog()\n                                    }\n\n                                    Download.STATE_DOWNLOADING -> {\n                                        songs.forEach { song ->\n                                            DownloadService.sendRemoveDownload(\n                                                context,\n                                                ExoDownloadService::class.java,\n                                                song.song.id,\n                                                false,\n                                            )\n                                        }\n                                    }\n\n                                    else -> {\n                                        songs.forEach { song ->\n                                            val downloadRequest =\n                                                DownloadRequest\n                                                    .Builder(song.song.id, song.song.id.toUri())\n                                                    .setCustomCacheKey(song.song.id)\n                                                    .setData(song.song.title.toByteArray())\n                                                    .build()\n                                            DownloadService.sendAddDownload(\n                                                context,\n                                                ExoDownloadService::class.java,\n                                                downloadRequest,\n                                                false,\n                                            )\n                                        }\n                                    }\n                                }\n                            },\n                            onDismiss = { menuState.dismiss() },\n                            songs = songs,\n                            playlistName = name,\n                        )\n                    }\n                },\n                shape = androidx.compose.foundation.shape.CircleShape,\n                color = MaterialTheme.colorScheme.surfaceVariant,\n                modifier = Modifier.size(48.dp),\n            ) {\n                Box(\n                    modifier = Modifier.fillMaxSize(),\n                    contentAlignment = Alignment.Center,\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.more_vert),\n                        contentDescription = null,\n                        modifier = Modifier.size(24.dp),\n                    )\n                }\n            }\n        }\n    }\n}\n\nenum class PlaylistType {\n    LIKE,\n    DOWNLOAD,\n    UPLOADED,\n    OTHER,\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/playlist/CachePlaylistScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.playlist\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.ime\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.union\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextField\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.listSaver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.toMutableStateList\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalFocusManager\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.pluralStringResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.util.fastForEachReversed\nimport androidx.core.net.toUri\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.media3.exoplayer.offline.Download\nimport androidx.media3.exoplayer.offline.DownloadRequest\nimport androidx.media3.exoplayer.offline.DownloadService\nimport androidx.navigation.NavController\nimport coil3.compose.AsyncImage\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.HideExplicitKey\nimport com.metrolist.music.constants.SongSortDescendingKey\nimport com.metrolist.music.constants.SongSortType\nimport com.metrolist.music.constants.SongSortTypeKey\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.playback.ExoDownloadService\nimport com.metrolist.music.playback.queues.ListQueue\nimport com.metrolist.music.ui.component.DraggableScrollbar\nimport com.metrolist.music.ui.component.EmptyPlaceholder\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.SongListItem\nimport com.metrolist.music.ui.component.SortHeader\nimport com.metrolist.music.ui.menu.CachePlaylistMenu\nimport com.metrolist.music.ui.menu.SelectionSongMenu\nimport com.metrolist.music.ui.menu.SongMenu\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.viewmodels.CachePlaylistViewModel\nimport java.time.LocalDateTime\n\n@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun CachePlaylistScreen(\n    navController: NavController,\n    viewModel: CachePlaylistViewModel = hiltViewModel(),\n) {\n    val context = LocalContext.current\n    val menuState = LocalMenuState.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val haptic = LocalHapticFeedback.current\n    val focusManager = LocalFocusManager.current\n\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n    val cachedSongs by viewModel.cachedSongs.collectAsState()\n\n    val (sortType, onSortTypeChange) = rememberEnumPreference(\n        SongSortTypeKey,\n        SongSortType.CREATE_DATE\n    )\n    val (sortDescending, onSortDescendingChange) = rememberPreference(SongSortDescendingKey, true)\n    val hideExplicit by rememberPreference(key = HideExplicitKey, defaultValue = false)\n\n    val sortedSongs = remember(cachedSongs, sortType, sortDescending) {\n        val sorted = when (sortType) {\n            SongSortType.CREATE_DATE -> cachedSongs.sortedBy { it.song.dateDownload ?: LocalDateTime.MIN }\n            SongSortType.NAME -> cachedSongs.sortedBy { it.song.title }\n            SongSortType.ARTIST -> cachedSongs.sortedBy { song ->\n                song.artists.joinToString(separator = \"\") { it.name }\n            }\n            SongSortType.PLAY_TIME -> cachedSongs.sortedBy { it.song.totalPlayTime }\n        }\n        if (sortDescending) sorted.reversed() else sorted\n    }\n\n    var inSelectMode by rememberSaveable { mutableStateOf(false) }\n    val selection = rememberSaveable(\n        saver = listSaver<MutableList<String>, String>(\n            save = { it.toList() },\n            restore = { it.toMutableStateList() }\n        )\n    ) { mutableStateListOf() }\n    var selectionAnchorSongId by rememberSaveable { mutableStateOf<String?>(null) }\n    val onExitSelectionMode = {\n        inSelectMode = false\n        selection.clear()\n        selectionAnchorSongId = null\n    }\n\n    var isSearching by remember { mutableStateOf(false) }\n    var query by remember { mutableStateOf(TextFieldValue()) }\n    val focusRequester = remember { FocusRequester() }\n    val lazyListState = rememberLazyListState()\n\n    LaunchedEffect(isSearching) {\n        if (isSearching) {\n            focusRequester.requestFocus()\n        }\n    }\n\n    if (isSearching) {\n        BackHandler {\n            isSearching = false\n            query = TextFieldValue()\n        }\n    } else if (inSelectMode) {\n        BackHandler(onBack = onExitSelectionMode)\n    }\n\n    val filteredSongs = remember(sortedSongs, query) {\n        if (query.text.isEmpty()) sortedSongs\n        else sortedSongs.filter { song ->\n            song.title.contains(query.text, true) ||\n                song.artists.any { it.name.contains(query.text, true) }\n        }\n    }\n\n    LaunchedEffect(filteredSongs) {\n        selection.fastForEachReversed { songId ->\n            if (filteredSongs.find { it.id == songId } == null) {\n                selection.remove(songId)\n            }\n        }\n\n        if (selectionAnchorSongId != null && filteredSongs.none { it.id == selectionAnchorSongId }) {\n            selectionAnchorSongId = filteredSongs.firstOrNull { it.id in selection }?.id\n        }\n    }\n\n    Box(\n        modifier = Modifier.fillMaxSize(),\n    ) {\n        LazyColumn(\n            state = lazyListState,\n            contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n        ) {\n            if (filteredSongs.isEmpty() && !isSearching) {\n                item(key = \"empty_placeholder\") {\n                    EmptyPlaceholder(\n                        icon = R.drawable.music_note,\n                        text = stringResource(R.string.playlist_is_empty),\n                        modifier = Modifier.animateItem()\n                    )\n                }\n            }\n\n            if (filteredSongs.isEmpty() && isSearching) {\n                item(key = \"no_results\") {\n                    EmptyPlaceholder(\n                        icon = R.drawable.search,\n                        text = stringResource(R.string.no_results_found),\n                        modifier = Modifier.animateItem()\n                    )\n                }\n            } else {\n                if (filteredSongs.isNotEmpty() && !isSearching) {\n                    item(key = \"playlist_header\") {\n                        CachePlaylistHeader(\n                            songs = filteredSongs,\n                            context = context,\n                            menuState = menuState,\n                            modifier = Modifier.animateItem()\n                        )\n                    }\n                }\n\n                if (filteredSongs.isNotEmpty()) {\n                    item(key = \"sort_header\") {\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            modifier = Modifier\n                                .padding(start = 16.dp)\n                                .animateItem(),\n                        ) {\n                            SortHeader(\n                                sortType = sortType,\n                                sortDescending = sortDescending,\n                                onSortTypeChange = onSortTypeChange,\n                                onSortDescendingChange = onSortDescendingChange,\n                                sortTypeText = { sortType ->\n                                    when (sortType) {\n                                        SongSortType.CREATE_DATE -> R.string.sort_by_create_date\n                                        SongSortType.NAME -> R.string.sort_by_name\n                                        SongSortType.ARTIST -> R.string.sort_by_artist\n                                        SongSortType.PLAY_TIME -> R.string.sort_by_play_time\n                                    }\n                                },\n                                modifier = Modifier.weight(1f),\n                            )\n                        }\n                    }\n                }\n\n                itemsIndexed(filteredSongs, key = { _, song -> song.id }) { index, song ->\n                    val onCheckedChange: (Boolean) -> Unit = {\n                        if (it) {\n                            selection.add(song.id)\n                        } else {\n                            selection.remove(song.id)\n                        }\n                    }\n\n                    SongListItem(\n                        song = song,\n                        isActive = song.id == mediaMetadata?.id,\n                        isPlaying = isPlaying,\n                        showInLibraryIcon = true,\n                        trailingContent = {\n                            if (inSelectMode) {\n                                Checkbox(\n                                    checked = song.id in selection,\n                                    onCheckedChange = onCheckedChange\n                                )\n                            } else {\n                                IconButton(onClick = {\n                                    menuState.show {\n                                        SongMenu(\n                                            originalSong = song,\n                                            navController = navController,\n                                            onDismiss = menuState::dismiss,\n                                            isFromCache = true,\n                                        )\n                                    }\n                                }) {\n                                    Icon(\n                                        painter = painterResource(R.drawable.more_vert),\n                                        contentDescription = null\n                                    )\n                                }\n                            }\n                        },\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .animateItem()\n                            .combinedClickable(\n                                onClick = {\n                                    if (inSelectMode) {\n                                        onCheckedChange(song.id !in selection)\n                                    } else if (song.id == mediaMetadata?.id) {\n                                        playerConnection.togglePlayPause()\n                                    } else {\n                                        playerConnection.playQueue(\n                                            ListQueue(\n                                                title = \"Cache Songs\",\n                                                items = cachedSongs.map { it.toMediaItem() },\n                                                startIndex = cachedSongs.indexOfFirst { it.id == song.id }\n                                            )\n                                        )\n                                    }\n                                },\n                                onLongClick = {\n                                    if (!inSelectMode) {\n                                        haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                        inSelectMode = true\n                                        onCheckedChange(true)\n                                        selectionAnchorSongId = song.id\n                                    } else {\n                                        val anchorIndex = selectionAnchorSongId?.let { anchorSongId ->\n                                            filteredSongs.indexOfFirst { it.id == anchorSongId }\n                                        } ?: -1\n\n                                        if (anchorIndex == -1) {\n                                            onCheckedChange(true)\n                                            selectionAnchorSongId = song.id\n                                        } else {\n                                            val range = if (anchorIndex <= index) anchorIndex..index else index..anchorIndex\n                                            for (rangeIndex in range) {\n                                                val rangeSongId = filteredSongs[rangeIndex].id\n                                                if (rangeSongId !in selection) {\n                                                    selection.add(rangeSongId)\n                                                }\n                                            }\n                                        }\n                                    }\n                                }\n                            )\n                            .animateItem()\n                    )\n                }\n            }\n        }\n\n        DraggableScrollbar(\n            modifier = Modifier\n                .padding(\n                    LocalPlayerAwareWindowInsets.current.union(WindowInsets.ime)\n                        .asPaddingValues()\n                )\n                .align(Alignment.CenterEnd),\n            scrollState = lazyListState,\n            headerItems = 2\n        )\n\n        TopAppBar(\n            title = {\n                when {\n                    inSelectMode -> {\n                        Text(\n                            text = pluralStringResource(R.plurals.n_song, selection.size, selection.size),\n                            style = MaterialTheme.typography.titleLarge\n                        )\n                    }\n                    isSearching -> {\n                        TextField(\n                            value = query,\n                            onValueChange = { query = it },\n                            placeholder = {\n                                Text(\n                                    text = stringResource(R.string.search),\n                                    style = MaterialTheme.typography.titleLarge\n                                )\n                            },\n                            singleLine = true,\n                            textStyle = MaterialTheme.typography.titleLarge,\n                            keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),\n                            colors = TextFieldDefaults.colors(\n                                focusedContainerColor = Color.Transparent,\n                                unfocusedContainerColor = Color.Transparent,\n                                focusedIndicatorColor = Color.Transparent,\n                                unfocusedIndicatorColor = Color.Transparent,\n                                disabledIndicatorColor = Color.Transparent,\n                            ),\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .focusRequester(focusRequester)\n                        )\n                    }\n                    else -> {\n                        Text(\n                            stringResource(R.string.cached_playlist),\n                            style = MaterialTheme.typography.titleLarge\n                        )\n                    }\n                }\n            },\n            navigationIcon = {\n                IconButton(onClick = {\n                    when {\n                        isSearching -> {\n                            isSearching = false\n                            query = TextFieldValue()\n                            focusManager.clearFocus()\n                        }\n                        inSelectMode -> {\n                            onExitSelectionMode()\n                        }\n                        else -> {\n                            navController.navigateUp()\n                        }\n                    }\n                }, onLongClick = {\n                    if (!isSearching && !inSelectMode) {\n                        navController.backToMain()\n                    }\n                }) {\n                    Icon(\n                        painter = painterResource(\n                            if (inSelectMode) R.drawable.close else R.drawable.arrow_back\n                        ),\n                        contentDescription = null\n                    )\n                }\n            },\n            actions = {\n                if (inSelectMode) {\n                    Checkbox(\n                        checked = selection.size == filteredSongs.size && selection.isNotEmpty(),\n                        onCheckedChange = {\n                            if (selection.size == filteredSongs.size) {\n                                selection.clear()\n                            } else {\n                                selection.clear()\n                                selection.addAll(filteredSongs.map { it.id })\n                            }\n                        }\n                    )\n                    IconButton(\n                        enabled = selection.isNotEmpty(),\n                        onClick = {\n                            menuState.show {\n                                SelectionSongMenu(\n                                    songSelection = filteredSongs.filter { it.id in selection },\n                                    onDismiss = menuState::dismiss,\n                                    clearAction = onExitSelectionMode\n                                )\n                            }\n                        }\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.more_vert),\n                            contentDescription = null\n                        )\n                    }\n                } else if (!isSearching) {\n                    IconButton(onClick = { isSearching = true }) {\n                        Icon(\n                            painter = painterResource(R.drawable.search),\n                            contentDescription = null\n                        )\n                    }\n                }\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun CachePlaylistHeader(\n    songs: List<Song>,\n    context: android.content.Context,\n    menuState: com.metrolist.music.ui.component.MenuState,\n    modifier: Modifier = Modifier\n) {\n    val playerConnection = LocalPlayerConnection.current ?: return\n    \n    Column(\n        modifier = modifier\n            .fillMaxWidth()\n            .padding(top = 8.dp, bottom = 20.dp),\n        horizontalAlignment = Alignment.CenterHorizontally\n    ) {\n        // Playlist Thumbnail - Large centered with shadow\n        Box(\n            modifier = Modifier.padding(top = 8.dp, bottom = 20.dp)\n        ) {\n            androidx.compose.material3.Surface(\n                modifier = Modifier\n                    .size(240.dp)\n                    .shadow(\n                        elevation = 24.dp,\n                        shape = RoundedCornerShape(3.dp),\n                        spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f)\n                    ),\n                shape = RoundedCornerShape(3.dp)\n            ) {\n                AsyncImage(\n                    model = songs.first().thumbnailUrl,\n                    contentDescription = null,\n                    contentScale = androidx.compose.ui.layout.ContentScale.Crop,\n                    modifier = Modifier.fillMaxSize()\n                )\n            }\n        }\n\n        // Playlist Name\n        Text(\n            text = stringResource(R.string.cached_playlist),\n            style = MaterialTheme.typography.headlineSmall,\n            fontWeight = FontWeight.Bold,\n            textAlign = androidx.compose.ui.text.style.TextAlign.Center,\n            maxLines = 2,\n            overflow = TextOverflow.Ellipsis,\n            modifier = Modifier.padding(horizontal = 32.dp)\n        )\n\n        Spacer(modifier = Modifier.height(12.dp))\n\n        // Metadata - Song Count\n        Text(\n            text = pluralStringResource(R.plurals.n_song, songs.size, songs.size),\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f)\n        )\n\n        Spacer(modifier = Modifier.height(24.dp))\n\n        // Action Buttons Row\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 24.dp),\n            horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            // Shuffle Button - Smaller secondary button\n            androidx.compose.material3.Surface(\n                onClick = {\n                    playerConnection.playQueue(\n                        ListQueue(\n                            title = \"Cache Songs\",\n                            items = songs.shuffled().map { it.toMediaItem() },\n                        )\n                    )\n                },\n                shape = androidx.compose.foundation.shape.CircleShape,\n                color = MaterialTheme.colorScheme.surfaceVariant,\n                modifier = Modifier.size(48.dp)\n            ) {\n                Box(\n                    modifier = Modifier.fillMaxSize(),\n                    contentAlignment = Alignment.Center\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.shuffle),\n                        contentDescription = stringResource(R.string.shuffle),\n                        modifier = Modifier.size(24.dp)\n                    )\n                }\n            }\n\n            // Play Button - Larger primary circular button\n            Surface(\n                onClick = {\n                    playerConnection.playQueue(\n                        ListQueue(\n                            title = \"Cache Songs\",\n                            items = songs.map { it.toMediaItem() },\n                        )\n                    )\n                },\n                color = MaterialTheme.colorScheme.primary,\n                shape = androidx.compose.foundation.shape.CircleShape,\n                modifier = Modifier.size(72.dp)\n            ) {\n                Box(\n                    contentAlignment = Alignment.Center,\n                    modifier = Modifier.fillMaxSize()\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.play),\n                        contentDescription = stringResource(R.string.play),\n                        tint = MaterialTheme.colorScheme.onPrimary,\n                        modifier = Modifier.size(32.dp)\n                    )\n                }\n            }\n\n            // Menu Button - Smaller secondary button\n            Surface(\n                onClick = {\n                    menuState.show {\n                        CachePlaylistMenu(\n                            downloadState = Download.STATE_STOPPED,\n                            onQueue = {\n                                playerConnection.addToQueue(\n                                    songs.map { it.toMediaItem() }\n                                )\n                            },\n                            onDownload = {\n                                // Download all cached songs\n                                songs.forEach { song ->\n                                    val downloadRequest = DownloadRequest\n                                        .Builder(song.song.id, song.song.id.toUri())\n                                        .setCustomCacheKey(song.song.id)\n                                        .setData(song.song.title.toByteArray())\n                                        .build()\n                                    DownloadService.sendAddDownload(\n                                        context,\n                                        ExoDownloadService::class.java,\n                                        downloadRequest,\n                                        false,\n                                    )\n                                }\n                            },\n                            onDismiss = { menuState.dismiss() }\n                        )\n                    }\n                },\n                shape = androidx.compose.foundation.shape.CircleShape,\n                color = MaterialTheme.colorScheme.surfaceVariant,\n                modifier = Modifier.size(48.dp)\n            ) {\n                Box(\n                    modifier = Modifier.fillMaxSize(),\n                    contentAlignment = Alignment.Center\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.more_vert),\n                        contentDescription = null,\n                        modifier = Modifier.size(24.dp)\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/playlist/LocalPlaylistScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.playlist\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.content.Intent\nimport android.graphics.Bitmap\nimport android.net.Uri\nimport androidx.activity.compose.BackHandler\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.PickVisualMediaRequest\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.ime\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.union\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.SwipeToDismissBox\nimport androidx.compose.material3.SwipeToDismissBoxValue\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TextField\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.rememberSwipeToDismissBoxState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.runtime.saveable.listSaver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.toMutableStateList\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.pluralStringResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.TextRange\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.util.fastAny\nimport androidx.compose.ui.util.fastForEachIndexed\nimport androidx.compose.ui.util.fastForEachReversed\nimport androidx.compose.ui.util.fastSumBy\nimport androidx.core.content.FileProvider\nimport androidx.core.net.toUri\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.lifecycle.viewModelScope\nimport androidx.media3.exoplayer.offline.Download\nimport androidx.media3.exoplayer.offline.DownloadRequest\nimport androidx.media3.exoplayer.offline.DownloadService\nimport androidx.navigation.NavController\nimport coil3.compose.AsyncImage\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.utils.completed\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalDownloadUtil\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.LocalSyncUtils\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.DarkModeKey\nimport com.metrolist.music.constants.PlaylistEditLockKey\nimport com.metrolist.music.constants.PlaylistSongSortDescendingKey\nimport com.metrolist.music.constants.PlaylistSongSortType\nimport com.metrolist.music.constants.PlaylistSongSortTypeKey\nimport com.metrolist.music.constants.SwipeToRemoveSongKey\nimport com.metrolist.music.db.entities.Playlist\nimport com.metrolist.music.db.entities.PlaylistSong\nimport com.metrolist.music.db.entities.PlaylistSongMap\nimport com.metrolist.music.extensions.move\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.playback.ExoDownloadService\nimport com.metrolist.music.playback.queues.ListQueue\nimport com.metrolist.music.ui.component.ActionPromptDialog\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.DraggableScrollbar\nimport com.metrolist.music.ui.component.EmptyPlaceholder\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.OverlayEditButton\nimport com.metrolist.music.ui.component.SongListItem\nimport com.metrolist.music.ui.component.SortHeader\nimport com.metrolist.music.ui.component.TextFieldDialog\nimport com.metrolist.music.ui.menu.CustomThumbnailMenu\nimport com.metrolist.music.ui.menu.LocalPlaylistMenu\nimport com.metrolist.music.ui.menu.SelectionSongMenu\nimport com.metrolist.music.ui.menu.SongMenu\nimport com.metrolist.music.ui.screens.settings.DarkMode\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.makeTimeString\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.utils.reportException\nimport com.metrolist.music.viewmodels.LocalPlaylistViewModel\nimport com.yalantis.ucrop.UCrop\nimport io.ktor.client.plugins.ClientRequestException\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport sh.calvin.reorderable.ReorderableItem\nimport sh.calvin.reorderable.rememberReorderableLazyListState\nimport java.time.LocalDateTime\n\n@SuppressLint(\"RememberReturnType\")\n@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun LocalPlaylistScreen(\n    navController: NavController,\n    viewModel: LocalPlaylistViewModel = hiltViewModel(),\n) {\n    val context = LocalContext.current\n    val menuState = LocalMenuState.current\n    val database = LocalDatabase.current\n    val haptic = LocalHapticFeedback.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n\n    val playlist by viewModel.playlist.collectAsState()\n    val songs by viewModel.playlistSongs.collectAsState()\n    val mutableSongs = remember { mutableStateListOf<PlaylistSong>() }\n    val playlistLength =\n        remember(songs) {\n            songs.fastSumBy { it.song.song.duration }\n        }\n    val (sortType, onSortTypeChange) =\n        rememberEnumPreference(\n            PlaylistSongSortTypeKey,\n            PlaylistSongSortType.CUSTOM,\n        )\n    val (sortDescending, onSortDescendingChange) =\n        rememberPreference(\n            PlaylistSongSortDescendingKey,\n            true,\n        )\n    var locked by rememberPreference(PlaylistEditLockKey, defaultValue = true)\n\n    val coroutineScope = rememberCoroutineScope()\n    val snackbarHostState = remember { SnackbarHostState() }\n\n    var isSearching by rememberSaveable { mutableStateOf(false) }\n\n    var query by rememberSaveable(stateSaver = TextFieldValue.Saver) {\n        mutableStateOf(TextFieldValue())\n    }\n\n    val filteredSongs =\n        remember(songs, query) {\n            if (query.text.isEmpty()) {\n                songs\n            } else {\n                songs.filter { song ->\n                    song.song.song.title\n                        .contains(query.text, ignoreCase = true) ||\n                        song.song.artists\n                            .fastAny { it.name.contains(query.text, ignoreCase = true) }\n                }\n            }\n        }\n\n    val focusRequester = remember { FocusRequester() }\n    LaunchedEffect(isSearching) {\n        if (isSearching) {\n            focusRequester.requestFocus()\n        }\n    }\n\n    var inSelectMode by rememberSaveable { mutableStateOf(false) }\n    val selection =\n        rememberSaveable(\n            saver =\n                listSaver<MutableList<Int>, Int>(\n                    save = { it.toList() },\n                    restore = { it.toMutableStateList() },\n                ),\n        ) { mutableStateListOf() }\n    var selectionAnchorMapId by rememberSaveable { mutableStateOf<Int?>(null) }\n    val onExitSelectionMode = {\n        inSelectMode = false\n        selection.clear()\n        selectionAnchorMapId = null\n    }\n\n    if (isSearching) {\n        BackHandler {\n            isSearching = false\n            query = TextFieldValue()\n        }\n    } else if (inSelectMode) {\n        BackHandler(onBack = onExitSelectionMode)\n    }\n\n    val downloadUtil = LocalDownloadUtil.current\n    var downloadState by remember {\n        mutableIntStateOf(Download.STATE_STOPPED)\n    }\n\n    val editable: Boolean = playlist?.playlist?.isEditable == true\n\n    LaunchedEffect(songs) {\n        selection.fastForEachReversed { mapId ->\n            if (songs.find { it.map.id == mapId } == null) {\n                selection.remove(Integer.valueOf(mapId))\n            }\n        }\n\n        if (selectionAnchorMapId != null && songs.none { it.map.id == selectionAnchorMapId }) {\n            selectionAnchorMapId = songs.firstOrNull { it.map.id in selection }?.map?.id\n        }\n    }\n\n    LaunchedEffect(songs) {\n        mutableSongs.apply {\n            clear()\n            addAll(songs)\n        }\n        if (songs.isEmpty()) return@LaunchedEffect\n        downloadUtil.downloads.collect { downloads ->\n            downloadState =\n                if (songs.all { downloads[it.song.id]?.state == Download.STATE_COMPLETED }) {\n                    Download.STATE_COMPLETED\n                } else if (songs.all {\n                        downloads[it.song.id]?.state == Download.STATE_QUEUED ||\n                            downloads[it.song.id]?.state == Download.STATE_DOWNLOADING ||\n                            downloads[it.song.id]?.state == Download.STATE_COMPLETED\n                    }\n                ) {\n                    Download.STATE_DOWNLOADING\n                } else {\n                    Download.STATE_STOPPED\n                }\n        }\n    }\n\n    var showEditDialog by remember {\n        mutableStateOf(false)\n    }\n\n    if (showEditDialog) {\n        playlist?.playlist?.let { playlistEntity ->\n            TextFieldDialog(\n                icon = {\n                    Icon(\n                        painter = painterResource(R.drawable.edit),\n                        contentDescription = null,\n                    )\n                },\n                title = { Text(text = stringResource(R.string.edit_playlist)) },\n                onDismiss = { showEditDialog = false },\n                initialTextFieldValue =\n                    TextFieldValue(\n                        playlistEntity.name,\n                        TextRange(playlistEntity.name.length),\n                    ),\n                onDone = { name ->\n                    database.query {\n                        update(\n                            playlistEntity.copy(\n                                name = name,\n                                lastUpdateTime = LocalDateTime.now(),\n                            ),\n                        )\n                    }\n                    viewModel.viewModelScope.launch(Dispatchers.IO) {\n                        playlistEntity.browseId?.let { YouTube.renamePlaylist(it, name) }\n                    }\n                },\n            )\n        }\n    }\n\n    var showRemoveDownloadDialog by remember {\n        mutableStateOf(false)\n    }\n\n    if (showRemoveDownloadDialog) {\n        DefaultDialog(\n            onDismiss = { showRemoveDownloadDialog = false },\n            content = {\n                Text(\n                    text =\n                        stringResource(\n                            R.string.remove_download_playlist_confirm,\n                            playlist?.playlist!!.name,\n                        ),\n                    style = MaterialTheme.typography.bodyLarge,\n                    modifier = Modifier.padding(horizontal = 18.dp),\n                )\n            },\n            buttons = {\n                TextButton(\n                    onClick = { showRemoveDownloadDialog = false },\n                ) {\n                    Text(text = stringResource(android.R.string.cancel))\n                }\n\n                TextButton(\n                    onClick = {\n                        showRemoveDownloadDialog = false\n                        if (!editable) {\n                            database.transaction {\n                                playlist?.id?.let { clearPlaylist(it) }\n                            }\n                        }\n                        songs.forEach { song ->\n                            DownloadService.sendRemoveDownload(\n                                context,\n                                ExoDownloadService::class.java,\n                                song.song.id,\n                                false,\n                            )\n                        }\n                    },\n                ) {\n                    Text(text = stringResource(android.R.string.ok))\n                }\n            },\n        )\n    }\n\n    var showDeletePlaylistDialog by remember {\n        mutableStateOf(false)\n    }\n    if (showDeletePlaylistDialog) {\n        DefaultDialog(\n            onDismiss = { showDeletePlaylistDialog = false },\n            content = {\n                Text(\n                    text =\n                        stringResource(\n                            R.string.delete_playlist_confirm,\n                            playlist?.playlist!!.name,\n                        ),\n                    style = MaterialTheme.typography.bodyLarge,\n                    modifier = Modifier.padding(horizontal = 18.dp),\n                )\n            },\n            buttons = {\n                TextButton(\n                    onClick = {\n                        showDeletePlaylistDialog = false\n                    },\n                ) {\n                    Text(text = stringResource(android.R.string.cancel))\n                }\n                TextButton(\n                    onClick = {\n                        showDeletePlaylistDialog = false\n                        database.query {\n                            playlist?.let { delete(it.playlist) }\n                        }\n                        viewModel.viewModelScope.launch(Dispatchers.IO) {\n                            playlist?.playlist?.browseId?.let { YouTube.deletePlaylist(it) }\n                        }\n                        navController.popBackStack()\n                    },\n                ) {\n                    Text(text = stringResource(android.R.string.ok))\n                }\n            },\n        )\n    }\n\n    val headerItems = 2\n    val lazyListState = rememberLazyListState()\n    var dragInfo by remember {\n        mutableStateOf<Pair<Int, Int>?>(null)\n    }\n    val reorderableState =\n        rememberReorderableLazyListState(\n            lazyListState = lazyListState,\n            scrollThresholdPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n        ) { from, to ->\n            if (to.index >= headerItems && from.index >= headerItems) {\n                val currentDragInfo = dragInfo\n                dragInfo =\n                    if (currentDragInfo == null) {\n                        (from.index - headerItems) to (to.index - headerItems)\n                    } else {\n                        currentDragInfo.first to (to.index - headerItems)\n                    }\n\n                mutableSongs.move(from.index - headerItems, to.index - headerItems)\n            }\n        }\n\n    LaunchedEffect(reorderableState.isAnyItemDragging) {\n        if (!reorderableState.isAnyItemDragging) {\n            dragInfo?.let { (from, to) ->\n                database.transaction {\n                    move(viewModel.playlistId, from, to)\n                }\n\n                // Sync order with YT Music\n                if (viewModel.playlist.value\n                        ?.playlist\n                        ?.browseId != null\n                ) {\n                    viewModel.viewModelScope.launch(Dispatchers.IO) {\n                        val playlistSongMap = database.playlistSongMaps(viewModel.playlistId, 0)\n                        val successorIndex = if (from > to) to else to + 1\n                        val successorSetVideoId = playlistSongMap.getOrNull(successorIndex)?.setVideoId\n\n                        playlistSongMap.getOrNull(from)?.setVideoId?.let { setVideoId ->\n                            YouTube.moveSongPlaylist(\n                                viewModel.playlist.value\n                                    ?.playlist\n                                    ?.browseId!!,\n                                setVideoId,\n                                successorSetVideoId,\n                            )\n                        }\n                    }\n                }\n\n                dragInfo = null\n            }\n        }\n    }\n\n    val showTopBarTitle by remember {\n        derivedStateOf {\n            lazyListState.firstVisibleItemIndex > 0\n        }\n    }\n\n    Box(\n        modifier = Modifier.fillMaxSize(),\n    ) {\n        LazyColumn(\n            state = lazyListState,\n            contentPadding = LocalPlayerAwareWindowInsets.current.union(WindowInsets.ime).asPaddingValues(),\n        ) {\n            playlist?.let { playlist ->\n                if (playlist.songCount == 0 && playlist.playlist.remoteSongCount == 0) {\n                    item(key = \"empty_placeholder\") {\n                        EmptyPlaceholder(\n                            icon = R.drawable.music_note,\n                            text = stringResource(R.string.playlist_is_empty),\n                            modifier = Modifier.animateItem(),\n                        )\n                    }\n                } else {\n                    if (!isSearching) {\n                        item(key = \"playlist_header\") {\n                            LocalPlaylistHeader(\n                                playlist = playlist,\n                                songs = songs,\n                                onShowEditDialog = { showEditDialog = true },\n                                onShowRemoveDownloadDialog = { showRemoveDownloadDialog = true },\n                                onshowDeletePlaylistDialog = { showDeletePlaylistDialog = true },\n                                onStartSearch = { isSearching = true },\n                                snackbarHostState = snackbarHostState,\n                                modifier = Modifier.animateItem(),\n                            )\n                        }\n                    }\n\n                    item(key = \"controls_row\") {\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            modifier =\n                                Modifier\n                                    .padding(start = 16.dp)\n                                    .animateItem(),\n                        ) {\n                            SortHeader(\n                                sortType = sortType,\n                                sortDescending = sortDescending,\n                                onSortTypeChange = onSortTypeChange,\n                                onSortDescendingChange = onSortDescendingChange,\n                                sortTypeText = { sortType ->\n                                    when (sortType) {\n                                        PlaylistSongSortType.CUSTOM -> R.string.sort_by_custom\n                                        PlaylistSongSortType.CREATE_DATE -> R.string.sort_by_create_date\n                                        PlaylistSongSortType.NAME -> R.string.sort_by_name\n                                        PlaylistSongSortType.ARTIST -> R.string.sort_by_artist\n                                        PlaylistSongSortType.PLAY_TIME -> R.string.sort_by_play_time\n                                    }\n                                },\n                                modifier = Modifier.weight(1f),\n                            )\n                            if (editable) {\n                                IconButton(\n                                    onClick = { locked = !locked },\n                                    modifier = Modifier.padding(horizontal = 6.dp),\n                                ) {\n                                    Icon(\n                                        painter = painterResource(if (locked) R.drawable.lock else R.drawable.lock_open),\n                                        contentDescription = null,\n                                    )\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n\n            val displayedSongs = if (isSearching) filteredSongs else mutableSongs\n\n            itemsIndexed(\n                items = displayedSongs,\n                key = { _, song -> song.map.id },\n            ) { index, song ->\n                ReorderableItem(\n                    state = reorderableState,\n                    key = song.map.id,\n                ) {\n                    val currentItem by rememberUpdatedState(song)\n\n                    fun deleteFromPlaylist() {\n                        database.transaction {\n                            coroutineScope.launch {\n                                playlist?.playlist?.browseId?.let { browseId ->\n                                    val setVideoId = getSetVideoId(currentItem.map.songId)\n                                    setVideoId?.setVideoId?.let { setVideoIdValue ->\n                                        YouTube.removeFromPlaylist(\n                                            browseId,\n                                            currentItem.map.songId,\n                                            setVideoIdValue,\n                                        )\n                                    }\n                                }\n                            }\n                            move(\n                                currentItem.map.playlistId,\n                                currentItem.map.position,\n                                Int.MAX_VALUE,\n                            )\n                            delete(currentItem.map.copy(position = Int.MAX_VALUE))\n                        }\n                    }\n\n                    val swipeRemoveEnabled by rememberPreference(SwipeToRemoveSongKey, defaultValue = false)\n                    val dismissBoxState =\n                        rememberSwipeToDismissBoxState(\n                            positionalThreshold = { totalDistance -> totalDistance },\n                        )\n                    var processedDismiss by remember { mutableStateOf(false) }\n                    LaunchedEffect(dismissBoxState.currentValue) {\n                        val dv = dismissBoxState.currentValue\n                        if (swipeRemoveEnabled && !processedDismiss && (\n                                dv == SwipeToDismissBoxValue.StartToEnd ||\n                                    dv == SwipeToDismissBoxValue.EndToStart\n                            )\n                        ) {\n                            processedDismiss = true\n                            deleteFromPlaylist()\n                        }\n                        if (dv == SwipeToDismissBoxValue.Settled) {\n                            processedDismiss = false\n                        }\n                    }\n\n                    val onCheckedChange: (Boolean) -> Unit = {\n                        if (it) {\n                            selection.add(song.map.id)\n                        } else {\n                            selection.remove(Integer.valueOf(song.map.id))\n                        }\n                    }\n\n                    val content: @Composable () -> Unit = {\n                        SongListItem(\n                            song = song.song,\n                            isActive = song.song.id == mediaMetadata?.id,\n                            isPlaying = isPlaying,\n                            showInLibraryIcon = true,\n                            trailingContent = {\n                                if (inSelectMode) {\n                                    Checkbox(\n                                        checked = selection.contains(song.map.id),\n                                        onCheckedChange = onCheckedChange,\n                                    )\n                                } else {\n                                    IconButton(\n                                        onClick = {\n                                            menuState.show {\n                                                SongMenu(\n                                                    originalSong = song.song,\n                                                    playlistSong = song,\n                                                    playlistBrowseId = playlist?.playlist?.browseId,\n                                                    navController = navController,\n                                                    onDismiss = menuState::dismiss,\n                                                )\n                                            }\n                                        },\n                                    ) {\n                                        Icon(\n                                            painter = painterResource(R.drawable.more_vert),\n                                            contentDescription = null,\n                                        )\n                                    }\n\n                                    if (sortType == PlaylistSongSortType.CUSTOM && !locked && !inSelectMode && !isSearching && editable) {\n                                        IconButton(\n                                            onClick = { },\n                                            modifier = Modifier.draggableHandle(),\n                                        ) {\n                                            Icon(\n                                                painter = painterResource(R.drawable.drag_handle),\n                                                contentDescription = null,\n                                            )\n                                        }\n                                    }\n                                }\n                            },\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .combinedClickable(\n                                        onClick = {\n                                            if (inSelectMode) {\n                                                onCheckedChange(!selection.contains(song.map.id))\n                                            } else if (song.song.id == mediaMetadata?.id) {\n                                                playerConnection.togglePlayPause()\n                                            } else {\n                                                playerConnection.playQueue(\n                                                    ListQueue(\n                                                        title = playlist!!.playlist.name,\n                                                        items = songs.map { it.song.toMediaItem() },\n                                                        startIndex = songs.indexOfFirst { it.map.id == song.map.id },\n                                                    ),\n                                                )\n                                            }\n                                        },\n                                        onLongClick = {\n                                            if (!inSelectMode) {\n                                                haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                inSelectMode = true\n                                                onCheckedChange(true)\n                                                selectionAnchorMapId = song.map.id\n                                            } else {\n                                                val anchorIndex =\n                                                    selectionAnchorMapId?.let { anchorMapId ->\n                                                        displayedSongs.indexOfFirst { it.map.id == anchorMapId }\n                                                    } ?: -1\n\n                                                if (anchorIndex == -1) {\n                                                    onCheckedChange(true)\n                                                    selectionAnchorMapId = song.map.id\n                                                } else {\n                                                    val range = if (anchorIndex <= index) anchorIndex..index else index..anchorIndex\n                                                    for (rangeIndex in range) {\n                                                        val rangeMapId = displayedSongs[rangeIndex].map.id\n                                                        if (rangeMapId !in selection) {\n                                                            selection.add(rangeMapId)\n                                                        }\n                                                    }\n                                                }\n                                            }\n                                        },\n                                    ),\n                        )\n                    }\n\n                    if (locked || inSelectMode || !swipeRemoveEnabled) {\n                        Box(modifier = Modifier.animateItem()) {\n                            content()\n                        }\n                    } else {\n                        SwipeToDismissBox(\n                            state = dismissBoxState,\n                            backgroundContent = {},\n                            modifier = Modifier.animateItem(),\n                        ) {\n                            content()\n                        }\n                    }\n                }\n            }\n        }\n\n        DraggableScrollbar(\n            modifier =\n                Modifier\n                    .padding(\n                        LocalPlayerAwareWindowInsets.current\n                            .union(WindowInsets.ime)\n                            .asPaddingValues(),\n                    ).align(Alignment.CenterEnd),\n            scrollState = lazyListState,\n            headerItems = 2,\n        )\n\n        TopAppBar(\n            title = {\n                if (inSelectMode) {\n                    Text(pluralStringResource(R.plurals.n_selected, selection.size, selection.size))\n                } else if (isSearching) {\n                    TextField(\n                        value = query,\n                        onValueChange = { query = it },\n                        placeholder = {\n                            Text(\n                                text = stringResource(R.string.search),\n                                style = MaterialTheme.typography.titleLarge,\n                            )\n                        },\n                        singleLine = true,\n                        textStyle = MaterialTheme.typography.titleLarge,\n                        keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),\n                        colors =\n                            TextFieldDefaults.colors(\n                                focusedContainerColor = Color.Transparent,\n                                unfocusedContainerColor = Color.Transparent,\n                                focusedIndicatorColor = Color.Transparent,\n                                unfocusedIndicatorColor = Color.Transparent,\n                                disabledIndicatorColor = Color.Transparent,\n                            ),\n                        modifier =\n                            Modifier\n                                .fillMaxWidth()\n                                .focusRequester(focusRequester),\n                    )\n                } else if (showTopBarTitle) {\n                    Text(playlist?.playlist?.name.orEmpty())\n                }\n            },\n            navigationIcon = {\n                if (inSelectMode) {\n                    IconButton(onClick = onExitSelectionMode) {\n                        Icon(\n                            painter = painterResource(R.drawable.close),\n                            contentDescription = null,\n                        )\n                    }\n                } else {\n                    IconButton(\n                        onClick = {\n                            if (isSearching) {\n                                isSearching = false\n                                query = TextFieldValue()\n                            } else {\n                                navController.navigateUp()\n                            }\n                        },\n                        onLongClick = {\n                            if (!isSearching) {\n                                navController.backToMain()\n                            }\n                        },\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.arrow_back),\n                            contentDescription = null,\n                        )\n                    }\n                }\n            },\n            actions = {\n                if (inSelectMode) {\n                    Checkbox(\n                        checked = selection.size == songs.size && selection.isNotEmpty(),\n                        onCheckedChange = {\n                            if (selection.size == songs.size) {\n                                selection.clear()\n                            } else {\n                                selection.clear()\n                                selection.addAll(songs.map { it.map.id })\n                            }\n                        },\n                    )\n                    IconButton(\n                        enabled = selection.isNotEmpty(),\n                        onClick = {\n                            menuState.show {\n                                SelectionSongMenu(\n                                    songSelection =\n                                        selection.mapNotNull { mapId ->\n                                            songs.find { it.map.id == mapId }?.song\n                                        },\n                                    songPosition =\n                                        selection.mapNotNull { mapId ->\n                                            songs.find { it.map.id == mapId }?.map\n                                        },\n                                    onDismiss = menuState::dismiss,\n                                    clearAction = onExitSelectionMode,\n                                )\n                            }\n                        },\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.more_vert),\n                            contentDescription = null,\n                        )\n                    }\n                } else if (!isSearching) {\n                    // Only search button remains in TopAppBar\n                    IconButton(\n                        onClick = { isSearching = true },\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.search),\n                            contentDescription = null,\n                        )\n                    }\n                }\n            },\n        )\n\n        SnackbarHost(\n            hostState = snackbarHostState,\n            modifier =\n                Modifier\n                    .windowInsetsPadding(LocalPlayerAwareWindowInsets.current.union(WindowInsets.ime))\n                    .align(Alignment.BottomCenter),\n        )\n    }\n}\n\n@Composable\nfun LocalPlaylistHeader(\n    playlist: Playlist,\n    songs: List<PlaylistSong>,\n    onShowEditDialog: () -> Unit,\n    onShowRemoveDownloadDialog: () -> Unit,\n    onshowDeletePlaylistDialog: () -> Unit,\n    onStartSearch: () -> Unit,\n    snackbarHostState: SnackbarHostState,\n    modifier: Modifier,\n) {\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val context = LocalContext.current\n    val database = LocalDatabase.current\n    val menuState = LocalMenuState.current\n    val syncUtils = LocalSyncUtils.current\n    val scope = rememberCoroutineScope()\n    val editPlaylistCoverStr = stringResource(R.string.edit_playlist_cover)\n    val playlistSyncedStr = stringResource(R.string.playlist_synced)\n\n    val playlistLength =\n        remember(songs) {\n            songs.fastSumBy { it.song.song.duration }\n        }\n\n    val downloadUtil = LocalDownloadUtil.current\n    var downloadState by remember {\n        mutableIntStateOf(Download.STATE_STOPPED)\n    }\n\n    val liked = playlist.playlist.bookmarkedAt != null\n    val editable: Boolean = playlist.playlist.isEditable\n\n    val overrideThumbnail = remember { mutableStateOf<String?>(null) }\n    var isCustomThumbnail: Boolean =\n        playlist.thumbnails.firstOrNull()?.let {\n            it.contains(\"studio_square_thumbnail\") || it.contains(\"content://com.metrolist.music\")\n        } ?: false\n\n    val result = remember { mutableStateOf<Uri?>(null) }\n    var pendingCropDestUri by remember { mutableStateOf<Uri?>(null) }\n    var showEditNoteDialog by remember { mutableStateOf(false) }\n\n    val cropLauncher =\n        rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { res ->\n            if (res.resultCode == android.app.Activity.RESULT_OK) {\n                val output = res.data?.let { UCrop.getOutput(it) } ?: pendingCropDestUri\n                if (output != null) result.value = output\n            }\n        }\n\n    val (darkMode, _) =\n        rememberEnumPreference(\n            DarkModeKey,\n            defaultValue = DarkMode.AUTO,\n        )\n\n    val cropColor = MaterialTheme.colorScheme\n    val darkTheme = darkMode == DarkMode.ON || (darkMode == DarkMode.AUTO && isSystemInDarkTheme())\n\n    val pickLauncher =\n        rememberLauncherForActivityResult(\n            ActivityResultContracts.PickVisualMedia(),\n        ) { uri ->\n            uri?.let { sourceUri ->\n                val destFile = java.io.File(context.cacheDir, \"playlist_cover_crop_${System.currentTimeMillis()}.jpg\")\n                val destUri = FileProvider.getUriForFile(context, \"${context.packageName}.FileProvider\", destFile)\n                pendingCropDestUri = destUri\n\n                val options =\n                    UCrop.Options().apply {\n                        setCompressionFormat(Bitmap.CompressFormat.JPEG)\n                        setCompressionQuality(90)\n                        setHideBottomControls(true)\n                        setToolbarTitle(editPlaylistCoverStr)\n\n                        setStatusBarLight(!darkTheme)\n\n                        setToolbarColor(cropColor.surface.toArgb())\n                        setToolbarWidgetColor(cropColor.inverseSurface.toArgb())\n                        setRootViewBackgroundColor(cropColor.surface.toArgb())\n                        setLogoColor(cropColor.surface.toArgb())\n                    }\n\n                val intent =\n                    UCrop\n                        .of(sourceUri, destUri)\n                        .withAspectRatio(1f, 1f)\n                        .withOptions(options)\n                        .getIntent(context)\n                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n                intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)\n                cropLauncher.launch(intent)\n            }\n        }\n\n    LaunchedEffect(result.value) {\n        val uri = result.value ?: return@LaunchedEffect\n        withContext(Dispatchers.IO) {\n            when {\n                playlist.playlist.browseId == null -> {\n                    overrideThumbnail.value = uri.toString()\n                    isCustomThumbnail = true\n\n                    // Update the database with the new thumbnail\n                    database.query {\n                        update(playlist.playlist.copy(thumbnailUrl = uri.toString()))\n                    }\n                }\n\n                else -> {\n                    val bytes = uriToByteArray(context, uri)\n                    YouTube\n                        .uploadCustomThumbnailLink(\n                            playlist.playlist.browseId,\n                            bytes!!,\n                        ).onSuccess { newThumbnailUrl ->\n                            overrideThumbnail.value = newThumbnailUrl\n                            isCustomThumbnail = true\n\n                            // Update the database with the new thumbnail URL\n                            database.query {\n                                update(playlist.playlist.copy(thumbnailUrl = newThumbnailUrl))\n                            }\n                        }.onFailure {\n                            if (it is ClientRequestException) {\n                                snackbarHostState.showSnackbar(\"${it.response.status.value} ${it.response.status.description}\")\n                            }\n                            reportException(it)\n                        }\n                }\n            }\n        }\n    }\n\n    LaunchedEffect(songs) {\n        if (songs.isEmpty()) return@LaunchedEffect\n        downloadUtil.downloads.collect { downloads ->\n            downloadState =\n                if (songs.all { downloads[it.song.id]?.state == Download.STATE_COMPLETED }) {\n                    Download.STATE_COMPLETED\n                } else if (songs.all {\n                        downloads[it.song.id]?.state == Download.STATE_QUEUED ||\n                            downloads[it.song.id]?.state == Download.STATE_DOWNLOADING ||\n                            downloads[it.song.id]?.state == Download.STATE_COMPLETED\n                    }\n                ) {\n                    Download.STATE_DOWNLOADING\n                } else {\n                    Download.STATE_STOPPED\n                }\n        }\n    }\n\n    Column(\n        modifier =\n            modifier\n                .fillMaxWidth()\n                .padding(top = 8.dp, bottom = 20.dp),\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n        if (showEditNoteDialog) {\n            ActionPromptDialog(\n                title = stringResource(R.string.edit_playlist_cover),\n                onDismiss = { showEditNoteDialog = false },\n                onConfirm = {\n                    showEditNoteDialog = false\n                    pickLauncher.launch(\n                        PickVisualMediaRequest(mediaType = ActivityResultContracts.PickVisualMedia.ImageOnly),\n                    )\n                },\n                onCancel = { showEditNoteDialog = false },\n            ) {\n                if (playlist.playlist.browseId != null) {\n                    Text(\n                        text = stringResource(R.string.edit_playlist_cover_note),\n                        style = MaterialTheme.typography.bodyMedium,\n                    )\n                    Spacer(Modifier.height(8.dp))\n                }\n                Text(\n                    text = stringResource(R.string.edit_playlist_cover_note_wait),\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),\n                )\n            }\n        }\n        // Playlist Thumbnail(s) - Large centered with shadow\n        Box(\n            modifier = Modifier.padding(top = 8.dp, bottom = 20.dp),\n        ) {\n            when (playlist.thumbnails.size) {\n                0 -> {\n                    Surface(\n                        modifier =\n                            Modifier\n                                .size(240.dp)\n                                .shadow(\n                                    elevation = 16.dp,\n                                    shape = RoundedCornerShape(3.dp),\n                                ),\n                        shape = RoundedCornerShape(3.dp),\n                        color = MaterialTheme.colorScheme.surfaceVariant,\n                    ) {\n                        Box(\n                            modifier = Modifier.fillMaxSize(),\n                            contentAlignment = Alignment.Center,\n                        ) {\n                            Icon(\n                                painter = painterResource(R.drawable.queue_music),\n                                contentDescription = null,\n                                modifier = Modifier.size(80.dp),\n                                tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                            )\n                        }\n                    }\n                }\n\n                1 -> {\n                    Surface(\n                        modifier =\n                            Modifier\n                                .size(240.dp)\n                                .shadow(\n                                    elevation = 24.dp,\n                                    shape = RoundedCornerShape(3.dp),\n                                    spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),\n                                ),\n                        shape = RoundedCornerShape(3.dp),\n                    ) {\n                        AsyncImage(\n                            model = overrideThumbnail.value ?: playlist.thumbnails[0],\n                            contentDescription = null,\n                            contentScale = ContentScale.Crop,\n                            modifier = Modifier.fillMaxSize(),\n                        )\n                    }\n                    if (editable) {\n                        OverlayEditButton(\n                            visible = true,\n                            alignment = Alignment.BottomEnd,\n                            onClick = {\n                                if (isCustomThumbnail) {\n                                    menuState.show(\n                                        {\n                                            CustomThumbnailMenu(\n                                                onEdit = {\n                                                    pickLauncher.launch(\n                                                        PickVisualMediaRequest(\n                                                            mediaType = ActivityResultContracts.PickVisualMedia.ImageOnly,\n                                                        ),\n                                                    )\n                                                },\n                                                onRemove = {\n                                                    when {\n                                                        playlist.playlist.browseId == null -> {\n                                                            overrideThumbnail.value = null\n                                                            database.query {\n                                                                update(playlist.playlist.copy(thumbnailUrl = null))\n                                                            }\n                                                        }\n\n                                                        else -> {\n                                                            scope.launch(Dispatchers.IO) {\n                                                                YouTube.removeThumbnailPlaylist(playlist.playlist.browseId).onSuccess { newThumbnailUrl ->\n                                                                    overrideThumbnail.value = newThumbnailUrl\n                                                                    database.query {\n                                                                        update(playlist.playlist.copy(thumbnailUrl = newThumbnailUrl))\n                                                                    }\n                                                                }\n                                                            }\n                                                        }\n                                                    }\n                                                    isCustomThumbnail = false\n                                                },\n                                                onDismiss = menuState::dismiss,\n                                            )\n                                        },\n                                    )\n                                } else {\n                                    showEditNoteDialog = true\n                                }\n                            },\n                        )\n                    }\n                }\n\n                else -> {\n                    Surface(\n                        modifier =\n                            Modifier\n                                .size(240.dp)\n                                .shadow(\n                                    elevation = 24.dp,\n                                    shape = RoundedCornerShape(3.dp),\n                                    spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),\n                                ),\n                        shape = RoundedCornerShape(3.dp),\n                    ) {\n                        Box(modifier = Modifier.fillMaxSize()) {\n                            listOf(\n                                Alignment.TopStart,\n                                Alignment.TopEnd,\n                                Alignment.BottomStart,\n                                Alignment.BottomEnd,\n                            ).fastForEachIndexed { index, alignment ->\n                                AsyncImage(\n                                    model = playlist.thumbnails.getOrNull(index),\n                                    contentDescription = null,\n                                    contentScale = ContentScale.Crop,\n                                    modifier =\n                                        Modifier\n                                            .align(alignment)\n                                            .size(120.dp),\n                                )\n                            }\n                        }\n                    }\n                    if (editable) {\n                        OverlayEditButton(\n                            visible = true,\n                            alignment = Alignment.BottomEnd,\n                            onClick = {\n                                if (isCustomThumbnail) {\n                                    menuState.show(\n                                        {\n                                            CustomThumbnailMenu(\n                                                onEdit = {\n                                                    pickLauncher.launch(\n                                                        PickVisualMediaRequest(\n                                                            mediaType = ActivityResultContracts.PickVisualMedia.ImageOnly,\n                                                        ),\n                                                    )\n                                                },\n                                                onRemove = {\n                                                    when {\n                                                        playlist.playlist.browseId == null -> {\n                                                            overrideThumbnail.value = null\n                                                            database.query {\n                                                                update(playlist.playlist.copy(thumbnailUrl = null))\n                                                            }\n                                                        }\n\n                                                        else -> {\n                                                            scope.launch(Dispatchers.IO) {\n                                                                YouTube.removeThumbnailPlaylist(playlist.playlist.browseId).onSuccess { newThumbnailUrl ->\n                                                                    overrideThumbnail.value = newThumbnailUrl\n                                                                    database.query {\n                                                                        update(playlist.playlist.copy(thumbnailUrl = newThumbnailUrl))\n                                                                    }\n                                                                }\n                                                            }\n                                                        }\n                                                    }\n                                                    isCustomThumbnail = false\n                                                },\n                                                onDismiss = menuState::dismiss,\n                                            )\n                                        },\n                                    )\n                                } else {\n                                    showEditNoteDialog = true\n                                }\n                            },\n                        )\n                    }\n                }\n            }\n        }\n\n        // Playlist Name\n        Text(\n            text = playlist.playlist.name,\n            style = MaterialTheme.typography.headlineSmall,\n            fontWeight = FontWeight.Bold,\n            textAlign = TextAlign.Center,\n            maxLines = 2,\n            overflow = TextOverflow.Ellipsis,\n            modifier = Modifier.padding(horizontal = 32.dp),\n        )\n\n        Spacer(modifier = Modifier.height(12.dp))\n\n        // Metadata - Song Count • Duration\n        val songCount =\n            if (playlist.songCount == 0 && playlist.playlist.remoteSongCount != null) {\n                playlist.playlist.remoteSongCount\n            } else {\n                playlist.songCount\n            }\n        Text(\n            text =\n                buildString {\n                    append(pluralStringResource(R.plurals.n_song, songCount, songCount))\n                    if (playlistLength > 0) {\n                        append(\" • \")\n                        append(makeTimeString(playlistLength * 1000L))\n                    }\n                },\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f),\n        )\n\n        Spacer(modifier = Modifier.height(24.dp))\n\n        // Action Buttons Row\n        Row(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = 24.dp),\n            horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            // Shuffle Button - Smaller secondary button\n            Surface(\n                onClick = {\n                    playerConnection.playQueue(\n                        ListQueue(\n                            title = playlist.playlist.name,\n                            items = songs.shuffled().map { it.song.toMediaItem() },\n                        ),\n                    )\n                },\n                shape = CircleShape,\n                color = MaterialTheme.colorScheme.surfaceVariant,\n                modifier = Modifier.size(48.dp),\n            ) {\n                Box(\n                    modifier = Modifier.fillMaxSize(),\n                    contentAlignment = Alignment.Center,\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.shuffle),\n                        contentDescription = stringResource(R.string.shuffle),\n                        modifier = Modifier.size(24.dp),\n                    )\n                }\n            }\n\n            // Play Button - Larger primary circular button\n            Surface(\n                onClick = {\n                    playerConnection.playQueue(\n                        ListQueue(\n                            title = playlist.playlist.name,\n                            items = songs.map { it.song.toMediaItem() },\n                        ),\n                    )\n                },\n                color = MaterialTheme.colorScheme.primary,\n                shape = CircleShape,\n                modifier = Modifier.size(72.dp),\n            ) {\n                Box(\n                    contentAlignment = Alignment.Center,\n                    modifier = Modifier.fillMaxSize(),\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.play),\n                        contentDescription = stringResource(R.string.play),\n                        tint = MaterialTheme.colorScheme.onPrimary,\n                        modifier = Modifier.size(32.dp),\n                    )\n                }\n            }\n\n            // Menu Button - Smaller secondary button\n            Surface(\n                onClick = {\n                    menuState.show {\n                        LocalPlaylistMenu(\n                            playlist = playlist,\n                            songs = songs,\n                            context = context,\n                            downloadState = downloadState,\n                            onEdit = onShowEditDialog,\n                            onSync = {\n                                scope.launch(Dispatchers.IO) {\n                                    val playlistPage =\n                                        YouTube\n                                            .playlist(playlist.playlist.browseId!!)\n                                            .completed()\n                                            .getOrNull() ?: return@launch\n                                    database.transaction {\n                                        clearPlaylist(playlist.id)\n                                        playlistPage.songs\n                                            .map(SongItem::toMediaMetadata)\n                                            .onEach(::insert)\n                                            .mapIndexed { position, song ->\n                                                PlaylistSongMap(\n                                                    songId = song.id,\n                                                    playlistId = playlist.id,\n                                                    position = position,\n                                                    setVideoId = song.setVideoId,\n                                                )\n                                            }.forEach(::insert)\n                                    }\n                                }\n                                scope.launch(Dispatchers.Main) {\n                                    snackbarHostState.showSnackbar(playlistSyncedStr)\n                                }\n                            },\n                            onDelete = onshowDeletePlaylistDialog,\n                            onDownload = {\n                                when (downloadState) {\n                                    Download.STATE_COMPLETED -> {\n                                        onShowRemoveDownloadDialog()\n                                    }\n\n                                    Download.STATE_DOWNLOADING -> {\n                                        songs.forEach { song ->\n                                            DownloadService.sendRemoveDownload(\n                                                context,\n                                                ExoDownloadService::class.java,\n                                                song.song.id,\n                                                false,\n                                            )\n                                        }\n                                    }\n\n                                    else -> {\n                                        songs.forEach { song ->\n                                            val downloadRequest =\n                                                DownloadRequest\n                                                    .Builder(song.song.id, song.song.id.toUri())\n                                                    .setCustomCacheKey(song.song.id)\n                                                    .setData(\n                                                        song.song.song.title\n                                                            .toByteArray(),\n                                                    ).build()\n                                            DownloadService.sendAddDownload(\n                                                context,\n                                                ExoDownloadService::class.java,\n                                                downloadRequest,\n                                                false,\n                                            )\n                                        }\n                                    }\n                                }\n                            },\n                            onQueue = {\n                                playerConnection.addToQueue(\n                                    items = songs.map { it.song.toMediaItem() },\n                                )\n                            },\n                            onDismiss = { menuState.dismiss() },\n                        )\n                    }\n                },\n                shape = CircleShape,\n                color = MaterialTheme.colorScheme.surfaceVariant,\n                modifier = Modifier.size(48.dp),\n            ) {\n                Box(\n                    modifier = Modifier.fillMaxSize(),\n                    contentAlignment = Alignment.Center,\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.more_vert),\n                        contentDescription = null,\n                        modifier = Modifier.size(24.dp),\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun MetadataChip(\n    icon: Int,\n    text: String,\n    modifier: Modifier = Modifier,\n) {\n    Surface(\n        modifier = modifier,\n        shape = RoundedCornerShape(20.dp),\n        color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f),\n    ) {\n        Row(\n            modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),\n            horizontalArrangement = Arrangement.spacedBy(6.dp),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            Icon(\n                painter = painterResource(icon),\n                contentDescription = null,\n                modifier = Modifier.size(16.dp),\n                tint = MaterialTheme.colorScheme.onSurfaceVariant,\n            )\n            Text(\n                text = text,\n                style = MaterialTheme.typography.labelMedium,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                maxLines = 1,\n            )\n        }\n    }\n}\n\nfun uriToByteArray(\n    context: Context,\n    uri: Uri,\n): ByteArray? =\n    try {\n        context.contentResolver.openInputStream(uri)?.use { it.readBytes() }\n    } catch (_: SecurityException) {\n        null\n    }\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/playlist/OnlinePlaylistScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.playlist\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.ime\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.union\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.ContainedLoadingIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextField\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.listSaver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.toMutableStateList\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.pluralStringResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.util.fastAny\nimport androidx.compose.ui.util.fastForEachReversed\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport coil3.compose.AsyncImage\nimport coil3.request.ImageRequest\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalListenTogetherManager\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.LocalSyncUtils\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.HideExplicitKey\nimport com.metrolist.music.db.entities.Playlist\nimport com.metrolist.music.db.entities.PlaylistEntity\nimport com.metrolist.music.db.entities.PlaylistSongMap\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.playback.queues.YouTubePlaylistQueue\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.YouTubeListItem\nimport com.metrolist.music.ui.menu.YouTubePlaylistMenu\nimport com.metrolist.music.ui.menu.YouTubeSelectionSongMenu\nimport com.metrolist.music.ui.menu.YouTubeSongMenu\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.makeTimeString\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.viewmodels.OnlinePlaylistViewModel\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun OnlinePlaylistScreen(\n    navController: NavController,\n    viewModel: OnlinePlaylistViewModel = hiltViewModel(),\n) {\n    val menuState = LocalMenuState.current\n    val database = LocalDatabase.current\n    val haptic = LocalHapticFeedback.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val isListenTogetherGuest = listenTogetherManager?.let { it.isInRoom && !it.isHost } ?: false\n    val coroutineScope = rememberCoroutineScope()\n\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n\n    val playlist by viewModel.playlist.collectAsState()\n    val songs by viewModel.playlistSongs.collectAsState()\n    val dbPlaylist by viewModel.dbPlaylist.collectAsState()\n    val isLoading by viewModel.isLoading.collectAsState()\n    val isLoadingMore by viewModel.isLoadingMore.collectAsState()\n    val error by viewModel.error.collectAsState()\n    val isPodcastPlaylist = viewModel.isPodcastPlaylist\n\n    val hideExplicit by rememberPreference(key = HideExplicitKey, defaultValue = false)\n\n    val lazyListState = rememberLazyListState()\n    val snackbarHostState = remember { SnackbarHostState() }\n\n    var isSearching by rememberSaveable { mutableStateOf(false) }\n    var query by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }\n\n    val filteredSongs =\n        remember(songs, query) {\n            if (query.text.isEmpty()) {\n                songs.mapIndexed { i, s -> i to s }\n            } else {\n                songs.mapIndexed { i, s -> i to s }.filter {\n                    it.second.title.contains(query.text, true) ||\n                        it.second.artists.fastAny { a -> a.name.contains(query.text, true) }\n                }\n            }\n        }\n\n    var inSelectMode by rememberSaveable { mutableStateOf(false) }\n    val selection =\n        rememberSaveable(\n            saver =\n                listSaver<MutableList<String>, String>(\n                    save = { it.toList() },\n                    restore = { it.toMutableStateList() },\n                ),\n        ) { mutableStateListOf() }\n    var selectionAnchorSongId by rememberSaveable { mutableStateOf<String?>(null) }\n    val onExitSelectionMode = {\n        inSelectMode = false\n        selection.clear()\n        selectionAnchorSongId = null\n    }\n\n    val focusRequester = remember { FocusRequester() }\n    LaunchedEffect(isSearching) { if (isSearching) focusRequester.requestFocus() }\n\n    LaunchedEffect(filteredSongs) {\n        selection.fastForEachReversed { songId ->\n            if (filteredSongs.find { it.second.id == songId } == null) {\n                selection.remove(songId)\n            }\n        }\n\n        if (selectionAnchorSongId != null && filteredSongs.none { it.second.id == selectionAnchorSongId }) {\n            selectionAnchorSongId = filteredSongs.firstOrNull { it.second.id in selection }?.second?.id\n        }\n    }\n\n    if (isSearching) {\n        BackHandler {\n            isSearching = false\n            query = TextFieldValue()\n        }\n    } else if (inSelectMode) {\n        BackHandler(onBack = onExitSelectionMode)\n    }\n\n    Box(Modifier.fillMaxSize()) {\n        LazyColumn(\n            state = lazyListState,\n            contentPadding = LocalPlayerAwareWindowInsets.current.union(WindowInsets.ime).asPaddingValues(),\n        ) {\n            if (playlist == null || songs.isEmpty()) {\n                if (isLoading) {\n                    item(key = \"loading_placeholder\") {\n                        Box(\n                            modifier =\n                                Modifier\n                                    .fillParentMaxSize()\n                                    .padding(32.dp),\n                            contentAlignment = Alignment.Center,\n                        ) {\n                            ContainedLoadingIndicator()\n                        }\n                    }\n                } else if (error != null) {\n                    item(key = \"error_placeholder\") {\n                        Column(\n                            modifier =\n                                Modifier\n                                    .fillParentMaxSize()\n                                    .padding(32.dp),\n                            horizontalAlignment = Alignment.CenterHorizontally,\n                            verticalArrangement = Arrangement.Center,\n                        ) {\n                            Text(\n                                text = error ?: stringResource(R.string.error_unknown),\n                                style = MaterialTheme.typography.bodyLarge,\n                                textAlign = TextAlign.Center,\n                            )\n                            Spacer(modifier = Modifier.height(16.dp))\n                            androidx.compose.material3.TextButton(onClick = { viewModel.retry() }) {\n                                Text(stringResource(R.string.retry))\n                            }\n                        }\n                    }\n                } else if (!isLoading && songs.isEmpty()) {\n                    item(key = \"empty_placeholder\") {\n                        Box(\n                            modifier =\n                                Modifier\n                                    .fillParentMaxSize()\n                                    .padding(32.dp),\n                            contentAlignment = Alignment.Center,\n                        ) {\n                            Text(\n                                text = stringResource(R.string.playlist_is_empty),\n                                style = MaterialTheme.typography.bodyLarge,\n                            )\n                        }\n                    }\n                }\n            } else {\n                playlist?.let { playlist ->\n                    if (!isSearching) {\n                        item(key = \"playlist_header\") {\n                            OnlinePlaylistHeader(\n                                playlist = playlist,\n                                songs = songs,\n                                dbPlaylist = dbPlaylist,\n                                navController = navController,\n                                coroutineScope = coroutineScope,\n                                continuation = viewModel.continuation,\n                                isPodcastPlaylist = isPodcastPlaylist,\n                                modifier = Modifier.animateItem(),\n                            )\n                        }\n                    }\n\n                    itemsIndexed(filteredSongs) { index, (_, songItem) ->\n                        val onCheckedChange: (Boolean) -> Unit = {\n                            if (it) {\n                                selection.add(songItem.id)\n                            } else {\n                                selection.remove(songItem.id)\n                            }\n                        }\n\n                        YouTubeListItem(\n                            item = songItem,\n                            isActive = mediaMetadata?.id == songItem.id,\n                            isPlaying = isPlaying,\n                            isSelected = inSelectMode && songItem.id in selection,\n                            modifier =\n                                Modifier\n                                    .combinedClickable(\n                                        enabled = !hideExplicit || !songItem.explicit,\n                                        onClick = {\n                                            if (inSelectMode) {\n                                                onCheckedChange(songItem.id !in selection)\n                                            } else if (songItem.id == mediaMetadata?.id) {\n                                                playerConnection.togglePlayPause()\n                                            } else {\n                                                playerConnection.playQueue(\n                                                    YouTubePlaylistQueue(\n                                                        playlistId = playlist.id,\n                                                        playlistTitle = playlist.title,\n                                                        initialSongs = filteredSongs.map { it.second },\n                                                        initialContinuation = viewModel.continuation,\n                                                        startIndex = index,\n                                                    ),\n                                                )\n                                            }\n                                        },\n                                        onLongClick = {\n                                            if (!inSelectMode) {\n                                                haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                                inSelectMode = true\n                                                onCheckedChange(true)\n                                                selectionAnchorSongId = songItem.id\n                                            } else {\n                                                val anchorIndex =\n                                                    selectionAnchorSongId?.let { anchorSongId ->\n                                                        filteredSongs.indexOfFirst { it.second.id == anchorSongId }\n                                                    } ?: -1\n\n                                                if (anchorIndex == -1) {\n                                                    onCheckedChange(true)\n                                                    selectionAnchorSongId = songItem.id\n                                                } else {\n                                                    val range = if (anchorIndex <= index) anchorIndex..index else index..anchorIndex\n                                                    for (rangeIndex in range) {\n                                                        val rangeSongId = filteredSongs[rangeIndex].second.id\n                                                        if (rangeSongId !in selection) {\n                                                            selection.add(rangeSongId)\n                                                        }\n                                                    }\n                                                }\n                                            }\n                                        },\n                                    ).animateItem(),\n                            trailingContent = {\n                                if (inSelectMode) {\n                                    Checkbox(\n                                        checked = songItem.id in selection,\n                                        onCheckedChange = onCheckedChange,\n                                    )\n                                } else {\n                                    IconButton(onClick = {\n                                        menuState.show {\n                                            YouTubeSongMenu(songItem, navController, menuState::dismiss)\n                                        }\n                                    }) {\n                                        Icon(painterResource(R.drawable.more_vert), null)\n                                    }\n                                }\n                            },\n                        )\n                    }\n\n                    if (isLoadingMore) {\n                        item(key = \"loading_more\") {\n                            Box(\n                                modifier =\n                                    Modifier\n                                        .fillMaxWidth()\n                                        .padding(16.dp),\n                                contentAlignment = Alignment.Center,\n                            ) {\n                                ContainedLoadingIndicator()\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        TopAppBar(\n            title = {\n                if (inSelectMode) {\n                    Text(\n                        text =\n                            if (isPodcastPlaylist) {\n                                pluralStringResource(R.plurals.n_episode, selection.size, selection.size)\n                            } else {\n                                pluralStringResource(R.plurals.n_song, selection.size, selection.size)\n                            },\n                        style = MaterialTheme.typography.titleLarge,\n                    )\n                } else if (isSearching) {\n                    TextField(\n                        value = query,\n                        onValueChange = { query = it },\n                        placeholder = {\n                            Text(\n                                text = stringResource(R.string.search),\n                                style = MaterialTheme.typography.titleLarge,\n                            )\n                        },\n                        singleLine = true,\n                        textStyle = MaterialTheme.typography.titleLarge,\n                        keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),\n                        colors =\n                            TextFieldDefaults.colors(\n                                focusedContainerColor = Color.Transparent,\n                                unfocusedContainerColor = Color.Transparent,\n                                focusedIndicatorColor = Color.Transparent,\n                                unfocusedIndicatorColor = Color.Transparent,\n                                disabledIndicatorColor = Color.Transparent,\n                            ),\n                        modifier =\n                            Modifier\n                                .fillMaxWidth()\n                                .focusRequester(focusRequester),\n                    )\n                } else if (lazyListState.firstVisibleItemIndex > 0) {\n                    Text(playlist?.title ?: \"\")\n                }\n            },\n            navigationIcon = {\n                IconButton(\n                    onClick = {\n                        if (isSearching) {\n                            isSearching = false\n                            query = TextFieldValue()\n                        } else if (inSelectMode) {\n                            onExitSelectionMode()\n                        } else {\n                            navController.navigateUp()\n                        }\n                    },\n                    onLongClick = {\n                        if (!isSearching && !inSelectMode) {\n                            navController.backToMain()\n                        }\n                    },\n                ) {\n                    Icon(\n                        painter =\n                            painterResource(\n                                if (inSelectMode) R.drawable.close else R.drawable.arrow_back,\n                            ),\n                        contentDescription = null,\n                    )\n                }\n            },\n            actions = {\n                if (inSelectMode) {\n                    Checkbox(\n                        checked = selection.size == filteredSongs.size && selection.isNotEmpty(),\n                        onCheckedChange = {\n                            if (selection.size == filteredSongs.size) {\n                                selection.clear()\n                            } else {\n                                selection.clear()\n                                selection.addAll(filteredSongs.map { it.second.id })\n                            }\n                        },\n                    )\n                    IconButton(\n                        enabled = selection.isNotEmpty(),\n                        onClick = {\n                            menuState.show {\n                                YouTubeSelectionSongMenu(\n                                    songSelection =\n                                        filteredSongs\n                                            .filter { it.second.id in selection }\n                                            .map { it.second },\n                                    onDismiss = menuState::dismiss,\n                                    clearAction = onExitSelectionMode,\n                                )\n                            }\n                        },\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.more_vert),\n                            contentDescription = null,\n                        )\n                    }\n                } else if (!isSearching) {\n                    IconButton(\n                        onClick = { isSearching = true },\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.search),\n                            contentDescription = null,\n                        )\n                    }\n                }\n            },\n        )\n\n        SnackbarHost(\n            hostState = snackbarHostState,\n            modifier = Modifier.align(Alignment.BottomCenter),\n        )\n    }\n}\n\n@Composable\nprivate fun OnlinePlaylistHeader(\n    playlist: PlaylistItem,\n    songs: List<SongItem>,\n    dbPlaylist: Playlist?,\n    navController: NavController,\n    coroutineScope: CoroutineScope,\n    continuation: String?,\n    isPodcastPlaylist: Boolean = false,\n    modifier: Modifier = Modifier,\n) {\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val listenTogetherManager = LocalListenTogetherManager.current\n    val isListenTogetherGuest = listenTogetherManager?.let { it.isInRoom && !it.isHost } ?: false\n    val database = LocalDatabase.current\n    val menuState = LocalMenuState.current\n    val syncUtils = LocalSyncUtils.current\n\n    Column(\n        modifier =\n            modifier\n                .fillMaxWidth()\n                .padding(top = 8.dp, bottom = 20.dp),\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n        Surface(\n            modifier =\n                Modifier\n                    .size(240.dp)\n                    .shadow(\n                        elevation = 24.dp,\n                        shape = RoundedCornerShape(3.dp),\n                        spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),\n                    ),\n            shape = RoundedCornerShape(3.dp),\n        ) {\n            AsyncImage(\n                model = ImageRequest.Builder(LocalContext.current).data(playlist.thumbnail).build(),\n                contentDescription = null,\n                contentScale = ContentScale.Crop,\n                modifier = Modifier.fillMaxSize(),\n            )\n        }\n\n        Spacer(modifier = Modifier.height(20.dp))\n\n        Text(\n            text = playlist.title,\n            style = MaterialTheme.typography.headlineSmall,\n            fontWeight = FontWeight.Bold,\n            textAlign = TextAlign.Center,\n            maxLines = 2,\n            overflow = TextOverflow.Ellipsis,\n            modifier = Modifier.padding(horizontal = 32.dp),\n        )\n\n        Spacer(modifier = Modifier.height(12.dp))\n\n        // Metadata - Song Count • Duration\n        val totalDuration = songs.sumOf { it.duration ?: 0 }\n        Text(\n            text =\n                buildString {\n                    append(\n                        if (isPodcastPlaylist) {\n                            pluralStringResource(R.plurals.n_episode, songs.size, songs.size)\n                        } else {\n                            pluralStringResource(R.plurals.n_song, songs.size, songs.size)\n                        },\n                    )\n                    if (totalDuration > 0) {\n                        append(\" • \")\n                        append(makeTimeString(totalDuration * 1000L))\n                    }\n                },\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f),\n        )\n\n        Spacer(modifier = Modifier.height(24.dp))\n\n        Row(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = 24.dp),\n            horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            // Like Button - Smaller secondary button\n            Surface(\n                onClick = {\n                    if (dbPlaylist != null) {\n                        database.transaction {\n                            val currentPlaylist = dbPlaylist.playlist\n                            update(currentPlaylist, playlist)\n                            update(currentPlaylist.toggleLike())\n                        }\n                    } else {\n                        database.transaction {\n                            val playlistEntity =\n                                PlaylistEntity(\n                                    name = playlist.title,\n                                    browseId = playlist.id,\n                                    thumbnailUrl = playlist.thumbnail,\n                                    isEditable = playlist.isEditable,\n                                    remoteSongCount =\n                                        playlist.songCountText?.let {\n                                            Regex(\"\"\"\\d+\"\"\").find(it)?.value?.toIntOrNull()\n                                        },\n                                    playEndpointParams = playlist.playEndpoint?.params,\n                                    shuffleEndpointParams = playlist.shuffleEndpoint?.params,\n                                    radioEndpointParams = playlist.radioEndpoint?.params,\n                                ).toggleLike()\n                            insert(playlistEntity)\n                            coroutineScope.launch(Dispatchers.IO) {\n                                songs\n                                    .map { it.toMediaMetadata() }\n                                    .onEach(::insert)\n                                    .mapIndexed { index, song ->\n                                        PlaylistSongMap(\n                                            songId = song.id,\n                                            playlistId = playlistEntity.id,\n                                            position = index,\n                                            setVideoId = song.setVideoId,\n                                        )\n                                    }.forEach(::insert)\n                            }\n                        }\n                    }\n                },\n                shape = CircleShape,\n                color = MaterialTheme.colorScheme.surfaceVariant,\n                modifier = Modifier.size(48.dp),\n            ) {\n                Box(\n                    modifier = Modifier.fillMaxSize(),\n                    contentAlignment = Alignment.Center,\n                ) {\n                    Icon(\n                        painter =\n                            painterResource(\n                                if (dbPlaylist?.playlist?.bookmarkedAt != null) R.drawable.favorite else R.drawable.favorite_border,\n                            ),\n                        contentDescription = null,\n                        tint =\n                            if (dbPlaylist?.playlist?.bookmarkedAt != null) {\n                                MaterialTheme.colorScheme.error\n                            } else {\n                                MaterialTheme.colorScheme.onSurfaceVariant\n                            },\n                        modifier = Modifier.size(24.dp),\n                    )\n                }\n            }\n\n            // Play Button - Larger primary circular button\n            Surface(\n                onClick = {\n                    if (!isListenTogetherGuest && songs.isNotEmpty()) {\n                        playerConnection.playQueue(\n                            YouTubePlaylistQueue(\n                                playlistId = playlist.id,\n                                playlistTitle = playlist.title,\n                                initialSongs = songs,\n                                initialContinuation = continuation,\n                            ),\n                        )\n                    }\n                },\n                color = MaterialTheme.colorScheme.primary,\n                shape = CircleShape,\n                modifier = Modifier.size(72.dp),\n            ) {\n                Box(\n                    contentAlignment = Alignment.Center,\n                    modifier = Modifier.fillMaxSize(),\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.play),\n                        contentDescription = stringResource(R.string.play),\n                        tint = MaterialTheme.colorScheme.onPrimary,\n                        modifier = Modifier.size(32.dp),\n                    )\n                }\n            }\n\n            // Menu Button - Smaller secondary button\n            Surface(\n                onClick = {\n                    menuState.show {\n                        YouTubePlaylistMenu(\n                            playlist = playlist,\n                            songs = songs,\n                            coroutineScope = coroutineScope,\n                            onDismiss = menuState::dismiss,\n                        )\n                    }\n                },\n                shape = CircleShape,\n                color = MaterialTheme.colorScheme.surfaceVariant,\n                modifier = Modifier.size(48.dp),\n            ) {\n                Box(\n                    modifier = Modifier.fillMaxSize(),\n                    contentAlignment = Alignment.Center,\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.more_vert),\n                        contentDescription = null,\n                        modifier = Modifier.size(24.dp),\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/playlist/TopPlaylistScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.playlist\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.ime\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.union\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TextField\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.listSaver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.toMutableStateList\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.shadow\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalFocusManager\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.pluralStringResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.util.fastForEachReversed\nimport androidx.compose.ui.util.fastSumBy\nimport androidx.core.net.toUri\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.media3.exoplayer.offline.Download\nimport androidx.media3.exoplayer.offline.DownloadRequest\nimport androidx.media3.exoplayer.offline.DownloadService\nimport androidx.navigation.NavController\nimport coil3.compose.AsyncImage\nimport com.metrolist.music.LocalDownloadUtil\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.MyTopFilter\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.playback.ExoDownloadService\nimport com.metrolist.music.playback.queues.ListQueue\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.DraggableScrollbar\nimport com.metrolist.music.ui.component.EmptyPlaceholder\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.SongListItem\nimport com.metrolist.music.ui.component.SortHeader\nimport com.metrolist.music.ui.menu.SelectionSongMenu\nimport com.metrolist.music.ui.menu.SongMenu\nimport com.metrolist.music.ui.menu.TopPlaylistMenu\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.makeTimeString\nimport com.metrolist.music.viewmodels.TopPlaylistViewModel\n\n@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun TopPlaylistScreen(\n    navController: NavController,\n    viewModel: TopPlaylistViewModel = hiltViewModel(),\n) {\n    val context = LocalContext.current\n    val menuState = LocalMenuState.current\n    val haptic = LocalHapticFeedback.current\n    val focusManager = LocalFocusManager.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n    val maxSize = viewModel.top\n\n    val songs by viewModel.topSongs.collectAsState(null)\n    val mutableSongs = remember { mutableStateListOf<Song>() }\n\n    val likeLength = remember(songs) {\n        songs?.fastSumBy { it.song.duration } ?: 0\n    }\n\n    var isSearching by remember { mutableStateOf(false) }\n    var query by remember { mutableStateOf(TextFieldValue()) }\n    val focusRequester = remember { FocusRequester() }\n    \n    LaunchedEffect(isSearching) {\n        if (isSearching) {\n            focusRequester.requestFocus()\n        }\n    }\n    \n    var inSelectMode by rememberSaveable { mutableStateOf(false) }\n    val selection = rememberSaveable(\n        saver = listSaver<MutableList<String>, String>(\n            save = { it.toList() },\n            restore = { it.toMutableStateList() }\n        )\n    ) { mutableStateListOf() }\n    var selectionAnchorSongId by rememberSaveable { mutableStateOf<String?>(null) }\n    val onExitSelectionMode = {\n        inSelectMode = false\n        selection.clear()\n        selectionAnchorSongId = null\n    }\n\n    val filteredSongs = remember(songs, query) {\n        if (query.text.isEmpty()) songs ?: emptyList()\n        else songs?.filter { song ->\n            song.title.contains(query.text, true) ||\n                song.artists.any { it.name.contains(query.text, true) }\n        } ?: emptyList()\n    }\n\n    LaunchedEffect(filteredSongs) {\n        selection.fastForEachReversed { songId ->\n            if (filteredSongs.find { it.id == songId } == null) {\n                selection.remove(songId)\n            }\n        }\n\n        if (selectionAnchorSongId != null && filteredSongs.none { it.id == selectionAnchorSongId }) {\n            selectionAnchorSongId = filteredSongs.firstOrNull { it.id in selection }?.id\n        }\n    }\n\n    if (isSearching) {\n        BackHandler {\n            isSearching = false\n            query = TextFieldValue()\n        }\n    } else if (inSelectMode) {\n        BackHandler(onBack = onExitSelectionMode)\n    }\n\n    val sortType by viewModel.topPeriod.collectAsState()\n    val name = stringResource(R.string.my_top) + \" $maxSize\"\n\n    val downloadUtil = LocalDownloadUtil.current\n    var downloadState by remember { mutableIntStateOf(Download.STATE_STOPPED) }\n\n    LaunchedEffect(songs) {\n        mutableSongs.apply {\n            clear()\n            songs?.let { addAll(it) }\n        }\n        if (songs?.isEmpty() == true) return@LaunchedEffect\n        downloadUtil.downloads.collect { downloads ->\n            downloadState =\n                if (songs?.all { downloads[it.song.id]?.state == Download.STATE_COMPLETED } == true) {\n                    Download.STATE_COMPLETED\n                } else if (songs?.all {\n                        downloads[it.song.id]?.state == Download.STATE_QUEUED ||\n                                downloads[it.song.id]?.state == Download.STATE_DOWNLOADING ||\n                                downloads[it.song.id]?.state == Download.STATE_COMPLETED\n                    } == true\n                ) {\n                    Download.STATE_DOWNLOADING\n                } else {\n                    Download.STATE_STOPPED\n                }\n        }\n    }\n\n    var showRemoveDownloadDialog by remember { mutableStateOf(false) }\n\n    if (showRemoveDownloadDialog) {\n        DefaultDialog(\n            onDismiss = { showRemoveDownloadDialog = false },\n            content = {\n                Text(\n                    text = stringResource(R.string.remove_download_playlist_confirm, name),\n                    style = MaterialTheme.typography.bodyLarge,\n                    modifier = Modifier.padding(horizontal = 18.dp),\n                )\n            },\n            buttons = {\n                TextButton(\n                    onClick = { showRemoveDownloadDialog = false },\n                ) {\n                    Text(text = stringResource(android.R.string.cancel))\n                }\n\n                TextButton(\n                    onClick = {\n                        showRemoveDownloadDialog = false\n                        songs!!.forEach { song ->\n                            DownloadService.sendRemoveDownload(\n                                context,\n                                ExoDownloadService::class.java,\n                                song.song.id,\n                                false,\n                            )\n                        }\n                    },\n                ) {\n                    Text(text = stringResource(android.R.string.ok))\n                }\n            },\n        )\n    }\n\n    val state = rememberLazyListState()\n\n    Box(\n        modifier = Modifier.fillMaxSize(),\n    ) {\n        LazyColumn(\n            state = state,\n            contentPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues(),\n        ) {\n            if (songs != null) {\n                if (songs!!.isEmpty()) {\n                    item(key = \"empty_placeholder\") {\n                        EmptyPlaceholder(\n                            icon = R.drawable.music_note,\n                            text = stringResource(R.string.playlist_is_empty),\n                        )\n                    }\n                } else {\n                    if (!isSearching) {\n                        item(key = \"playlist_header\") {\n                            TopPlaylistHeader(\n                                name = name,\n                                songs = songs!!,\n                                likeLength = likeLength,\n                                downloadState = downloadState,\n                                onShowRemoveDownloadDialog = { showRemoveDownloadDialog = true },\n                                menuState = menuState,\n                                modifier = Modifier.animateItem()\n                            )\n                        }\n                    }\n\n                    item(key = \"songs_header\") {\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            modifier = Modifier.padding(start = 16.dp),\n                        ) {\n                            SortHeader(\n                                sortType = sortType,\n                                sortDescending = false,\n                                onSortTypeChange = {\n                                    viewModel.topPeriod.value = it\n                                },\n                                onSortDescendingChange = {},\n                                sortTypeText = { sortType ->\n                                    when (sortType) {\n                                        MyTopFilter.ALL_TIME -> R.string.all_time\n                                        MyTopFilter.DAY -> R.string.past_24_hours\n                                        MyTopFilter.WEEK -> R.string.past_week\n                                        MyTopFilter.MONTH -> R.string.past_month\n                                        MyTopFilter.YEAR -> R.string.past_year\n                                    }\n                                },\n                                modifier = Modifier.weight(1f),\n                                showDescending = false,\n                            )\n                        }\n                    }\n                }\n\n                if (filteredSongs.isNotEmpty()) {\n                    itemsIndexed(\n                        items = filteredSongs,\n                        key = { _, song -> song.id },\n                    ) { index, song ->\n                        val onCheckedChange: (Boolean) -> Unit = {\n                            if (it) {\n                                selection.add(song.id)\n                            } else {\n                                selection.remove(song.id)\n                            }\n                        }\n\n                        SongListItem(\n                            song = song,\n                            albumIndex = index + 1,\n                            isActive = song.song.id == mediaMetadata?.id,\n                            isPlaying = isPlaying,\n                            showInLibraryIcon = true,\n                            trailingContent = {\n                                if (inSelectMode) {\n                                    Checkbox(\n                                        checked = song.id in selection,\n                                        onCheckedChange = onCheckedChange\n                                    )\n                                } else {\n                                    IconButton(\n                                        onClick = {\n                                            menuState.show {\n                                                SongMenu(\n                                                    originalSong = song,\n                                                    navController = navController,\n                                                    onDismiss = menuState::dismiss,\n                                                )\n                                            }\n                                        },\n                                    ) {\n                                        Icon(\n                                            painter = painterResource(R.drawable.more_vert),\n                                            contentDescription = null,\n                                        )\n                                    }\n                                }\n                            },\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .combinedClickable(\n                                    onClick = {\n                                        if (inSelectMode) {\n                                            onCheckedChange(song.id !in selection)\n                                        } else if (song.song.id == mediaMetadata?.id) {\n                                            playerConnection.togglePlayPause()\n                                        } else {\n                                            playerConnection.playQueue(\n                                                ListQueue(\n                                                    title = name,\n                                                    items = songs!!.map { it.toMediaItem() },\n                                                    startIndex = songs!!.indexOfFirst { it.id == song.id }\n                                                ),\n                                            )\n                                        }\n                                    },\n                                    onLongClick = {\n                                        if (!inSelectMode) {\n                                            haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                            inSelectMode = true\n                                            onCheckedChange(true)\n                                            selectionAnchorSongId = song.id\n                                        } else {\n                                            val anchorIndex = selectionAnchorSongId?.let { anchorSongId ->\n                                                filteredSongs.indexOfFirst { it.id == anchorSongId }\n                                            } ?: -1\n\n                                            if (anchorIndex == -1) {\n                                                onCheckedChange(true)\n                                                selectionAnchorSongId = song.id\n                                            } else {\n                                                val range = if (anchorIndex <= index) anchorIndex..index else index..anchorIndex\n                                                for (rangeIndex in range) {\n                                                    val rangeSongId = filteredSongs[rangeIndex].id\n                                                    if (rangeSongId !in selection) {\n                                                        selection.add(rangeSongId)\n                                                    }\n                                                }\n                                            }\n                                        }\n                                    },\n                                )\n                                .animateItem()\n                        )\n                    }\n                }\n            }\n        }\n\n        DraggableScrollbar(\n            modifier = Modifier\n                .padding(\n                    LocalPlayerAwareWindowInsets.current.union(WindowInsets.ime)\n                        .asPaddingValues()\n                )\n                .align(Alignment.CenterEnd),\n            scrollState = state,\n            headerItems = 2\n        )\n\n        TopAppBar(\n            title = {\n                when {\n                    inSelectMode -> {\n                        Text(\n                            text = pluralStringResource(R.plurals.n_song, selection.size, selection.size),\n                            style = MaterialTheme.typography.titleLarge\n                        )\n                    }\n                    isSearching -> {\n                        TextField(\n                            value = query,\n                            onValueChange = { query = it },\n                            placeholder = {\n                                Text(\n                                    text = stringResource(R.string.search),\n                                    style = MaterialTheme.typography.titleLarge\n                                )\n                            },\n                            singleLine = true,\n                            textStyle = MaterialTheme.typography.titleLarge,\n                            keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),\n                            colors = TextFieldDefaults.colors(\n                                focusedContainerColor = Color.Transparent,\n                                unfocusedContainerColor = Color.Transparent,\n                                focusedIndicatorColor = Color.Transparent,\n                                unfocusedIndicatorColor = Color.Transparent,\n                                disabledIndicatorColor = Color.Transparent,\n                            ),\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .focusRequester(focusRequester)\n                        )\n                    }\n                    else -> {\n                        Text(text = name)\n                    }\n                }\n            },\n            navigationIcon = {\n                IconButton(\n                    onClick = {\n                        when {\n                            isSearching -> {\n                                isSearching = false\n                                query = TextFieldValue()\n                                focusManager.clearFocus()\n                            }\n                            inSelectMode -> {\n                                onExitSelectionMode()\n                            }\n                            else -> {\n                                navController.navigateUp()\n                            }\n                        }\n                    },\n                    onLongClick = {\n                        if (!isSearching && !inSelectMode) {\n                            navController.backToMain()\n                        }\n                    }\n                ) {\n                    Icon(\n                        painter = painterResource(\n                            if (inSelectMode) R.drawable.close else R.drawable.arrow_back\n                        ),\n                        contentDescription = null\n                    )\n                }\n            },\n            actions = {\n                if (inSelectMode) {\n                    Checkbox(\n                        checked = selection.size == filteredSongs.size && selection.isNotEmpty(),\n                        onCheckedChange = {\n                            if (selection.size == filteredSongs.size) {\n                                selection.clear()\n                            } else {\n                                selection.clear()\n                                selection.addAll(filteredSongs.map { it.id })\n                            }\n                        }\n                    )\n                    IconButton(\n                        enabled = selection.isNotEmpty(),\n                        onClick = {\n                            menuState.show {\n                                SelectionSongMenu(\n                                    songSelection = filteredSongs.filter { it.id in selection },\n                                    onDismiss = menuState::dismiss,\n                                    clearAction = onExitSelectionMode,\n                                )\n                            }\n                        },\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.more_vert),\n                            contentDescription = null\n                        )\n                    }\n                } else if (!isSearching) {\n                    IconButton(\n                        onClick = { isSearching = true }\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.search),\n                            contentDescription = null\n                        )\n                    }\n                }\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun TopPlaylistHeader(\n    name: String,\n    songs: List<Song>,\n    likeLength: Int,\n    downloadState: Int,\n    onShowRemoveDownloadDialog: () -> Unit,\n    menuState: com.metrolist.music.ui.component.MenuState,\n    modifier: Modifier = Modifier\n) {\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val context = LocalContext.current\n    \n    Column(\n        modifier = modifier\n            .fillMaxWidth()\n            .padding(top = 8.dp, bottom = 20.dp),\n        horizontalAlignment = Alignment.CenterHorizontally\n    ) {\n        // Playlist Thumbnail - Large centered with shadow\n        Box(\n            modifier = Modifier.padding(top = 8.dp, bottom = 20.dp)\n        ) {\n            androidx.compose.material3.Surface(\n                modifier = Modifier\n                    .size(240.dp)\n                    .shadow(\n                        elevation = 24.dp,\n                        shape = RoundedCornerShape(3.dp),\n                        spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f)\n                    ),\n                shape = RoundedCornerShape(3.dp)\n            ) {\n                AsyncImage(\n                    model = songs[0].thumbnailUrl,\n                    contentDescription = null,\n                    contentScale = androidx.compose.ui.layout.ContentScale.Crop,\n                    modifier = Modifier.fillMaxSize()\n                )\n            }\n        }\n\n        // Playlist Name\n        Text(\n            text = name,\n            style = MaterialTheme.typography.headlineSmall,\n            fontWeight = FontWeight.Bold,\n            textAlign = androidx.compose.ui.text.style.TextAlign.Center,\n            maxLines = 2,\n            overflow = TextOverflow.Ellipsis,\n            modifier = Modifier.padding(horizontal = 32.dp)\n        )\n\n        Spacer(modifier = Modifier.height(12.dp))\n\n        // Metadata - Song Count • Duration\n        Text(\n            text = buildString {\n                append(pluralStringResource(R.plurals.n_song, songs.size, songs.size))\n                if (likeLength > 0) {\n                    append(\" • \")\n                    append(makeTimeString(likeLength * 1000L))\n                }\n            },\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.7f)\n        )\n\n        Spacer(modifier = Modifier.height(24.dp))\n\n        // Action Buttons Row\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 24.dp),\n            horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            // Shuffle Button - Smaller secondary button\n            androidx.compose.material3.Surface(\n                onClick = {\n                    playerConnection.playQueue(\n                        ListQueue(\n                            title = name,\n                            items = songs.shuffled().map { it.toMediaItem() },\n                        ),\n                    )\n                },\n                shape = androidx.compose.foundation.shape.CircleShape,\n                color = MaterialTheme.colorScheme.surfaceVariant,\n                modifier = Modifier.size(48.dp)\n            ) {\n                Box(\n                    modifier = Modifier.fillMaxSize(),\n                    contentAlignment = Alignment.Center\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.shuffle),\n                        contentDescription = stringResource(R.string.shuffle),\n                        modifier = Modifier.size(24.dp)\n                    )\n                }\n            }\n\n            // Play Button - Larger primary circular button\n            Surface(\n                onClick = {\n                    playerConnection.playQueue(\n                        ListQueue(\n                            title = name,\n                            items = songs.map { it.toMediaItem() },\n                        ),\n                    )\n                },\n                color = MaterialTheme.colorScheme.primary,\n                shape = androidx.compose.foundation.shape.CircleShape,\n                modifier = Modifier.size(72.dp)\n            ) {\n                Box(\n                    contentAlignment = Alignment.Center,\n                    modifier = Modifier.fillMaxSize()\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.play),\n                        contentDescription = stringResource(R.string.play),\n                        tint = MaterialTheme.colorScheme.onPrimary,\n                        modifier = Modifier.size(32.dp)\n                    )\n                }\n            }\n\n            // Menu Button - Smaller secondary button\n            androidx.compose.material3.Surface(\n                onClick = {\n                    menuState.show {\n                        TopPlaylistMenu(\n                            downloadState = downloadState,\n                            onQueue = {\n                                playerConnection.addToQueue(\n                                    songs.map { it.toMediaItem() }\n                                )\n                            },\n                            onDownload = {\n                                when (downloadState) {\n                                    Download.STATE_COMPLETED -> onShowRemoveDownloadDialog()\n                                    Download.STATE_DOWNLOADING -> {\n                                        songs.forEach { song ->\n                                            DownloadService.sendRemoveDownload(\n                                                context,\n                                                ExoDownloadService::class.java,\n                                                song.id,\n                                                false,\n                                            )\n                                        }\n                                    }\n                                    else -> {\n                                        songs.forEach { song ->\n                                            val downloadRequest = DownloadRequest\n                                                .Builder(song.id, song.id.toUri())\n                                                .setCustomCacheKey(song.id)\n                                                .setData(song.title.toByteArray())\n                                                .build()\n                                            DownloadService.sendAddDownload(\n                                                context,\n                                                ExoDownloadService::class.java,\n                                                downloadRequest,\n                                                false,\n                                            )\n                                        }\n                                    }\n                                }\n                            },\n                            onDismiss = { menuState.dismiss() }\n                        )\n                    }\n                },\n                shape = androidx.compose.foundation.shape.CircleShape,\n                color = MaterialTheme.colorScheme.surfaceVariant,\n                modifier = Modifier.size(48.dp)\n            ) {\n                Box(\n                    modifier = Modifier.fillMaxSize(),\n                    contentAlignment = Alignment.Center\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.more_vert),\n                        contentDescription = null,\n                        modifier = Modifier.size(24.dp)\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/podcast/OnlinePodcastScreen.kt",
    "content": "package com.metrolist.music.ui.screens.podcast\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.ime\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.union\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.ContainedLoadingIndicator\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.OutlinedButton\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextField\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarScrollBehavior\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.util.fastAny\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport coil3.compose.AsyncImage\nimport coil3.request.ImageRequest\nimport com.metrolist.innertube.models.EpisodeItem\nimport com.metrolist.innertube.models.PodcastItem\nimport timber.log.Timber\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.db.entities.PodcastEntity\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.playback.queues.ListQueue\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.YouTubeListItem\nimport com.metrolist.music.ui.menu.YouTubeSongMenu\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.viewmodels.OnlinePodcastViewModel\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun OnlinePodcastScreen(\n    navController: NavController,\n    scrollBehavior: TopAppBarScrollBehavior,\n    viewModel: OnlinePodcastViewModel = hiltViewModel(),\n) {\n    val menuState = LocalMenuState.current\n    val haptic = LocalHapticFeedback.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val database = LocalDatabase.current\n\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n\n    val podcast by viewModel.podcast.collectAsState()\n    val episodes by viewModel.episodes.collectAsState()\n    val isLoading by viewModel.isLoading.collectAsState()\n    val error by viewModel.error.collectAsState()\n    val libraryPodcast by viewModel.libraryPodcast.collectAsState()\n\n    val lazyListState = rememberLazyListState()\n\n    var isSearching by rememberSaveable { mutableStateOf(false) }\n    var query by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }\n\n    val filteredEpisodes = remember(episodes, query) {\n        if (query.text.isEmpty()) episodes\n        else episodes.filter { episode ->\n            episode.title.contains(query.text, ignoreCase = true) ||\n                episode.author?.name?.contains(query.text, ignoreCase = true) == true\n        }\n    }\n\n    val focusRequester = remember { FocusRequester() }\n    LaunchedEffect(isSearching) { if (isSearching) focusRequester.requestFocus() }\n\n    if (isSearching) {\n        BackHandler {\n            isSearching = false\n            query = TextFieldValue()\n        }\n    }\n\n    Box(Modifier.fillMaxSize()) {\n        LazyColumn(\n            state = lazyListState,\n            contentPadding = LocalPlayerAwareWindowInsets.current.union(WindowInsets.ime).asPaddingValues(),\n        ) {\n            if (podcast == null && isLoading) {\n                item(key = \"loading_placeholder\") {\n                    Box(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(32.dp),\n                        contentAlignment = Alignment.Center\n                    ) {\n                        ContainedLoadingIndicator()\n                    }\n                }\n            } else if (error != null) {\n                item(key = \"error\") {\n                    Column(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(32.dp),\n                        horizontalAlignment = Alignment.CenterHorizontally,\n                        verticalArrangement = Arrangement.spacedBy(16.dp)\n                    ) {\n                        Text(\n                            text = error ?: stringResource(R.string.error_unknown),\n                            style = MaterialTheme.typography.bodyLarge,\n                            textAlign = TextAlign.Center\n                        )\n                        Button(onClick = { viewModel.retry() }) {\n                            Text(stringResource(R.string.retry))\n                        }\n                    }\n                }\n            } else {\n                podcast?.let { podcastItem ->\n                    if (!isSearching) {\n                        item(key = \"podcast_header\") {\n                            val context = LocalContext.current\n                            PodcastHeader(\n                                podcast = podcastItem,\n                                episodeCount = episodes.size,\n                                inLibrary = libraryPodcast?.inLibrary == true,\n                                onLibraryClick = { viewModel.toggleLibrary() },\n                                onViewChannelClick = {\n                                    val channelId = podcastItem.channelId ?: podcastItem.author?.id\n                                    if (channelId != null) {\n                                        navController.navigate(\"artist/$channelId?isPodcastChannel=true\")\n                                    }\n                                }\n                            )\n                        }\n                    }\n\n                    itemsIndexed(\n                        items = filteredEpisodes,\n                        key = { _, episode -> episode.id }\n                    ) { index, episode ->\n                        YouTubeListItem(\n                            item = episode,\n                            isActive = mediaMetadata?.id == episode.id,\n                            isPlaying = isPlaying,\n                            modifier = Modifier\n                                .combinedClickable(\n                                    onClick = {\n                                        if (episode.id == mediaMetadata?.id) {\n                                            playerConnection.togglePlayPause()\n                                        } else {\n                                            Timber.d(\"Playing episode: ${episode.title}, index: $index, total episodes: ${filteredEpisodes.size}\")\n                                            val mediaItems = filteredEpisodes.map { it.toMediaMetadata().toMediaItem() }\n                                            Timber.d(\"Created ${mediaItems.size} media items for queue\")\n                                            playerConnection.playQueue(\n                                                ListQueue(\n                                                    title = podcast?.title,\n                                                    items = mediaItems,\n                                                    startIndex = index\n                                                )\n                                            )\n                                        }\n                                    },\n                                    onLongClick = {\n                                        haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                        menuState.show {\n                                            YouTubeSongMenu(episode.asSongItem(), navController, menuState::dismiss)\n                                        }\n                                    }\n                                )\n                                .animateItem(),\n                            trailingContent = {\n                                IconButton(onClick = {\n                                    menuState.show {\n                                        YouTubeSongMenu(episode.asSongItem(), navController, menuState::dismiss)\n                                    }\n                                }) {\n                                    Icon(painterResource(R.drawable.more_vert), null)\n                                }\n                            }\n                        )\n                    }\n                }\n            }\n        }\n\n        TopAppBar(\n            title = {\n                if (isSearching) {\n                    TextField(\n                        value = query,\n                        onValueChange = { query = it },\n                        placeholder = {\n                            Text(\n                                text = stringResource(R.string.search),\n                                style = MaterialTheme.typography.titleLarge\n                            )\n                        },\n                        singleLine = true,\n                        textStyle = MaterialTheme.typography.titleLarge,\n                        keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),\n                        colors = TextFieldDefaults.colors(\n                            focusedContainerColor = Color.Transparent,\n                            unfocusedContainerColor = Color.Transparent,\n                            focusedIndicatorColor = Color.Transparent,\n                            unfocusedIndicatorColor = Color.Transparent,\n                            disabledIndicatorColor = Color.Transparent,\n                        ),\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .focusRequester(focusRequester)\n                    )\n                } else if (lazyListState.firstVisibleItemIndex > 0) {\n                    Text(podcast?.title ?: \"\")\n                }\n            },\n            navigationIcon = {\n                IconButton(\n                    onClick = {\n                        if (isSearching) {\n                            isSearching = false\n                            query = TextFieldValue()\n                        } else {\n                            navController.navigateUp()\n                        }\n                    },\n                    onLongClick = {\n                        if (!isSearching) navController.backToMain()\n                    }\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.arrow_back),\n                        contentDescription = null\n                    )\n                }\n            },\n            actions = {\n                if (!isSearching) {\n                    IconButton(onClick = { isSearching = true }) {\n                        Icon(\n                            painter = painterResource(R.drawable.search),\n                            contentDescription = stringResource(R.string.search)\n                        )\n                    }\n                }\n            },\n            scrollBehavior = scrollBehavior\n        )\n    }\n}\n\n@Composable\nprivate fun PodcastHeader(\n    podcast: PodcastItem,\n    episodeCount: Int,\n    inLibrary: Boolean,\n    onLibraryClick: () -> Unit,\n    onViewChannelClick: () -> Unit\n) {\n    val context = LocalContext.current\n\n    Column(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(horizontal = 16.dp, vertical = 8.dp),\n        horizontalAlignment = Alignment.CenterHorizontally\n    ) {\n        AsyncImage(\n            model = ImageRequest.Builder(context)\n                .data(podcast.thumbnail)\n                .build(),\n            contentDescription = null,\n            contentScale = ContentScale.Crop,\n            modifier = Modifier\n                .size(200.dp)\n                .clip(RoundedCornerShape(8.dp))\n        )\n\n        Spacer(modifier = Modifier.height(16.dp))\n\n        Text(\n            text = podcast.title,\n            style = MaterialTheme.typography.headlineSmall,\n            fontWeight = FontWeight.Bold,\n            textAlign = TextAlign.Center,\n            maxLines = 2,\n            overflow = TextOverflow.Ellipsis\n        )\n\n        podcast.author?.name?.let { authorName ->\n            Spacer(modifier = Modifier.height(4.dp))\n            Text(\n                text = authorName,\n                style = MaterialTheme.typography.bodyLarge,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                textAlign = TextAlign.Center\n            )\n        }\n\n        Spacer(modifier = Modifier.height(4.dp))\n        Text(\n            text = podcast.episodeCountText ?: \"$episodeCount episodes\",\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.onSurfaceVariant\n        )\n\n        Spacer(modifier = Modifier.height(16.dp))\n\n        Column(\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            OutlinedButton(\n                onClick = onLibraryClick,\n                colors = ButtonDefaults.outlinedButtonColors(\n                    containerColor = if (inLibrary)\n                        MaterialTheme.colorScheme.secondaryContainer\n                    else\n                        Color.Transparent\n                ),\n                shape = RoundedCornerShape(50),\n                modifier = Modifier.height(40.dp)\n            ) {\n                Icon(\n                    painter = painterResource(if (inLibrary) R.drawable.library_add_check else R.drawable.library_add),\n                    contentDescription = null,\n                    modifier = Modifier.size(20.dp)\n                )\n                Spacer(modifier = Modifier.size(8.dp))\n                Text(\n                    text = stringResource(if (inLibrary) R.string.remove_from_library else R.string.add_to_library)\n                )\n            }\n\n            OutlinedButton(\n                onClick = onViewChannelClick,\n                shape = RoundedCornerShape(50),\n                modifier = Modifier.height(40.dp)\n            ) {\n                Icon(\n                    painter = painterResource(R.drawable.person),\n                    contentDescription = null,\n                    modifier = Modifier.size(20.dp)\n                )\n                Spacer(modifier = Modifier.size(8.dp))\n                Text(\n                    text = stringResource(R.string.view_channel)\n                )\n            }\n        }\n\n        Spacer(modifier = Modifier.height(16.dp))\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/recognition/RecognitionHistoryScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.recognition\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation.NavController\nimport coil3.compose.AsyncImage\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.ThumbnailCornerRadius\nimport com.metrolist.music.db.entities.RecognitionHistory\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.utils.backToMain\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport java.time.format.DateTimeFormatter\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun RecognitionHistoryScreen(\n    navController: NavController\n) {\n    val database = LocalDatabase.current\n    val menuState = LocalMenuState.current\n    val coroutineScope = rememberCoroutineScope()\n    \n    val historyItems by database.recognitionHistory().collectAsState(initial = emptyList())\n    var showClearDialog by remember { mutableStateOf(false) }\n    var itemToDelete by remember { mutableStateOf<RecognitionHistory?>(null) }\n    \n    if (showClearDialog) {\n        DefaultDialog(\n            onDismiss = { showClearDialog = false },\n            icon = {\n                Icon(\n                    painter = painterResource(R.drawable.delete),\n                    contentDescription = null\n                )\n            },\n            title = { Text(stringResource(R.string.clear_recognition_history)) },\n            buttons = {\n                TextButton(onClick = { showClearDialog = false }) {\n                    Text(stringResource(R.string.cancel))\n                }\n                TextButton(\n                    onClick = {\n                        coroutineScope.launch(Dispatchers.IO) {\n                            database.query {\n                                clearRecognitionHistory()\n                            }\n                        }\n                        showClearDialog = false\n                    }\n                ) {\n                    Text(stringResource(R.string.clear))\n                }\n            }\n        ) {\n            Text(\n                text = stringResource(R.string.clear_recognition_history_confirm),\n                style = MaterialTheme.typography.bodyMedium\n            )\n        }\n    }\n\n    itemToDelete?.let { item ->\n        DefaultDialog(\n            onDismiss = { itemToDelete = null },\n            icon = {\n                Icon(\n                    painter = painterResource(R.drawable.delete),\n                    contentDescription = null\n                )\n            },\n            title = { Text(stringResource(R.string.delete)) },\n            buttons = {\n                TextButton(onClick = { itemToDelete = null }) {\n                    Text(stringResource(R.string.cancel))\n                }\n                TextButton(\n                    onClick = {\n                        coroutineScope.launch(Dispatchers.IO) {\n                            database.query {\n                                deleteRecognitionHistoryById(item.id)\n                            }\n                        }\n                        itemToDelete = null\n                    }\n                ) {\n                    Text(stringResource(R.string.delete))\n                }\n            }\n        ) {\n            Text(\n                text = stringResource(R.string.delete_playlist_confirm, item.title),\n                style = MaterialTheme.typography.bodyMedium\n            )\n        }\n    }\n    \n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(stringResource(R.string.recognition_history)) },\n                navigationIcon = {\n                    IconButton(\n                        onClick = { navController.navigateUp() },\n                        onLongClick = { navController.backToMain() }\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.arrow_back),\n                            contentDescription = null\n                        )\n                    }\n                },\n                actions = {\n                    if (historyItems.isNotEmpty()) {\n                        IconButton(onClick = { showClearDialog = true }) {\n                            Icon(\n                                painter = painterResource(R.drawable.clear_all),\n                                contentDescription = stringResource(R.string.clear_recognition_history)\n                            )\n                        }\n                    }\n                }\n            )\n        }\n    ) { paddingValues ->\n        if (historyItems.isEmpty()) {\n            Box(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .padding(paddingValues),\n                contentAlignment = Alignment.Center\n            ) {\n                Column(\n                    horizontalAlignment = Alignment.CenterHorizontally\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.history),\n                        contentDescription = null,\n                        modifier = Modifier.size(64.dp),\n                        tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)\n                    )\n                    Spacer(modifier = Modifier.height(16.dp))\n                    Text(\n                        text = \"No recognition history\",\n                        style = MaterialTheme.typography.bodyLarge,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant\n                    )\n                }\n            }\n        } else {\n            LazyColumn(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .padding(paddingValues),\n                contentPadding = LocalPlayerAwareWindowInsets.current\n                    .only(WindowInsetsSides.Bottom)\n                    .asPaddingValues()\n            ) {\n                items(\n                    items = historyItems,\n                    key = { it.id }\n                ) { item ->\n                    RecognitionHistoryItem(\n                        item = item,\n                        onClick = {\n                            // Search for the track on YouTube Music\n                            val searchQuery = \"${item.title} ${item.artist}\"\n                            navController.navigate(\"search/${java.net.URLEncoder.encode(searchQuery, \"UTF-8\")}\")\n                        },\n                        onDelete = {\n                            itemToDelete = item\n                        }\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun RecognitionHistoryItem(\n    item: RecognitionHistory,\n    onClick: () -> Unit,\n    onDelete: () -> Unit\n) {\n    val dateFormatter = remember { DateTimeFormatter.ofPattern(\"MMM dd, yyyy HH:mm\") }\n    \n    Card(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(horizontal = 16.dp, vertical = 8.dp)\n            .clickable { onClick() },\n        shape = RoundedCornerShape(ThumbnailCornerRadius),\n        colors = CardDefaults.cardColors(\n            containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)\n        )\n    ) {\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(12.dp),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            // Album art\n            AsyncImage(\n                model = item.coverArtUrl,\n                contentDescription = null,\n                modifier = Modifier\n                    .size(60.dp)\n                    .clip(RoundedCornerShape(ThumbnailCornerRadius)),\n                contentScale = ContentScale.Crop\n            )\n            \n            Spacer(modifier = Modifier.width(12.dp))\n            \n            // Track info\n            Column(\n                modifier = Modifier.weight(1f)\n            ) {\n                Text(\n                    text = item.title,\n                    style = MaterialTheme.typography.titleMedium,\n                    fontWeight = FontWeight.SemiBold,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis\n                )\n                Text(\n                    text = item.artist,\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    maxLines = 1,\n                    overflow = TextOverflow.Ellipsis\n                )\n                Text(\n                    text = item.recognizedAt.format(dateFormatter),\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)\n                )\n            }\n            \n            // Delete action\n            IconButton(onClick = onDelete) {\n                Icon(\n                    painter = painterResource(R.drawable.delete),\n                    contentDescription = stringResource(R.string.delete_from_history),\n                    tint = MaterialTheme.colorScheme.onSurfaceVariant\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/recognition/RecognitionScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.recognition\n\nimport android.Manifest\nimport android.content.pm.PackageManager\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.animation.AnimatedContent\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.LinearEasing\nimport androidx.compose.animation.core.RepeatMode\nimport androidx.compose.animation.core.animateFloat\nimport androidx.compose.animation.core.infiniteRepeatable\nimport androidx.compose.animation.core.rememberInfiniteTransition\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.scaleIn\nimport androidx.compose.animation.scaleOut\nimport androidx.compose.animation.togetherWith\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedButton\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.scale\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.core.content.ContextCompat\nimport androidx.navigation.NavController\nimport coil3.compose.AsyncImage\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.R\nimport com.metrolist.music.db.entities.RecognitionHistory\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.shazamkit.models.RecognitionResult\nimport com.metrolist.shazamkit.models.RecognitionStatus\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport java.time.LocalDateTime\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun RecognitionScreen(\n    navController: NavController,\n    autoStart: Boolean = false,\n) {\n    val context = LocalContext.current\n    val database = LocalDatabase.current\n    val coroutineScope = rememberCoroutineScope()\n\n    // Only reset in Ready state: Listening/Processing belong to a running widget-service\n    // recognition that must not be cancelled; Success/NoMatch/Error are results pending\n    // display and history saving.\n    LaunchedEffect(Unit) {\n        if (com.metrolist.music.recognition.MusicRecognitionService.recognitionStatus.value\n                is RecognitionStatus.Ready\n        ) {\n            com.metrolist.music.recognition.MusicRecognitionService\n                .reset()\n        }\n    }\n\n    DisposableEffect(Unit) {\n        onDispose {\n            com.metrolist.music.recognition.MusicRecognitionService\n                .reset()\n        }\n    }\n\n    // Observe recognition status from service for real-time updates (Listening -> Processing -> Result)\n    val recognitionStatus by com.metrolist.music.recognition.MusicRecognitionService.recognitionStatus\n        .collectAsState()\n\n    var hasPermission by remember {\n        mutableStateOf(\n            ContextCompat.checkSelfPermission(context, Manifest.permission.RECORD_AUDIO)\n                == PackageManager.PERMISSION_GRANTED,\n        )\n    }\n\n    val permissionLauncher =\n        rememberLauncherForActivityResult(\n            contract = ActivityResultContracts.RequestPermission(),\n        ) { isGranted ->\n            hasPermission = isGranted\n            if (isGranted) {\n                coroutineScope.launch {\n                    com.metrolist.music.recognition.MusicRecognitionService\n                        .recognize(context)\n                }\n            }\n        }\n\n    fun startRecognition() {\n        if (hasPermission) {\n            coroutineScope.launch {\n                com.metrolist.music.recognition.MusicRecognitionService\n                    .recognize(context)\n            }\n        } else {\n            permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        if (autoStart &&\n            com.metrolist.music.recognition.MusicRecognitionService.recognitionStatus.value\n                is RecognitionStatus.Ready\n        ) {\n            startRecognition()\n        }\n    }\n\n    fun resetToReady() {\n        com.metrolist.music.recognition.MusicRecognitionService\n            .reset()\n    }\n\n    fun saveToHistory(result: RecognitionResult) {\n        // Skip if the widget service already persisted this result to avoid a duplicate entry\n        if (com.metrolist.music.recognition.MusicRecognitionService.resultSavedExternally) return\n        coroutineScope.launch(Dispatchers.IO) {\n            database.query {\n                insert(\n                    RecognitionHistory(\n                        trackId = result.trackId,\n                        title = result.title,\n                        artist = result.artist,\n                        album = result.album,\n                        coverArtUrl = result.coverArtUrl,\n                        coverArtHqUrl = result.coverArtHqUrl,\n                        genre = result.genre,\n                        releaseDate = result.releaseDate,\n                        label = result.label,\n                        shazamUrl = result.shazamUrl,\n                        appleMusicUrl = result.appleMusicUrl,\n                        spotifyUrl = result.spotifyUrl,\n                        isrc = result.isrc,\n                        youtubeVideoId = result.youtubeVideoId,\n                        recognizedAt = LocalDateTime.now(),\n                    ),\n                )\n            }\n        }\n    }\n\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(stringResource(R.string.recognize_music)) },\n                navigationIcon = {\n                    IconButton(\n                        onClick = { navController.navigateUp() },\n                        onLongClick = { navController.backToMain() },\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.arrow_back),\n                            contentDescription = null,\n                        )\n                    }\n                },\n                actions = {\n                    IconButton(onClick = { navController.navigate(\"recognition_history\") }) {\n                        Icon(\n                            painter = painterResource(R.drawable.history),\n                            contentDescription = stringResource(R.string.recognition_history),\n                        )\n                    }\n                },\n            )\n        },\n    ) { paddingValues ->\n        Column(\n            modifier =\n                Modifier\n                    .fillMaxSize()\n                    .padding(paddingValues)\n                    .padding(16.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.Center,\n        ) {\n            AnimatedContent(\n                targetState = recognitionStatus,\n                transitionSpec = {\n                    (fadeIn() + scaleIn()).togetherWith(fadeOut() + scaleOut())\n                },\n                label = \"recognition_content\",\n            ) { status ->\n                when (status) {\n                    is RecognitionStatus.Ready -> {\n                        ReadyState(onStartRecognition = ::startRecognition)\n                    }\n\n                    is RecognitionStatus.Listening -> {\n                        ListeningState(\n                            onCancel = {\n                                com.metrolist.music.recognition.MusicRecognitionService\n                                    .reset()\n                            },\n                        )\n                    }\n\n                    is RecognitionStatus.Processing -> {\n                        ProcessingState()\n                    }\n\n                    is RecognitionStatus.Success -> {\n                        SuccessState(\n                            result = status.result,\n                            onPlayOnApp = { result ->\n                                // Search for the track on YouTube Music\n                                val searchQuery = \"${result.title} ${result.artist}\"\n                                navController.navigate(\"search/${java.net.URLEncoder.encode(searchQuery, \"UTF-8\")}\")\n                            },\n                            onTryAgain = {\n                                startRecognition()\n                            },\n                            onClose = ::resetToReady,\n                            onSaveToHistory = ::saveToHistory,\n                        )\n                    }\n\n                    is RecognitionStatus.NoMatch -> {\n                        NoMatchState(\n                            message = status.message,\n                            onTryAgain = {\n                                startRecognition()\n                            },\n                        )\n                    }\n\n                    is RecognitionStatus.Error -> {\n                        ErrorState(\n                            message = status.message,\n                            onTryAgain = {\n                                startRecognition()\n                            },\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ReadyState(onStartRecognition: () -> Unit) {\n    Column(\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.spacedBy(24.dp),\n    ) {\n        Box(\n            modifier =\n                Modifier\n                    .size(200.dp)\n                    .clip(CircleShape)\n                    .background(\n                        Brush.radialGradient(\n                            colors =\n                                listOf(\n                                    MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),\n                                    MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),\n                                    Color.Transparent,\n                                ),\n                        ),\n                    ).clickable { onStartRecognition() },\n            contentAlignment = Alignment.Center,\n        ) {\n            Box(\n                modifier =\n                    Modifier\n                        .size(160.dp)\n                        .clip(CircleShape)\n                        .background(MaterialTheme.colorScheme.primary),\n                contentAlignment = Alignment.Center,\n            ) {\n                Icon(\n                    painter = painterResource(R.drawable.mic),\n                    contentDescription = null,\n                    modifier = Modifier.size(64.dp),\n                    tint = MaterialTheme.colorScheme.onPrimary,\n                )\n            }\n        }\n\n        Text(\n            text = stringResource(R.string.tap_to_recognize),\n            style = MaterialTheme.typography.titleMedium,\n            color = MaterialTheme.colorScheme.onSurface,\n        )\n    }\n}\n\n@Composable\nprivate fun ListeningState(onCancel: () -> Unit) {\n    val infiniteTransition = rememberInfiniteTransition(label = \"pulse\")\n    val scale by infiniteTransition.animateFloat(\n        initialValue = 1f,\n        targetValue = 1.2f,\n        animationSpec =\n            infiniteRepeatable(\n                animation = tween(1000, easing = LinearEasing),\n                repeatMode = RepeatMode.Reverse,\n            ),\n        label = \"scale\",\n    )\n\n    Column(\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.spacedBy(24.dp),\n    ) {\n        // Container large enough for scaled animation (200dp * 1.2 = 240dp)\n        Box(\n            modifier = Modifier.size(260.dp),\n            contentAlignment = Alignment.Center,\n        ) {\n            // Outer pulsing ring\n            Box(\n                modifier =\n                    Modifier\n                        .size(200.dp)\n                        .scale(scale)\n                        .clip(CircleShape)\n                        .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.2f)),\n            )\n\n            // Inner pulsing ring\n            Box(\n                modifier =\n                    Modifier\n                        .size(180.dp)\n                        .scale(scale * 0.9f)\n                        .clip(CircleShape)\n                        .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.3f)),\n            )\n\n            // Main button\n            Box(\n                modifier =\n                    Modifier\n                        .size(160.dp)\n                        .clip(CircleShape)\n                        .background(MaterialTheme.colorScheme.primary)\n                        .clickable { onCancel() },\n                contentAlignment = Alignment.Center,\n            ) {\n                Icon(\n                    painter = painterResource(R.drawable.mic),\n                    contentDescription = null,\n                    modifier = Modifier.size(64.dp),\n                    tint = MaterialTheme.colorScheme.onPrimary,\n                )\n            }\n        }\n\n        Text(\n            text = stringResource(R.string.listening),\n            style = MaterialTheme.typography.titleMedium,\n            color = MaterialTheme.colorScheme.primary,\n        )\n\n        OutlinedButton(onClick = onCancel) {\n            Text(stringResource(R.string.cancel))\n        }\n    }\n}\n\n@Composable\nprivate fun ProcessingState() {\n    Column(\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.spacedBy(24.dp),\n    ) {\n        val infiniteTransition = rememberInfiniteTransition(label = \"rotate\")\n        val rotation by infiniteTransition.animateFloat(\n            initialValue = 0f,\n            targetValue = 360f,\n            animationSpec =\n                infiniteRepeatable(\n                    animation = tween(2000, easing = LinearEasing),\n                ),\n            label = \"rotation\",\n        )\n\n        Box(\n            modifier = Modifier.size(160.dp),\n            contentAlignment = Alignment.Center,\n        ) {\n            Box(\n                modifier =\n                    Modifier\n                        .size(160.dp)\n                        .clip(CircleShape)\n                        .border(\n                            width = 4.dp,\n                            brush =\n                                Brush.sweepGradient(\n                                    colors =\n                                        listOf(\n                                            MaterialTheme.colorScheme.primary,\n                                            MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),\n                                            Color.Transparent,\n                                            MaterialTheme.colorScheme.primary.copy(alpha = 0.5f),\n                                            MaterialTheme.colorScheme.primary,\n                                        ),\n                                ),\n                            shape = CircleShape,\n                        ),\n            )\n\n            Icon(\n                painter = painterResource(R.drawable.music_note),\n                contentDescription = null,\n                modifier = Modifier.size(48.dp),\n                tint = MaterialTheme.colorScheme.primary,\n            )\n        }\n\n        Text(\n            text = stringResource(R.string.processing),\n            style = MaterialTheme.typography.titleMedium,\n            color = MaterialTheme.colorScheme.onSurface,\n        )\n    }\n}\n\n@Composable\nprivate fun SuccessState(\n    result: RecognitionResult,\n    onPlayOnApp: (RecognitionResult) -> Unit,\n    onTryAgain: () -> Unit,\n    onClose: () -> Unit,\n    onSaveToHistory: (RecognitionResult) -> Unit,\n) {\n    // Save to history when success is shown\n    LaunchedEffect(result) {\n        onSaveToHistory(result)\n    }\n\n    Column(\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.spacedBy(16.dp),\n        modifier = Modifier.padding(horizontal = 16.dp),\n    ) {\n        // Album art\n        Card(\n            modifier =\n                Modifier\n                    .size(180.dp)\n                    .aspectRatio(1f),\n            shape = RoundedCornerShape(com.metrolist.music.constants.ThumbnailCornerRadius),\n            elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),\n        ) {\n            AsyncImage(\n                model = result.coverArtHqUrl ?: result.coverArtUrl,\n                contentDescription = null,\n                modifier = Modifier.fillMaxSize(),\n                contentScale = ContentScale.Crop,\n            )\n        }\n\n        Spacer(modifier = Modifier.height(8.dp))\n\n        // Track info\n        Text(\n            text = result.title,\n            style = MaterialTheme.typography.headlineSmall,\n            fontWeight = FontWeight.Bold,\n            textAlign = TextAlign.Center,\n            maxLines = 2,\n            overflow = TextOverflow.Ellipsis,\n        )\n\n        Text(\n            text = result.artist,\n            style = MaterialTheme.typography.titleMedium,\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n            textAlign = TextAlign.Center,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n        )\n\n        result.album?.let { album ->\n            Text(\n                text = album,\n                style = MaterialTheme.typography.bodyMedium,\n                color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),\n                textAlign = TextAlign.Center,\n                maxLines = 1,\n                overflow = TextOverflow.Ellipsis,\n            )\n        }\n\n        Spacer(modifier = Modifier.height(16.dp))\n\n        // Action buttons - stacked vertically\n        Column(\n            verticalArrangement = Arrangement.spacedBy(12.dp),\n            modifier = Modifier.fillMaxWidth(),\n        ) {\n            Button(\n                onClick = { onPlayOnApp(result) },\n                modifier = Modifier.fillMaxWidth(),\n            ) {\n                Icon(\n                    painter = painterResource(R.drawable.play),\n                    contentDescription = null,\n                    modifier = Modifier.size(18.dp),\n                )\n                Spacer(modifier = Modifier.width(8.dp))\n                Text(stringResource(R.string.play_on_app))\n            }\n\n            FilledTonalButton(\n                onClick = onTryAgain,\n                modifier = Modifier.fillMaxWidth(),\n            ) {\n                Icon(\n                    painter = painterResource(R.drawable.mic),\n                    contentDescription = null,\n                    modifier = Modifier.size(18.dp),\n                )\n                Spacer(modifier = Modifier.width(8.dp))\n                Text(stringResource(R.string.re_listen))\n            }\n\n            // Close button - Material 3 Expressive outlined style\n            OutlinedButton(\n                onClick = onClose,\n                modifier = Modifier.fillMaxWidth(),\n            ) {\n                Icon(\n                    painter = painterResource(R.drawable.close),\n                    contentDescription = null,\n                    modifier = Modifier.size(18.dp),\n                )\n                Spacer(modifier = Modifier.width(8.dp))\n                Text(stringResource(R.string.close))\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun NoMatchState(\n    message: String,\n    onTryAgain: () -> Unit,\n) {\n    Column(\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.spacedBy(24.dp),\n    ) {\n        Box(\n            modifier =\n                Modifier\n                    .size(120.dp)\n                    .clip(CircleShape)\n                    .background(MaterialTheme.colorScheme.errorContainer),\n            contentAlignment = Alignment.Center,\n        ) {\n            Icon(\n                painter = painterResource(R.drawable.close),\n                contentDescription = null,\n                modifier = Modifier.size(48.dp),\n                tint = MaterialTheme.colorScheme.onErrorContainer,\n            )\n        }\n\n        Text(\n            text = stringResource(R.string.no_match_found),\n            style = MaterialTheme.typography.titleLarge,\n            fontWeight = FontWeight.Bold,\n        )\n\n        Text(\n            text = message,\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n            textAlign = TextAlign.Center,\n            modifier = Modifier.padding(horizontal = 32.dp),\n        )\n\n        Button(onClick = onTryAgain) {\n            Icon(\n                painter = painterResource(R.drawable.refresh),\n                contentDescription = null,\n                modifier = Modifier.size(18.dp),\n            )\n            Spacer(modifier = Modifier.width(8.dp))\n            Text(stringResource(R.string.try_again))\n        }\n    }\n}\n\n@Composable\nprivate fun ErrorState(\n    message: String,\n    onTryAgain: () -> Unit,\n) {\n    Column(\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.spacedBy(24.dp),\n    ) {\n        Box(\n            modifier =\n                Modifier\n                    .size(120.dp)\n                    .clip(CircleShape)\n                    .background(MaterialTheme.colorScheme.errorContainer),\n            contentAlignment = Alignment.Center,\n        ) {\n            Icon(\n                painter = painterResource(R.drawable.error),\n                contentDescription = null,\n                modifier = Modifier.size(48.dp),\n                tint = MaterialTheme.colorScheme.onErrorContainer,\n            )\n        }\n\n        Text(\n            text = stringResource(R.string.recognition_error),\n            style = MaterialTheme.typography.titleLarge,\n            fontWeight = FontWeight.Bold,\n        )\n\n        Text(\n            text = message,\n            style = MaterialTheme.typography.bodyMedium,\n            color = MaterialTheme.colorScheme.onSurfaceVariant,\n            textAlign = TextAlign.Center,\n            modifier = Modifier.padding(horizontal = 32.dp),\n        )\n\n        Button(onClick = onTryAgain) {\n            Icon(\n                painter = painterResource(R.drawable.refresh),\n                contentDescription = null,\n                modifier = Modifier.size(18.dp),\n            )\n            Spacer(modifier = Modifier.width(8.dp))\n            Text(stringResource(R.string.try_again))\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/search/LocalSearchScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.search\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.snapshotFlow\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalSoftwareKeyboardController\nimport androidx.compose.ui.platform.LocalWindowInfo\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.CONTENT_TYPE_LIST\nimport com.metrolist.music.constants.ListItemHeight\nimport com.metrolist.music.db.entities.Album\nimport com.metrolist.music.db.entities.Artist\nimport com.metrolist.music.db.entities.Playlist\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.extensions.toMediaItem\nimport com.metrolist.music.playback.queues.ListQueue\nimport com.metrolist.music.ui.component.AlbumListItem\nimport com.metrolist.music.ui.component.ArtistListItem\nimport com.metrolist.music.ui.component.ChipsRow\nimport com.metrolist.music.ui.component.EmptyPlaceholder\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.PlaylistListItem\nimport com.metrolist.music.ui.component.SongListItem\nimport com.metrolist.music.ui.menu.SongMenu\nimport com.metrolist.music.viewmodels.LocalFilter\nimport com.metrolist.music.viewmodels.LocalSearchViewModel\nimport kotlinx.coroutines.flow.drop\n\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nfun LocalSearchScreen(\n    query: String,\n    navController: NavController,\n    onDismiss: () -> Unit,\n    isFromCache: Boolean = false,\n    pureBlack: Boolean,\n    viewModel: LocalSearchViewModel = hiltViewModel(),\n) {\n    val queueSearchedSongsStr = stringResource(R.string.queue_searched_songs)\n    val keyboardController = LocalSoftwareKeyboardController.current\n    val menuState = LocalMenuState.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n\n    val searchFilter by viewModel.filter.collectAsState()\n    val result by viewModel.result.collectAsState()\n\n    val lazyListState = rememberLazyListState()\n\n    LaunchedEffect(Unit) {\n        snapshotFlow { lazyListState.firstVisibleItemScrollOffset }\n            .drop(1)\n            .collect {\n                keyboardController?.hide()\n            }\n    }\n\n    LaunchedEffect(query) {\n        viewModel.query.value = query\n    }\n\n    val configuration = LocalWindowInfo.current\n    val isLandscape = configuration.containerSize.width > configuration.containerSize.height\n\n    Column(\n        modifier =\n            Modifier\n                .fillMaxSize()\n                .background(if (pureBlack) Color.Black else MaterialTheme.colorScheme.background)\n                .let { base ->\n                    if (isLandscape) {\n                        base.windowInsetsPadding(\n                            WindowInsets.systemBars.only(WindowInsetsSides.Horizontal),\n                        )\n                    } else {\n                        base\n                    }\n                },\n    ) {\n        ChipsRow(\n            chips =\n                listOf(\n                    LocalFilter.ALL to stringResource(R.string.filter_all),\n                    LocalFilter.SONG to stringResource(R.string.filter_songs),\n                    LocalFilter.ALBUM to stringResource(R.string.filter_albums),\n                    LocalFilter.ARTIST to stringResource(R.string.filter_artists),\n                    LocalFilter.PLAYLIST to stringResource(R.string.filter_playlists),\n                ),\n            currentValue = searchFilter,\n            onValueUpdate = { viewModel.filter.value = it },\n        )\n\n        LazyColumn(\n            state = lazyListState,\n            modifier = Modifier.weight(1f),\n            contentPadding =\n                WindowInsets.systemBars\n                    .only(WindowInsetsSides.Bottom)\n                    .asPaddingValues(),\n        ) {\n            result.map.forEach { (filter, items) ->\n                if (result.filter == LocalFilter.ALL) {\n                    item(key = filter) {\n                        Row(\n                            horizontalArrangement = Arrangement.spacedBy(16.dp),\n                            verticalAlignment = Alignment.CenterVertically,\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .height(ListItemHeight)\n                                    .clickable { viewModel.filter.value = filter }\n                                    .padding(start = 12.dp, end = 18.dp),\n                        ) {\n                            Text(\n                                text =\n                                    stringResource(\n                                        when (filter) {\n                                            LocalFilter.SONG -> R.string.filter_songs\n                                            LocalFilter.ALBUM -> R.string.filter_albums\n                                            LocalFilter.ARTIST -> R.string.filter_artists\n                                            LocalFilter.PLAYLIST -> R.string.filter_playlists\n                                            LocalFilter.ALL -> error(\"\")\n                                        },\n                                    ),\n                                style = MaterialTheme.typography.titleLarge,\n                                modifier = Modifier.weight(1f),\n                            )\n\n                            Icon(\n                                painter = painterResource(R.drawable.navigate_next),\n                                contentDescription = null,\n                            )\n                        }\n                    }\n                }\n\n                items(\n                    items = items.distinctBy { it.id },\n                    key = { it.id },\n                    contentType = { CONTENT_TYPE_LIST },\n                ) { item ->\n                    when (item) {\n                        is Song -> {\n                            SongListItem(\n                                song = item,\n                                showInLibraryIcon = true,\n                                isActive = item.id == mediaMetadata?.id,\n                                isPlaying = isPlaying,\n                                trailingContent = {\n                                    IconButton(\n                                        onClick = {\n                                            menuState.show {\n                                                SongMenu(\n                                                    originalSong = item,\n                                                    navController = navController,\n                                                    onDismiss = {\n                                                        onDismiss()\n                                                        menuState.dismiss()\n                                                    },\n                                                    isFromCache = isFromCache,\n                                                )\n                                            }\n                                        },\n                                    ) {\n                                        Icon(\n                                            painter = painterResource(R.drawable.more_vert),\n                                            contentDescription = null,\n                                        )\n                                    }\n                                },\n                                modifier =\n                                    Modifier\n                                        .combinedClickable(\n                                            onClick = {\n                                                if (item.id == mediaMetadata?.id) {\n                                                    playerConnection.togglePlayPause()\n                                                } else {\n                                                    val songs =\n                                                        result.map\n                                                            .getOrDefault(LocalFilter.SONG, emptyList())\n                                                            .filterIsInstance<Song>()\n                                                            .map { it.toMediaItem() }\n                                                    playerConnection.playQueue(\n                                                        ListQueue(\n                                                            title = queueSearchedSongsStr,\n                                                            items = songs,\n                                                            startIndex = songs.indexOfFirst { it.mediaId == item.id },\n                                                        ),\n                                                    )\n                                                }\n                                            },\n                                            onLongClick = {\n                                                menuState.show {\n                                                    SongMenu(\n                                                        originalSong = item,\n                                                        navController = navController,\n                                                        onDismiss = {\n                                                            onDismiss()\n                                                            menuState.dismiss()\n                                                        },\n                                                        isFromCache = isFromCache,\n                                                    )\n                                                }\n                                            },\n                                        ).animateItem(),\n                            )\n                        }\n\n                        is Album -> {\n                            AlbumListItem(\n                                album = item,\n                                isActive = item.id == mediaMetadata?.album?.id,\n                                isPlaying = isPlaying,\n                                modifier =\n                                    Modifier\n                                        .clickable {\n                                            onDismiss()\n                                            navController.navigate(\"album/${item.id}\")\n                                        }.animateItem(),\n                            )\n                        }\n\n                        is Artist -> {\n                            ArtistListItem(\n                                artist = item,\n                                modifier =\n                                    Modifier\n                                        .clickable {\n                                            onDismiss()\n                                            navController.navigate(\"artist/${item.id}\")\n                                        }.animateItem(),\n                            )\n                        }\n\n                        is Playlist -> {\n                            PlaylistListItem(\n                                playlist = item,\n                                modifier =\n                                    Modifier\n                                        .clickable {\n                                            onDismiss()\n                                            navController.navigate(\"local_playlist/${item.id}\")\n                                        }.animateItem(),\n                            )\n                        }\n                    }\n                }\n            }\n\n            if (result.query.isNotEmpty() && result.map.isEmpty()) {\n                item(key = \"no_result\") {\n                    EmptyPlaceholder(\n                        icon = R.drawable.search,\n                        text = stringResource(R.string.no_results_found),\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/search/OnlineSearchResult.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.search\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyItemScope\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.OutlinedTextFieldDefaults\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.snapshotFlow\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.platform.LocalFocusManager\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.TextRange\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport com.metrolist.innertube.YouTube.SearchFilter.Companion.FILTER_ALBUM\nimport com.metrolist.innertube.YouTube.SearchFilter.Companion.FILTER_ARTIST\nimport com.metrolist.innertube.YouTube.SearchFilter.Companion.FILTER_COMMUNITY_PLAYLIST\nimport com.metrolist.innertube.YouTube.SearchFilter.Companion.FILTER_EPISODE\nimport com.metrolist.innertube.YouTube.SearchFilter.Companion.FILTER_FEATURED_PLAYLIST\nimport com.metrolist.innertube.YouTube.SearchFilter.Companion.FILTER_PODCAST\nimport com.metrolist.innertube.YouTube.SearchFilter.Companion.FILTER_PROFILE\nimport com.metrolist.innertube.YouTube.SearchFilter.Companion.FILTER_SONG\nimport com.metrolist.innertube.YouTube.SearchFilter.Companion.FILTER_VIDEO\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.ArtistItem\nimport com.metrolist.innertube.models.EpisodeItem\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.PodcastItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.WatchEndpoint\nimport com.metrolist.innertube.models.YTItem\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.HideVideoSongsKey\nimport com.metrolist.music.constants.MiniPlayerBottomSpacing\nimport com.metrolist.music.constants.MiniPlayerHeight\nimport com.metrolist.music.constants.NavigationBarHeight\nimport com.metrolist.music.constants.PauseSearchHistoryKey\nimport com.metrolist.music.db.entities.SearchHistory\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.playback.queues.YouTubeQueue\nimport com.metrolist.music.ui.component.ChipsRow\nimport com.metrolist.music.ui.component.EmptyPlaceholder\nimport com.metrolist.music.ui.component.HideOnScrollFAB\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.NavigationTitle\nimport com.metrolist.music.ui.component.YouTubeListItem\nimport com.metrolist.music.ui.component.shimmer.ListItemPlaceHolder\nimport com.metrolist.music.ui.component.shimmer.ShimmerHost\nimport com.metrolist.music.ui.menu.YouTubeAlbumMenu\nimport com.metrolist.music.ui.menu.YouTubeArtistMenu\nimport com.metrolist.music.ui.menu.YouTubePlaylistMenu\nimport com.metrolist.music.ui.menu.YouTubeSongMenu\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.viewmodels.OnlineSearchViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport java.net.URLDecoder\nimport java.net.URLEncoder\n\n@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun OnlineSearchResult(\n    navController: NavController,\n    viewModel: OnlineSearchViewModel = hiltViewModel(),\n    pureBlack: Boolean = false,\n) {\n    val database = LocalDatabase.current\n    val menuState = LocalMenuState.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val haptic = LocalHapticFeedback.current\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n\n    val coroutineScope = rememberCoroutineScope()\n    val lazyListState = rememberLazyListState()\n    val focusManager = LocalFocusManager.current\n    val focusRequester = remember { FocusRequester() }\n\n    var isSearchFocused by remember { mutableStateOf(false) }\n\n    val pauseSearchHistory by rememberPreference(PauseSearchHistoryKey, defaultValue = false)\n    val hideVideoSongs by rememberPreference(HideVideoSongsKey, defaultValue = false)\n\n    BackHandler(enabled = isSearchFocused) {\n        isSearchFocused = false\n        focusManager.clearFocus()\n    }\n\n    // Extract query from navigation arguments\n    val encodedQuery = navController.currentBackStackEntry?.arguments?.getString(\"query\") ?: \"\"\n    val decodedQuery =\n        remember(encodedQuery) {\n            try {\n                URLDecoder.decode(encodedQuery, \"UTF-8\")\n            } catch (e: Exception) {\n                encodedQuery\n            }\n        }\n\n    var query by rememberSaveable(stateSaver = TextFieldValue.Saver) {\n        mutableStateOf(TextFieldValue(decodedQuery, TextRange(decodedQuery.length)))\n    }\n\n    val onSearch: (String) -> Unit =\n        remember {\n            { searchQuery ->\n                if (searchQuery.isNotEmpty()) {\n                    isSearchFocused = false\n                    focusManager.clearFocus()\n\n                    navController.navigate(\"search/${URLEncoder.encode(searchQuery, \"UTF-8\")}\") {\n                        popUpTo(\"search/${URLEncoder.encode(decodedQuery, \"UTF-8\")}\") {\n                            inclusive = true\n                        }\n\n                        if (!pauseSearchHistory) {\n                            coroutineScope.launch(Dispatchers.IO) {\n                                database.query {\n                                    insert(SearchHistory(query = searchQuery))\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n    // Update query when decodedQuery changes\n    LaunchedEffect(decodedQuery) {\n        query = TextFieldValue(decodedQuery, TextRange(decodedQuery.length))\n    }\n\n    // Clear video filter if hideVideoSongs setting is enabled and filter is set to FILTER_VIDEO\n    LaunchedEffect(hideVideoSongs) {\n        if (hideVideoSongs && viewModel.filter.value == FILTER_VIDEO) {\n            viewModel.filter.value = null\n        }\n    }\n\n    val searchFilter by viewModel.filter.collectAsState()\n    val searchSummary = viewModel.summaryPage\n    val itemsPage by remember(searchFilter) {\n        derivedStateOf {\n            searchFilter?.value?.let {\n                viewModel.viewStateMap[it]\n            }\n        }\n    }\n\n    LaunchedEffect(lazyListState) {\n        snapshotFlow {\n            lazyListState.layoutInfo.visibleItemsInfo.any { it.key == \"loading\" }\n        }.collect { shouldLoadMore ->\n            if (!shouldLoadMore) return@collect\n            viewModel.loadMore()\n        }\n    }\n\n    val ytItemContent: @Composable LazyItemScope.(YTItem) -> Unit = { item: YTItem ->\n        val longClick = {\n            haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n            menuState.show {\n                when (item) {\n                    is SongItem -> {\n                        YouTubeSongMenu(\n                            song = item,\n                            navController = navController,\n                            onDismiss = menuState::dismiss,\n                        )\n                    }\n\n                    is AlbumItem -> {\n                        YouTubeAlbumMenu(\n                            albumItem = item,\n                            navController = navController,\n                            onDismiss = menuState::dismiss,\n                        )\n                    }\n\n                    is ArtistItem -> {\n                        YouTubeArtistMenu(\n                            artist = item,\n                            onDismiss = menuState::dismiss,\n                        )\n                    }\n\n                    is PlaylistItem -> {\n                        YouTubePlaylistMenu(\n                            playlist = item,\n                            coroutineScope = coroutineScope,\n                            onDismiss = menuState::dismiss,\n                        )\n                    }\n\n                    is PodcastItem -> {\n                        YouTubePlaylistMenu(\n                            playlist = item.asPlaylistItem(),\n                            coroutineScope = coroutineScope,\n                            onDismiss = menuState::dismiss,\n                        )\n                    }\n\n                    is EpisodeItem -> {\n                        YouTubeSongMenu(\n                            song = item.asSongItem(),\n                            navController = navController,\n                            onDismiss = menuState::dismiss,\n                        )\n                    }\n                }\n            }\n        }\n        YouTubeListItem(\n            item = item,\n            isActive =\n                when (item) {\n                    is SongItem -> mediaMetadata?.id == item.id\n                    is AlbumItem -> mediaMetadata?.album?.id == item.id\n                    is EpisodeItem -> mediaMetadata?.id == item.id\n                    else -> false\n                },\n            isPlaying = isPlaying,\n            trailingContent = {\n                IconButton(\n                    onClick = longClick,\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.more_vert),\n                        contentDescription = null,\n                    )\n                }\n            },\n            modifier =\n                Modifier\n                    .combinedClickable(\n                        onClick = {\n                            when (item) {\n                                is SongItem -> {\n                                    if (item.id == mediaMetadata?.id) {\n                                        playerConnection.togglePlayPause()\n                                    } else {\n                                        playerConnection.playQueue(\n                                            YouTubeQueue(\n                                                WatchEndpoint(videoId = item.id),\n                                                item.toMediaMetadata(),\n                                            ),\n                                        )\n                                    }\n                                }\n\n                                is AlbumItem -> {\n                                    navController.navigate(\"album/${item.id}\")\n                                }\n\n                                is ArtistItem -> {\n                                    navController.navigate(\"artist/${item.id}\")\n                                }\n\n                                is PlaylistItem -> {\n                                    navController.navigate(\"online_playlist/${item.id}\")\n                                }\n\n                                is PodcastItem -> {\n                                    navController.navigate(\"online_podcast/${item.id}\")\n                                }\n\n                                is EpisodeItem -> {\n                                    if (item.id == mediaMetadata?.id) {\n                                        playerConnection.togglePlayPause()\n                                    } else {\n                                        playerConnection.playQueue(\n                                            YouTubeQueue(\n                                                WatchEndpoint(videoId = item.id),\n                                                item.toMediaMetadata(),\n                                            ),\n                                        )\n                                    }\n                                }\n                            }\n                        },\n                        onLongClick = longClick,\n                    ).animateItem(),\n        )\n    }\n\n    Column(\n        modifier =\n            Modifier\n                .fillMaxSize()\n                .background(if (pureBlack) Color.Black else MaterialTheme.colorScheme.background)\n                .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top)),\n    ) {\n        // Google-style SearchBar with Material 3 design\n        OutlinedTextField(\n            value = query,\n            onValueChange = { newQuery ->\n                query = newQuery\n            },\n            placeholder = {\n                Text(\n                    text = stringResource(R.string.search_yt_music),\n                    style = MaterialTheme.typography.bodyLarge,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n            },\n            leadingIcon = {\n                IconButton(\n                    onClick = { navController.navigateUp() },\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.arrow_back),\n                        contentDescription = stringResource(R.string.dismiss),\n                        tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n                }\n            },\n            trailingIcon = {\n                if (query.text.isNotEmpty()) {\n                    IconButton(\n                        onClick = {\n                            query = TextFieldValue(\"\")\n                        },\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.close),\n                            contentDescription = null,\n                            tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                        )\n                    }\n                }\n            },\n            keyboardOptions =\n                KeyboardOptions(\n                    imeAction = ImeAction.Search,\n                ),\n            keyboardActions =\n                KeyboardActions(\n                    onSearch = {\n                        onSearch(query.text)\n                    },\n                ),\n            singleLine = true,\n            shape = RoundedCornerShape(28.dp),\n            colors =\n                OutlinedTextFieldDefaults.colors(\n                    focusedContainerColor =\n                        if (pureBlack) {\n                            MaterialTheme.colorScheme.surface\n                        } else {\n                            MaterialTheme.colorScheme.surfaceContainerHigh\n                        },\n                    unfocusedContainerColor =\n                        if (pureBlack) {\n                            MaterialTheme.colorScheme.surface\n                        } else {\n                            MaterialTheme.colorScheme.surfaceContainerHigh\n                        },\n                    focusedBorderColor = Color.Transparent,\n                    unfocusedBorderColor = Color.Transparent,\n                ),\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = 16.dp, vertical = 8.dp)\n                    .focusRequester(focusRequester)\n                    .onFocusChanged { focusState ->\n                        if (focusState.isFocused) {\n                            isSearchFocused = true\n                        }\n                    },\n        )\n\n        // Main content area below search bar\n        Box(modifier = Modifier.weight(1f)) {\n            Column(\n                modifier = Modifier.fillMaxWidth(),\n            ) {\n                val visibleChips =\n                    listOf(\n                        null to stringResource(R.string.filter_all),\n                        FILTER_SONG to stringResource(R.string.filter_songs),\n                    ).let { baseChips ->\n                        if (!hideVideoSongs) {\n                            baseChips + (FILTER_VIDEO to stringResource(R.string.filter_videos))\n                        } else {\n                            baseChips\n                        }\n                    } +\n                        listOf(\n                            FILTER_ALBUM to stringResource(R.string.filter_albums),\n                            FILTER_ARTIST to stringResource(R.string.filter_artists),\n                            FILTER_COMMUNITY_PLAYLIST to stringResource(R.string.filter_community_playlists),\n                            FILTER_FEATURED_PLAYLIST to stringResource(R.string.filter_featured_playlists),\n                            FILTER_PODCAST to stringResource(R.string.filter_podcasts),\n                            FILTER_EPISODE to stringResource(R.string.filter_episodes),\n                            FILTER_PROFILE to stringResource(R.string.filter_profiles),\n                        )\n\n                ChipsRow(\n                    chips = visibleChips,\n                    currentValue = searchFilter,\n                    onValueUpdate = {\n                        if (viewModel.filter.value != it) {\n                            viewModel.filter.value = it\n                        }\n                        coroutineScope.launch {\n                            lazyListState.animateScrollToItem(0)\n                        }\n                    },\n                    modifier = Modifier.fillMaxWidth(),\n                )\n\n                LazyColumn(\n                    state = lazyListState,\n                    modifier = Modifier.fillMaxWidth(),\n                ) {\n                    if (searchFilter == null) {\n                        searchSummary?.summaries?.forEach { summary ->\n                            item {\n                                NavigationTitle(summary.title)\n                            }\n\n                            items(\n                                items = summary.items,\n                                key = { \"${summary.title}/${it.id}/${summary.items.indexOf(it)}\" },\n                                itemContent = ytItemContent,\n                            )\n                        }\n\n                        if (searchSummary?.summaries?.isEmpty() == true) {\n                            item {\n                                EmptyPlaceholder(\n                                    icon = R.drawable.search,\n                                    text = stringResource(R.string.no_results_found),\n                                )\n                            }\n                        }\n                    } else {\n                        items(\n                            items = itemsPage?.items.orEmpty().distinctBy { it.id },\n                            key = { \"filtered_${it.id}\" },\n                            itemContent = ytItemContent,\n                        )\n\n                        if (itemsPage?.continuation != null) {\n                            item(key = \"loading\") {\n                                ShimmerHost {\n                                    repeat(3) {\n                                        ListItemPlaceHolder()\n                                    }\n                                }\n                            }\n                        }\n\n                        if (itemsPage?.items?.isEmpty() == true) {\n                            item {\n                                EmptyPlaceholder(\n                                    icon = R.drawable.search,\n                                    text = stringResource(R.string.no_results_found),\n                                )\n                            }\n                        }\n                    }\n\n                    if (searchFilter == null && searchSummary == null || searchFilter != null && itemsPage == null) {\n                        item {\n                            ShimmerHost {\n                                repeat(8) {\n                                    ListItemPlaceHolder()\n                                }\n                            }\n                        }\n                    }\n\n                    item(key = \"bottom_spacer\") {\n                        Spacer(modifier = Modifier.height(MiniPlayerHeight + MiniPlayerBottomSpacing + NavigationBarHeight))\n                    }\n                }\n            }\n            if (isSearchFocused) {\n                OnlineSearchScreen(\n                    query = query.text,\n                    onQueryChange = { query = it },\n                    navController = navController,\n                    onSearch = onSearch,\n                    onDismiss = {\n                        isSearchFocused = false\n                        focusManager.clearFocus()\n                    },\n                    pureBlack = pureBlack,\n                )\n            }\n            HideOnScrollFAB(\n                lazyListState = lazyListState,\n                icon = R.drawable.mic,\n                onClick = { navController.navigate(\"recognition\") },\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/search/OnlineSearchScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.search\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.snapshotFlow\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.platform.LocalSoftwareKeyboardController\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.TextRange\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.ArtistItem\nimport com.metrolist.innertube.models.EpisodeItem\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.PodcastItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.SuggestionItemHeight\nimport com.metrolist.music.models.toMediaMetadata\nimport com.metrolist.music.playback.queues.YouTubeQueue\nimport com.metrolist.music.ui.component.LocalMenuState\nimport com.metrolist.music.ui.component.YouTubeListItem\nimport com.metrolist.music.ui.menu.YouTubeAlbumMenu\nimport com.metrolist.music.ui.menu.YouTubeArtistMenu\nimport com.metrolist.music.ui.menu.YouTubePlaylistMenu\nimport com.metrolist.music.ui.menu.YouTubeSongMenu\nimport com.metrolist.music.viewmodels.OnlineSearchSuggestionViewModel\nimport kotlinx.coroutines.FlowPreview\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.drop\n\n@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class, FlowPreview::class)\n@Composable\nfun OnlineSearchScreen(\n    query: String,\n    onQueryChange: (TextFieldValue) -> Unit,\n    navController: NavController,\n    onSearch: (String) -> Unit,\n    onDismiss: () -> Unit,\n    pureBlack: Boolean,\n    viewModel: OnlineSearchSuggestionViewModel = hiltViewModel(),\n) {\n    val database = LocalDatabase.current\n    val keyboardController = LocalSoftwareKeyboardController.current\n    val menuState = LocalMenuState.current\n    val playerConnection = LocalPlayerConnection.current ?: return\n\n    val coroutineScope = rememberCoroutineScope()\n\n    val haptic = LocalHapticFeedback.current\n    val isPlaying by playerConnection.isEffectivelyPlaying.collectAsState()\n    val mediaMetadata by playerConnection.mediaMetadata.collectAsState()\n    val viewState by viewModel.viewState.collectAsState()\n\n    val lazyListState = rememberLazyListState()\n\n    LaunchedEffect(Unit) {\n        snapshotFlow { lazyListState.firstVisibleItemScrollOffset }\n            .drop(1)\n            .collect {\n                keyboardController?.hide()\n            }\n    }\n\n    LaunchedEffect(query) {\n        snapshotFlow { query }.debounce(300L).collectLatest {\n            viewModel.query.value = it\n        }\n    }\n\n    LazyColumn(\n        state = lazyListState,\n        contentPadding = WindowInsets.systemBars.only(WindowInsetsSides.Bottom).asPaddingValues(),\n        modifier = Modifier\n            .fillMaxSize()\n            .background(if (pureBlack) Color.Black else MaterialTheme.colorScheme.background)\n    ) {\n        items(viewState.history, key = { \"history_${it.query}\" }) { history ->\n            SuggestionItem(\n                query = history.query,\n                online = false,\n                onClick = {\n                    onSearch(history.query)\n                    onDismiss()\n                },\n                onDelete = {\n                    database.query {\n                        delete(history)\n                    }\n                },\n                onFillTextField = {\n                    onQueryChange(TextFieldValue(history.query, TextRange(history.query.length)))\n                },\n                modifier = Modifier.animateItem(),\n                pureBlack = pureBlack\n            )\n        }\n\n        items(viewState.suggestions, key = { \"suggestion_$it\" }) { query ->\n            SuggestionItem(\n                query = query,\n                online = true,\n                onClick = {\n                    onSearch(query)\n                    onDismiss()\n                },\n                onFillTextField = {\n                    onQueryChange(TextFieldValue(query, TextRange(query.length)))\n                },\n                modifier = Modifier.animateItem(),\n                pureBlack = pureBlack\n            )\n        }\n\n        if (viewState.items.isNotEmpty() && viewState.history.size + viewState.suggestions.size > 0) {\n            item(key = \"search_divider\") {\n                HorizontalDivider(\n                    modifier = Modifier.animateItem()\n                )\n            }\n            item(key = \"search_divider_spacer\") {\n                Spacer(modifier = Modifier.height(8.dp))\n            }\n        }\n\n        items(viewState.items, key = { \"item_${it.id}\" }) { item ->\n            YouTubeListItem(\n                item = item,\n                isActive = when (item) {\n                    is SongItem -> mediaMetadata?.id == item.id\n                    is AlbumItem -> mediaMetadata?.album?.id == item.id\n                    is EpisodeItem -> mediaMetadata?.id == item.id\n                    else -> false\n                },\n                isPlaying = isPlaying,\n                trailingContent = {\n                    IconButton(\n                        onClick = {\n                            menuState.show {\n                                when (item) {\n                                    is SongItem -> YouTubeSongMenu(\n                                        song = item,\n                                        navController = navController,\n                                        onDismiss = {\n                                            menuState.dismiss()\n                                            onDismiss()\n                                        }\n                                    )\n                                    is AlbumItem -> YouTubeAlbumMenu(\n                                        albumItem = item,\n                                        navController = navController,\n                                        onDismiss = {\n                                            menuState.dismiss()\n                                            onDismiss()\n                                        }\n                                    )\n                                    is ArtistItem -> YouTubeArtistMenu(\n                                        artist = item,\n                                        onDismiss = {\n                                            menuState.dismiss()\n                                            onDismiss()\n                                        }\n                                    )\n                                    is PlaylistItem -> YouTubePlaylistMenu(\n                                        playlist = item,\n                                        coroutineScope = coroutineScope,\n                                        onDismiss = {\n                                            menuState.dismiss()\n                                            onDismiss()\n                                        }\n                                    )\n                                    is PodcastItem -> YouTubePlaylistMenu(\n                                        playlist = item.asPlaylistItem(),\n                                        coroutineScope = coroutineScope,\n                                        onDismiss = {\n                                            menuState.dismiss()\n                                            onDismiss()\n                                        }\n                                    )\n                                    is EpisodeItem -> YouTubeSongMenu(\n                                        song = item.asSongItem(),\n                                        navController = navController,\n                                        onDismiss = {\n                                            menuState.dismiss()\n                                            onDismiss()\n                                        }\n                                    )\n                                }\n                            }\n                        }\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.more_vert),\n                            contentDescription = null\n                        )\n                    }\n                },\n                modifier = Modifier\n                    .combinedClickable(\n                        onClick = {\n                            when (item) {\n                                is SongItem -> {\n                                    if (item.id == mediaMetadata?.id) {\n                                        playerConnection.togglePlayPause()\n                                    } else {\n                                        playerConnection.playQueue(\n                                            YouTubeQueue.radio(item.toMediaMetadata())\n                                        )\n                                        onDismiss()\n                                    }\n                                }\n                                is AlbumItem -> {\n                                    navController.navigate(\"album/${item.id}\")\n                                    onDismiss()\n                                }\n                                is ArtistItem -> {\n                                    navController.navigate(\"artist/${item.id}\")\n                                    onDismiss()\n                                }\n                                is PlaylistItem -> {\n                                    navController.navigate(\"online_playlist/${item.id}\")\n                                    onDismiss()\n                                }\n                                is PodcastItem -> {\n                                    navController.navigate(\"online_podcast/${item.id}\")\n                                    onDismiss()\n                                }\n                                is EpisodeItem -> {\n                                    if (item.id == mediaMetadata?.id) {\n                                        playerConnection.togglePlayPause()\n                                    } else {\n                                        playerConnection.playQueue(\n                                            YouTubeQueue.radio(item.toMediaMetadata())\n                                        )\n                                        onDismiss()\n                                    }\n                                }\n                            }\n                        },\n                        onLongClick = {\n                            haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                            menuState.show {\n                                when (item) {\n                                    is SongItem -> YouTubeSongMenu(\n                                        song = item,\n                                        navController = navController,\n                                        onDismiss = {\n                                            menuState.dismiss()\n                                            onDismiss()\n                                        }\n                                    )\n                                    is AlbumItem -> YouTubeAlbumMenu(\n                                        albumItem = item,\n                                        navController = navController,\n                                        onDismiss = {\n                                            menuState.dismiss()\n                                            onDismiss()\n                                        }\n                                    )\n                                    is ArtistItem -> YouTubeArtistMenu(\n                                        artist = item,\n                                        onDismiss = {\n                                            menuState.dismiss()\n                                            onDismiss()\n                                        }\n                                    )\n                                    is PlaylistItem -> YouTubePlaylistMenu(\n                                        playlist = item,\n                                        coroutineScope = coroutineScope,\n                                        onDismiss = {\n                                            menuState.dismiss()\n                                            onDismiss()\n                                        }\n                                    )\n                                    is PodcastItem -> YouTubePlaylistMenu(\n                                        playlist = item.asPlaylistItem(),\n                                        coroutineScope = coroutineScope,\n                                        onDismiss = {\n                                            menuState.dismiss()\n                                            onDismiss()\n                                        }\n                                    )\n                                    is EpisodeItem -> YouTubeSongMenu(\n                                        song = item.asSongItem(),\n                                        navController = navController,\n                                        onDismiss = {\n                                            menuState.dismiss()\n                                            onDismiss()\n                                        }\n                                    )\n                                }\n                            }\n                        }\n                    )\n                    .background(if (pureBlack) Color.Black else MaterialTheme.colorScheme.surface)\n                    .animateItem()\n            )\n        }\n    }\n}\n\n@Composable\nfun SuggestionItem(\n    modifier: Modifier = Modifier,\n    query: String,\n    online: Boolean,\n    onClick: () -> Unit,\n    onDelete: () -> Unit = {},\n    onFillTextField: () -> Unit,\n    pureBlack: Boolean\n) {\n    Row(\n        verticalAlignment = Alignment.CenterVertically,\n        modifier = modifier\n            .fillMaxWidth()\n            .height(SuggestionItemHeight)\n            .background(if (pureBlack) Color.Black else MaterialTheme.colorScheme.surface)\n            .clickable(onClick = onClick)\n            .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)),\n    ) {\n        Icon(\n            painterResource(if (online) R.drawable.search else R.drawable.history),\n            contentDescription = null,\n            modifier = Modifier.padding(horizontal = 16.dp).alpha(0.5f)\n        )\n\n        Text(\n            text = query,\n            maxLines = 1,\n            overflow = TextOverflow.Ellipsis,\n            modifier = Modifier.weight(1f),\n        )\n\n        if (!online) {\n            IconButton(\n                onClick = onDelete,\n                modifier = Modifier.alpha(0.5f),\n            ) {\n                Icon(\n                    painter = painterResource(R.drawable.close),\n                    contentDescription = null,\n                )\n            }\n        }\n\n        IconButton(\n            onClick = onFillTextField,\n            modifier = Modifier.alpha(0.5f),\n        ) {\n            Icon(\n                painter = painterResource(R.drawable.arrow_top_left),\n                contentDescription = null,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/search/SearchScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.search\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.text.BasicTextField\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.platform.LocalFocusManager\nimport androidx.compose.ui.platform.LocalSoftwareKeyboardController\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.unit.sp\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.LifecycleEventObserver\nimport androidx.navigation.NavController\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalIsPlayerExpanded\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.PauseSearchHistoryKey\nimport com.metrolist.music.constants.SearchSource\nimport com.metrolist.music.constants.SearchSourceKey\nimport com.metrolist.music.db.entities.SearchHistory\nimport com.metrolist.music.ui.component.HideOnScrollFAB\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport java.net.URLEncoder\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun SearchScreen(\n    navController: NavController,\n    pureBlack: Boolean\n) {\n    val database = LocalDatabase.current\n    val coroutineScope = rememberCoroutineScope()\n    val focusManager = LocalFocusManager.current\n    val focusRequester = remember { FocusRequester() }\n    val keyboardController = LocalSoftwareKeyboardController.current\n    val isPlayerExpanded = LocalIsPlayerExpanded.current\n    val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current\n    val lazyListState = rememberLazyListState()\n\n    var searchSource by rememberEnumPreference(SearchSourceKey, SearchSource.ONLINE)\n    var query by rememberSaveable(stateSaver = TextFieldValue.Saver) {\n        mutableStateOf(TextFieldValue())\n    }\n    val pauseSearchHistory by rememberPreference(PauseSearchHistoryKey, defaultValue = false)\n    var isFirstLaunch by rememberSaveable { mutableStateOf(true) }\n\n    val onSearch: (String) -> Unit = remember {\n        { searchQuery ->\n            if (searchQuery.isNotEmpty()) {\n                focusManager.clearFocus()\n                navController.navigate(\"search/${URLEncoder.encode(searchQuery, \"UTF-8\")}\")\n\n                if (!pauseSearchHistory) {\n                    coroutineScope.launch(Dispatchers.IO) {\n                        database.query {\n                            insert(SearchHistory(query = searchQuery))\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    val onSearchFromSuggestion: (String) -> Unit = remember {\n        { searchQuery ->\n            if (searchQuery.isNotEmpty()) {\n                focusManager.clearFocus()\n                navController.navigate(\"search/${URLEncoder.encode(searchQuery, \"UTF-8\")}\")\n\n                if (!pauseSearchHistory) {\n                    coroutineScope.launch(Dispatchers.IO) {\n                        database.query {\n                            insert(SearchHistory(query = searchQuery))\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = {\n                    Row(\n                        modifier = Modifier.fillMaxWidth(),\n                        verticalAlignment = Alignment.CenterVertically\n                    ) {\n                        BasicTextField(\n                            value = query,\n                            onValueChange = { query = it },\n                            modifier = Modifier\n                                .weight(1f)\n                                .focusRequester(focusRequester),\n                            textStyle = TextStyle(\n                                color = MaterialTheme.colorScheme.onSurface,\n                                fontSize = 16.sp\n                            ),\n                            cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),\n                            singleLine = true,\n                            decorationBox = { innerTextField ->\n                                if (query.text.isEmpty()) {\n                                    Text(\n                                        text = stringResource(\n                                            when (searchSource) {\n                                                SearchSource.LOCAL -> R.string.search_library\n                                                SearchSource.ONLINE -> R.string.search_yt_music\n                                            }\n                                        ),\n                                        style = TextStyle(\n                                            color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),\n                                            fontSize = 16.sp\n                                        )\n                                    )\n                                }\n                                innerTextField()\n                            },\n                            keyboardOptions = KeyboardOptions(\n                                imeAction = ImeAction.Search\n                            ),\n                            keyboardActions = KeyboardActions(\n                                onSearch = { onSearch(query.text) }\n                            )\n                        )\n                        \n                        Row {\n                            if (query.text.isNotEmpty()) {\n                                IconButton(onClick = { query = TextFieldValue(\"\") }) {\n                                    Icon(\n                                        painter = painterResource(R.drawable.close),\n                                        contentDescription = null,\n                                        tint = MaterialTheme.colorScheme.onSurface\n                                    )\n                                }\n                            }\n                            IconButton(\n                                onClick = {\n                                    searchSource = if (searchSource == SearchSource.ONLINE) \n                                        SearchSource.LOCAL else SearchSource.ONLINE\n                                }\n                            ) {\n                                Icon(\n                                    painter = painterResource(\n                                        when (searchSource) {\n                                            SearchSource.LOCAL -> R.drawable.library_music\n                                            SearchSource.ONLINE -> R.drawable.language\n                                        }\n                                    ),\n                                    contentDescription = null,\n                                    tint = MaterialTheme.colorScheme.onSurface\n                                )\n                            }\n                        }\n                    }\n                },\n                navigationIcon = {\n                    IconButton(onClick = { navController.navigateUp() }) {\n                        Icon(\n                            painter = painterResource(R.drawable.arrow_back),\n                            contentDescription = stringResource(R.string.dismiss),\n                            tint = MaterialTheme.colorScheme.onSurface\n                        )\n                    }\n                },\n                colors = TopAppBarDefaults.topAppBarColors(\n                    containerColor = if (pureBlack) Color.Black else MaterialTheme.colorScheme.surfaceContainer\n                )\n            )\n        },\n        containerColor = if (pureBlack) Color.Black else MaterialTheme.colorScheme.background\n    ) { paddingValues ->\n        val bottomPadding = LocalPlayerAwareWindowInsets.current.asPaddingValues().calculateBottomPadding()\n        \n        Box(\n            modifier = Modifier\n                .padding(paddingValues)\n                .fillMaxSize()\n        ) {\n            Box(\n                modifier = Modifier\n                    .padding(bottom = bottomPadding)\n                    .fillMaxSize()\n            ) {\n                when (searchSource) {\n                    SearchSource.LOCAL -> LocalSearchScreen(\n                        query = query.text,\n                        navController = navController,\n                        onDismiss = { navController.navigateUp() },\n                        pureBlack = pureBlack\n                    )\n                    SearchSource.ONLINE -> OnlineSearchScreen(\n                        query = query.text,\n                        onQueryChange = { query = it },\n                        navController = navController,\n                        onSearch = onSearchFromSuggestion,\n                        onDismiss = { /* Don't dismiss when searching from suggestions */ },\n                        pureBlack = pureBlack\n                    )\n                }\n            }\n            \n            HideOnScrollFAB(\n                lazyListState = lazyListState,\n                icon = R.drawable.mic,\n                onClick = { navController.navigate(\"recognition\") }\n            )\n        }\n    }\n\n    // Handle lifecycle events to manage keyboard visibility\n    DisposableEffect(lifecycleOwner, isPlayerExpanded) {\n        val observer = LifecycleEventObserver { _, event ->\n            when (event) {\n                Lifecycle.Event.ON_RESUME -> {\n                    // Always hide keyboard when resuming if player is expanded\n                    if (isPlayerExpanded) {\n                        keyboardController?.hide()\n                        focusManager.clearFocus()\n                    } else if (isFirstLaunch) {\n                        // Only request focus on first launch when player is not expanded\n                        try {\n                            focusRequester.requestFocus()\n                        } catch (e: Exception) {\n                            // Ignore focus request failures\n                        }\n                        isFirstLaunch = false\n                    }\n                }\n                Lifecycle.Event.ON_PAUSE -> {\n                    // Clear focus when pausing to prevent keyboard from showing on resume\n                    focusManager.clearFocus()\n                    keyboardController?.hide()\n                }\n                else -> {}\n            }\n        }\n        lifecycleOwner.lifecycle.addObserver(observer)\n        \n        // Initial check - hide keyboard if player is expanded\n        if (isPlayerExpanded) {\n            keyboardController?.hide()\n            focusManager.clearFocus()\n        }\n        \n        onDispose {\n            lifecycleOwner.lifecycle.removeObserver(observer)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/settings/AboutScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.settings\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LinearWavyProgressIndicator\nimport androidx.compose.material3.ListItem\nimport androidx.compose.material3.ListItemDefaults\nimport androidx.compose.material3.MaterialShapes\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.SnackbarDuration\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.SnackbarResult\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarScrollBehavior\nimport androidx.compose.material3.toShape\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.BlendMode\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.ColorFilter\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalUriHandler\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.graphics.shapes.RoundedPolygon\nimport androidx.navigation.NavController\nimport coil3.compose.AsyncImage\nimport com.metrolist.innertube.models.WatchEndpoint\nimport com.metrolist.music.BuildConfig\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.playback.PlayerConnection\nimport com.metrolist.music.playback.queues.YouTubeQueue\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.Material3SettingsGroup\nimport com.metrolist.music.ui.component.Material3SettingsItem\nimport com.metrolist.music.ui.utils.backToMain\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.launch\n\nprivate data class Contributor(\n    val name: String,\n    val roleRes: Int,\n    val githubHandle: String,\n    val avatarUrl: String = \"https://github.com/$githubHandle.png\",\n    val githubUrl: String = \"https://github.com/$githubHandle\",\n    val polygon: RoundedPolygon? = null,\n    val favoriteSongVideoId: String? = null\n)\n\nprivate data class CommunityLink(\n    val labelRes: Int,\n    val iconRes: Int,\n    val url: String\n)\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\nprivate val leadDeveloper = Contributor(\n    name = \"Mo Agamy\",\n    roleRes = R.string.credits_lead_developer,\n    githubHandle = \"mostafaalagamy\",\n    polygon = MaterialShapes.Cookie9Sided,\n    favoriteSongVideoId = \"Mh2JWGWvy_Y\"\n)\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\nprivate val collaborators = listOf(\n    Contributor(name = \"Adriel O'Connel\", roleRes = R.string.credits_collaborator, githubHandle = \"adrielGGmotion\", polygon = MaterialShapes.Cookie4Sided, favoriteSongVideoId = \"m2zUrruKjDQ\"),\n    Contributor(name = \"Nyx\", roleRes = R.string.credits_collaborator, githubHandle = \"nyxiereal\", polygon = MaterialShapes.Cookie12Sided, favoriteSongVideoId = \"zselaN6zPXw\"), // More mass for face\n)\n\nprivate val communityLinks = listOf(\n    CommunityLink(R.string.credits_discord, R.drawable.discord, \"https://discord.com/invite/zrdbeRG2Mt\"),\n    CommunityLink(R.string.credits_telegram, R.drawable.telegram, \"https://t.me/metrolistapp\"),\n    CommunityLink(R.string.credits_view_repo, R.drawable.github, \"https://github.com/MetrolistGroup/Metrolist\"),\n    CommunityLink(R.string.credits_license_name, R.drawable.info, \"https://github.com/MetrolistGroup/Metrolist/blob/main/LICENSE\")\n)\n\nprivate fun handleEasterEggClick(\n    clickCount: Int,\n    favoriteSongVideoId: String?,\n    coroutineScope: CoroutineScope,\n    snackbarHostState: SnackbarHostState,\n    playerConnection: PlayerConnection?,\n    wannaPlayStr: String,\n    yeahStr: String,\n    onCountUpdate: (Int) -> Unit\n) {\n    if (favoriteSongVideoId != null) {\n        val newCount = clickCount + 1\n        onCountUpdate(newCount)\n        if (newCount >= 3) {\n            onCountUpdate(0)\n            coroutineScope.launch {\n                val result = snackbarHostState.showSnackbar(\n                    message = wannaPlayStr,\n                    actionLabel = yeahStr,\n                    duration = SnackbarDuration.Short\n                )\n                if (result == SnackbarResult.ActionPerformed) {\n                    playerConnection?.playQueue(YouTubeQueue(WatchEndpoint(videoId = favoriteSongVideoId)))\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun SectionHeader(title: String) {\n    Text(\n        text = title,\n        style = MaterialTheme.typography.labelLarge,\n        color = MaterialTheme.colorScheme.primary,\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(horizontal = 24.dp)\n            .padding(bottom = 8.dp, top = 8.dp),\n        textAlign = TextAlign.Start\n    )\n}\n\n@Composable\nprivate fun ContributorAvatar(\n    avatarUrl: String,\n    sizeDp: Int,\n    modifier: Modifier = Modifier,\n    shape: Shape = CircleShape,\n    contentDescription: String? = null,\n    onClick: (() -> Unit)? = null\n) {\n    val fallback = painterResource(R.drawable.small_icon)\n    Surface(\n        onClick = onClick ?: {},\n        enabled = onClick != null,\n        modifier = modifier.size(sizeDp.dp),\n        shape = shape,\n        color = MaterialTheme.colorScheme.surfaceContainerHighest,\n        tonalElevation = 4.dp,\n    ) {\n        AsyncImage(\n            model = avatarUrl,\n            contentDescription = contentDescription,\n            contentScale = ContentScale.Crop,\n            modifier = Modifier.fillMaxSize(),\n            placeholder = fallback,\n            fallback = fallback,\n            error = fallback,\n        )\n    }\n}\n\n/** Action button for the 3-segment row under the lead developer */\n@Composable\nprivate fun RowScope.SegmentedActionButton(\n    label: String,\n    iconRes: Int,\n    iconSize: androidx.compose.ui.unit.Dp = 24.dp,\n    onClick: () -> Unit\n) {\n    Surface(\n        onClick = onClick,\n        color = Color.Transparent,\n        modifier = Modifier.weight(1f)\n    ) {\n        Column(\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.Center,\n            modifier = Modifier.height(72.dp)\n        ) {\n            Icon(\n                painter = painterResource(iconRes),\n                contentDescription = null,\n                tint = MaterialTheme.colorScheme.onSurface,\n                modifier = Modifier.size(iconSize)\n            )\n            Spacer(Modifier.height(4.dp))\n            Text(\n                text = label,\n                style = MaterialTheme.typography.labelMedium,\n                color = MaterialTheme.colorScheme.onSurfaceVariant\n            )\n        }\n    }\n}\n\n/** A generic clickable card for secondary actions like Buy Me a Coffee */\n@Composable\nprivate fun ActionCard(\n    title: String,\n    subtitle: String,\n    iconRes: Int,\n    onClick: () -> Unit,\n) {\n    Surface(\n        onClick = onClick,\n        shape = RoundedCornerShape(24.dp),\n        color = MaterialTheme.colorScheme.surfaceContainerHigh,\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(horizontal = 24.dp)\n    ) {\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n            modifier = Modifier.padding(20.dp)\n        ) {\n            Surface(\n                shape = CircleShape,\n                color = MaterialTheme.colorScheme.primaryContainer,\n                modifier = Modifier.size(48.dp)\n            ) {\n                Box(contentAlignment = Alignment.Center) {\n                    androidx.compose.foundation.Image(\n                        painter = painterResource(iconRes),\n                        contentDescription = null,\n                        colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimaryContainer),\n                        contentScale = ContentScale.Fit,\n                        modifier = Modifier.size(24.dp)\n                    )\n                }\n            }\n            \n            Spacer(Modifier.width(20.dp))\n            Column {\n                Text(\n                    text = title,\n                    style = MaterialTheme.typography.titleMedium,\n                    fontWeight = FontWeight.Bold,\n                    color = MaterialTheme.colorScheme.onSurface\n                )\n                Spacer(Modifier.height(2.dp))\n                Text(\n                    text = subtitle,\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant\n                )\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun AboutScreen(\n    navController: NavController,\n    scrollBehavior: TopAppBarScrollBehavior,\n) {\n    val uriHandler = LocalUriHandler.current\n    val playerConnection = LocalPlayerConnection.current\n    val coroutineScope = rememberCoroutineScope()\n    val localSnackbarHostState = remember { SnackbarHostState() }\n    val wannaPlayStr = stringResource(R.string.wanna_play_favorite_song)\n    val yeahStr = stringResource(R.string.yeah)\n    \n    Box(modifier = Modifier.fillMaxSize()) {\n        Column(\n            modifier = Modifier\n                .fillMaxWidth()\n                .windowInsetsPadding(\n                    LocalPlayerAwareWindowInsets.current.only(\n                        WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom\n                    )\n                )\n                .verticalScroll(rememberScrollState())\n                .nestedScroll(scrollBehavior.nestedScrollConnection),\n            horizontalAlignment = Alignment.CenterHorizontally,\n        ) {\n            Spacer(\n                Modifier.windowInsetsPadding(\n                    LocalPlayerAwareWindowInsets.current.only(WindowInsetsSides.Top)\n                )\n            )\n    \n            Spacer(Modifier.height(16.dp))\n    \n            Surface(\n                shape = RoundedCornerShape(32.dp),\n                color = MaterialTheme.colorScheme.surfaceContainer,\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = 24.dp)\n            ) {\n                Column(\n                    horizontalAlignment = Alignment.CenterHorizontally,\n                    modifier = Modifier.padding(vertical = 32.dp)\n                ) {\n                    Box(\n                        modifier = Modifier\n                            .size(80.dp)\n                            .clip(MaterialShapes.SoftBurst.toShape())\n                            .background(MaterialTheme.colorScheme.primaryContainer),\n                        contentAlignment = Alignment.Center\n                    ) {\n                        androidx.compose.foundation.Image(\n                            painter = painterResource(R.drawable.small_icon),\n                            contentDescription = stringResource(R.string.metrolist),\n                            colorFilter = ColorFilter.tint(\n                                color = MaterialTheme.colorScheme.onPrimaryContainer,\n                                blendMode = BlendMode.SrcIn,\n                            ),\n                            modifier = Modifier.size(40.dp)\n                        )\n                    }\n            \n                    Spacer(Modifier.height(16.dp))\n            \n                    Text(\n                        text = stringResource(R.string.metrolist),\n                        style = MaterialTheme.typography.headlineMedium,\n                        fontWeight = FontWeight.Black,\n                        color = MaterialTheme.colorScheme.onSurface,\n                        letterSpacing = MaterialTheme.typography.headlineMedium.letterSpacing\n                    )\n            \n                    Spacer(Modifier.height(8.dp))\n            \n                    Surface(\n                        shape = CircleShape,\n                        color = MaterialTheme.colorScheme.secondaryContainer\n                    ) {\n                        val archText = BuildConfig.ARCHITECTURE.uppercase()\n                        val versionText = if (BuildConfig.DEBUG) {\n                            stringResource(R.string.app_version_info, BuildConfig.VERSION_NAME, \"$archText • DEBUG\")\n                        } else {\n                            stringResource(R.string.app_version_info, BuildConfig.VERSION_NAME, archText)\n                        }\n                        Text(\n                            text = versionText,\n                            style = MaterialTheme.typography.labelMedium,\n                            fontWeight = FontWeight.Bold,\n                            color = MaterialTheme.colorScheme.onSecondaryContainer,\n                            modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp)\n                        )\n                    }\n                }\n            }\n    \n            Spacer(Modifier.height(32.dp))\n    \n            LinearWavyProgressIndicator(\n                progress = { 1f },\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = 48.dp),\n                color = MaterialTheme.colorScheme.secondary.copy(alpha = 0.5f),\n                trackColor = Color.Transparent,\n                amplitude = { 1f }\n            )\n    \n            Spacer(Modifier.height(32.dp))\n    \n            SectionHeader(stringResource(R.string.credits_lead_developer))\n    \n            var leadClickCount by remember(leadDeveloper.name) { mutableIntStateOf(0) }\n    \n            // Large Avatar\n            ContributorAvatar(\n                avatarUrl = leadDeveloper.avatarUrl,\n                sizeDp = 180,\n                shape = leadDeveloper.polygon?.toShape() ?: CircleShape,\n                contentDescription = leadDeveloper.name,\n                onClick = {\n                    handleEasterEggClick(\n                        clickCount = leadClickCount,\n                        favoriteSongVideoId = leadDeveloper.favoriteSongVideoId,\n                        coroutineScope = coroutineScope,\n                        snackbarHostState = localSnackbarHostState,\n                        playerConnection = playerConnection,\n                        wannaPlayStr = wannaPlayStr,\n                        yeahStr = yeahStr,\n                        onCountUpdate = { leadClickCount = it }\n                    )\n                }\n            )\n    \n            Spacer(Modifier.height(24.dp))\n    \n            Text(\n                text = leadDeveloper.name,\n                style = MaterialTheme.typography.headlineSmall,\n                fontWeight = FontWeight.Bold,\n                color = MaterialTheme.colorScheme.onSurface,\n            )\n    \n            Spacer(Modifier.height(32.dp))\n    \n            // Segmented buttons (Website, GitHub, Instagram)\n            Surface(\n                shape = RoundedCornerShape(24.dp),\n                color = MaterialTheme.colorScheme.surfaceContainerHigh,\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = 24.dp)\n            ) {\n                Row(modifier = Modifier.fillMaxWidth()) {\n                    SegmentedActionButton(\n                        label = stringResource(R.string.credits_website),\n                        iconRes = R.drawable.language,\n                        iconSize = 24.dp,\n                        onClick = { uriHandler.openUri(\"https://metrolist.meowery.eu\") }\n                    )\n                    \n                    Box(modifier = Modifier.width(1.dp).height(72.dp).background(MaterialTheme.colorScheme.outlineVariant.copy(alpha=0.5f)))\n                    \n                    SegmentedActionButton(\n                        label = stringResource(R.string.credits_github),\n                        iconRes = R.drawable.github,\n                        iconSize = 24.dp,\n                        onClick = { uriHandler.openUri(\"https://github.com/mostafaalagamy\") }\n                    )\n                    \n                    Box(modifier = Modifier.width(1.dp).height(72.dp).background(MaterialTheme.colorScheme.outlineVariant.copy(alpha=0.5f)))\n                    \n                    SegmentedActionButton(\n                        label = stringResource(R.string.credits_instagram),\n                        iconRes = R.drawable.instagram,\n                        iconSize = 20.dp,\n                        onClick = { uriHandler.openUri(\"https://www.instagram.com/mostafaalagamy\") }\n                    )\n                }\n            }\n    \n            Spacer(Modifier.height(16.dp))\n    \n            ActionCard(\n                title = stringResource(R.string.like_what_i_do),\n                subtitle = stringResource(R.string.buy_mo_a_coffee),\n                iconRes = R.drawable.buymeacoffee,\n                onClick = { uriHandler.openUri(\"https://buymeacoffee.com/mostafaalagamy\") }\n            )\n    \n            Spacer(Modifier.height(48.dp))\n    \n            SectionHeader(stringResource(R.string.credits_collaborators_section))\n    \n            Surface(\n                shape = RoundedCornerShape(24.dp),\n                color = MaterialTheme.colorScheme.surfaceContainerLow,\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = 24.dp)\n            ) {\n                Column(modifier = Modifier.padding(vertical = 8.dp)) {\n                    collaborators.forEachIndexed { index, contributor ->\n                        var clickCount by remember(contributor.name) { mutableIntStateOf(0) }\n                        ListItem(\n                            headlineContent = {\n                                Text(\n                                    text = contributor.name,\n                                    fontWeight = FontWeight.SemiBold\n                                )\n                            },\n                            supportingContent = { Text(stringResource(contributor.roleRes)) },\n                            leadingContent = {\n                                    ContributorAvatar(\n                                        avatarUrl = contributor.avatarUrl,\n                                        sizeDp = 56,\n                                        shape = contributor.polygon?.toShape() ?: CircleShape,\n                                        contentDescription = contributor.name,\n                                        onClick = {\n                                        handleEasterEggClick(\n                                            clickCount = clickCount,\n                                            favoriteSongVideoId = contributor.favoriteSongVideoId,\n                                            coroutineScope = coroutineScope,\n                                            snackbarHostState = localSnackbarHostState,\n                                            playerConnection = playerConnection,\n                                            wannaPlayStr = wannaPlayStr,\n                                            yeahStr = yeahStr,\n                                            onCountUpdate = { clickCount = it }\n                                        )\n                                    }\n                                )\n                            },\n                            trailingContent = {\n                                Icon(\n                                    painter = painterResource(R.drawable.github),\n                                    contentDescription = stringResource(R.string.credits_github),\n                                    modifier = Modifier.size(20.dp)\n                                )\n                            },\n                            colors = ListItemDefaults.colors(containerColor = Color.Transparent),\n                            modifier = Modifier.clickable { uriHandler.openUri(contributor.githubUrl) }\n                        )\n                        \n                        if (index < collaborators.lastIndex) {\n                            HorizontalDivider(\n                                modifier = Modifier.padding(horizontal = 20.dp),\n                                color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)\n                            )\n                        }\n                    }\n                }\n            }\n    \n            Spacer(Modifier.height(32.dp))\n    \n            SectionHeader(stringResource(R.string.community_and_info))\n    \n            Surface(\n                shape = RoundedCornerShape(24.dp),\n                color = MaterialTheme.colorScheme.surfaceContainerLow,\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = 24.dp)\n            ) {\n                Column(modifier = Modifier.padding(vertical = 8.dp)) {\n                    communityLinks.forEachIndexed { index, link ->\n                        ListItem(\n                            headlineContent = { Text(stringResource(link.labelRes), fontWeight = FontWeight.SemiBold) },\n                            supportingContent = if (link.labelRes == R.string.credits_license_name) {\n                                { Text(stringResource(R.string.credits_license_desc)) }\n                            } else null,\n                            leadingContent = { Icon(painterResource(link.iconRes), null, modifier = Modifier.size(24.dp)) },\n                            colors = ListItemDefaults.colors(containerColor = Color.Transparent),\n                            modifier = Modifier.clickable { uriHandler.openUri(link.url) }\n                        )\n                        \n                        if (index < communityLinks.lastIndex) {\n                            HorizontalDivider(\n                                modifier = Modifier.padding(horizontal = 20.dp),\n                                color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)\n                            )\n                        }\n                    }\n                }\n            }\n    \n            Spacer(Modifier.height(32.dp))\n            \n            Text(\n                text = stringResource(R.string.stands_with_palestine),\n                style = MaterialTheme.typography.labelLarge,\n                fontWeight = FontWeight.Bold,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n            )\n            \n            Spacer(Modifier.height(40.dp))\n        }\n\n        TopAppBar(\n            title = { Text(stringResource(R.string.about)) },\n            navigationIcon = {\n                IconButton(\n                    onClick = navController::navigateUp,\n                    onLongClick = navController::backToMain,\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.arrow_back),\n                        contentDescription = stringResource(R.string.cd_back),\n                    )\n                }\n            },\n            scrollBehavior = scrollBehavior,\n        )\n\n        androidx.compose.material3.SnackbarHost(\n            hostState = localSnackbarHostState,\n            modifier = Modifier\n                .align(Alignment.BottomCenter)\n                .windowInsetsPadding(\n                    LocalPlayerAwareWindowInsets.current.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom)\n                )\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/settings/AccountSettings.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.settings\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.Badge\nimport androidx.compose.material3.BadgedBox\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedButton\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.SwitchDefaults\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport kotlinx.coroutines.launch\nimport timber.log.Timber\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalUriHandler\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport coil3.compose.AsyncImage\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.utils.parseCookieString\nimport com.metrolist.music.BuildConfig\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.AccountChannelHandleKey\nimport com.metrolist.music.constants.AccountEmailKey\nimport com.metrolist.music.constants.AccountNameKey\nimport com.metrolist.music.constants.DataSyncIdKey\nimport com.metrolist.music.constants.InnerTubeCookieKey\nimport com.metrolist.music.constants.UseLoginForBrowse\nimport com.metrolist.music.constants.VisitorDataKey\nimport com.metrolist.music.constants.YtmSyncKey\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.InfoLabel\nimport com.metrolist.music.ui.component.Material3SettingsGroup\nimport com.metrolist.music.ui.component.Material3SettingsItem\nimport com.metrolist.music.ui.component.PreferenceEntry\nimport com.metrolist.music.ui.component.TextFieldDialog\nimport com.metrolist.music.utils.Updater\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.viewmodels.AccountSettingsViewModel\nimport com.metrolist.music.viewmodels.HomeViewModel\n\n@Composable\nfun AccountSettings(\n    navController: NavController,\n    onClose: () -> Unit,\n    latestVersionName: String\n) {\n    val context = LocalContext.current\n    val uriHandler = LocalUriHandler.current\n\n    val (accountNamePref, onAccountNameChange) = rememberPreference(AccountNameKey, \"\")\n    val (accountEmail, onAccountEmailChange) = rememberPreference(AccountEmailKey, \"\")\n    val (accountChannelHandle, onAccountChannelHandleChange) = rememberPreference(AccountChannelHandleKey, \"\")\n    val (innerTubeCookie, onInnerTubeCookieChange) = rememberPreference(InnerTubeCookieKey, \"\")\n    val (visitorData, onVisitorDataChange) = rememberPreference(VisitorDataKey, \"\")\n    val (dataSyncId, onDataSyncIdChange) = rememberPreference(DataSyncIdKey, \"\")\n\n    val isLoggedIn = remember(innerTubeCookie) {\n        \"SAPISID\" in parseCookieString(innerTubeCookie)\n    }\n    val (useLoginForBrowse, onUseLoginForBrowseChange) = rememberPreference(UseLoginForBrowse, true)\n    val (ytmSync, onYtmSyncChange) = rememberPreference(YtmSyncKey, true)\n\n    val homeViewModel: HomeViewModel = hiltViewModel()\n    val accountSettingsViewModel: AccountSettingsViewModel = hiltViewModel()\n    val accountName by homeViewModel.accountName.collectAsState()\n    val accountImageUrl by homeViewModel.accountImageUrl.collectAsState()\n\n    var showToken by remember { mutableStateOf(false) }\n    var showTokenEditor by remember { mutableStateOf(false) }\n    var showLogoutDialog by remember { mutableStateOf(false) }\n    val scope = rememberCoroutineScope()\n\n    Column(\n        modifier = Modifier\n            .background(MaterialTheme.colorScheme.surfaceContainer)\n            .padding(16.dp)\n            .verticalScroll(rememberScrollState())\n    ) {\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(start = 8.dp, end = 8.dp),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Text(\n                text = stringResource(id = R.string.app_name),\n                style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),\n                modifier = Modifier.padding(start = 4.dp)\n            )\n            Spacer(modifier = Modifier.weight(1f))\n            IconButton(onClick = onClose) {\n                Icon(painterResource(R.drawable.close), contentDescription = null)\n            }\n        }\n\n        Spacer(Modifier.height(12.dp))\n\n        // Logout confirmation dialog\n        if (showLogoutDialog) {\n            DefaultDialog(\n                onDismiss = { showLogoutDialog = false },\n                title = { Text(stringResource(R.string.logout_dialog_title)) },\n                content = {\n                    Text(\n                        text = stringResource(R.string.logout_dialog_message),\n                        style = MaterialTheme.typography.bodyLarge,\n                        modifier = Modifier.padding(horizontal = 18.dp)\n                    )\n                },\n                buttons = {\n                    TextButton(\n                        onClick = {\n                            Timber.d(\"[LOGOUT_CLEAR] User chose to clear data\")\n                            scope.launch {\n                                Timber.d(\"[LOGOUT_CLEAR] Starting clear and logout process\")\n                                accountSettingsViewModel.clearAllLibraryData()\n                                Timber.d(\"[LOGOUT_CLEAR] Library data cleared, now logging out\")\n                                accountSettingsViewModel.logoutKeepData(context, onInnerTubeCookieChange)\n                                Timber.d(\"[LOGOUT_CLEAR] Logout complete\")\n                                showLogoutDialog = false\n                                onClose()\n                            }\n                        }\n                    ) {\n                        Text(stringResource(R.string.logout_clear))\n                    }\n                    TextButton(\n                        onClick = {\n                            Timber.d(\"[LOGOUT_KEEP] User chose to keep data\")\n                            scope.launch {\n                                Timber.d(\"[LOGOUT_KEEP] Starting logout process (keeping data)\")\n                                accountSettingsViewModel.logoutKeepData(context, onInnerTubeCookieChange)\n                                Timber.d(\"[LOGOUT_KEEP] Logout complete\")\n                                showLogoutDialog = false\n                                onClose()\n                            }\n                        }\n                    ) {\n                        Text(stringResource(R.string.logout_keep))\n                    }\n                }\n            )\n        }\n\n        if (showTokenEditor) {\n            val text = \"\"\"\n                ***INNERTUBE COOKIE*** =$innerTubeCookie\n                ***VISITOR DATA*** =$visitorData\n                ***DATASYNC ID*** =$dataSyncId\n                ***ACCOUNT NAME*** =$accountNamePref\n                ***ACCOUNT EMAIL*** =$accountEmail\n                ***ACCOUNT CHANNEL HANDLE*** =$accountChannelHandle\n            \"\"\".trimIndent()\n\n            TextFieldDialog(\n                initialTextFieldValue = TextFieldValue(text),\n                onDone = { data ->\n                    var cookie = \"\"\n                    var visitorDataValue = \"\"\n                    var dataSyncIdValue = \"\"\n                    var accountNameValue = \"\"\n                    var accountEmailValue = \"\"\n                    var accountChannelHandleValue = \"\"\n\n                    data.split(\"\\n\").forEach {\n                        when {\n                            it.startsWith(\"***INNERTUBE COOKIE*** =\") -> cookie = it.substringAfter(\"=\")\n                            it.startsWith(\"***VISITOR DATA*** =\") -> visitorDataValue = it.substringAfter(\"=\")\n                            it.startsWith(\"***DATASYNC ID*** =\") -> dataSyncIdValue = it.substringAfter(\"=\")\n                            it.startsWith(\"***ACCOUNT NAME*** =\") -> accountNameValue = it.substringAfter(\"=\")\n                            it.startsWith(\"***ACCOUNT EMAIL*** =\") -> accountEmailValue = it.substringAfter(\"=\")\n                            it.startsWith(\"***ACCOUNT CHANNEL HANDLE*** =\") -> accountChannelHandleValue = it.substringAfter(\"=\")\n                        }\n                    }\n                    // Write all credentials atomically to DataStore and wait for completion\n                    // before restarting, preventing the race condition where the process\n                    // would be killed before async DataStore coroutines finished writing.\n                    accountSettingsViewModel.saveTokenAndRestart(\n                        context = context,\n                        cookie = cookie,\n                        visitorData = visitorDataValue,\n                        dataSyncId = dataSyncIdValue,\n                        accountName = accountNameValue,\n                        accountEmail = accountEmailValue,\n                        accountChannelHandle = accountChannelHandleValue,\n                    )\n                },\n                onDismiss = { showTokenEditor = false },\n                singleLine = false,\n                maxLines = 20,\n                isInputValid = { fullText ->\n                    // Extract the cookie value from the formatted template line,\n                    // then validate it separately — avoids the bug where parseCookieString\n                    // received the entire multi-line template and failed to find \"SAPISID\"\n                    // as a key because the \"***INNERTUBE COOKIE*** =\" prefix shadowed it.\n                    val cookieLine = fullText.lines()\n                        .find { it.startsWith(\"***INNERTUBE COOKIE*** =\") }\n                    val cookieValue = cookieLine?.substringAfter(\"***INNERTUBE COOKIE*** =\")?.trim() ?: \"\"\n                    cookieValue.isNotEmpty() && \"SAPISID\" in parseCookieString(cookieValue)\n                },\n                extraContent = {\n                    Spacer(Modifier.height(8.dp))\n                    InfoLabel(text = stringResource(R.string.token_adv_login_description))\n                }\n            )\n        }\n\n        Material3SettingsGroup(\n            items = listOf(\n                Material3SettingsItem(\n                    title = {\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically\n                        ) {\n                            if (isLoggedIn && accountImageUrl != null) {\n                                AsyncImage(\n                                    model = accountImageUrl,\n                                    contentDescription = null,\n                                    contentScale = ContentScale.Crop,\n                                    modifier = Modifier.size(40.dp).clip(CircleShape)\n                                )\n\n                                Spacer(Modifier.width(12.dp))\n                            }\n\n                            Text(\n                                text = if (isLoggedIn) accountName else stringResource(R.string.login),\n                            )\n                        }\n                    },\n                    icon = if (!isLoggedIn) painterResource(R.drawable.login) else null,\n                    trailingContent = {\n                        if (isLoggedIn) {\n                            OutlinedButton(\n                                onClick = {\n                                    Timber.d(\"[LOGOUT] User clicked logout button, showing dialog\")\n                                    showLogoutDialog = true\n                                },\n                                colors = ButtonDefaults.outlinedButtonColors(\n                                    containerColor = MaterialTheme.colorScheme.surfaceContainer,\n                                    contentColor = MaterialTheme.colorScheme.onSurface\n                                )\n                            ) {\n                                Text(stringResource(R.string.action_logout))\n                            }\n                        }\n                    },\n                    onClick = {\n                        onClose()\n                        if (isLoggedIn) {\n                            navController.navigate(\"account\")\n                        } else {\n                            navController.navigate(\"login\")\n                        }\n                    }\n                )\n            ),\n            useLowContrast = true\n        )\n\n        Spacer(Modifier.height(8.dp))\n\n        Material3SettingsGroup(\n            items = listOf(\n                Material3SettingsItem(\n                    title = {\n                        Text(\n                            when {\n                                !isLoggedIn -> stringResource(R.string.advanced_login)\n                                showToken -> stringResource(R.string.token_shown)\n                                else -> stringResource(R.string.token_hidden)\n                            }\n                        )\n                    },\n                    icon = painterResource(R.drawable.token),\n                    onClick = {\n                        if (!isLoggedIn) showTokenEditor = true\n                        else if (!showToken) showToken = true\n                        else showTokenEditor = true\n                    }\n                ),\n                Material3SettingsItem(\n                    title = { Text(stringResource(R.string.more_content)) },\n                    icon = painterResource(R.drawable.cached),\n                    trailingContent = {\n                        Switch(\n                            enabled = isLoggedIn,\n                            checked = useLoginForBrowse,\n                            onCheckedChange = {\n                                YouTube.useLoginForBrowse = it\n                                onUseLoginForBrowseChange(it)\n                            },\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (useLoginForBrowse) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    enabled = isLoggedIn\n                ),\n                Material3SettingsItem(\n                    title = { Text(stringResource(R.string.yt_sync)) },\n                    icon = painterResource(R.drawable.cached),\n                    trailingContent = {\n                        Switch(\n                            enabled = isLoggedIn,\n                            checked = ytmSync,\n                            onCheckedChange = onYtmSyncChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (ytmSync) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    enabled = isLoggedIn\n                )\n            ),\n            useLowContrast = true\n        )\n\n        Spacer(Modifier.height(12.dp))\n\n        Column(\n            modifier = Modifier\n                .clip(RoundedCornerShape(16.dp))\n                .background(MaterialTheme.colorScheme.surfaceContainer)\n        ) {\n            PreferenceEntry(\n                title = { Text(stringResource(R.string.integrations)) },\n                icon = { Icon(painterResource(R.drawable.integration), null) },\n                onClick = {\n                    onClose()\n                    navController.navigate(\"settings/integrations\")\n                },\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .background(MaterialTheme.colorScheme.surfaceContainer)\n            )\n\n            Spacer(Modifier.height(4.dp))\n\n            PreferenceEntry(\n                title = { Text(stringResource(R.string.settings)) },\n                icon = {\n                    BadgedBox(\n                        badge = {\n                            if (BuildConfig.UPDATER_AVAILABLE && latestVersionName != BuildConfig.VERSION_NAME) {\n                                Badge()\n                            }\n                        }\n                    ) {\n                        Icon(painterResource(R.drawable.settings), contentDescription = null)\n                    }\n                },\n                onClick = {\n                    onClose()\n                    navController.navigate(\"settings\")\n                },\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .background(MaterialTheme.colorScheme.surfaceContainer)\n            )\n\n            Spacer(Modifier.height(4.dp))\n\n            if (BuildConfig.UPDATER_AVAILABLE && latestVersionName != BuildConfig.VERSION_NAME) {\n                val releaseInfo = Updater.getCachedLatestRelease()\n                val downloadUrl = releaseInfo?.let { Updater.getDownloadUrlForCurrentVariant(it) }\n                \n                if (downloadUrl != null) {\n                    PreferenceEntry(\n                        title = {\n                            Text(text = stringResource(R.string.new_version_available))\n                        },\n                        description = latestVersionName,\n                        icon = {\n                            BadgedBox(badge = { Badge() }) {\n                                Icon(painterResource(R.drawable.update), null)\n                            }\n                        },\n                        onClick = {\n                            uriHandler.openUri(downloadUrl)\n                        }\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/settings/AiSettings.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.settings\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.LinkAnnotation\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.TextLinkStyles\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation.NavController\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.AiProviderKey\nimport com.metrolist.music.constants.AiSystemPromptKey\nimport com.metrolist.music.constants.DEFAULT_AI_SYSTEM_PROMPT\nimport com.metrolist.music.constants.DeeplApiKey\nimport com.metrolist.music.constants.DeeplFormalityKey\nimport com.metrolist.music.constants.LanguageCodeToName\nimport com.metrolist.music.constants.OpenRouterApiKey\nimport com.metrolist.music.constants.OpenRouterBaseUrlKey\nimport com.metrolist.music.constants.OpenRouterModelKey\nimport com.metrolist.music.constants.TranslateLanguageKey\nimport com.metrolist.music.constants.TranslateModeKey\nimport com.metrolist.music.ui.component.EnumDialog\nimport com.metrolist.music.ui.component.Material3SettingsGroup\nimport com.metrolist.music.ui.component.Material3SettingsItem\nimport com.metrolist.music.ui.component.TextFieldDialog\nimport com.metrolist.music.utils.rememberPreference\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun AiSettings(navController: NavController) {\n    var aiProvider by rememberPreference(AiProviderKey, \"OpenRouter\")\n    var openRouterApiKey by rememberPreference(OpenRouterApiKey, \"\")\n    var openRouterBaseUrl by rememberPreference(OpenRouterBaseUrlKey, \"https://openrouter.ai/api/v1/chat/completions\")\n    var openRouterModel by rememberPreference(OpenRouterModelKey, \"google/gemini-2.5-flash-lite\")\n    var translateLanguage by rememberPreference(TranslateLanguageKey, \"en\")\n    var translateMode by rememberPreference(TranslateModeKey, \"Literal\")\n    var deeplApiKey by rememberPreference(DeeplApiKey, \"\")\n    var deeplFormality by rememberPreference(DeeplFormalityKey, \"default\")\n    var aiSystemPrompt by rememberPreference(AiSystemPromptKey, \"\")\n\n    val aiProviders =\n        mapOf(\n            \"OpenRouter\" to \"https://openrouter.ai/api/v1/chat/completions\",\n            \"OpenAI\" to \"https://api.openai.com/v1/chat/completions\",\n            \"Perplexity\" to \"https://api.perplexity.ai/chat/completions\",\n            \"Claude\" to \"https://api.anthropic.com/v1/messages\",\n            \"Gemini\" to \"https://generativelanguage.googleapis.com/v1beta/openai/chat/completions\",\n            \"XAi\" to \"https://api.x.ai/v1/chat/completions\",\n            \"Mistral\" to \"https://api.mistral.ai/v1/chat/completions\",\n            \"DeepL\" to \"https://api.deepl.com/v2/translate\",\n            \"Custom\" to \"\",\n        )\n\n    val providerHelpText =\n        mapOf(\n            \"OpenRouter\" to stringResource(R.string.ai_provider_openrouter_help),\n            \"OpenAI\" to stringResource(R.string.ai_provider_openai_help),\n            \"Perplexity\" to stringResource(R.string.ai_provider_perplexity_help),\n            \"Claude\" to stringResource(R.string.ai_provider_claude_help),\n            \"Gemini\" to stringResource(R.string.ai_provider_gemini_help),\n            \"XAi\" to stringResource(R.string.ai_provider_xai_help),\n            \"Mistral\" to stringResource(R.string.ai_provider_mistral_help),\n            \"DeepL\" to stringResource(R.string.ai_provider_deepl_help),\n            \"Custom\" to \"\",\n        )\n\n    val modelsByProvider =\n        mapOf(\n            \"OpenRouter\" to\n                listOf(\n                    \"google/gemini-2.5-flash-lite\",\n                    \"google/gemini-2.5-flash\",\n                    \"x-ai/grok-4.1-fast\",\n                    \"deepseek/deepseek-v3.1-terminus:exacto\",\n                    \"openai/gpt-4o-mini\",\n                    \"meta-llama/llama-4-scout\",\n                    \"openai/gpt-5-nano\",\n                    \"openai/gpt-oss-120b\",\n                    \"google/gemini-3-flash-preview\",\n                ),\n            \"OpenAI\" to\n                listOf(\n                    \"gpt-4o-mini\",\n                    \"gpt-4o\",\n                    \"gpt-4-turbo\",\n                ),\n            \"Claude\" to\n                listOf(\n                    \"claude-opus-4-6\",\n                    \"claude-sonnet-4-6\",\n                    \"claude-haiku-4-5-20251001\",\n                ),\n            \"Gemini\" to\n                listOf(\n                    \"gemini-flash-lite-latest\",\n                    \"gemini-2.5-flash-lite\",\n                    \"gemini-flash-latest\",\n                    \"gemini-2.5-flash\",\n                    \"gemini-3-flash-preview\",\n                ),\n            \"Perplexity\" to\n                listOf(\n                    \"sonar\",\n                    \"sonar-pro\",\n                    \"sonar-reasoning\",\n                ),\n            \"XAi\" to\n                listOf(\n                    \"grok-4-1-fast\",\n                    \"grok-vision-beta\",\n                ),\n            \"Mistral\" to\n                listOf(\n                    \"mistral-large-latest\",\n                    \"mistral-medium-latest\",\n                    \"mistral-small-latest\",\n                    \"mistral-tiny-latest\",\n                ),\n            \"DeepL\" to listOf(),\n            \"Custom\" to listOf(),\n        )\n\n    val commonModels = modelsByProvider[aiProvider] ?: listOf()\n\n    var showProviderDialog by rememberSaveable { mutableStateOf(false) }\n    var showProviderHelpDialog by rememberSaveable { mutableStateOf(false) }\n    var showTranslateModeDialog by rememberSaveable { mutableStateOf(false) }\n    var showTranslateModeHelpDialog by rememberSaveable { mutableStateOf(false) }\n    var showLanguageDialog by rememberSaveable { mutableStateOf(false) }\n    var showApiKeyDialog by rememberSaveable { mutableStateOf(false) }\n    var showDeeplApiKeyDialog by rememberSaveable { mutableStateOf(false) }\n    var showDeeplFormalityDialog by rememberSaveable { mutableStateOf(false) }\n    var showBaseUrlDialog by rememberSaveable { mutableStateOf(false) }\n    var showModelDialog by rememberSaveable { mutableStateOf(false) }\n    var showCustomModelInput by rememberSaveable { mutableStateOf(false) }\n    var showSystemPromptDialog by rememberSaveable { mutableStateOf(false) }\n\n    if (showProviderHelpDialog) {\n        AlertDialog(\n            onDismissRequest = { showProviderHelpDialog = false },\n            confirmButton = {\n                TextButton(onClick = { showProviderHelpDialog = false }) {\n                    Text(stringResource(android.R.string.ok))\n                }\n            },\n            icon = { Icon(painterResource(R.drawable.info), null) },\n            title = { Text(stringResource(R.string.ai_provider_help)) },\n            text = {\n                Column {\n                    providerHelpText.forEach { (provider, help) ->\n                        if (help.isNotEmpty()) {\n                            val primaryColor = MaterialTheme.colorScheme.primary\n                            val annotatedString =\n                                buildAnnotatedString {\n                                    append(\"$provider: \")\n                                    // Extract URL from text\n                                    val urlRegex = \"https?://[^\\\\s]+\".toRegex()\n                                    val match = urlRegex.find(help)\n                                    if (match != null) {\n                                        val url = match.value\n                                        val beforeUrl = help.substring(0, match.range.first)\n                                        val afterUrl = help.substring(match.range.last + 1)\n\n                                        append(beforeUrl)\n                                        val linkStart = length\n                                        append(url)\n                                        val linkEnd = length\n                                        append(afterUrl)\n\n                                        addLink(\n                                            LinkAnnotation.Url(\n                                                url = url,\n                                                styles =\n                                                    TextLinkStyles(\n                                                        style =\n                                                            SpanStyle(\n                                                                color = primaryColor,\n                                                                textDecoration = TextDecoration.Underline,\n                                                            ),\n                                                    ),\n                                            ),\n                                            start = linkStart,\n                                            end = linkEnd,\n                                        )\n                                    } else {\n                                        append(help)\n                                    }\n                                }\n\n                            Text(\n                                text = annotatedString,\n                                style =\n                                    MaterialTheme.typography.bodyMedium.copy(\n                                        color = MaterialTheme.colorScheme.onSurface,\n                                    ),\n                                modifier = Modifier.padding(vertical = 4.dp),\n                            )\n                        }\n                    }\n                }\n            },\n        )\n    }\n\n    if (showTranslateModeHelpDialog) {\n        AlertDialog(\n            onDismissRequest = { showTranslateModeHelpDialog = false },\n            confirmButton = {\n                TextButton(onClick = { showTranslateModeHelpDialog = false }) {\n                    Text(stringResource(android.R.string.ok))\n                }\n            },\n            icon = { Icon(painterResource(R.drawable.info), null) },\n            title = { Text(stringResource(R.string.ai_translation_mode)) },\n            text = {\n                Column {\n                    Text(\n                        text = \"${stringResource(R.string.ai_translation_literal)}:\",\n                        style = MaterialTheme.typography.titleSmall,\n                        modifier = Modifier.padding(top = 8.dp),\n                    )\n                    Text(\n                        text = stringResource(R.string.ai_translation_literal_desc),\n                        style = MaterialTheme.typography.bodyMedium,\n                        modifier = Modifier.padding(bottom = 12.dp),\n                    )\n\n                    Text(\n                        text = \"${stringResource(R.string.ai_translation_transcribed)}:\",\n                        style = MaterialTheme.typography.titleSmall,\n                    )\n                    Text(\n                        text = stringResource(R.string.ai_translation_transcribed_desc),\n                        style = MaterialTheme.typography.bodyMedium,\n                    )\n                }\n            },\n        )\n    }\n\n    if (showProviderDialog) {\n        EnumDialog(\n            onDismiss = { showProviderDialog = false },\n            onSelect = {\n                aiProvider = it\n                if (it != \"Custom\" && it != \"DeepL\") {\n                    openRouterBaseUrl = aiProviders[it] ?: \"\"\n                } else {\n                    openRouterBaseUrl = \"\"\n                }\n                // Set model to first available model for the selected provider\n                val modelsForProvider = modelsByProvider[it] ?: listOf()\n                openRouterModel =\n                    if (modelsForProvider.isNotEmpty()) {\n                        modelsForProvider[0]\n                    } else {\n                        \"\"\n                    }\n                showProviderDialog = false\n            },\n            title = stringResource(R.string.ai_provider),\n            current = aiProvider,\n            values = aiProviders.keys.toList(),\n            valueText = { it },\n        )\n    }\n\n    if (showTranslateModeDialog) {\n        EnumDialog(\n            onDismiss = { showTranslateModeDialog = false },\n            onSelect = {\n                translateMode = it\n                showTranslateModeDialog = false\n            },\n            title = stringResource(R.string.ai_translation_mode),\n            current = translateMode,\n            values = listOf(\"Literal\", \"Transcribed\"),\n            valueText = {\n                when (it) {\n                    \"Literal\" -> stringResource(R.string.ai_translation_literal)\n                    \"Transcribed\" -> stringResource(R.string.ai_translation_transcribed)\n                    else -> it\n                }\n            },\n        )\n    }\n\n    if (showLanguageDialog) {\n        EnumDialog(\n            onDismiss = { showLanguageDialog = false },\n            onSelect = {\n                translateLanguage = it\n                showLanguageDialog = false\n            },\n            title = stringResource(R.string.ai_target_language),\n            current = translateLanguage,\n            values = LanguageCodeToName.keys.sortedBy { LanguageCodeToName[it] },\n            valueText = { LanguageCodeToName[it] ?: it },\n        )\n    }\n\n    if (showApiKeyDialog) {\n        TextFieldDialog(\n            title = { Text(stringResource(R.string.ai_api_key)) },\n            icon = { Icon(painterResource(R.drawable.key), null) },\n            initialTextFieldValue = TextFieldValue(text = openRouterApiKey),\n            onDone = {\n                openRouterApiKey = it\n                showApiKeyDialog = false\n            },\n            onDismiss = { showApiKeyDialog = false },\n        )\n    }\n\n    if (showDeeplApiKeyDialog) {\n        TextFieldDialog(\n            title = { Text(\"DeepL ${stringResource(R.string.ai_api_key)}\") },\n            icon = { Icon(painterResource(R.drawable.key), null) },\n            initialTextFieldValue = TextFieldValue(text = deeplApiKey),\n            onDone = {\n                deeplApiKey = it\n                showDeeplApiKeyDialog = false\n            },\n            onDismiss = { showDeeplApiKeyDialog = false },\n        )\n    }\n\n    if (showDeeplFormalityDialog) {\n        EnumDialog(\n            onDismiss = { showDeeplFormalityDialog = false },\n            onSelect = {\n                deeplFormality = it\n                showDeeplFormalityDialog = false\n            },\n            title = stringResource(R.string.ai_deepl_formality),\n            current = deeplFormality,\n            values = listOf(\"default\", \"more\", \"less\"),\n            valueText = {\n                when (it) {\n                    \"default\" -> stringResource(R.string.ai_deepl_formality_default)\n                    \"more\" -> stringResource(R.string.ai_deepl_formality_more)\n                    \"less\" -> stringResource(R.string.ai_deepl_formality_less)\n                    else -> it\n                }\n            },\n        )\n    }\n\n    if (showBaseUrlDialog && aiProvider == \"Custom\") {\n        TextFieldDialog(\n            title = { Text(stringResource(R.string.ai_base_url)) },\n            icon = { Icon(painterResource(R.drawable.link), null) },\n            initialTextFieldValue = TextFieldValue(text = openRouterBaseUrl),\n            onDone = {\n                openRouterBaseUrl = it\n                showBaseUrlDialog = false\n            },\n            onDismiss = { showBaseUrlDialog = false },\n        )\n    }\n\n    if (showModelDialog) {\n        EnumDialog(\n            onDismiss = { showModelDialog = false },\n            onSelect = {\n                if (it == \"custom_input\") {\n                    showCustomModelInput = true\n                    showModelDialog = false\n                } else {\n                    openRouterModel = it\n                    showModelDialog = false\n                }\n            },\n            title = stringResource(R.string.ai_model),\n            current = if (openRouterModel in commonModels) openRouterModel else \"custom_input\",\n            values = commonModels + \"custom_input\",\n            valueText = {\n                if (it == \"custom_input\") \"Custom\" else it\n            },\n        )\n    }\n\n    if (showCustomModelInput) {\n        TextFieldDialog(\n            title = { Text(stringResource(R.string.ai_model)) },\n            icon = { Icon(painterResource(R.drawable.discover_tune), null) },\n            initialTextFieldValue = TextFieldValue(text = openRouterModel),\n            onDone = {\n                openRouterModel = it\n                showCustomModelInput = false\n            },\n            onDismiss = { showCustomModelInput = false },\n        )\n    }\n\n    if (showSystemPromptDialog) {\n        TextFieldDialog(\n            title = { Text(stringResource(R.string.ai_system_prompt)) },\n            icon = { Icon(painterResource(R.drawable.edit), null) },\n            initialTextFieldValue = TextFieldValue(text = aiSystemPrompt.ifBlank { DEFAULT_AI_SYSTEM_PROMPT }),\n            singleLine = false,\n            maxLines = 12,\n            isInputValid = { true },\n            onDone = {\n                // Treat saving the unmodified default (or blank) as \"use default\"\n                aiSystemPrompt = if (it.isBlank() || it == DEFAULT_AI_SYSTEM_PROMPT) \"\" else it\n                showSystemPromptDialog = false\n            },\n            onDismiss = { showSystemPromptDialog = false },\n            extraContent = {\n                if (aiSystemPrompt.isNotBlank()) {\n                    Row(\n                        modifier =\n                            Modifier\n                                .fillMaxWidth()\n                                .padding(top = 8.dp),\n                        horizontalArrangement = Arrangement.End,\n                    ) {\n                        TextButton(\n                            onClick = {\n                                aiSystemPrompt = \"\"\n                                showSystemPromptDialog = false\n                            },\n                        ) {\n                            Text(stringResource(R.string.ai_system_prompt_reset))\n                        }\n                    }\n                }\n            },\n        )\n    }\n\n    Column(\n        Modifier\n            .windowInsetsPadding(\n                LocalPlayerAwareWindowInsets.current.only(\n                    WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom,\n                ),\n            ).verticalScroll(rememberScrollState())\n            .padding(horizontal = 16.dp),\n    ) {\n        Spacer(\n            Modifier.windowInsetsPadding(\n                LocalPlayerAwareWindowInsets.current.only(\n                    WindowInsetsSides.Top,\n                ),\n            ),\n        )\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.ai_provider),\n            items =\n                listOf(\n                    Material3SettingsItem(\n                        icon = painterResource(R.drawable.explore_outlined),\n                        title = { Text(stringResource(R.string.ai_provider)) },\n                        description = { Text(aiProvider) },\n                        onClick = { showProviderDialog = true },\n                        trailingContent = {\n                            IconButton(onClick = { showProviderHelpDialog = true }) {\n                                Icon(\n                                    painterResource(R.drawable.info),\n                                    contentDescription = stringResource(R.string.ai_provider_help),\n                                    modifier = Modifier.size(20.dp),\n                                )\n                            }\n                        },\n                    ),\n                    if (aiProvider == \"Custom\") {\n                        Material3SettingsItem(\n                            icon = painterResource(R.drawable.link),\n                            title = { Text(stringResource(R.string.ai_base_url)) },\n                            description = { Text(openRouterBaseUrl.ifBlank { stringResource(R.string.not_set) }) },\n                            onClick = { showBaseUrlDialog = true },\n                        )\n                    } else {\n                        null\n                    },\n                ).filterNotNull(),\n        )\n\n        Spacer(modifier = Modifier.height(27.dp))\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.ai_setup_guide),\n            items =\n                buildList {\n                    if (aiProvider == \"DeepL\") {\n                        add(\n                            Material3SettingsItem(\n                                icon = painterResource(R.drawable.key),\n                                title = { Text(\"DeepL ${stringResource(R.string.ai_api_key)}\") },\n                                description = {\n                                    Text(\n                                        if (deeplApiKey.isNotEmpty()) {\n                                            \"•\".repeat(minOf(deeplApiKey.length, 8))\n                                        } else {\n                                            stringResource(R.string.not_set)\n                                        },\n                                    )\n                                },\n                                onClick = { showDeeplApiKeyDialog = true },\n                            ),\n                        )\n                        add(\n                            Material3SettingsItem(\n                                icon = painterResource(R.drawable.tune),\n                                title = { Text(stringResource(R.string.ai_deepl_formality)) },\n                                description = {\n                                    Text(\n                                        when (deeplFormality) {\n                                            \"default\" -> stringResource(R.string.ai_deepl_formality_default)\n                                            \"more\" -> stringResource(R.string.ai_deepl_formality_more)\n                                            \"less\" -> stringResource(R.string.ai_deepl_formality_less)\n                                            else -> deeplFormality\n                                        },\n                                    )\n                                },\n                                onClick = { showDeeplFormalityDialog = true },\n                            ),\n                        )\n                    } else {\n                        add(\n                            Material3SettingsItem(\n                                icon = painterResource(R.drawable.key),\n                                title = { Text(stringResource(R.string.ai_api_key)) },\n                                description = {\n                                    Text(\n                                        if (openRouterApiKey.isNotEmpty()) {\n                                            \"•\".repeat(minOf(openRouterApiKey.length, 8))\n                                        } else {\n                                            stringResource(R.string.not_set)\n                                        },\n                                    )\n                                },\n                                onClick = { showApiKeyDialog = true },\n                            ),\n                        )\n                        add(\n                            Material3SettingsItem(\n                                icon = painterResource(R.drawable.discover_tune),\n                                title = { Text(stringResource(R.string.ai_model)) },\n                                description = { Text(openRouterModel.ifBlank { stringResource(R.string.not_set) }) },\n                                onClick = { showModelDialog = true },\n                            ),\n                        )\n                    }\n                },\n        )\n\n        Spacer(modifier = Modifier.height(27.dp))\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.ai_translation_mode),\n            items =\n                buildList {\n                    if (aiProvider != \"DeepL\") {\n                        add(\n                            Material3SettingsItem(\n                                icon = painterResource(R.drawable.translate),\n                                title = { Text(stringResource(R.string.ai_translation_mode)) },\n                                description = {\n                                    Text(\n                                        when (translateMode) {\n                                            \"Literal\" -> stringResource(R.string.ai_translation_literal)\n                                            \"Transcribed\" -> stringResource(R.string.ai_translation_transcribed)\n                                            else -> translateMode\n                                        },\n                                    )\n                                },\n                                onClick = { showTranslateModeDialog = true },\n                                trailingContent = {\n                                    IconButton(onClick = { showTranslateModeHelpDialog = true }) {\n                                        Icon(\n                                            painterResource(R.drawable.info),\n                                            contentDescription = null,\n                                            modifier = Modifier.size(20.dp),\n                                        )\n                                    }\n                                },\n                            ),\n                        )\n                        add(\n                            Material3SettingsItem(\n                                icon = painterResource(R.drawable.edit),\n                                title = { Text(stringResource(R.string.ai_system_prompt)) },\n                                description = {\n                                    Text(\n                                        if (aiSystemPrompt.isNotBlank()) {\n                                            aiSystemPrompt.take(60).let {\n                                                if (aiSystemPrompt.length > 60) \"$it…\" else it\n                                            }\n                                        } else {\n                                            stringResource(R.string.ai_system_prompt_default)\n                                        },\n                                    )\n                                },\n                                onClick = { showSystemPromptDialog = true },\n                            ),\n                        )\n                    }\n                    add(\n                        Material3SettingsItem(\n                            icon = painterResource(R.drawable.language),\n                            title = { Text(stringResource(R.string.ai_target_language)) },\n                            description = { Text(LanguageCodeToName[translateLanguage] ?: translateLanguage) },\n                            onClick = { showLanguageDialog = true },\n                        ),\n                    )\n                },\n        )\n\n        Spacer(modifier = Modifier.height(16.dp))\n    }\n\n    TopAppBar(\n        title = { Text(stringResource(R.string.ai_lyrics_translation)) },\n        navigationIcon = {\n            IconButton(onClick = { navController.navigateUp() }) {\n                Icon(\n                    painterResource(R.drawable.arrow_back),\n                    contentDescription = null,\n                )\n            }\n        },\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/settings/AlarmSettings.kt",
    "content": "package com.metrolist.music.ui.screens.settings\n\nimport android.app.AlarmManager\nimport android.content.ActivityNotFoundException\nimport android.content.Intent\nimport android.os.Build\nimport android.os.PowerManager\nimport android.provider.Settings\nimport android.widget.Toast\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedButton\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.SwitchDefaults\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TimePicker\nimport androidx.compose.material3.rememberTimePickerState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalLocale\nimport androidx.compose.ui.res.pluralStringResource\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.core.net.toUri\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.R\nimport com.metrolist.music.db.entities.Playlist\nimport com.metrolist.music.playback.alarm.MusicAlarmEntry\nimport com.metrolist.music.playback.alarm.MusicAlarmScheduler\nimport com.metrolist.music.playback.alarm.MusicAlarmStore\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.Material3SettingsGroup\nimport com.metrolist.music.ui.component.Material3SettingsItem\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport kotlinx.coroutines.withContext\nimport java.time.Instant\nimport java.time.ZoneId\nimport java.time.format.DateTimeFormatter\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun AlarmSettingsSection(showTitle: Boolean = true) {\n    val context = LocalContext.current\n    val locale = LocalLocale.current.platformLocale\n    val database = LocalDatabase.current\n    val scope = rememberCoroutineScope()\n    val playlists by database.playlistsByNameAsc().collectAsState(initial = emptyList())\n    val persistMutex = remember { Mutex() }\n    val selectPlaylistText = stringResource(R.string.alarm_select_playlist)\n    val randomEnabledText = stringResource(R.string.alarm_random_enabled)\n    val randomDisabledText = stringResource(R.string.alarm_random_disabled)\n    val notScheduledText = stringResource(R.string.alarm_not_scheduled)\n\n    var alarms by remember { mutableStateOf(emptyList<MusicAlarmEntry>()) }\n    var showEditor by remember { mutableStateOf(false) }\n    var editorTarget by remember { mutableStateOf<MusicAlarmEntry?>(null) }\n\n    val alarmManager = context.getSystemService(AlarmManager::class.java)\n    val canScheduleExact =\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {\n            alarmManager?.canScheduleExactAlarms() == true\n        } else {\n            true\n        }\n    val powerManager = context.getSystemService(PowerManager::class.java)\n    val ignoringBatteryOptimization =\n        powerManager?.isIgnoringBatteryOptimizations(context.packageName) == true\n    val systemItems = buildList {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !canScheduleExact) {\n            add(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.warning),\n                    title = { Text(stringResource(R.string.alarm_exact_permission_title)) },\n                    description = { Text(stringResource(R.string.alarm_exact_permission_desc)) },\n                    onClick = {\n                        try {\n                            val intent = Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)\n                                .setData(\"package:${context.packageName}\".toUri())\n                            context.startActivity(intent)\n                        } catch (_: ActivityNotFoundException) {\n                        }\n                    }\n                )\n            )\n        }\n        if (!ignoringBatteryOptimization) {\n            add(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.warning),\n                    title = { Text(stringResource(R.string.alarm_battery_optimization_title)) },\n                    description = { Text(stringResource(R.string.alarm_battery_optimization_desc)) },\n                    onClick = {\n                        try {\n                            context.startActivity(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS))\n                        } catch (_: ActivityNotFoundException) {\n                        }\n                    }\n                )\n            )\n        }\n    }\n\n    suspend fun loadAlarms(): List<MusicAlarmEntry> {\n        return withContext(Dispatchers.IO) {\n            MusicAlarmStore.load(context)\n        }.sortedBy { it.hour * 60 + it.minute }\n    }\n\n    fun persistAndSchedule(transform: (List<MusicAlarmEntry>) -> List<MusicAlarmEntry>) {\n        scope.launch {\n            persistMutex.withLock {\n                val latest = loadAlarms()\n                val newList = transform(latest)\n                withContext(Dispatchers.IO) {\n                    MusicAlarmScheduler.scheduleAll(context, newList)\n                }\n                alarms = loadAlarms()\n            }\n        }\n    }\n\n    androidx.compose.runtime.LaunchedEffect(Unit) {\n        alarms = loadAlarms()\n    }\n\n    if (showEditor) {\n        AlarmEditorDialog(\n            existing = editorTarget,\n            allAlarms = alarms,\n            playlists = playlists,\n            onDismiss = {\n                showEditor = false\n                editorTarget = null\n            },\n            onSave = { updated ->\n                persistAndSchedule { current ->\n                    current.filterNot { it.id == updated.id } + updated\n                }\n                showEditor = false\n                editorTarget = null\n            }\n        )\n    }\n\n    Material3SettingsGroup(\n        title = if (showTitle) stringResource(R.string.alarm) else null,\n        items = buildList {\n            add(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.add_circle),\n                    title = { Text(stringResource(R.string.alarm_add)) },\n                    onClick = {\n                        editorTarget = null\n                        showEditor = true\n                    }\n                )\n            )\n\n            if (alarms.isEmpty()) {\n                add(\n                    Material3SettingsItem(\n                        icon = painterResource(R.drawable.bedtime),\n                        title = { Text(stringResource(R.string.alarm_empty)) }\n                    )\n                )\n            } else {\n                addAll(\n                    alarms.map { alarm ->\n                        val playlistTitle =\n                            playlists.firstOrNull { it.id == alarm.playlistId }?.title\n                                ?: selectPlaylistText\n                        val triggerText =\n                            if (alarm.nextTriggerAt > 0L) {\n                                DateTimeFormatter.ofPattern(\"EEE, HH:mm\", locale)\n                                    .format(\n                                        Instant.ofEpochMilli(alarm.nextTriggerAt)\n                                            .atZone(ZoneId.systemDefault())\n                                    )\n                            } else {\n                                notScheduledText\n                            }\n                        val description = buildString {\n                            append(playlistTitle)\n                            append(\" • \")\n                            append(if (alarm.randomSong) randomEnabledText else randomDisabledText)\n                            append(\"\\n\")\n                            append(stringResource(R.string.alarm_next_prefix, triggerText))\n                        }\n\n                        Material3SettingsItem(\n                            icon = painterResource(R.drawable.bedtime),\n                            title = {\n                                Text(\n                                    String.format(locale, \"%02d:%02d\", alarm.hour, alarm.minute) +\n                                        if (alarm.enabled) {\n                                            \"\"\n                                        } else {\n                                            \" (${stringResource(R.string.alarm_disabled)})\"\n                                        }\n                                )\n                            },\n                            description = { Text(description) },\n                            trailingContent = {\n                                Row(verticalAlignment = Alignment.CenterVertically) {\n                                    AlarmSwitch(\n                                        checked = alarm.enabled,\n                                        onCheckedChange = { enabled ->\n                                            persistAndSchedule { current ->\n                                                current.map {\n                                                    if (it.id == alarm.id) it.copy(enabled = enabled) else it\n                                                }\n                                            }\n                                        }\n                                    )\n                                    IconButton(\n                                        onClick = {\n                                            persistAndSchedule { current ->\n                                                current.filterNot { it.id == alarm.id }\n                                            }\n                                        }\n                                    ) {\n                                        Icon(\n                                            painter = painterResource(R.drawable.delete),\n                                            contentDescription = stringResource(R.string.alarm_delete)\n                                        )\n                                    }\n                                }\n                            },\n                            onClick = {\n                                editorTarget = alarm\n                                showEditor = true\n                            }\n                        )\n                    }\n                )\n            }\n        }\n    )\n\n    if (systemItems.isNotEmpty()) {\n        Spacer(modifier = Modifier.height(16.dp))\n        Material3SettingsGroup(\n            title = stringResource(R.string.settings_section_system),\n            items = systemItems\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun AlarmTimePickerDialog(\n    title: String,\n    initialHour: Int,\n    initialMinute: Int,\n    onDismiss: () -> Unit,\n    onConfirm: (Int, Int) -> Unit\n) {\n    val timePickerState = rememberTimePickerState(\n        initialHour = initialHour,\n        initialMinute = initialMinute,\n        is24Hour = true\n    )\n\n    DefaultDialog(\n        title = { Text(title) },\n        onDismiss = onDismiss,\n        buttons = {\n            TextButton(onClick = onDismiss) {\n                Text(stringResource(android.R.string.cancel))\n            }\n            TextButton(onClick = { onConfirm(timePickerState.hour, timePickerState.minute) }) {\n                Text(stringResource(android.R.string.ok))\n            }\n        }\n    ) {\n        TimePicker(state = timePickerState)\n    }\n}\n\n@Composable\nprivate fun AlarmSwitch(\n    checked: Boolean,\n    onCheckedChange: (Boolean) -> Unit,\n    enabled: Boolean = true\n) {\n    Switch(\n        checked = checked,\n        onCheckedChange = onCheckedChange,\n        enabled = enabled,\n        thumbContent = {\n            Icon(\n                painter = painterResource(if (checked) R.drawable.check else R.drawable.close),\n                contentDescription = null,\n                modifier = Modifier.size(SwitchDefaults.IconSize)\n            )\n        }\n    )\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun AlarmEditorDialog(\n    existing: MusicAlarmEntry?,\n    allAlarms: List<MusicAlarmEntry>,\n    playlists: List<Playlist>,\n    onDismiss: () -> Unit,\n    onSave: (MusicAlarmEntry) -> Unit\n) {\n    val context = LocalContext.current\n    val locale = LocalLocale.current.platformLocale\n    val noPlaylistsText = stringResource(R.string.alarm_no_playlists)\n    val selectPlaylistText = stringResource(R.string.alarm_select_playlist)\n\n    var showPlaylistDialog by remember { mutableStateOf(false) }\n    var showTimePickerDialog by remember { mutableStateOf(false) }\n    var enabled by remember { mutableStateOf(existing?.enabled ?: true) }\n    var hour by remember { mutableIntStateOf(existing?.hour ?: 7) }\n    var minute by remember { mutableIntStateOf(existing?.minute ?: 0) }\n    var playlistId by remember { mutableStateOf(existing?.playlistId.orEmpty()) }\n    var randomSong by remember { mutableStateOf(existing?.randomSong ?: false) }\n\n    val hasSameTimeAlarm = remember(hour, minute, existing, allAlarms) {\n        allAlarms.any { it.id != existing?.id && it.hour == hour && it.minute == minute }\n    }\n    val selectedPlaylist = playlists.firstOrNull { it.id == playlistId }\n    val hasValidPlaylist = selectedPlaylist != null\n\n    if (showTimePickerDialog) {\n        AlarmTimePickerDialog(\n            title = stringResource(R.string.alarm_time),\n            initialHour = hour,\n            initialMinute = minute,\n            onDismiss = { showTimePickerDialog = false },\n            onConfirm = { selectedHour, selectedMinute ->\n                hour = selectedHour\n                minute = selectedMinute\n                showTimePickerDialog = false\n            }\n        )\n    }\n\n    if (showPlaylistDialog) {\n        DefaultDialog(\n            onDismiss = { showPlaylistDialog = false },\n            title = { Text(stringResource(R.string.alarm_playlist)) },\n            buttons = {\n                TextButton(onClick = { showPlaylistDialog = false }) {\n                    Text(stringResource(android.R.string.ok))\n                }\n            }\n        ) {\n            if (playlists.isEmpty()) {\n                Text(noPlaylistsText)\n            } else {\n                LazyColumn(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .heightIn(max = 380.dp)\n                ) {\n                    items(items = playlists, key = { it.id }) { playlist ->\n                        val selected = playlist.id == playlistId\n                        Card(\n                            onClick = {\n                                playlistId = playlist.id\n                                showPlaylistDialog = false\n                            },\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .padding(vertical = 4.dp),\n                            colors = CardDefaults.cardColors(\n                                containerColor = if (selected) {\n                                    MaterialTheme.colorScheme.primaryContainer\n                                } else {\n                                    MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.35f)\n                                }\n                            )\n                        ) {\n                            Row(\n                                modifier = Modifier\n                                    .fillMaxWidth()\n                                    .padding(horizontal = 14.dp, vertical = 12.dp),\n                                verticalAlignment = Alignment.CenterVertically\n                            ) {\n                                Column(modifier = Modifier.weight(1f)) {\n                                    Text(\n                                        text = playlist.title,\n                                        style = MaterialTheme.typography.titleSmall\n                                    )\n                                    Text(\n                                        text = pluralStringResource(\n                                            R.plurals.alarm_playlist_song_count,\n                                            playlist.songCount,\n                                            playlist.songCount\n                                        ),\n                                        style = MaterialTheme.typography.bodySmall,\n                                        color = MaterialTheme.colorScheme.onSurfaceVariant\n                                    )\n                                }\n                                if (selected) {\n                                    Icon(\n                                        painter = painterResource(R.drawable.check),\n                                        contentDescription = stringResource(R.string.alarm_selected),\n                                        modifier = Modifier.size(18.dp)\n                                    )\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n\n    DefaultDialog(\n        onDismiss = onDismiss,\n        title = {\n            Text(\n                if (existing == null) {\n                    stringResource(R.string.alarm_new)\n                } else {\n                    stringResource(R.string.alarm_edit)\n                }\n            )\n        },\n        buttons = {\n            TextButton(onClick = onDismiss) {\n                Text(stringResource(android.R.string.cancel))\n            }\n            TextButton(\n                enabled = !hasSameTimeAlarm && hasValidPlaylist,\n                onClick = {\n                    if (hasSameTimeAlarm) {\n                        return@TextButton\n                    }\n                    if (!hasValidPlaylist) {\n                        Toast.makeText(context, selectPlaylistText, Toast.LENGTH_SHORT).show()\n                        return@TextButton\n                    }\n                    onSave(\n                        (existing ?: MusicAlarmStore.createEmpty()).copy(\n                            enabled = enabled,\n                            hour = hour,\n                            minute = minute,\n                            playlistId = playlistId,\n                            randomSong = randomSong\n                        )\n                    )\n                }\n            ) {\n                Text(stringResource(R.string.alarm_save))\n            }\n        }\n    ) {\n        Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                Text(\n                    text = stringResource(R.string.alarm_enabled),\n                    modifier = Modifier.weight(1f)\n                )\n                AlarmSwitch(checked = enabled, onCheckedChange = { enabled = it })\n            }\n\n            FilledTonalButton(\n                onClick = { showTimePickerDialog = true },\n                modifier = Modifier.fillMaxWidth()\n            ) {\n                Text(\n                    text = stringResource(\n                        R.string.alarm_time_picker_value,\n                        String.format(locale, \"%02d:%02d\", hour, minute)\n                    )\n                )\n            }\n\n            HorizontalDivider()\n\n            OutlinedButton(\n                onClick = {\n                    if (playlists.isEmpty()) {\n                        Toast.makeText(context, noPlaylistsText, Toast.LENGTH_SHORT).show()\n                    } else {\n                        showPlaylistDialog = true\n                    }\n                },\n                enabled = playlists.isNotEmpty(),\n                modifier = Modifier.fillMaxWidth()\n            ) {\n                Text(text = selectedPlaylist?.title ?: selectPlaylistText)\n            }\n\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                Text(\n                    text = stringResource(R.string.alarm_random_song),\n                    modifier = Modifier.weight(1f)\n                )\n                AlarmSwitch(checked = randomSong, onCheckedChange = { randomSong = it })\n            }\n\n            if (hasSameTimeAlarm) {\n                Text(\n                    text = stringResource(R.string.alarm_duplicate_time_warning),\n                    style = MaterialTheme.typography.bodySmall,\n                    color = MaterialTheme.colorScheme.error\n                )\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/settings/AndroidAutoSettings.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.settings\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.SwitchDefaults\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarScrollBehavior\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation.NavController\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.AndroidAutoSectionsOrderKey\nimport com.metrolist.music.constants.AndroidAutoTargetPlaylistKey\nimport com.metrolist.music.constants.AndroidAutoYouTubePlaylistsKey\nimport com.metrolist.music.constants.MediaSessionConstants\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.Material3SettingsGroup\nimport com.metrolist.music.ui.component.Material3SettingsItem\nimport com.metrolist.music.ui.component.PreferenceEntry\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.rememberPreference\nimport kotlinx.coroutines.flow.map\nimport sh.calvin.reorderable.ReorderableItem\nimport sh.calvin.reorderable.rememberReorderableLazyListState\n\nenum class AndroidAutoSection(val id: String) {\n    LIKED(\"liked\"),\n    SONGS(\"songs\"),\n    ARTISTS(\"artists\"),\n    ALBUMS(\"albums\"),\n    PLAYLISTS(\"playlists\"),\n}\n\n@Composable\nfun AndroidAutoSection.label(): String = when (this) {\n    AndroidAutoSection.LIKED -> stringResource(R.string.liked_songs)\n    AndroidAutoSection.SONGS -> stringResource(R.string.songs)\n    AndroidAutoSection.ARTISTS -> stringResource(R.string.artists)\n    AndroidAutoSection.ALBUMS -> stringResource(R.string.albums)\n    AndroidAutoSection.PLAYLISTS -> stringResource(R.string.playlists)\n}\n\nfun serializeSections(sections: List<Pair<AndroidAutoSection, Boolean>>): String =\n    sections.joinToString(\",\") { (section, enabled) -> \"${section.id}:$enabled\" }\n\nfun deserializeSections(raw: String): List<Pair<AndroidAutoSection, Boolean>> {\n    if (raw.isBlank()) return AndroidAutoSection.values().map { it to true }\n    return raw.split(\",\").mapNotNull { token ->\n        val parts = token.split(\":\")\n        if (parts.size != 2) return@mapNotNull null\n        val section = AndroidAutoSection.values().find { it.id == parts[0] } ?: return@mapNotNull null\n        val enabled = parts[1].toBooleanStrictOrNull() ?: true\n        section to enabled\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun AndroidAutoSettings(\n    navController: NavController,\n    scrollBehavior: TopAppBarScrollBehavior,\n) {\n    val haptic = LocalHapticFeedback.current\n    val database = LocalDatabase.current\n\n    val userPlaylists by remember {\n        database.playlistsByCreateDateAsc().map { list -> list.map { it.playlist } }\n    }.collectAsState(initial = emptyList())\n\n    val (youtubePlaylistsEnabled, onYoutubePlaylistsChange) = rememberPreference(\n        key = AndroidAutoYouTubePlaylistsKey,\n        defaultValue = false\n    )\n\n    val (sectionsRaw, onSectionsChange) = rememberPreference(\n        key = AndroidAutoSectionsOrderKey,\n        defaultValue = serializeSections(AndroidAutoSection.values().map { it to true })\n    )\n\n    val (targetPlaylist, onTargetPlaylistChange) = rememberPreference(\n        key = AndroidAutoTargetPlaylistKey,\n        defaultValue = MediaSessionConstants.TARGET_PLAYLIST_AUTO\n    )\n\n    var sections by remember(sectionsRaw) {\n        mutableStateOf(deserializeSections(sectionsRaw))\n    }\n\n    val lazyListState = rememberLazyListState()\n    val reorderableState = rememberReorderableLazyListState(lazyListState) { from, to ->\n        val fromReal = from.index\n        val toReal = to.index\n        if (fromReal >= 0 && toReal >= 0 && fromReal < sections.size && toReal < sections.size) {\n            sections = sections.toMutableList().apply {\n                add(toReal, removeAt(fromReal))\n            }\n            onSectionsChange(serializeSections(sections))\n            haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n        }\n    }\n\n    val playlistOptions = listOf(MediaSessionConstants.TARGET_PLAYLIST_AUTO) +\n            userPlaylists.map { it.id }\n\n    val playlistLabels: @Composable (String) -> String = { id ->\n        if (id == MediaSessionConstants.TARGET_PLAYLIST_AUTO) {\n            stringResource(R.string.android_auto_target_playlist_auto)\n        } else {\n            userPlaylists.find { it.id == id }?.name ?: id\n        }\n    }\n\n    Column(\n        modifier = Modifier\n            .windowInsetsPadding(LocalPlayerAwareWindowInsets.current)\n            .verticalScroll(rememberScrollState())\n            .padding(horizontal = 16.dp)\n    ) {\n        // Visible sections\n        Material3SettingsGroup(\n            title = stringResource(R.string.android_auto_visible_sections),\n            items = listOf(\n                Material3SettingsItem(\n                    title = {},\n                    description = { Text(stringResource(R.string.android_auto_reorder_hint)) },\n                    onClick = null\n                )\n            )\n        )\n\n        LazyColumn(\n            state = lazyListState,\n            modifier = Modifier\n                .fillMaxWidth()\n                .height((sections.size * 80).dp),\n            userScrollEnabled = false,\n        ) {\n            items(sections, key = { (section, _) -> section.id }) { (section, enabled) ->\n                ReorderableItem(reorderableState, key = section.id) {\n                    PreferenceEntry(\n                        modifier = Modifier.fillMaxWidth(),\n                        icon = {\n                            Icon(\n                                painter = painterResource(\n                                    when (section) {\n                                        AndroidAutoSection.LIKED -> R.drawable.favorite\n                                        AndroidAutoSection.SONGS -> R.drawable.music_note\n                                        AndroidAutoSection.ARTISTS -> R.drawable.artist\n                                        AndroidAutoSection.ALBUMS -> R.drawable.album\n                                        AndroidAutoSection.PLAYLISTS -> R.drawable.queue_music\n                                    }\n                                ),\n                                contentDescription = null,\n                            )\n                        },\n                        title = { Text(section.label()) },\n                        trailingContent = {\n                            Row(verticalAlignment = Alignment.CenterVertically) {\n                                Icon(\n                                    painter = painterResource(R.drawable.drag_handle),\n                                    contentDescription = null,\n                                    modifier = Modifier\n                                        .size(24.dp)\n                                        .longPressDraggableHandle(\n                                            onDragStarted = {\n                                                haptic.performHapticFeedback(HapticFeedbackType.LongPress)\n                                            }\n                                        ),\n                                    tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                                )\n                                Spacer(Modifier.width(12.dp))\n                                Switch(\n                                    checked = enabled,\n                                    onCheckedChange = { newValue ->\n                                        sections = sections.map { (s, e) ->\n                                            if (s == section) s to newValue else s to e\n                                        }\n                                        onSectionsChange(serializeSections(sections))\n                                    },\n                                    thumbContent = {\n                                        Icon(\n                                            painter = painterResource(\n                                                if (enabled) R.drawable.check else R.drawable.close\n                                            ),\n                                            contentDescription = null,\n                                            modifier = Modifier.size(SwitchDefaults.IconSize),\n                                        )\n                                    }\n                                )\n                            }\n                        },\n                        onClick = {\n                            sections = sections.map { (s, e) ->\n                                if (s == section) s to !e else s to e\n                            }\n                            onSectionsChange(serializeSections(sections))\n                        },\n                    )\n                }\n            }\n        }\n\n        Spacer(Modifier.height(27.dp))\n\n        // Quick-add destination playlist\n        var showTargetPlaylistDialog by remember { mutableStateOf(false) }\n\n        if (showTargetPlaylistDialog) {\n            androidx.compose.material3.AlertDialog(\n                onDismissRequest = { showTargetPlaylistDialog = false },\n                title = { Text(stringResource(R.string.android_auto_target_playlist)) },\n                text = {\n                    androidx.compose.foundation.lazy.LazyColumn {\n                        items(playlistOptions) { value ->\n                            Row(\n                                verticalAlignment = Alignment.CenterVertically,\n                                modifier = Modifier\n                                    .fillMaxWidth()\n                                    .clickable {\n                                        showTargetPlaylistDialog = false\n                                        onTargetPlaylistChange(value)\n                                    }\n                                    .padding(vertical = 12.dp),\n                            ) {\n                                androidx.compose.material3.RadioButton(\n                                    selected = value == targetPlaylist,\n                                    onClick = null,\n                                )\n                                Text(\n                                    text = playlistLabels(value),\n                                    style = MaterialTheme.typography.bodyLarge,\n                                    modifier = Modifier.padding(start = 16.dp),\n                                )\n                            }\n                        }\n                    }\n                },\n                confirmButton = {\n                    androidx.compose.material3.TextButton(\n                        onClick = { showTargetPlaylistDialog = false }\n                    ) {\n                        Text(stringResource(android.R.string.cancel))\n                    }\n                }\n            )\n        }\n        \n        Material3SettingsGroup(\n            title = stringResource(R.string.android_auto_target_playlist),\n            items = listOf(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.playlist_add),\n                    title = { Text(stringResource(R.string.android_auto_target_playlist)) },\n                    description = { Text(playlistLabels(targetPlaylist)) },\n                    onClick = { showTargetPlaylistDialog = true }\n                )\n            )\n        )\n\n        Spacer(Modifier.height(27.dp))\n\n        // YouTube playlists\n        Material3SettingsGroup(\n            title = stringResource(R.string.your_youtube_playlists),\n            items = listOf(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.queue_music),\n                    title = { Text(stringResource(R.string.android_auto_youtube_playlists)) },\n                    description = { Text(stringResource(R.string.android_auto_youtube_playlists_desc)) },\n                    trailingContent = {\n                        Switch(\n                            checked = youtubePlaylistsEnabled,\n                            onCheckedChange = onYoutubePlaylistsChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        if (youtubePlaylistsEnabled) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize),\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onYoutubePlaylistsChange(!youtubePlaylistsEnabled) }\n                )\n            )\n        )\n\n        Spacer(Modifier.height(27.dp))\n    }\n\n    TopAppBar(\n        title = { Text(stringResource(R.string.android_auto)) },\n        navigationIcon = {\n            IconButton(\n                onClick = navController::navigateUp,\n                onLongClick = navController::backToMain,\n            ) {\n                Icon(\n                    painterResource(R.drawable.arrow_back),\n                    contentDescription = null,\n                )\n            }\n        },\n        scrollBehavior = scrollBehavior,\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/settings/AppearanceSettings.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.settings\n\nimport android.app.Activity\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Build\nimport androidx.compose.foundation.border\nimport androidx.core.content.edit\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Slider\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.SnackbarResult\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.SwitchDefaults\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation.NavController\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.ChipSortTypeKey\nimport com.metrolist.music.constants.CropAlbumArtKey\nimport com.metrolist.music.constants.DefaultOpenTabKey\nimport com.metrolist.music.constants.DensityScale\nimport com.metrolist.music.constants.DensityScaleKey\nimport com.metrolist.music.constants.DynamicThemeKey\nimport com.metrolist.music.constants.EnableDynamicIconKey\nimport com.metrolist.music.constants.EnableHighRefreshRateKey\nimport com.metrolist.music.constants.GridItemSize\nimport com.metrolist.music.constants.GridItemsSizeKey\nimport com.metrolist.music.constants.HidePlayerThumbnailKey\nimport com.metrolist.music.constants.LibraryFilter\nimport com.metrolist.music.constants.ListenTogetherInTopBarKey\nimport com.metrolist.music.constants.LyricsAnimationStyle\nimport com.metrolist.music.constants.LyricsAnimationStyleKey\nimport com.metrolist.music.constants.LyricsClickKey\nimport com.metrolist.music.constants.LyricsGlowEffectKey\nimport com.metrolist.music.constants.LyricsLineSpacingKey\nimport com.metrolist.music.constants.LyricsScrollKey\nimport com.metrolist.music.constants.LyricsTextPositionKey\nimport com.metrolist.music.constants.LyricsTextSizeKey\nimport com.metrolist.music.constants.PlayerBackgroundStyle\nimport com.metrolist.music.constants.PlayerBackgroundStyleKey\nimport com.metrolist.music.constants.PlayerButtonsStyle\nimport com.metrolist.music.constants.PlayerButtonsStyleKey\nimport com.metrolist.music.constants.PureBlackMiniPlayerKey\nimport com.metrolist.music.constants.SelectedThemeColorKey\nimport com.metrolist.music.constants.ShowCachedPlaylistKey\nimport com.metrolist.music.constants.ShowDownloadedPlaylistKey\nimport com.metrolist.music.constants.ShowLikedPlaylistKey\nimport com.metrolist.music.constants.ShowTopPlaylistKey\nimport com.metrolist.music.constants.ShowUploadedPlaylistKey\nimport com.metrolist.music.constants.SliderStyle\nimport com.metrolist.music.constants.SliderStyleKey\nimport com.metrolist.music.constants.SlimNavBarKey\nimport com.metrolist.music.constants.SquigglySliderKey\nimport com.metrolist.music.constants.SwipeSensitivityKey\nimport com.metrolist.music.constants.SwipeThumbnailKey\nimport com.metrolist.music.constants.SwipeToRemoveSongKey\nimport com.metrolist.music.constants.SwipeToSongKey\nimport com.metrolist.music.constants.UseNewMiniPlayerDesignKey\nimport com.metrolist.music.constants.UseNewPlayerDesignKey\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.EnumDialog\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.Material3SettingsGroup\nimport com.metrolist.music.ui.component.Material3SettingsItem\nimport com.metrolist.music.ui.component.PlayerSliderTrack\nimport com.metrolist.music.ui.component.SquigglySlider\nimport com.metrolist.music.ui.component.WavySlider\nimport com.metrolist.music.ui.theme.DefaultThemeColor\nimport com.metrolist.music.ui.theme.PlayerSliderColors\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.IconUtils\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport kotlinx.coroutines.launch\nimport kotlin.math.roundToInt\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun AppearanceSettings(\n    navController: NavController,\n    activity: Activity,\n    snackbarHostState: SnackbarHostState,\n) {\n    val (dynamicTheme, onDynamicThemeChange) = rememberPreference(\n        DynamicThemeKey,\n        defaultValue = true\n    )\n    val (enableDynamicIcon, onEnableDynamicIconChange) = rememberPreference(\n        EnableDynamicIconKey,\n        defaultValue = true\n    )\n    val (enableHighRefreshRate, onEnableHighRefreshRateChange) = rememberPreference(\n        EnableHighRefreshRateKey,\n        defaultValue = true\n    )\n    val (selectedThemeColorInt) = rememberPreference(\n        SelectedThemeColorKey,\n        defaultValue = DefaultThemeColor.toArgb()\n    )\n    // Check if user has selected a custom color (not the default/dynamic color)\n    val isUsingCustomColor = selectedThemeColorInt != DefaultThemeColor.toArgb()\n    val coroutineScope = rememberCoroutineScope()\n\n    fun handleIconChange(enabled: Boolean) {\n        onEnableDynamicIconChange(enabled)\n        IconUtils.setIcon(activity, enabled)\n        coroutineScope.launch {\n            val result = snackbarHostState.showSnackbar(\n                message = \"Icon updated, restart to apply\",\n                actionLabel = \"Restart\"\n            )\n            if (result == SnackbarResult.ActionPerformed) {\n                val packageManager = activity.packageManager\n                val intent = packageManager.getLaunchIntentForPackage(activity.packageName)\n                val componentName = intent?.component\n                val mainIntent = Intent.makeRestartActivityTask(componentName)\n                activity.startActivity(mainIntent)\n                Runtime.getRuntime().exit(0)\n            }\n        }\n    }\n\n\n    val (useNewPlayerDesign, onUseNewPlayerDesignChange) = rememberPreference(\n        UseNewPlayerDesignKey,\n        defaultValue = true\n    )\n    val (useNewMiniPlayerDesign, onUseNewMiniPlayerDesignChange) = rememberPreference(\n        UseNewMiniPlayerDesignKey,\n        defaultValue = true\n    )\n    val (hidePlayerThumbnail, onHidePlayerThumbnailChange) = rememberPreference(\n        HidePlayerThumbnailKey,\n        defaultValue = false\n    )\n    val (cropAlbumArt, onCropAlbumArtChange) = rememberPreference(\n        CropAlbumArtKey,\n        defaultValue = false\n    )\n    val (playerBackground, onPlayerBackgroundChange) =\n        rememberEnumPreference(\n            PlayerBackgroundStyleKey,\n            defaultValue = PlayerBackgroundStyle.DEFAULT,\n        )\n\n    val (defaultOpenTab, onDefaultOpenTabChange) = rememberEnumPreference(\n        DefaultOpenTabKey,\n        defaultValue = NavigationTab.HOME\n    )\n    val (playerButtonsStyle, onPlayerButtonsStyleChange) = rememberEnumPreference(\n        PlayerButtonsStyleKey,\n        defaultValue = PlayerButtonsStyle.DEFAULT\n    )\n    val (lyricsPosition, onLyricsPositionChange) = rememberEnumPreference(\n        LyricsTextPositionKey,\n        defaultValue = LyricsPosition.CENTER\n    )\n    val (lyricsClick, onLyricsClickChange) = rememberPreference(LyricsClickKey, defaultValue = true)\n    val (lyricsScroll, onLyricsScrollChange) = rememberPreference(\n        LyricsScrollKey,\n        defaultValue = true\n    )\n    val (lyricsAnimationStyle, onLyricsAnimationStyleChange) = rememberEnumPreference(\n        LyricsAnimationStyleKey,\n        defaultValue = LyricsAnimationStyle.NONE\n    )\n    val (lyricsTextSize, onLyricsTextSizeChange) = rememberPreference(LyricsTextSizeKey, defaultValue = 24f)\n    val (lyricsLineSpacing, onLyricsLineSpacingChange) = rememberPreference(LyricsLineSpacingKey, defaultValue = 1.3f)\n    val (lyricsGlowEffect, onLyricsGlowEffectChange) = rememberPreference(LyricsGlowEffectKey, defaultValue = false)\n\n    val (sliderStyle, onSliderStyleChange) = rememberEnumPreference(\n        SliderStyleKey,\n        defaultValue = SliderStyle.DEFAULT\n    )\n    val (squigglySlider, onSquigglySliderChange) = rememberPreference(\n        SquigglySliderKey,\n        defaultValue = false\n    )\n    val (swipeThumbnail, onSwipeThumbnailChange) = rememberPreference(\n        SwipeThumbnailKey,\n        defaultValue = true\n    )\n    val (swipeSensitivity, onSwipeSensitivityChange) = rememberPreference(\n        SwipeSensitivityKey,\n        defaultValue = 0.73f\n    )\n    val (gridItemSize, onGridItemSizeChange) = rememberEnumPreference(\n        GridItemsSizeKey,\n        defaultValue = GridItemSize.SMALL\n    )\n\n    val (slimNav, onSlimNavChange) = rememberPreference(\n        SlimNavBarKey,\n        defaultValue = false\n    )\n\n    // Density scale preferences\n    val context = activity as Context\n    val sharedPreferences = remember { context.getSharedPreferences(\"metrolist_settings\", Context.MODE_PRIVATE) }\n    val prefDensityScale = remember(sharedPreferences) {\n        sharedPreferences.getFloat(\"density_scale_factor\", 1.0f)\n    }\n    val (densityScale, setDensityScale) = rememberPreference(DensityScaleKey, defaultValue = prefDensityScale)\n    var showRestartDialog by rememberSaveable { mutableStateOf(false) }\n    var showDensityScaleDialog by rememberSaveable { mutableStateOf(false) }\n\n    val onDensityScaleChange: (Float) -> Unit = { newScale ->\n        setDensityScale(newScale)\n        // Write to SharedPreferences for DensityScaler to read on next startup\n        sharedPreferences.edit {\n            putFloat(\"density_scale_factor\", newScale)\n        }\n        showRestartDialog = true\n    }\n\n    val (listenTogetherInTopBar, onListenTogetherInTopBarChange) = rememberPreference(\n        ListenTogetherInTopBarKey,\n        defaultValue = true\n    )\n\n    val (swipeToSong, onSwipeToSongChange) = rememberPreference(\n        SwipeToSongKey,\n        defaultValue = false\n    )\n\n    val (swipeToRemoveSong, onSwipeToRemoveSongChange) = rememberPreference(\n        SwipeToRemoveSongKey,\n        defaultValue = false\n    )\n\n    val (showLikedPlaylist, onShowLikedPlaylistChange) = rememberPreference(\n        ShowLikedPlaylistKey,\n        defaultValue = true\n    )\n    val (showDownloadedPlaylist, onShowDownloadedPlaylistChange) = rememberPreference(\n        ShowDownloadedPlaylistKey,\n        defaultValue = true\n    )\n    val (showTopPlaylist, onShowTopPlaylistChange) = rememberPreference(\n        ShowTopPlaylistKey,\n        defaultValue = true\n    )\n    val (showUploadedPlaylist, onShowUploadedPlaylistChange) = rememberPreference(\n        ShowUploadedPlaylistKey,\n        defaultValue = true\n    )\n    val (showCachedPlaylist, onShowCachedPlaylistChange) = rememberPreference(\n        ShowCachedPlaylistKey,\n        defaultValue = true\n    )\n\n    val availableBackgroundStyles = PlayerBackgroundStyle.entries.filter {\n        it != PlayerBackgroundStyle.BLUR || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S\n    }\n\n\n\n    val (defaultChip, onDefaultChipChange) = rememberEnumPreference(\n        key = ChipSortTypeKey,\n        defaultValue = LibraryFilter.LIBRARY\n    )\n\n    var showSliderOptionDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n\n\n    var showPlayerBackgroundDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    var showPlayerButtonsStyleDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    var showLyricsPositionDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    var showLyricsAnimationStyleDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    var showLyricsTextSizeDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    var showLyricsLineSpacingDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    if (showLyricsPositionDialog) {\n        EnumDialog(\n            onDismiss = { showLyricsPositionDialog = false },\n            onSelect = {\n                onLyricsPositionChange(it)\n                showLyricsPositionDialog = false\n            },\n            title = stringResource(R.string.lyrics_text_position),\n            current = lyricsPosition,\n            values = LyricsPosition.values().toList(),\n            valueText = {\n                when (it) {\n                    LyricsPosition.LEFT -> stringResource(R.string.left)\n                    LyricsPosition.CENTER -> stringResource(R.string.center)\n                    LyricsPosition.RIGHT -> stringResource(R.string.right)\n                }\n            }\n        )\n    }\n\n    if (showLyricsAnimationStyleDialog) {\n        EnumDialog(\n            onDismiss = { showLyricsAnimationStyleDialog = false },\n            onSelect = {\n                onLyricsAnimationStyleChange(it)\n                showLyricsAnimationStyleDialog = false\n            },\n            title = stringResource(R.string.lyrics_animation_style),\n            current = lyricsAnimationStyle,\n            values = LyricsAnimationStyle.values().toList(),\n            valueText = {\n                when (it) {\n                    LyricsAnimationStyle.NONE -> stringResource(R.string.none)\n                    LyricsAnimationStyle.FADE -> stringResource(R.string.fade)\n                    LyricsAnimationStyle.GLOW -> stringResource(R.string.glow)\n                    LyricsAnimationStyle.SLIDE -> stringResource(R.string.slide)\n                    LyricsAnimationStyle.KARAOKE -> stringResource(R.string.karaoke)\n                    LyricsAnimationStyle.APPLE -> stringResource(R.string.apple_music_style)\n                }\n            }\n        )\n    }\n\n    if (showLyricsTextSizeDialog) {\n        var tempTextSize by remember { mutableFloatStateOf(lyricsTextSize) }\n\n        DefaultDialog(\n            onDismiss = {\n                tempTextSize = lyricsTextSize\n                showLyricsTextSizeDialog = false\n            },\n            buttons = {\n                TextButton(\n                    onClick = {\n                        tempTextSize = 24f\n                    }\n                ) {\n                    Text(stringResource(R.string.reset))\n                }\n\n                Spacer(modifier = Modifier.weight(1f))\n\n                TextButton(\n                    onClick = {\n                        tempTextSize = lyricsTextSize\n                        showLyricsTextSizeDialog = false\n                    }\n                ) {\n                    Text(stringResource(android.R.string.cancel))\n                }\n                TextButton(\n                    onClick = {\n                        onLyricsTextSizeChange(tempTextSize)\n                        showLyricsTextSizeDialog = false\n                    }\n                ) {\n                    Text(stringResource(android.R.string.ok))\n                }\n            }\n        ) {\n            Column(\n                horizontalAlignment = Alignment.CenterHorizontally,\n                modifier = Modifier.padding(16.dp)\n            ) {\n                Text(\n                    text = stringResource(R.string.lyrics_text_size),\n                    style = MaterialTheme.typography.headlineSmall,\n                    modifier = Modifier.padding(bottom = 16.dp)\n                )\n\n                Text(\n                    text = \"${tempTextSize.roundToInt()} sp\",\n                    style = MaterialTheme.typography.bodyLarge,\n                    modifier = Modifier.padding(bottom = 16.dp)\n                )\n\n                Slider(\n                    value = tempTextSize,\n                    onValueChange = { tempTextSize = it },\n                    valueRange = 16f..36f,\n                    steps = 19,\n                    modifier = Modifier.fillMaxWidth()\n                )\n            }\n        }\n    }\n\n    if (showLyricsLineSpacingDialog) {\n        var tempLineSpacing by remember { mutableFloatStateOf(lyricsLineSpacing) }\n\n        DefaultDialog(\n            onDismiss = {\n                tempLineSpacing = lyricsLineSpacing\n                showLyricsLineSpacingDialog = false\n            },\n            buttons = {\n                TextButton(\n                    onClick = {\n                        tempLineSpacing = 1.3f\n                    }\n                ) {\n                    Text(stringResource(R.string.reset))\n                }\n\n                Spacer(modifier = Modifier.weight(1f))\n\n                TextButton(\n                    onClick = {\n                        tempLineSpacing = lyricsLineSpacing\n                        showLyricsLineSpacingDialog = false\n                    }\n                ) {\n                    Text(stringResource(android.R.string.cancel))\n                }\n                TextButton(\n                    onClick = {\n                        onLyricsLineSpacingChange(tempLineSpacing)\n                        showLyricsLineSpacingDialog = false\n                    }\n                ) {\n                    Text(stringResource(android.R.string.ok))\n                }\n            }\n        ) {\n            Column(\n                horizontalAlignment = Alignment.CenterHorizontally,\n                modifier = Modifier.padding(16.dp)\n            ) {\n                Text(\n                    text = stringResource(R.string.lyrics_line_spacing),\n                    style = MaterialTheme.typography.headlineSmall,\n                    modifier = Modifier.padding(bottom = 16.dp)\n                )\n\n                Text(\n                    text = \"${String.format(\"%.1f\", tempLineSpacing)}x\",\n                    style = MaterialTheme.typography.bodyLarge,\n                    modifier = Modifier.padding(bottom = 16.dp)\n                )\n\n                Slider(\n                    value = tempLineSpacing,\n                    onValueChange = { tempLineSpacing = it },\n                    valueRange = 1.0f..2.0f,\n                    steps = 19,\n                    modifier = Modifier.fillMaxWidth()\n                )\n            }\n        }\n    }\n\n    if (showPlayerButtonsStyleDialog) {\n        EnumDialog(\n            onDismiss = { showPlayerButtonsStyleDialog = false },\n            onSelect = {\n                onPlayerButtonsStyleChange(it)\n                showPlayerButtonsStyleDialog = false\n            },\n            title = stringResource(R.string.player_buttons_style),\n            current = playerButtonsStyle,\n            values = PlayerButtonsStyle.values().toList(),\n            valueText = {\n                when (it) {\n                    PlayerButtonsStyle.DEFAULT -> stringResource(R.string.default_style)\n                    PlayerButtonsStyle.PRIMARY -> stringResource(R.string.primary_color_style)\n                    PlayerButtonsStyle.TERTIARY -> stringResource(R.string.tertiary_color_style)\n                }\n            }\n        )\n    }\n\n    if (showPlayerBackgroundDialog) {\n        EnumDialog(\n            onDismiss = { showPlayerBackgroundDialog = false },\n            onSelect = {\n                onPlayerBackgroundChange(it)\n                showPlayerBackgroundDialog = false\n            },\n            title = stringResource(R.string.player_background_style),\n            current = playerBackground,\n            values = availableBackgroundStyles,\n            valueText = {\n                when (it) {\n                    PlayerBackgroundStyle.DEFAULT -> stringResource(R.string.follow_theme)\n                    PlayerBackgroundStyle.GRADIENT -> stringResource(R.string.gradient)\n                    PlayerBackgroundStyle.BLUR -> stringResource(R.string.player_background_blur)\n                }\n            }\n        )\n    }\n\n\n    var showDefaultOpenTabDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    if (showDefaultOpenTabDialog) {\n        EnumDialog(\n            onDismiss = { showDefaultOpenTabDialog = false },\n            onSelect = {\n                onDefaultOpenTabChange(it)\n                showDefaultOpenTabDialog = false\n            },\n            title = stringResource(R.string.default_open_tab),\n            current = defaultOpenTab,\n            values = NavigationTab.values().toList(),\n            valueText = {\n                when (it) {\n                    NavigationTab.HOME -> stringResource(R.string.home)\n                    NavigationTab.SEARCH -> stringResource(R.string.search)\n                    NavigationTab.LIBRARY -> stringResource(R.string.filter_library)\n                }\n            }\n        )\n    }\n\n    var showDefaultChipDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    if (showDefaultChipDialog) {\n        EnumDialog(\n            onDismiss = { showDefaultChipDialog = false },\n            onSelect = {\n                onDefaultChipChange(it)\n                showDefaultChipDialog = false\n            },\n            title = stringResource(R.string.default_lib_chips),\n            current = defaultChip,\n            values = LibraryFilter.values().toList(),\n            valueText = {\n                when (it) {\n                    LibraryFilter.SONGS -> stringResource(R.string.songs)\n                    LibraryFilter.ARTISTS -> stringResource(R.string.artists)\n                    LibraryFilter.ALBUMS -> stringResource(R.string.albums)\n                    LibraryFilter.PLAYLISTS -> stringResource(R.string.playlists)\n                    LibraryFilter.PODCASTS -> stringResource(R.string.filter_podcasts)\n                    LibraryFilter.LIBRARY -> stringResource(R.string.filter_library)\n                }\n            }\n        )\n    }\n\n    var showGridSizeDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    if (showGridSizeDialog) {\n        EnumDialog(\n            onDismiss = { showGridSizeDialog = false },\n            onSelect = {\n                onGridItemSizeChange(it)\n                showGridSizeDialog = false\n            },\n            title = stringResource(R.string.grid_cell_size),\n            current = gridItemSize,\n            values = GridItemSize.values().toList(),\n            valueText = {\n                when (it) {\n                    GridItemSize.BIG -> stringResource(R.string.big)\n                    GridItemSize.SMALL -> stringResource(R.string.small)\n                }\n            }\n        )\n    }\n\n    if (showRestartDialog) {\n        DefaultDialog(\n            onDismiss = { showRestartDialog = false },\n            buttons = {\n                TextButton(\n                    onClick = { showRestartDialog = false }\n                ) {\n                    Text(text = stringResource(android.R.string.cancel))\n                }\n                TextButton(\n                    onClick = {\n                        showRestartDialog = false\n                        val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply {\n                            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)\n                        }\n                        context.startActivity(intent)\n                        Runtime.getRuntime().exit(0)\n                    }\n                ) {\n                    Text(text = stringResource(R.string.restart))\n                }\n            }\n        ) {\n            Column(\n                verticalArrangement = Arrangement.spacedBy(16.dp)\n            ) {\n                Text(\n                    text = stringResource(R.string.restart_required),\n                    style = MaterialTheme.typography.titleLarge\n                )\n                Text(\n                    text = stringResource(R.string.density_restart_message),\n                    style = MaterialTheme.typography.bodyMedium\n                )\n            }\n        }\n    }\n\n    if (showDensityScaleDialog) {\n        DefaultDialog(\n            onDismiss = { showDensityScaleDialog = false },\n            buttons = {\n                TextButton(\n                    onClick = { showDensityScaleDialog = false }\n                ) {\n                    Text(text = stringResource(android.R.string.cancel))\n                }\n            }\n        ) {\n            Column {\n                DensityScale.entries.forEach { scale ->\n                    Row(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .clickable {\n                                onDensityScaleChange(scale.value)\n                                showDensityScaleDialog = false\n                            }\n                            .padding(16.dp),\n                        verticalAlignment = Alignment.CenterVertically\n                    ) {\n                        Text(\n                            text = scale.label,\n                            style = MaterialTheme.typography.bodyLarge,\n                            color = if (densityScale == scale.value) {\n                                MaterialTheme.colorScheme.primary\n                            } else {\n                                MaterialTheme.colorScheme.onSurface\n                            }\n                        )\n                    }\n                }\n            }\n        }\n    }\n\n    if (showSliderOptionDialog) {\n        DefaultDialog(\n            buttons = {\n                TextButton(\n                    onClick = { showSliderOptionDialog = false }\n                ) {\n                    Text(text = stringResource(android.R.string.cancel))\n                }\n            },\n            onDismiss = {\n                showSliderOptionDialog = false\n            }\n        ) {\n            val sliderPreviewColors = PlayerSliderColors.getSliderColors(\n                MaterialTheme.colorScheme.primary,\n                PlayerBackgroundStyle.DEFAULT,\n                isSystemInDarkTheme()\n            )\n\n            Column(\n                verticalArrangement = Arrangement.spacedBy(12.dp)\n            ) {\n                Row(\n                    horizontalArrangement = Arrangement.spacedBy(12.dp)\n                ) {\n                    Column(\n                        horizontalAlignment = Alignment.CenterHorizontally,\n                        verticalArrangement = Arrangement.spacedBy(4.dp),\n                        modifier = Modifier\n                            .aspectRatio(1f)\n                            .weight(1f)\n                            .clip(RoundedCornerShape(16.dp))\n                            .border(\n                                1.dp,\n                                if (sliderStyle == SliderStyle.DEFAULT && !squigglySlider) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outlineVariant,\n                                RoundedCornerShape(16.dp)\n                            )\n                            .clickable {\n                                onSliderStyleChange(SliderStyle.DEFAULT)\n                                onSquigglySliderChange(false)\n                                showSliderOptionDialog = false\n                            }\n                            .padding(12.dp)\n                    ) {\n                        val sliderValue = 0.35f\n                        Slider(\n                            value = sliderValue,\n                            valueRange = 0f..1f,\n                            onValueChange = { /* preview only */ },\n                            colors = sliderPreviewColors,\n                            enabled = false,\n                            modifier = Modifier.weight(1f)\n                        )\n                        Text(\n                            text = stringResource(R.string.default_),\n                            style = MaterialTheme.typography.labelSmall,\n                            maxLines = 1,\n                            overflow = TextOverflow.Ellipsis\n                        )\n                    }\n                    Column(\n                        horizontalAlignment = Alignment.CenterHorizontally,\n                        verticalArrangement = Arrangement.spacedBy(4.dp),\n                        modifier = Modifier\n                            .aspectRatio(1f)\n                            .weight(1f)\n                            .clip(RoundedCornerShape(16.dp))\n                            .border(\n                                1.dp,\n                                if (sliderStyle == SliderStyle.WAVY && !squigglySlider) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outlineVariant,\n                                RoundedCornerShape(16.dp)\n                            )\n                            .clickable {\n                                onSliderStyleChange(SliderStyle.WAVY)\n                                onSquigglySliderChange(false)\n                                showSliderOptionDialog = false\n                            }\n                            .padding(12.dp)\n                    ) {\n                        val sliderValue = 0.5f\n                        WavySlider(\n                            value = sliderValue,\n                            valueRange = 0f..1f,\n                            onValueChange = { /* preview only */ },\n                            colors = sliderPreviewColors,\n                            modifier = Modifier.weight(1f),\n                            isPlaying = true,\n                            enabled = false\n                        )\n                        Text(\n                            text = stringResource(R.string.wavy),\n                            style = MaterialTheme.typography.labelSmall,\n                            maxLines = 1,\n                            overflow = TextOverflow.Ellipsis\n                        )\n                    }\n                }\n                Row(\n                    horizontalArrangement = Arrangement.spacedBy(12.dp)\n                ) {\n                    Column(\n                        horizontalAlignment = Alignment.CenterHorizontally,\n                        verticalArrangement = Arrangement.spacedBy(4.dp),\n                        modifier = Modifier\n                            .aspectRatio(1f)\n                            .weight(1f)\n                            .clip(RoundedCornerShape(16.dp))\n                            .border(\n                                1.dp,\n                                if (sliderStyle == SliderStyle.SLIM) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outlineVariant,\n                                RoundedCornerShape(16.dp)\n                            )\n                            .clickable {\n                                onSliderStyleChange(SliderStyle.SLIM)\n                                onSquigglySliderChange(false)\n                                showSliderOptionDialog = false\n                            }\n                            .padding(12.dp)\n                    ) {\n                        val sliderValue = 0.65f\n                        Slider(\n                            value = sliderValue,\n                            valueRange = 0f..1f,\n                            onValueChange = { /* preview only */ },\n                            thumb = { Spacer(modifier = Modifier.size(0.dp)) },\n                            track = { sliderState ->\n                                PlayerSliderTrack(\n                                    sliderState = sliderState,\n                                    colors = sliderPreviewColors\n                                )\n                            },\n                            colors = sliderPreviewColors,\n                            enabled = false,\n                            modifier = Modifier.weight(1f)\n                        )\n\n                        Text(\n                            text = stringResource(R.string.slim),\n                            style = MaterialTheme.typography.labelSmall,\n                            maxLines = 1,\n                            overflow = TextOverflow.Ellipsis\n                        )\n                    }\n                    Column(\n                        horizontalAlignment = Alignment.CenterHorizontally,\n                        verticalArrangement = Arrangement.spacedBy(4.dp),\n                        modifier = Modifier\n                            .aspectRatio(1f)\n                            .weight(1f)\n                            .clip(RoundedCornerShape(16.dp))\n                            .border(\n                                1.dp,\n                                if (sliderStyle == SliderStyle.WAVY && squigglySlider) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outlineVariant,\n                                RoundedCornerShape(16.dp)\n                            )\n                            .clickable {\n                                onSliderStyleChange(SliderStyle.WAVY)\n                                onSquigglySliderChange(true)\n                                showSliderOptionDialog = false\n                            }\n                            .padding(12.dp)\n                    ) {\n                        val sliderValue = 0.5f\n                        SquigglySlider(\n                            value = sliderValue,\n                            valueRange = 0f..1f,\n                            onValueChange = { /* preview only */ },\n                            modifier = Modifier.weight(1f),\n                            enabled = false,\n                            colors = sliderPreviewColors,\n                            isPlaying = true,\n                        )\n                        Text(\n                            text = stringResource(R.string.squiggly),\n                            style = MaterialTheme.typography.labelSmall,\n                            maxLines = 2,\n                            overflow = TextOverflow.Ellipsis\n                        )\n                    }\n                }\n            }\n        }\n    }\n\n    Column(\n        Modifier\n            .windowInsetsPadding(LocalPlayerAwareWindowInsets.current)\n            .verticalScroll(rememberScrollState())\n            .padding(horizontal = 16.dp),\n    ) {\n        Material3SettingsGroup(\n            title = stringResource(R.string.theme),\n            items = buildList {\n                add(\n                    Material3SettingsItem(\n                        icon = painterResource(R.drawable.ic_dynamic_icon),\n                        title = { Text(stringResource(R.string.enable_dynamic_icon)) },\n                        trailingContent = {\n                            Switch(\n                                checked = enableDynamicIcon,\n                                onCheckedChange = { handleIconChange(it) },\n                                thumbContent = {\n                                    Icon(\n                                        painter = painterResource(\n                                            id = if (enableDynamicIcon) R.drawable.check else R.drawable.close\n                                        ),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(SwitchDefaults.IconSize)\n                                    )\n                                }\n                            )\n                        },\n                        onClick = { handleIconChange(!enableDynamicIcon) }\n                    )\n                )\n                add(\n                    Material3SettingsItem(\n                        icon = painterResource(R.drawable.speed),\n                        title = { Text(stringResource(R.string.enable_high_refresh_rate)) },\n                        description = { Text(stringResource(R.string.enable_high_refresh_rate_desc)) },\n                        trailingContent = {\n                            Switch(\n                                checked = enableHighRefreshRate,\n                                onCheckedChange = onEnableHighRefreshRateChange,\n                                thumbContent = {\n                                    Icon(\n                                        painter = painterResource(\n                                            id = if (enableHighRefreshRate) R.drawable.check else R.drawable.close\n                                        ),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(SwitchDefaults.IconSize)\n                                    )\n                                }\n                            )\n                        },\n                        onClick = { onEnableHighRefreshRateChange(!enableHighRefreshRate) }\n                    )\n                )\n                // Only show dynamic theme option when using the default/dynamic color\n                // When a custom color is selected, dynamic theme is automatically disabled\n                if (!isUsingCustomColor) {\n                    add(\n                        Material3SettingsItem(\n                            icon = painterResource(R.drawable.palette),\n                            title = { Text(stringResource(R.string.enable_dynamic_theme)) },\n                            trailingContent = {\n                                Switch(\n                                    checked = dynamicTheme,\n                                    onCheckedChange = onDynamicThemeChange,\n                                    thumbContent = {\n                                        Icon(\n                                            painter = painterResource(\n                                                id = if (dynamicTheme) R.drawable.check else R.drawable.close\n                                            ),\n                                            contentDescription = null,\n                                            modifier = Modifier.size(SwitchDefaults.IconSize)\n                                        )\n                                    }\n                                )\n                            },\n                            onClick = { onDynamicThemeChange(!dynamicTheme) }\n                        )\n                    )\n                }\n                add(\n                    Material3SettingsItem(\n                        icon = painterResource(R.drawable.palette),\n                        title = { Text(stringResource(R.string.theme)) },\n                        description = { Text(stringResource(R.string.theme_desc)) },\n                        onClick = { navController.navigate(\"settings/appearance/theme\") }\n                    )\n                )\n            }\n        )\n\n        Spacer(modifier = Modifier.height(27.dp))\n\n        val (pureBlackMiniPlayer, onPureBlackMiniPlayerChange) = rememberPreference(\n            PureBlackMiniPlayerKey,\n            defaultValue = false\n        )\n\n        Material3SettingsGroup(\n            title = stringResource(id = R.string.mini_player),\n            items = buildList {\n                add(\n                    Material3SettingsItem(\n                        icon = painterResource(R.drawable.nav_bar),\n                        title = { Text(stringResource(R.string.new_mini_player_design)) },\n                        trailingContent = {\n                            Switch(\n                                checked = useNewMiniPlayerDesign,\n                                onCheckedChange = onUseNewMiniPlayerDesignChange,\n                                thumbContent = {\n                                    Icon(\n                                        painter = painterResource(\n                                            id = if (useNewMiniPlayerDesign) R.drawable.check else R.drawable.close\n                                        ),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(SwitchDefaults.IconSize)\n                                    )\n                                }\n                            )\n                        },\n                        onClick = { onUseNewMiniPlayerDesignChange(!useNewMiniPlayerDesign) }\n                    )\n                )\n                add(\n                    Material3SettingsItem(\n                        icon = painterResource(R.drawable.contrast),\n                        title = { Text(stringResource(R.string.pure_black_mini_player)) },\n                        trailingContent = {\n                            Switch(\n                                checked = pureBlackMiniPlayer,\n                                onCheckedChange = onPureBlackMiniPlayerChange,\n                                thumbContent = {\n                                    Icon(\n                                        painter = painterResource(\n                                            id = if (pureBlackMiniPlayer) R.drawable.check else R.drawable.close\n                                        ),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(SwitchDefaults.IconSize)\n                                    )\n                                }\n                            )\n                        },\n                        onClick = { onPureBlackMiniPlayerChange(!pureBlackMiniPlayer) }\n                    )\n                )\n            }\n        )\n\n        Spacer(modifier = Modifier.height(27.dp))\n\n        var showSensitivityDialog by rememberSaveable { mutableStateOf(false) }\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.player),\n            items = listOf(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.palette),\n                    title = { Text(stringResource(R.string.new_player_design)) },\n                    trailingContent = {\n                        Switch(\n                            checked = useNewPlayerDesign,\n                            onCheckedChange = onUseNewPlayerDesignChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (useNewPlayerDesign) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onUseNewPlayerDesignChange(!useNewPlayerDesign) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.gradient),\n                    title = { Text(stringResource(R.string.player_background_style)) },\n                    description = {\n                        Text(\n                            when (playerBackground) {\n                                PlayerBackgroundStyle.DEFAULT -> stringResource(R.string.follow_theme)\n                                PlayerBackgroundStyle.GRADIENT -> stringResource(R.string.gradient)\n                                PlayerBackgroundStyle.BLUR -> stringResource(R.string.player_background_blur)\n                            }\n                        )\n                    },\n                    onClick = { showPlayerBackgroundDialog = true }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.hide_image),\n                    title = { Text(stringResource(R.string.hide_player_thumbnail)) },\n                    description = { Text(stringResource(R.string.hide_player_thumbnail_desc)) },\n                    trailingContent = {\n                        Switch(\n                            checked = hidePlayerThumbnail,\n                            onCheckedChange = onHidePlayerThumbnailChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (hidePlayerThumbnail) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onHidePlayerThumbnailChange(!hidePlayerThumbnail) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.crop),\n                    title = { Text(stringResource(R.string.crop_album_art)) },\n                    description = { Text(stringResource(R.string.crop_album_art_desc)) },\n                    trailingContent = {\n                        Switch(\n                            checked = cropAlbumArt,\n                            onCheckedChange = onCropAlbumArtChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (cropAlbumArt) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onCropAlbumArtChange(!cropAlbumArt) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.palette),\n                    title = { Text(stringResource(R.string.player_buttons_style)) },\n                    description = {\n                        Text(\n                            when (playerButtonsStyle) {\n                                PlayerButtonsStyle.DEFAULT -> stringResource(R.string.default_style)\n                                PlayerButtonsStyle.PRIMARY -> stringResource(R.string.primary_color_style)\n                                PlayerButtonsStyle.TERTIARY -> stringResource(R.string.tertiary_color_style)\n                            }\n                        )\n                    },\n                    onClick = { showPlayerButtonsStyleDialog = true }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.sliders),\n                    title = { Text(stringResource(R.string.player_slider_style)) },\n                    description = {\n                        Text(\n                            when (sliderStyle) {\n                                SliderStyle.DEFAULT -> stringResource(R.string.default_)\n                                SliderStyle.WAVY -> if (squigglySlider) stringResource(R.string.squiggly) else stringResource(\n                                    R.string.wavy\n                                )\n                                SliderStyle.SLIM -> stringResource(R.string.slim)\n                            }\n                        )\n                    },\n                    onClick = { showSliderOptionDialog = true }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.swipe),\n                    title = { Text(stringResource(R.string.enable_swipe_thumbnail)) },\n                    trailingContent = {\n                        Switch(\n                            checked = swipeThumbnail,\n                            onCheckedChange = onSwipeThumbnailChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (swipeThumbnail) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onSwipeThumbnailChange(!swipeThumbnail) }\n                )\n            ) + if (swipeThumbnail) listOf(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.tune),\n                    title = { Text(stringResource(R.string.swipe_sensitivity)) },\n                    description = {\n                        Text(\n                            stringResource(\n                                R.string.sensitivity_percentage,\n                                (swipeSensitivity * 100).roundToInt()\n                            )\n                        )\n                    },\n                    onClick = { showSensitivityDialog = true }\n                )\n            ) else emptyList()\n        )\n\n        if (showSensitivityDialog) {\n            var tempSensitivity by remember { mutableFloatStateOf(swipeSensitivity) }\n\n            DefaultDialog(\n                onDismiss = {\n                    tempSensitivity = swipeSensitivity\n                    showSensitivityDialog = false\n                },\n                buttons = {\n                    TextButton(\n                        onClick = {\n                            tempSensitivity = 0.73f\n                        }\n                    ) {\n                        Text(stringResource(R.string.reset))\n                    }\n\n                    Spacer(modifier = Modifier.weight(1f))\n\n                    TextButton(\n                        onClick = {\n                            tempSensitivity = swipeSensitivity\n                            showSensitivityDialog = false\n                        }\n                    ) {\n                        Text(stringResource(android.R.string.cancel))\n                    }\n                    TextButton(\n                        onClick = {\n                            onSwipeSensitivityChange(tempSensitivity)\n                            showSensitivityDialog = false\n                        }\n                    ) {\n                        Text(stringResource(android.R.string.ok))\n                    }\n                }\n            ) {\n                Column(\n                    horizontalAlignment = Alignment.CenterHorizontally,\n                    modifier = Modifier.padding(16.dp)\n                ) {\n                    Text(\n                        text = stringResource(R.string.swipe_sensitivity),\n                        style = MaterialTheme.typography.headlineSmall,\n                        modifier = Modifier.padding(bottom = 16.dp)\n                    )\n\n                    Text(\n                        text = stringResource(\n                            R.string.sensitivity_percentage,\n                            (tempSensitivity * 100).roundToInt()\n                        ),\n                        style = MaterialTheme.typography.bodyLarge,\n                        modifier = Modifier.padding(bottom = 16.dp)\n                    )\n\n                    Slider(\n                        value = tempSensitivity,\n                        onValueChange = { tempSensitivity = it },\n                        valueRange = 0f..1f,\n                        modifier = Modifier.fillMaxWidth()\n                    )\n                }\n            }\n        }\n\n        Spacer(modifier = Modifier.height(27.dp))\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.lyrics),\n            items = listOf(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.lyrics),\n                    title = { Text(stringResource(R.string.lyrics_text_position)) },\n                    description = {\n                        Text(\n                            when (lyricsPosition) {\n                                LyricsPosition.LEFT -> stringResource(R.string.left)\n                                LyricsPosition.CENTER -> stringResource(R.string.center)\n                                LyricsPosition.RIGHT -> stringResource(R.string.right)\n                            }\n                        )\n                    },\n                    onClick = { showLyricsPositionDialog = true }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.lyrics),\n                    title = { Text(stringResource(R.string.lyrics_animation_style)) },\n                    description = {\n                        Text(\n                            when (lyricsAnimationStyle) {\n                                LyricsAnimationStyle.NONE -> stringResource(R.string.none)\n                                LyricsAnimationStyle.FADE -> stringResource(R.string.fade)\n                                LyricsAnimationStyle.GLOW -> stringResource(R.string.glow)\n                                LyricsAnimationStyle.SLIDE -> stringResource(R.string.slide)\n                                LyricsAnimationStyle.KARAOKE -> stringResource(R.string.karaoke)\n                                LyricsAnimationStyle.APPLE -> stringResource(R.string.apple_music_style)\n                            }\n                        )\n                    },\n                    onClick = { showLyricsAnimationStyleDialog = true }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.lyrics),\n                    title = { Text(stringResource(R.string.lyrics_glow_effect)) },\n                    description = { Text(stringResource(R.string.lyrics_glow_effect_desc)) },\n                    trailingContent = {\n                        Switch(\n                            checked = lyricsGlowEffect,\n                            onCheckedChange = onLyricsGlowEffectChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (lyricsGlowEffect) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onLyricsGlowEffectChange(!lyricsGlowEffect) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.lyrics),\n                    title = { Text(stringResource(R.string.lyrics_text_size)) },\n                    description = { Text(\"${lyricsTextSize.roundToInt()} sp\") },\n                    onClick = { showLyricsTextSizeDialog = true }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.lyrics),\n                    title = { Text(stringResource(R.string.lyrics_line_spacing)) },\n                    description = { Text(\"${String.format(\"%.1f\", lyricsLineSpacing)}x\") },\n                    onClick = { showLyricsLineSpacingDialog = true }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.lyrics),\n                    title = { Text(stringResource(R.string.lyrics_click_change)) },\n                    trailingContent = {\n                        Switch(\n                            checked = lyricsClick,\n                            onCheckedChange = onLyricsClickChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (lyricsClick) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onLyricsClickChange(!lyricsClick) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.lyrics),\n                    title = { Text(stringResource(R.string.lyrics_auto_scroll)) },\n                    trailingContent = {\n                        Switch(\n                            checked = lyricsScroll,\n                            onCheckedChange = onLyricsScrollChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (lyricsScroll) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onLyricsScrollChange(!lyricsScroll) }\n                )\n            )\n        )\n\n        Spacer(modifier = Modifier.height(27.dp))\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.misc),\n            items = listOf(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.nav_bar),\n                    title = { Text(stringResource(R.string.default_open_tab)) },\n                    description = {\n                        Text(\n                            when (defaultOpenTab) {\n                                NavigationTab.HOME -> stringResource(R.string.home)\n                                NavigationTab.SEARCH -> stringResource(R.string.search)\n                                NavigationTab.LIBRARY -> stringResource(R.string.filter_library)\n                            }\n                        )\n                    },\n                    onClick = { showDefaultOpenTabDialog = true }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.tab),\n                    title = { Text(stringResource(R.string.default_lib_chips)) },\n                    description = {\n                        Text(\n                            when (defaultChip) {\n                                LibraryFilter.SONGS -> stringResource(R.string.songs)\n                                LibraryFilter.ARTISTS -> stringResource(R.string.artists)\n                                LibraryFilter.ALBUMS -> stringResource(R.string.albums)\n                                LibraryFilter.PLAYLISTS -> stringResource(R.string.playlists)\n                                LibraryFilter.PODCASTS -> stringResource(R.string.filter_podcasts)\n                                LibraryFilter.LIBRARY -> stringResource(R.string.filter_library)\n                            }\n                        )\n                    },\n                    onClick = { showDefaultChipDialog = true }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.swipe),\n                    title = { Text(stringResource(R.string.swipe_song_to_add)) },\n                    trailingContent = {\n                        Switch(\n                            checked = swipeToSong,\n                            onCheckedChange = onSwipeToSongChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (swipeToSong) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onSwipeToSongChange(!swipeToSong) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.swipe),\n                    title = { Text(stringResource(R.string.swipe_song_to_remove)) },\n                    trailingContent = {\n                        Switch(\n                            checked = swipeToRemoveSong,\n                            onCheckedChange = onSwipeToRemoveSongChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (swipeToRemoveSong) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onSwipeToRemoveSongChange(!swipeToRemoveSong) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.nav_bar),\n                    title = { Text(stringResource(R.string.slim_navbar)) },\n                    trailingContent = {\n                        Switch(\n                            checked = slimNav,\n                            onCheckedChange = onSlimNavChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (slimNav) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onSlimNavChange(!slimNav) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.group_outlined),\n                    title = { Text(stringResource(R.string.listen_together_in_top_bar)) },\n                    description = { Text(stringResource(R.string.listen_together_in_top_bar_desc)) },\n                    trailingContent = {\n                        Switch(\n                            checked = listenTogetherInTopBar,\n                            onCheckedChange = onListenTogetherInTopBarChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (listenTogetherInTopBar) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onListenTogetherInTopBarChange(!listenTogetherInTopBar) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.grid_view),\n                    title = { Text(stringResource(R.string.grid_cell_size)) },\n                    description = {\n                        Text(\n                            when (gridItemSize) {\n                                GridItemSize.BIG -> stringResource(R.string.big)\n                                GridItemSize.SMALL -> stringResource(R.string.small)\n                            }\n                        )\n                    },\n                    onClick = { showGridSizeDialog = true }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.grid_view),\n                    title = { Text(stringResource(R.string.display_density)) },\n                    description = {\n                        Text(DensityScale.fromValue(densityScale).label)\n                    },\n                    onClick = { showDensityScaleDialog = true }\n                )\n            )\n        )\n\n        Spacer(modifier = Modifier.height(27.dp))\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.auto_playlists),\n            items = listOf(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.favorite),\n                    title = { Text(stringResource(R.string.show_liked_playlist)) },\n                    trailingContent = {\n                        Switch(\n                            checked = showLikedPlaylist,\n                            onCheckedChange = onShowLikedPlaylistChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (showLikedPlaylist) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onShowLikedPlaylistChange(!showLikedPlaylist) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.offline),\n                    title = { Text(stringResource(R.string.show_downloaded_playlist)) },\n                    trailingContent = {\n                        Switch(\n                            checked = showDownloadedPlaylist,\n                            onCheckedChange = onShowDownloadedPlaylistChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (showDownloadedPlaylist) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onShowDownloadedPlaylistChange(!showDownloadedPlaylist) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.trending_up),\n                    title = { Text(stringResource(R.string.show_top_playlist)) },\n                    trailingContent = {\n                        Switch(\n                            checked = showTopPlaylist,\n                            onCheckedChange = onShowTopPlaylistChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (showTopPlaylist) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onShowTopPlaylistChange(!showTopPlaylist) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.backup),\n                    title = { Text(stringResource(R.string.show_uploaded_playlist)) },\n                    trailingContent = {\n                        Switch(\n                            checked = showUploadedPlaylist,\n                            onCheckedChange = onShowUploadedPlaylistChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (showUploadedPlaylist) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onShowUploadedPlaylistChange(!showUploadedPlaylist) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.cached),\n                    title = { Text(stringResource(R.string.show_cached_playlist)) },\n                    trailingContent = {\n                        Switch(\n                            checked = showCachedPlaylist,\n                            onCheckedChange = onShowCachedPlaylistChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (showCachedPlaylist) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onShowCachedPlaylistChange(!showCachedPlaylist) }\n                )\n            )\n        )\n        Spacer(modifier = Modifier.height(16.dp))\n    }\n\n    TopAppBar(\n        title = { Text(stringResource(R.string.appearance)) },\n        navigationIcon = {\n            IconButton(\n                onClick = navController::navigateUp,\n                onLongClick = navController::backToMain,\n            ) {\n                Icon(\n                    painterResource(R.drawable.arrow_back),\n                    contentDescription = null,\n                )\n            }\n        }\n    )\n}\n\nenum class DarkMode {\n    ON,\n    OFF,\n    AUTO,\n}\n\nenum class NavigationTab {\n    HOME,\n    SEARCH,\n    LIBRARY,\n}\n\nenum class LyricsPosition {\n    LEFT,\n    CENTER,\n    RIGHT,\n}\n\nenum class PlayerTextAlignment {\n    SIDED,\n    CENTER,\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/settings/BackupAndRestore.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.settings\n\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport coil3.compose.AsyncImage\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.Material3SettingsGroup\nimport com.metrolist.music.ui.component.Material3SettingsItem\nimport com.metrolist.music.ui.menu.AddToPlaylistDialogOnline\nimport com.metrolist.music.ui.menu.CsvColumnMappingDialog\nimport com.metrolist.music.ui.menu.CsvImportProgressDialog\nimport com.metrolist.music.ui.menu.LoadingScreen\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.viewmodels.BackupPreviewInfo\nimport com.metrolist.music.viewmodels.BackupRestoreViewModel\nimport com.metrolist.music.viewmodels.ConvertedSongLog\nimport com.metrolist.music.viewmodels.CsvImportState\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport java.time.LocalDateTime\nimport java.time.format.DateTimeFormatter\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun BackupAndRestore(\n    navController: NavController,\n    viewModel: BackupRestoreViewModel = hiltViewModel(),\n) {\n    var importedTitle by remember { mutableStateOf(\"\") }\n    val importedSongs = remember { mutableStateListOf<Song>() }\n    var showChoosePlaylistDialogOnline by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    var currentImportSong by rememberSaveable { mutableStateOf(\"\") }\n    var isProgressStarted by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    var progressPercentage by rememberSaveable {\n        mutableIntStateOf(0)\n    }\n\n    // CSV column mapping state\n    var csvImportState by remember { mutableStateOf<CsvImportState?>(null) }\n    var showCsvColumnMapping by rememberSaveable { mutableStateOf(false) }\n    var showCsvImportProgress by rememberSaveable { mutableStateOf(false) }\n    var csvImportProgress by rememberSaveable { mutableIntStateOf(0) }\n    val csvRecentLogs = remember { mutableStateListOf<ConvertedSongLog>() }\n    var pendingCsvUri by remember { mutableStateOf<android.net.Uri?>(null) }\n\n    // Restore confirmation dialog state\n    var showRestoreConfirmDialog by rememberSaveable { mutableStateOf(false) }\n    var pendingRestoreUri by remember { mutableStateOf<android.net.Uri?>(null) }\n    var backupPreviewInfo by remember { mutableStateOf<BackupPreviewInfo?>(null) }\n    var isLoadingAccountInfo by remember { mutableStateOf(false) }\n    var accountCheckFailed by remember { mutableStateOf(false) }\n\n    val context = LocalContext.current\n    val coroutineScope = rememberCoroutineScope()\n\n    val backupLauncher =\n        rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument(\"application/octet-stream\")) { uri ->\n            if (uri != null) {\n                viewModel.backup(context, uri)\n            }\n        }\n    val restoreLauncher =\n        rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->\n            if (uri != null) {\n                pendingRestoreUri = uri\n                val preview = viewModel.previewBackup(context, uri)\n                backupPreviewInfo = preview\n                showRestoreConfirmDialog = true\n\n                // Fetch account info asynchronously if backup has auth data\n                accountCheckFailed = false\n                if (preview.hasAuthData && preview.cookie != null) {\n                    isLoadingAccountInfo = true\n                    coroutineScope.launch(Dispatchers.IO) {\n                        val accountInfo = viewModel.fetchAccountInfoFromBackup(preview.cookie)\n                        if (accountInfo != null) {\n                            backupPreviewInfo = accountInfo\n                        } else {\n                            accountCheckFailed = true\n                        }\n                        isLoadingAccountInfo = false\n                    }\n                }\n            }\n        }\n    val importPlaylistFromCsv =\n        rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->\n            if (uri == null) return@rememberLauncherForActivityResult\n            pendingCsvUri = uri\n            val previewState = viewModel.previewCsvFile(context, uri)\n            csvImportState = previewState\n            showCsvColumnMapping = true\n        }\n    val importM3uLauncherOnline =\n        rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->\n            if (uri == null) return@rememberLauncherForActivityResult\n            val result = viewModel.loadM3UOnline(context, uri)\n            importedSongs.clear()\n            importedSongs.addAll(result)\n\n            if (importedSongs.isNotEmpty()) {\n                showChoosePlaylistDialogOnline = true\n            }\n        }\n\n    Column(\n        Modifier\n            .windowInsetsPadding(LocalPlayerAwareWindowInsets.current.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom))\n            .verticalScroll(rememberScrollState())\n            .padding(horizontal = 16.dp),\n    ) {\n        Spacer(\n            Modifier.windowInsetsPadding(\n                LocalPlayerAwareWindowInsets.current.only(\n                    WindowInsetsSides.Top,\n                ),\n            ),\n        )\n\n        val appName = stringResource(R.string.app_name)\n        Material3SettingsGroup(\n            items =\n                listOf(\n                    Material3SettingsItem(\n                        title = { Text(stringResource(R.string.action_backup)) },\n                        icon = painterResource(R.drawable.backup),\n                        onClick = {\n                            val formatter = DateTimeFormatter.ofPattern(\"yyyyMMddHHmmss\")\n                            backupLauncher.launch(\n                                \"${appName}_${\n                                    LocalDateTime.now().format(formatter)\n                                }.backup\",\n                            )\n                        },\n                    ),\n                    Material3SettingsItem(\n                        title = { Text(stringResource(R.string.action_restore)) },\n                        icon = painterResource(R.drawable.restore),\n                        onClick = {\n                            restoreLauncher.launch(arrayOf(\"application/octet-stream\"))\n                        },\n                    ),\n                    Material3SettingsItem(\n                        title = { Text(stringResource(R.string.import_online)) },\n                        icon = painterResource(R.drawable.playlist_add),\n                        onClick = {\n                            importM3uLauncherOnline.launch(arrayOf(\"audio/*\"))\n                        },\n                    ),\n                    Material3SettingsItem(\n                        title = { Text(stringResource(R.string.import_csv)) },\n                        icon = painterResource(R.drawable.playlist_add),\n                        onClick = {\n                            importPlaylistFromCsv.launch(\n                                arrayOf(\"text/csv\", \"text/comma-separated-values\", \"application/csv\", \"text/plain\"),\n                            )\n                        },\n                    ),\n                ),\n        )\n    }\n\n    TopAppBar(\n        title = { Text(stringResource(R.string.backup_restore)) },\n        navigationIcon = {\n            IconButton(\n                onClick = navController::navigateUp,\n                onLongClick = navController::backToMain,\n            ) {\n                Icon(\n                    painterResource(R.drawable.arrow_back),\n                    contentDescription = null,\n                )\n            }\n        },\n    )\n\n    AddToPlaylistDialogOnline(\n        isVisible = showChoosePlaylistDialogOnline,\n        allowSyncing = false,\n        initialTextFieldValue = importedTitle,\n        songs = importedSongs,\n        onDismiss = { showChoosePlaylistDialogOnline = false },\n        onProgressStart = { newVal -> isProgressStarted = newVal },\n        onPercentageChange = { newPercentage -> progressPercentage = newPercentage },\n        onSongChange = { currentImportSong = it },\n    )\n\n    LoadingScreen(\n        isVisible = isProgressStarted,\n        value = progressPercentage,\n        songTitle = currentImportSong,\n    )\n\n    // CSV column mapping dialog\n    csvImportState?.let { state ->\n        CsvColumnMappingDialog(\n            isVisible = showCsvColumnMapping,\n            csvState = state,\n            onDismiss = {\n                showCsvColumnMapping = false\n                csvImportState = null\n            },\n            onConfirm = { mappingState ->\n                showCsvColumnMapping = false\n                csvImportState = mappingState\n                pendingCsvUri?.let { uri ->\n                    showCsvImportProgress = true\n                    coroutineScope.launch(Dispatchers.Default) {\n                        val result =\n                            viewModel.importPlaylistFromCsv(\n                                context,\n                                uri,\n                                mappingState,\n                                onProgress = { progress ->\n                                    csvImportProgress = progress\n                                },\n                                onLogUpdate = { logs ->\n                                    csvRecentLogs.clear()\n                                    csvRecentLogs.addAll(logs)\n                                },\n                            )\n                        importedSongs.clear()\n                        importedSongs.addAll(result)\n                        if (result.isNotEmpty()) {\n                            showCsvImportProgress = false\n                            csvImportProgress = 0\n                            csvRecentLogs.clear()\n                            showChoosePlaylistDialogOnline = true\n                        }\n                    }\n                }\n            },\n        )\n    }\n\n    // CSV import progress dialog\n    CsvImportProgressDialog(\n        isVisible = showCsvImportProgress,\n        progress = csvImportProgress,\n        recentLogs = csvRecentLogs.toList(),\n        onDismiss = {\n            // Cannot dismiss while importing\n        },\n    )\n\n    // Restore confirmation dialog\n    if (showRestoreConfirmDialog) {\n        DefaultDialog(\n            onDismiss = {\n                showRestoreConfirmDialog = false\n                pendingRestoreUri = null\n                backupPreviewInfo = null\n                accountCheckFailed = false\n            },\n            icon = {\n                Icon(\n                    painter = painterResource(R.drawable.restore),\n                    contentDescription = null,\n                )\n            },\n            title = { Text(stringResource(R.string.restore_confirm_title)) },\n            buttons = {\n                TextButton(\n                    onClick = {\n                        showRestoreConfirmDialog = false\n                        pendingRestoreUri = null\n                        backupPreviewInfo = null\n                        accountCheckFailed = false\n                    },\n                ) {\n                    Text(stringResource(android.R.string.cancel))\n                }\n                TextButton(\n                    onClick = {\n                        showRestoreConfirmDialog = false\n                        pendingRestoreUri?.let { uri ->\n                            viewModel.restore(context, uri, clearAuthData = true)\n                        }\n                        pendingRestoreUri = null\n                        backupPreviewInfo = null\n                        accountCheckFailed = false\n                    },\n                ) {\n                    Text(stringResource(R.string.restore))\n                }\n            },\n        ) {\n            // Supporting text\n            Text(\n                text = stringResource(R.string.restore_confirm_message),\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                style = MaterialTheme.typography.bodyMedium,\n            )\n\n            // Show warning about account sign out if account found\n            if (backupPreviewInfo?.accountName != null) {\n                Spacer(modifier = Modifier.height(16.dp))\n                Text(\n                    text = stringResource(R.string.restore_account_warning),\n                    color = MaterialTheme.colorScheme.onSurface,\n                    style = MaterialTheme.typography.titleSmall,\n                )\n            }\n\n            // Show loading or account info\n            if (isLoadingAccountInfo) {\n                Spacer(modifier = Modifier.height(16.dp))\n                HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)\n                Spacer(modifier = Modifier.height(16.dp))\n\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    modifier = Modifier.fillMaxWidth(),\n                ) {\n                    CircularProgressIndicator(\n                        modifier = Modifier.size(24.dp),\n                        strokeWidth = 2.dp,\n                    )\n                    Spacer(modifier = Modifier.size(16.dp))\n                    Text(\n                        text = stringResource(R.string.checking_previous_account),\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n                }\n\n                Spacer(modifier = Modifier.height(16.dp))\n                HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)\n            }\n\n            // Show \"No account found\" if check failed OR backup has no auth data\n            val hasNoAccount =\n                backupPreviewInfo?.let {\n                    !it.hasAuthData || (it.hasAuthData && it.accountName == null && !isLoadingAccountInfo)\n                } ?: false\n            if (!isLoadingAccountInfo && (accountCheckFailed || hasNoAccount)) {\n                Spacer(modifier = Modifier.height(16.dp))\n                HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)\n                Spacer(modifier = Modifier.height(16.dp))\n\n                Text(\n                    text = stringResource(R.string.no_account_found),\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n\n                Spacer(modifier = Modifier.height(16.dp))\n                HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)\n            }\n\n            // Show account info if backup contains auth data and we have account details\n            backupPreviewInfo?.let { preview ->\n                if (!isLoadingAccountInfo && preview.hasAuthData && preview.accountName != null) {\n                    Spacer(modifier = Modifier.height(16.dp))\n\n                    HorizontalDivider(\n                        color = MaterialTheme.colorScheme.outlineVariant,\n                    )\n\n                    Spacer(modifier = Modifier.height(16.dp))\n\n                    Row(\n                        verticalAlignment = Alignment.CenterVertically,\n                        modifier = Modifier.fillMaxWidth(),\n                    ) {\n                        if (preview.accountImageUrl != null) {\n                            AsyncImage(\n                                model = preview.accountImageUrl,\n                                contentDescription = null,\n                                modifier =\n                                    Modifier\n                                        .size(40.dp)\n                                        .clip(CircleShape),\n                                contentScale = ContentScale.Crop,\n                            )\n                        } else {\n                            Box(\n                                modifier =\n                                    Modifier\n                                        .size(40.dp)\n                                        .clip(CircleShape)\n                                        .background(MaterialTheme.colorScheme.secondaryContainer),\n                                contentAlignment = Alignment.Center,\n                            ) {\n                                Icon(\n                                    painter = painterResource(R.drawable.person),\n                                    contentDescription = null,\n                                    tint = MaterialTheme.colorScheme.onSecondaryContainer,\n                                    modifier = Modifier.size(24.dp),\n                                )\n                            }\n                        }\n\n                        Spacer(modifier = Modifier.size(16.dp))\n\n                        Text(\n                            text = preview.accountEmail ?: preview.accountName,\n                            style = MaterialTheme.typography.bodyLarge,\n                            color = MaterialTheme.colorScheme.onSurface,\n                        )\n                    }\n\n                    Spacer(modifier = Modifier.height(16.dp))\n\n                    HorizontalDivider(\n                        color = MaterialTheme.colorScheme.outlineVariant,\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/settings/ChangelogScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.settings\n\nimport androidx.compose.animation.*\nimport androidx.compose.foundation.layout.*\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.text.ClickableText\nimport androidx.compose.material3.*\nimport androidx.compose.runtime.*\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalUriHandler\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.text.withStyle\nimport androidx.compose.ui.unit.dp\nimport com.metrolist.music.R\nimport com.metrolist.music.BuildConfig\nimport com.metrolist.music.utils.ReleaseInfo\nimport com.metrolist.music.utils.Updater\n\nprivate val markdownLinkRegex = Regex(\"(@[a-zA-Z0-9_-]+)|(https?://[\\\\w-]+(\\\\.[\\\\w-]+)+[\\\\w.,@?^=%&:/~+#-]*[\\\\w@?^=%&/~+#-])\")\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun ChangelogScreen(\n    onDismiss: () -> Unit\n) {\n    var releases by remember { mutableStateOf<List<ReleaseInfo>>(emptyList()) }\n    var isLoading by remember { mutableStateOf(true) }\n    val uriHandler = LocalUriHandler.current\n\n    LaunchedEffect(Unit) {\n        Updater.getAllReleases().onSuccess { allReleases ->\n            releases = allReleases.filter { release ->\n                Updater.compareVersions(BuildConfig.VERSION_NAME, release.tagName) >= 0\n            }\n            isLoading = false\n        }.onFailure {\n            isLoading = false\n        }\n    }\n\n    val sheetState = rememberModalBottomSheetState(\n        skipPartiallyExpanded = false\n    )\n\n    val showFab by remember {\n        derivedStateOf { sheetState.targetValue != SheetValue.Hidden }\n    }\n\n    ModalBottomSheet(\n        onDismissRequest = onDismiss,\n        sheetState = sheetState,\n        containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,\n        shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp),\n        dragHandle = { BottomSheetDefaults.DragHandle() }\n    ) {\n        Box(modifier = Modifier.fillMaxWidth()) {\n            LazyColumn(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = 16.dp),\n                verticalArrangement = Arrangement.spacedBy(16.dp),\n                contentPadding = PaddingValues(bottom = 80.dp)\n            ) {\n                item {\n                    Text(\n                        text = stringResource(R.string.changelog),\n                        style = MaterialTheme.typography.displaySmall,\n                        fontWeight = FontWeight.Bold,\n                        modifier = Modifier.fillMaxWidth(),\n                        textAlign = TextAlign.Center\n                    )\n                }\n\n                item {\n                    val density = LocalDensity.current\n                    val stroke = remember(density) {\n                        Stroke(width = with(density) { 3.dp.toPx() }, cap = StrokeCap.Round)\n                    }\n                    LinearWavyProgressIndicator(\n                        progress = { 1f },\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(horizontal = 32.dp),\n                        color = MaterialTheme.colorScheme.primary,\n                        trackColor = Color.Transparent,\n                        stroke = stroke,\n                        trackStroke = stroke,\n                        amplitude = { 1f }\n                    )\n                }\n\n                if (isLoading) {\n                    item {\n                        Box(modifier = Modifier.fillMaxWidth().height(200.dp), contentAlignment = Alignment.Center) {\n                            CircularProgressIndicator()\n                        }\n                    }\n                } else if (releases.isEmpty()) {\n                    item {\n                        Box(modifier = Modifier.fillMaxWidth().height(200.dp), contentAlignment = Alignment.Center) {\n                            Text(text = stringResource(R.string.changelog_empty))\n                        }\n                    }\n                } else {\n                    items(releases) { release ->\n                        ReleaseItem(release)\n                    }\n                }\n            }\n\n            androidx.compose.animation.AnimatedVisibility(\n                visible = showFab,\n                enter = fadeIn() + slideInVertically { it },\n                exit = fadeOut() + slideOutVertically { it },\n                modifier = Modifier\n                    .align(Alignment.BottomEnd)\n                    .padding(16.dp)\n            ) {\n                val githubReleasesUrl = stringResource(R.string.github_releases_url)\n                ExtendedFloatingActionButton(\n                    onClick = { uriHandler.openUri(githubReleasesUrl) },\n                    icon = { Icon(painterResource(R.drawable.github), contentDescription = null, modifier = Modifier.size(24.dp)) },\n                    text = { Text(stringResource(R.string.view_on_github)) },\n                    containerColor = MaterialTheme.colorScheme.onPrimary,\n                    contentColor = MaterialTheme.colorScheme.primary\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun ReleaseItem(release: ReleaseInfo) {\n    Column(modifier = Modifier.fillMaxWidth()) {\n        Row(\n            modifier = Modifier.fillMaxWidth(),\n            horizontalArrangement = Arrangement.SpaceBetween,\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Surface(\n                color = MaterialTheme.colorScheme.secondaryContainer,\n                shape = CircleShape\n            ) {\n                Text(\n                    text = release.tagName,\n                    modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),\n                    style = MaterialTheme.typography.labelLarge,\n                    color = MaterialTheme.colorScheme.onSecondaryContainer\n                )\n            }\n\n            Text(\n                text = release.releaseDate.split(\"T\").firstOrNull() ?: \"\",\n                style = MaterialTheme.typography.bodyMedium,\n                color = MaterialTheme.colorScheme.onSurfaceVariant\n            )\n        }\n\n        Spacer(modifier = Modifier.height(12.dp))\n\n        Card(\n            modifier = Modifier.fillMaxWidth(),\n            colors = CardDefaults.cardColors(\n                containerColor = MaterialTheme.colorScheme.surfaceContainer\n            ),\n            shape = RoundedCornerShape(16.dp)\n        ) {\n            Column(modifier = Modifier.padding(16.dp)) {\n                MarkdownText(release.description)\n            }\n        }\n    }\n}\n\n@Suppress(\"DEPRECATION\")\n@Composable\nfun MarkdownText(text: String) {\n    val lines = text.split(\"\\n\")\n    val uriHandler = LocalUriHandler.current\n\n    Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {\n        lines.filter { it.isNotBlank() }.forEach { line ->\n            val trimmedLine = line.trim()\n\n            if (trimmedLine.startsWith(\"#\")) {\n                val level = trimmedLine.takeWhile { it == '#' }.length\n                val headerText = trimmedLine.substring(level).trim()\n                Box(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), contentAlignment = Alignment.Center) {\n                    Text(\n                        text = headerText,\n                        style = when (level) {\n                            1 -> MaterialTheme.typography.headlineMedium\n                            2 -> MaterialTheme.typography.headlineSmall\n                            else -> MaterialTheme.typography.titleMedium\n                        },\n                        fontWeight = FontWeight.Bold,\n                        textAlign = TextAlign.Center\n                    )\n                }\n            } else {\n                val isListItem = trimmedLine.startsWith(\"- \") || trimmedLine.startsWith(\"* \")\n                val contentText = if (isListItem) {\n                    trimmedLine.substring(2).trim()\n                } else {\n                    trimmedLine\n                }\n\n                val annotatedString = buildAnnotatedString {\n                    var lastIndex = 0\n                    markdownLinkRegex.findAll(contentText).forEach { result ->\n                        append(contentText.substring(lastIndex, result.range.first))\n                        \n                        val match = result.value\n                        val link = if (match.startsWith(\"@\")) \"https://github.com/${match.substring(1)}\" else match\n                        \n                        pushStringAnnotation(tag = \"URL\", annotation = link)\n                        withStyle(style = SpanStyle(\n                            color = MaterialTheme.colorScheme.primary, \n                            fontWeight = if (match.startsWith(\"@\")) FontWeight.Bold else FontWeight.Normal,\n                            textDecoration = if (match.startsWith(\"@\")) TextDecoration.None else TextDecoration.Underline\n                        )) {\n                            append(match)\n                        }\n                        pop()\n                        lastIndex = result.range.last + 1\n                    }\n                    append(contentText.substring(lastIndex))\n                }\n\n                Column(modifier = Modifier.fillMaxWidth()) {\n                    Row(modifier = Modifier.fillMaxWidth()) {\n                        if (isListItem) {\n                            Text(\n                                text = stringResource(R.string.list_bullet),\n                                modifier = Modifier.padding(end = 8.dp),\n                                style = MaterialTheme.typography.bodyLarge\n                            )\n                        }\n                        ClickableText(\n                            text = annotatedString,\n                            style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface),\n                            onClick = { offset ->\n                                annotatedString.getStringAnnotations(tag = \"URL\", start = offset, end = offset)\n                                    .firstOrNull()?.let { annotation ->\n                                        uriHandler.openUri(annotation.item)\n                                    }\n                            }\n                        )\n                    }\n                    \n                    if (isListItem) {\n                        Spacer(modifier = Modifier.height(4.dp))\n                        HorizontalDivider(\n                            thickness = 0.5.dp,\n                            color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f)\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/settings/ContentSettings.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.settings\n\nimport android.content.Intent\nimport android.os.Build\nimport android.provider.Settings\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExposedDropdownMenuAnchorType\nimport androidx.compose.material3.ExposedDropdownMenuBox\nimport androidx.compose.material3.ExposedDropdownMenuDefaults\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Slider\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.SwitchDefaults\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.core.net.toUri\nimport androidx.navigation.NavController\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.AppLanguageKey\nimport com.metrolist.music.constants.ContentCountryKey\nimport com.metrolist.music.constants.ContentLanguageKey\nimport com.metrolist.music.constants.CountryCodeToName\nimport com.metrolist.music.constants.EnableBetterLyricsKey\nimport com.metrolist.music.constants.EnableKugouKey\nimport com.metrolist.music.constants.EnableLrcLibKey\nimport com.metrolist.music.constants.EnableSimpMusicKey\nimport com.metrolist.music.constants.EnableLyricsPlus\nimport com.metrolist.music.constants.HideExplicitKey\nimport com.metrolist.music.constants.HideVideoSongsKey\nimport com.metrolist.music.constants.HideYoutubeShortsKey\nimport com.metrolist.music.constants.LanguageCodeToName\nimport com.metrolist.music.constants.LyricsProviderOrderKey\nimport com.metrolist.music.constants.ProxyEnabledKey\nimport com.metrolist.music.constants.ProxyPasswordKey\nimport com.metrolist.music.constants.ProxyTypeKey\nimport com.metrolist.music.constants.ProxyUrlKey\nimport com.metrolist.music.constants.ProxyUsernameKey\nimport com.metrolist.music.constants.QuickPicks\nimport com.metrolist.music.constants.QuickPicksKey\nimport com.metrolist.music.constants.RandomizeHomeOrderKey\nimport com.metrolist.music.constants.SYSTEM_DEFAULT\nimport com.metrolist.music.constants.ShowArtistDescriptionKey\nimport com.metrolist.music.constants.ShowArtistSubscriberCountKey\nimport com.metrolist.music.constants.ShowMonthlyListenersKey\nimport com.metrolist.music.constants.ShowWrappedCardKey\nimport com.metrolist.music.constants.TopSize\nimport com.metrolist.music.ui.component.EnumDialog\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.Material3SettingsGroup\nimport com.metrolist.music.ui.component.Material3SettingsItem\nimport com.metrolist.music.ui.component.DraggableLyricsProviderItem\nimport com.metrolist.music.ui.component.DraggableLyricsProviderList\nimport com.metrolist.music.lyrics.LyricsProviderRegistry\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport java.net.Proxy\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun ContentSettings(\n    navController: NavController\n) {\n    val context = LocalContext.current\n    // Used only before Android 13\n    val (appLanguage, onAppLanguageChange) = rememberPreference(key = AppLanguageKey, defaultValue = SYSTEM_DEFAULT)\n\n    val (contentLanguage, onContentLanguageChange) = rememberPreference(key = ContentLanguageKey, defaultValue = \"system\")\n    val (contentCountry, onContentCountryChange) = rememberPreference(key = ContentCountryKey, defaultValue = \"system\")\n    val (hideExplicit, onHideExplicitChange) = rememberPreference(key = HideExplicitKey, defaultValue = false)\n    val (hideVideoSongs, onHideVideoSongsChange) = rememberPreference(key = HideVideoSongsKey, defaultValue = false)\n    val (hideYoutubeShorts, onHideYoutubeShortsChange) = rememberPreference(key = HideYoutubeShortsKey, defaultValue = false)\n    val (showArtistDescription, onShowArtistDescriptionChange) = rememberPreference(key = ShowArtistDescriptionKey, defaultValue = true)\n    val (showArtistSubscriberCount, onShowArtistSubscriberCountChange) = rememberPreference(key = ShowArtistSubscriberCountKey, defaultValue = true)\n    val (showMonthlyListeners, onShowMonthlyListenersChange) = rememberPreference(key = ShowMonthlyListenersKey, defaultValue = true)\n    val (proxyEnabled, onProxyEnabledChange) = rememberPreference(key = ProxyEnabledKey, defaultValue = false)\n    val (proxyType, onProxyTypeChange) = rememberEnumPreference(key = ProxyTypeKey, defaultValue = Proxy.Type.HTTP)\n    val (proxyUrl, onProxyUrlChange) = rememberPreference(key = ProxyUrlKey, defaultValue = \"host:port\")\n    val (proxyUsername, onProxyUsernameChange) = rememberPreference(key = ProxyUsernameKey, defaultValue = \"username\")\n    val (proxyPassword, onProxyPasswordChange) = rememberPreference(key = ProxyPasswordKey, defaultValue = \"password\")\n    val (enableKugou, onEnableKugouChange) = rememberPreference(key = EnableKugouKey, defaultValue = true)\n    val (enableLrclib, onEnableLrclibChange) = rememberPreference(key = EnableLrcLibKey, defaultValue = true)\n    val (enableBetterLyrics, onEnableBetterLyricsChange) = rememberPreference(key = EnableBetterLyricsKey, defaultValue = true)\n    val (enableSimpMusic, onEnableSimpMusicChange) = rememberPreference(key = EnableSimpMusicKey, defaultValue = true)\n    val (enableLyricsPlus, onEnableLyricsPlusChange) = rememberPreference(key = EnableLyricsPlus, defaultValue = false)\n    val (lyricsProviderOrder, onLyricsProviderOrderChange) = rememberPreference(\n        key = LyricsProviderOrderKey,\n        defaultValue = LyricsProviderRegistry.serializeProviderOrder(LyricsProviderRegistry.getDefaultProviderOrder())\n    )\n    val (lengthTop, onLengthTopChange) = rememberPreference(key = TopSize, defaultValue = \"50\")\n    val (quickPicks, onQuickPicksChange) = rememberEnumPreference(key = QuickPicksKey, defaultValue = QuickPicks.QUICK_PICKS)\n    val (showWrappedCard, onShowWrappedCardChange) = rememberPreference(key = ShowWrappedCardKey, defaultValue = false)\n    val (randomizeHomeOrder, onRandomizeHomeOrderChange) = rememberPreference(\n        RandomizeHomeOrderKey,\n        defaultValue = true\n    )\n\n    val providerDisplayNames =\n        mapOf(\n            \"BetterLyrics\" to \"Better Lyrics\",\n            \"SimpMusic\" to \"SimpMusic\",\n            \"LrcLib\" to \"LrcLib\",\n            \"KuGou\" to \"KuGou\",\n            \"LyricsPlus\" to \"LyricsPlus\",\n            \"YouTubeSubtitle\" to \"YouTube Subtitles\",\n            \"YouTube\" to \"YouTube\",\n        )\n\n    var showProxyConfigurationDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    if (showProxyConfigurationDialog) {\n        var expandedDropdown by remember { mutableStateOf(false) }\n\n        var tempProxyUrl by rememberSaveable { mutableStateOf(proxyUrl) }\n        var tempProxyUsername by rememberSaveable { mutableStateOf(proxyUsername) }\n        var tempProxyPassword by rememberSaveable { mutableStateOf(proxyPassword) }\n        var authEnabled by rememberSaveable { mutableStateOf(proxyUsername.isNotBlank() || proxyPassword.isNotBlank()) }\n\n        AlertDialog(\n            onDismissRequest = { showProxyConfigurationDialog = false },\n            title = {\n                Text(stringResource(R.string.config_proxy))\n            },\n            text = {\n                Column(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .verticalScroll(rememberScrollState()),\n                    verticalArrangement = Arrangement.spacedBy(12.dp)\n                ) {\n                    ExposedDropdownMenuBox(\n                        expanded = expandedDropdown,\n                        onExpandedChange = { expandedDropdown = !expandedDropdown },\n                        modifier = Modifier.fillMaxWidth()\n                    ) {\n                        OutlinedTextField(\n                            value = proxyType.name,\n                            onValueChange = {},\n                            readOnly = true,\n                            label = { Text(stringResource(R.string.proxy_type)) },\n                            trailingIcon = {\n                                ExposedDropdownMenuDefaults.TrailingIcon(expanded = expandedDropdown)\n                            },\n                            colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),\n                            modifier = Modifier\n                                .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)\n                                .fillMaxWidth()\n                        )\n                        ExposedDropdownMenu(\n                            expanded = expandedDropdown,\n                            onDismissRequest = { expandedDropdown = false }\n                        ) {\n                            listOf(Proxy.Type.HTTP, Proxy.Type.SOCKS).forEach { type ->\n                                DropdownMenuItem(\n                                    text = { Text(type.name) },\n                                    onClick = {\n                                        onProxyTypeChange(type)\n                                        expandedDropdown = false\n                                    }\n                                )\n                            }\n                        }\n                    }\n\n                    OutlinedTextField(\n                        value = tempProxyUrl,\n                        onValueChange = { tempProxyUrl = it },\n                        label = { Text(stringResource(R.string.proxy_url)) },\n                        modifier = Modifier.fillMaxWidth()\n                    )\n\n                    Row(\n                        modifier = Modifier.fillMaxWidth(),\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.SpaceBetween\n                    ) {\n                        Text(stringResource(R.string.enable_authentication))\n                        Switch(\n                            checked = authEnabled,\n                            onCheckedChange = {\n                                authEnabled = it\n                                if (!it) {\n                                    tempProxyUsername = \"\"\n                                    tempProxyPassword = \"\"\n                                }\n                            }\n                        )\n                    }\n\n                    AnimatedVisibility(visible = authEnabled) {\n                        Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {\n                            OutlinedTextField(\n                                value = tempProxyUsername,\n                                onValueChange = { tempProxyUsername = it },\n                                label = { Text(stringResource(R.string.proxy_username)) },\n                                modifier = Modifier.fillMaxWidth()\n                            )\n                            OutlinedTextField(\n                                value = tempProxyPassword,\n                                onValueChange = { tempProxyPassword = it },\n                                label = { Text(stringResource(R.string.proxy_password)) },\n                                modifier = Modifier.fillMaxWidth()\n                            )\n                        }\n                    }\n                }\n            },\n            confirmButton = {\n                TextButton(\n                    onClick = {\n                        onProxyUrlChange(tempProxyUrl)\n                        onProxyUsernameChange(if (authEnabled) tempProxyUsername else \"\")\n                        onProxyPasswordChange(if (authEnabled) tempProxyPassword else \"\")\n                        showProxyConfigurationDialog = false\n                    }\n                ) {\n                    Text(stringResource(R.string.save))\n                }\n            },\n            dismissButton = {\n                TextButton(onClick = {\n                    showProxyConfigurationDialog = false\n                }) {\n                    Text(stringResource(R.string.cancel))\n                }\n            }\n        )\n    }\n\n    var showContentLanguageDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    if (showContentLanguageDialog) {\n        EnumDialog(\n            onDismiss = { showContentLanguageDialog = false },\n            onSelect = {\n                onContentLanguageChange(it)\n                showContentLanguageDialog = false\n            },\n            title = stringResource(R.string.content_language),\n            current = contentLanguage,\n            values = (listOf(SYSTEM_DEFAULT) + LanguageCodeToName.keys.toList()),\n            valueText = {\n                LanguageCodeToName.getOrElse(it) { stringResource(R.string.system_default) }\n            }\n        )\n    }\n\n    var showContentCountryDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    if (showContentCountryDialog) {\n        EnumDialog(\n            onDismiss = { showContentCountryDialog = false },\n            onSelect = {\n                onContentCountryChange(it)\n                showContentCountryDialog = false\n            },\n            title = stringResource(R.string.content_country),\n            current = contentCountry,\n            values = (listOf(SYSTEM_DEFAULT) + CountryCodeToName.keys.toList()),\n            valueText = {\n                CountryCodeToName.getOrElse(it) { stringResource(R.string.system_default) }\n            }\n        )\n    }\n\n    var showAppLanguageDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    if (showAppLanguageDialog) {\n        EnumDialog(\n            onDismiss = { showAppLanguageDialog = false },\n            onSelect = {\n                onAppLanguageChange(it)\n                showAppLanguageDialog = false\n            },\n            title = stringResource(R.string.app_language),\n            current = appLanguage,\n            values = (listOf(SYSTEM_DEFAULT) + LanguageCodeToName.keys.toList()),\n            valueText = {\n                LanguageCodeToName.getOrElse(it) { stringResource(R.string.system_default) }\n            }\n        )\n    }\n\n    var showProviderSelectionDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    if (showProviderSelectionDialog) {\n        AlertDialog(\n            onDismissRequest = { showProviderSelectionDialog = false },\n            title = { Text(stringResource(R.string.lyrics_provider_selection)) },\n            text = {\n                Column(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .verticalScroll(rememberScrollState()),\n                    verticalArrangement = Arrangement.spacedBy(12.dp)\n                ) {\n                    Row(\n                        modifier = Modifier.fillMaxWidth(),\n                        horizontalArrangement = Arrangement.SpaceBetween,\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        Column(\n                            modifier = Modifier.weight(1f)\n                        ) {\n                            Text(stringResource(R.string.enable_lrclib))\n                            Text(\n                                text = stringResource(R.string.enable_lrclib_desc),\n                                style = MaterialTheme.typography.bodySmall,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant\n                            )\n                        }\n                        Switch(\n                            checked = enableLrclib,\n                            onCheckedChange = onEnableLrclibChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (enableLrclib) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    }\n                    Row(\n                        modifier = Modifier.fillMaxWidth(),\n                        horizontalArrangement = Arrangement.SpaceBetween,\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        Column(\n                            modifier = Modifier.weight(1f)\n                        ) {\n                            Text(stringResource(R.string.enable_kugou))\n                            Text(\n                                text = stringResource(R.string.enable_kugou_desc),\n                                style = MaterialTheme.typography.bodySmall,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant\n                            )\n                        }\n                        Switch(\n                            checked = enableKugou,\n                            onCheckedChange = onEnableKugouChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (enableKugou) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    }\n                    Row(\n                        modifier = Modifier.fillMaxWidth(),\n                        horizontalArrangement = Arrangement.SpaceBetween,\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        Column(\n                            modifier = Modifier.weight(1f)\n                        ) {\n                            Text(stringResource(R.string.enable_better_lyrics))\n                            Text(\n                                text = stringResource(R.string.enable_better_lyrics_desc),\n                                style = MaterialTheme.typography.bodySmall,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant\n                            )\n                        }\n                        Switch(\n                            checked = enableBetterLyrics,\n                            onCheckedChange = onEnableBetterLyricsChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (enableBetterLyrics) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    }\n                    Row(\n                        modifier = Modifier.fillMaxWidth(),\n                        horizontalArrangement = Arrangement.SpaceBetween,\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        Column(\n                            modifier = Modifier.weight(1f)\n                        ) {\n                            Text(stringResource(R.string.enable_simpmusic))\n                            Text(\n                                text = stringResource(R.string.enable_simpmusic_desc),\n                                style = MaterialTheme.typography.bodySmall,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant\n                            )\n                        }\n                        Switch(\n                            checked = enableSimpMusic,\n                            onCheckedChange = onEnableSimpMusicChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (enableSimpMusic) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    }\n                    Row(\n                        modifier = Modifier.fillMaxWidth(),\n                        horizontalArrangement = Arrangement.SpaceBetween,\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        Column(\n                            modifier = Modifier.weight(1f)\n                        ) {\n                            Text(stringResource(R.string.enable_lyricsplus))\n                            Text(\n                                text = stringResource(R.string.enable_lyricsplus_desc),\n                                style = MaterialTheme.typography.bodySmall,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant\n                            )\n                        }\n                        Switch(\n                            checked = enableLyricsPlus,\n                            onCheckedChange = onEnableLyricsPlusChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (enableLyricsPlus) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    }\n                    Column(modifier = Modifier.padding(2.dp)) {\n                        Text(\n                            text = stringResource(R.string.youtube_music_lyrics_note),\n                            style = MaterialTheme.typography.bodySmall,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant\n                        )\n                    }\n                }\n            },\n            confirmButton = {\n                TextButton(\n                    onClick = { showProviderSelectionDialog = false }\n                ) {\n                    Text(stringResource(R.string.close))\n                }\n            }\n        )\n    }\n\n    var showQuickPicksDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    if (showQuickPicksDialog) {\n        EnumDialog(\n            onDismiss = { showQuickPicksDialog = false },\n            onSelect = {\n                onQuickPicksChange(it)\n                showQuickPicksDialog = false\n            },\n            title = stringResource(R.string.set_quick_picks),\n            current = quickPicks,\n            values = QuickPicks.values().toList(),\n            valueText = {\n                when (it) {\n                    QuickPicks.QUICK_PICKS -> stringResource(R.string.quick_picks)\n                    QuickPicks.LAST_LISTEN -> stringResource(R.string.last_song_listened)\n                }\n            }\n        )\n    }\n\n    var showTopLengthDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    if (showTopLengthDialog) {\n        var tempLength by rememberSaveable { mutableFloatStateOf(lengthTop.toFloat()) }\n\n        AlertDialog(\n            onDismissRequest = { showTopLengthDialog = false },\n            title = { Text(stringResource(R.string.top_length)) },\n            text = {\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.spacedBy(8.dp)\n                ) {\n                    Text(tempLength.toInt().toString())\n                    Slider(\n                        value = tempLength,\n                        onValueChange = { tempLength = it },\n                        valueRange = 1f..100f,\n                        steps = 98\n                    )\n                }\n            },\n            confirmButton = {\n                TextButton(\n                    onClick = {\n                        onLengthTopChange(tempLength.toInt().toString())\n                        showTopLengthDialog = false\n                    }\n                ) {\n                    Text(stringResource(R.string.save))\n                }\n            }\n        )\n    }\n\n    var showProviderPriorityDialog by rememberSaveable {\n        mutableStateOf(false)\n    }\n\n    if (showProviderPriorityDialog) {\n        val currentOrder = LyricsProviderRegistry.deserializeProviderOrder(lyricsProviderOrder)\n        val enabledProviders = setOf(\n            \"LrcLib\".takeIf { enableLrclib },\n            \"KuGou\".takeIf { enableKugou },\n            \"BetterLyrics\".takeIf { enableBetterLyrics },\n            \"SimpMusic\".takeIf { enableSimpMusic },\n            \"LyricsPlus\".takeIf { enableLyricsPlus },\n        ).filterNotNull().toSet()\n        val lyricsIcon = painterResource(R.drawable.lyrics)\n        val draggableItems = remember { mutableStateListOf<DraggableLyricsProviderItem>() }\n\n        LaunchedEffect(currentOrder, enableLrclib, enableKugou, enableBetterLyrics, enableSimpMusic, enableLyricsPlus) {\n            val orderedEnabledProviders = currentOrder.filter { it in enabledProviders }\n            draggableItems.clear()\n            draggableItems.addAll(\n                orderedEnabledProviders.mapNotNull { providerName ->\n                    LyricsProviderRegistry.getProviderByName(providerName) ?: return@mapNotNull null\n                    DraggableLyricsProviderItem(\n                        id = providerName,\n                        name = providerDisplayNames[providerName] ?: providerName,\n                        icon = lyricsIcon,\n                    )\n                }\n            )\n        }\n\n        AlertDialog(\n            onDismissRequest = { showProviderPriorityDialog = false },\n            title = { Text(stringResource(R.string.lyrics_provider_priority)) },\n            text = {\n                Column(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .height(300.dp)\n                ) {\n                    Text(\n                        stringResource(R.string.lyrics_provider_priority_desc),\n                        style = MaterialTheme.typography.bodySmall,\n                        modifier = Modifier.padding(bottom = 8.dp),\n                        color = MaterialTheme.colorScheme.onSurfaceVariant\n                    )\n                    DraggableLyricsProviderList(\n                        items = draggableItems,\n                        onItemsReordered = { reorderedItems ->\n                            val enabledOrder = reorderedItems.map { it.id }\n                            val disabledOrder = currentOrder.filter { it !in enabledProviders }\n                            onLyricsProviderOrderChange(\n                                LyricsProviderRegistry.serializeProviderOrder(enabledOrder + disabledOrder)\n                            )\n                        },\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .weight(1f)\n                    )\n                }\n            },\n            confirmButton = {\n                TextButton(\n                    onClick = { showProviderPriorityDialog = false }\n                ) {\n                    Text(stringResource(R.string.close))\n                }\n            }\n        )\n    }\n\n    Column(\n        Modifier\n            .windowInsetsPadding(LocalPlayerAwareWindowInsets.current)\n            .verticalScroll(rememberScrollState())\n            .padding(horizontal = 16.dp),\n    ) {\n        Material3SettingsGroup(\n            title = stringResource(R.string.general),\n            items = listOf(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.language),\n                    title = { Text(stringResource(R.string.content_language)) },\n                    description = {\n                        Text(\n                            LanguageCodeToName.getOrElse(contentLanguage) { stringResource(R.string.system_default) }\n                        )\n                    },\n                    onClick = { showContentLanguageDialog = true }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.location_on),\n                    title = { Text(stringResource(R.string.content_country)) },\n                    description = {\n                        Text(\n                            CountryCodeToName.getOrElse(contentCountry) { stringResource(R.string.system_default) }\n                        )\n                    },\n                    onClick = { showContentCountryDialog = true }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.explicit),\n                    title = { Text(stringResource(R.string.hide_explicit)) },\n                    trailingContent = {\n                        Switch(\n                            checked = hideExplicit,\n                            onCheckedChange = onHideExplicitChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (hideExplicit) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onHideExplicitChange(!hideExplicit) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.slow_motion_video),\n                    title = { Text(stringResource(R.string.hide_video_songs)) },\n                    trailingContent = {\n                        Switch(\n                            checked = hideVideoSongs,\n                            onCheckedChange = onHideVideoSongsChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (hideVideoSongs) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onHideVideoSongsChange(!hideVideoSongs) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.hide_image),\n                    title = { Text(stringResource(R.string.hide_youtube_shorts)) },\n                    trailingContent = {\n                        Switch(\n                            checked = hideYoutubeShorts,\n                            onCheckedChange = onHideYoutubeShortsChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (hideYoutubeShorts) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onHideYoutubeShortsChange(!hideYoutubeShorts) }\n                )\n            )\n        )\n\n        Spacer(modifier = Modifier.height(27.dp))\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.artist_page_settings),\n            items = listOf(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.info),\n                    title = { Text(stringResource(R.string.show_artist_description)) },\n                    trailingContent = {\n                        Switch(\n                            checked = showArtistDescription,\n                            onCheckedChange = onShowArtistDescriptionChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (showArtistDescription) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onShowArtistDescriptionChange(!showArtistDescription) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.person),\n                    title = { Text(stringResource(R.string.show_artist_subscriber_count)) },\n                    trailingContent = {\n                        Switch(\n                            checked = showArtistSubscriberCount,\n                            onCheckedChange = onShowArtistSubscriberCountChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (showArtistSubscriberCount) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onShowArtistSubscriberCountChange(!showArtistSubscriberCount) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.person),\n                    title = { Text(stringResource(R.string.show_artist_monthly_listeners)) },\n                    trailingContent = {\n                        Switch(\n                            checked = showMonthlyListeners,\n                            onCheckedChange = onShowMonthlyListenersChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (showMonthlyListeners) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onShowMonthlyListenersChange(!showMonthlyListeners) }\n                )\n            )\n        )\n\n        Spacer(modifier = Modifier.height(27.dp))\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.app_language),\n            items = listOf(\n                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n                    Material3SettingsItem(\n                        icon = painterResource(R.drawable.language),\n                        title = { Text(stringResource(R.string.app_language)) },\n                        onClick = {\n                            context.startActivity(\n                                Intent(\n                                    Settings.ACTION_APP_LOCALE_SETTINGS,\n                                    \"package:${context.packageName}\".toUri()\n                                )\n                            )\n                        }\n                    )\n                } else {\n                    Material3SettingsItem(\n                        icon = painterResource(R.drawable.language),\n                        title = { Text(stringResource(R.string.app_language)) },\n                        description = {\n                            Text(\n                                LanguageCodeToName.getOrElse(appLanguage) { stringResource(R.string.system_default) }\n                            )\n                        },\n                        onClick = { showAppLanguageDialog = true }\n                    )\n                }\n            )\n        )\n\n        Spacer(modifier = Modifier.height(27.dp))\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.proxy),\n            items = buildList {\n                add(\n                    Material3SettingsItem(\n                        icon = painterResource(R.drawable.wifi_proxy),\n                        title = { Text(stringResource(R.string.enable_proxy)) },\n                        trailingContent = {\n                            Switch(\n                                checked = proxyEnabled,\n                                onCheckedChange = onProxyEnabledChange,\n                                thumbContent = {\n                                    Icon(\n                                        painter = painterResource(\n                                            id = if (proxyEnabled) R.drawable.check else R.drawable.close\n                                        ),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(SwitchDefaults.IconSize)\n                                    )\n                                }\n                            )\n                        },\n                        onClick = { onProxyEnabledChange(!proxyEnabled) }\n                    )\n                )\n                if (proxyEnabled) {\n                    add(\n                        Material3SettingsItem(\n                            icon = painterResource(R.drawable.settings),\n                            title = { Text(stringResource(R.string.config_proxy)) },\n                            onClick = { showProxyConfigurationDialog = true }\n                        )\n                    )\n                }\n            }\n        )\n\n        Spacer(modifier = Modifier.height(27.dp))\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.lyrics),\n            items = listOf(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.lyrics),\n                    title = { Text(stringResource(R.string.lyrics_provider_selection)) },\n                    description = { Text(stringResource(R.string.lyrics_provider_selection_desc)) },\n                    onClick = { showProviderSelectionDialog = true }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.lyrics),\n                    title = { Text(stringResource(R.string.lyrics_provider_priority)) },\n                    description = { Text(stringResource(R.string.lyrics_provider_priority_desc)) },\n                    onClick = { showProviderPriorityDialog = true }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.language_korean_latin),\n                    title = { Text(stringResource(R.string.lyrics_romanization)) },\n                    onClick = { navController.navigate(\"settings/content/romanization\") }\n                )\n            )\n        )\n\n        Spacer(modifier = Modifier.height(27.dp))\n\n        Material3SettingsGroup(\n            title = \"Wrapped\",\n            items = listOf(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.trending_up),\n                    title = { Text(stringResource(R.string.show_wrapped_card)) },\n                    trailingContent = {\n                        Switch(\n                            checked = showWrappedCard,\n                            onCheckedChange = onShowWrappedCardChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (showWrappedCard) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onShowWrappedCardChange(!showWrappedCard) }\n                )\n            )\n        )\n\n        Spacer(modifier = Modifier.height(27.dp))\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.misc),\n            items = listOf(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.shuffle),\n                    title = { Text(stringResource(R.string.randomize_home_order)) },\n                    description = { Text(stringResource(R.string.randomize_home_order_desc)) },\n                    trailingContent = {\n                        Switch(\n                            checked = randomizeHomeOrder,\n                            onCheckedChange = onRandomizeHomeOrderChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (randomizeHomeOrder) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onRandomizeHomeOrderChange(!randomizeHomeOrder) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.trending_up),\n                    title = { Text(stringResource(R.string.top_length)) },\n                    description = { Text(lengthTop) },\n                    onClick = { showTopLengthDialog = true }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.home_outlined),\n                    title = { Text(stringResource(R.string.set_quick_picks)) },\n                    description = {\n                        Text(\n                            when (quickPicks) {\n                                QuickPicks.QUICK_PICKS -> stringResource(R.string.quick_picks)\n                                QuickPicks.LAST_LISTEN -> stringResource(R.string.last_song_listened)\n                            }\n                        )\n                    },\n                    onClick = { showQuickPicksDialog = true }\n                )\n            )\n        )\n        Spacer(modifier = Modifier.height(16.dp))\n    }\n\n    TopAppBar(\n        title = { Text(stringResource(R.string.content)) },\n        navigationIcon = {\n            IconButton(\n                onClick = navController::navigateUp,\n                onLongClick = navController::backToMain,\n            ) {\n                Icon(\n                    painterResource(R.drawable.arrow_back),\n                    contentDescription = null,\n                )\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/settings/DiscordLoginScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.settings\n\nimport android.annotation.SuppressLint\nimport android.os.Build\nimport android.view.View\nimport android.view.ViewGroup\nimport android.webkit.JsResult\nimport android.webkit.WebChromeClient\nimport android.webkit.WebView\nimport android.webkit.WebViewClient\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.viewinterop.AndroidView\nimport androidx.navigation.NavController\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.DiscordTokenKey\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.rememberPreference\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport timber.log.Timber\n\nprivate const val JS_SNIPPET =\n    \"javascript:(function()%7Bvar%20i%3Ddocument.createElement('iframe')%3Bdocument.body.appendChild(i)%3Balert(i.contentWindow.localStorage.token.slice(1,-1))%7D)()\"\n\nprivate const val MOTOROLA = \"motorola\"\nprivate const val SAMSUNG_USER_AGENT =\n    \"Mozilla/5.0 (Linux; Android 14; SM-S921U; Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Mobile Safari/537.36\"\n\n@SuppressLint(\"SetJavaScriptEnabled\")\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun DiscordLoginScreen(navController: NavController) {\n    val scope = rememberCoroutineScope()\n    var discordToken by rememberPreference(DiscordTokenKey, \"\")\n    var webView: WebView? = null\n\n    AndroidView(\n        modifier = Modifier\n            .windowInsetsPadding(LocalPlayerAwareWindowInsets.current)\n            .fillMaxSize(),\n        factory = { context ->\n            WebView(context).apply {\n                layoutParams = ViewGroup.LayoutParams(\n                    ViewGroup.LayoutParams.MATCH_PARENT,\n                    ViewGroup.LayoutParams.MATCH_PARENT\n                )\n\n                settings.javaScriptEnabled = true\n                settings.domStorageEnabled = true\n\n                // Fix for Motorola devices - UA parsing issue breaks Discord login\n                // See: https://github.com/dead8309/Kizzy/issues/345#issuecomment-2699729072\n                if (Build.MANUFACTURER.equals(MOTOROLA, ignoreCase = true)) {\n                    settings.userAgentString = SAMSUNG_USER_AGENT\n                }\n\n                webViewClient = object : WebViewClient() {\n                    @Deprecated(\"Deprecated in Java\")\n                    override fun shouldOverrideUrlLoading(\n                        view: WebView,\n                        url: String,\n                    ): Boolean {\n                        if (url.endsWith(\"/app\")) {\n                            view.stopLoading()\n                            view.loadUrl(JS_SNIPPET)\n                            view.visibility = View.GONE\n                        }\n                        return false\n                    }\n                }\n\n                webChromeClient = object : WebChromeClient() {\n                    override fun onJsAlert(\n                        view: WebView,\n                        url: String,\n                        message: String,\n                        result: JsResult\n                    ): Boolean {\n                        Timber.d(\"Discord Token received\")\n                        if (message.isNotBlank() && message != \"null\" && message != \"undefined\") {\n                            discordToken = message\n                            scope.launch(Dispatchers.Main) {\n                                navController.navigateUp()\n                            }\n                        }\n                        view.visibility = View.GONE\n                        result.confirm()\n                        return true\n                    }\n                }\n\n                webView = this\n                loadUrl(\"https://discord.com/login\")\n            }\n        }\n    )\n\n    TopAppBar(\n        title = { Text(stringResource(R.string.action_login)) },\n        navigationIcon = {\n            IconButton(\n                onClick = navController::navigateUp,\n                onLongClick = navController::backToMain\n            ) {\n                Icon(\n                    painterResource(R.drawable.arrow_back),\n                    contentDescription = null\n                )\n            }\n        }\n    )\n\n    BackHandler(enabled = webView?.canGoBack() == true) {\n        webView?.goBack()\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/settings/PlayerSettings.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.settings\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Slider\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.SwitchDefaults\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.pluralStringResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation.NavController\nimport com.metrolist.music.BuildConfig\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.AudioNormalizationKey\nimport com.metrolist.music.constants.AudioOffload\nimport com.metrolist.music.constants.AudioQuality\nimport com.metrolist.music.constants.AudioQualityKey\nimport com.metrolist.music.constants.AutoDownloadOnLikeKey\nimport com.metrolist.music.constants.CrossfadeDurationKey\nimport com.metrolist.music.constants.CrossfadeEnabledKey\nimport com.metrolist.music.constants.CrossfadeGaplessKey\nimport com.metrolist.music.constants.AutoLoadMoreKey\nimport com.metrolist.music.constants.AutoSkipNextOnErrorKey\nimport com.metrolist.music.constants.DisableLoadMoreWhenRepeatAllKey\nimport com.metrolist.music.constants.EnableGoogleCastKey\nimport com.metrolist.music.constants.HistoryDuration\nimport com.metrolist.music.constants.KeepScreenOn\nimport com.metrolist.music.constants.PauseOnMute\nimport com.metrolist.music.constants.PersistentQueueKey\nimport com.metrolist.music.constants.PersistentShuffleAcrossQueuesKey\nimport com.metrolist.music.constants.PreventDuplicateTracksInQueueKey\nimport com.metrolist.music.constants.RememberShuffleAndRepeatKey\nimport com.metrolist.music.constants.ResumeOnBluetoothConnectKey\nimport com.metrolist.music.constants.SeekExtraSeconds\nimport com.metrolist.music.constants.ShufflePlaylistFirstKey\nimport com.metrolist.music.constants.SimilarContent\nimport com.metrolist.music.constants.SkipSilenceInstantKey\nimport com.metrolist.music.constants.SkipSilenceKey\nimport com.metrolist.music.constants.StopMusicOnTaskClearKey\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.EnumDialog\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.Material3SettingsGroup\nimport com.metrolist.music.ui.component.Material3SettingsItem\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\nimport kotlin.math.roundToInt\nimport com.metrolist.music.ui.component.SleepTimerDialog\nimport com.metrolist.music.constants.SleepTimerEnabledKey\nimport com.metrolist.music.constants.SleepTimerRepeatKey\nimport com.metrolist.music.constants.SleepTimerCustomDaysKey\nimport com.metrolist.music.constants.SleepTimerEndTimeKey\nimport com.metrolist.music.constants.SleepTimerStartTimeKey\nimport com.metrolist.music.constants.SleepTimerDayTimesKey\nimport com.metrolist.music.ui.component.decodeDayTimes\nimport com.metrolist.music.ui.component.encodeDayTimes\nimport com.metrolist.music.constants.SleepTimerFadeOutKey\nimport com.metrolist.music.constants.SleepTimerStopAfterCurrentSongKey\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun PlayerSettings(\n    navController: NavController\n) {\n    val (audioQuality, onAudioQualityChange) = rememberEnumPreference(\n        AudioQualityKey,\n        defaultValue = AudioQuality.AUTO\n    )\n    val (crossfadeEnabled, onCrossfadeEnabledChange) = rememberPreference(\n        CrossfadeEnabledKey,\n        defaultValue = false\n    )\n    val (crossfadeDuration, onCrossfadeDurationChange) = rememberPreference(\n        CrossfadeDurationKey,\n        defaultValue = 5f\n    )\n    val (crossfadeGapless, onCrossfadeGaplessChange) = rememberPreference(\n        CrossfadeGaplessKey,\n        defaultValue = true\n    )\n    val (persistentQueue, onPersistentQueueChange) = rememberPreference(\n        PersistentQueueKey,\n        defaultValue = true\n    )\n    val (skipSilence, onSkipSilenceChange) = rememberPreference(\n        SkipSilenceKey,\n        defaultValue = false\n    )\n    val (skipSilenceInstant, onSkipSilenceInstantChange) = rememberPreference(\n        SkipSilenceInstantKey,\n        defaultValue = false\n    )\n    val (audioNormalization, onAudioNormalizationChange) = rememberPreference(\n        AudioNormalizationKey,\n        defaultValue = true\n    )\n\n    val (audioOffload, onAudioOffloadChange) = rememberPreference(\n        key = AudioOffload,\n        defaultValue = false\n    )\n\n    val (enableGoogleCast, onEnableGoogleCastChange) = rememberPreference(\n        key = EnableGoogleCastKey,\n        defaultValue = true\n    )\n\n    val (seekExtraSeconds, onSeekExtraSeconds) = rememberPreference(\n        SeekExtraSeconds,\n        defaultValue = false\n    )\n\n    val (autoLoadMore, onAutoLoadMoreChange) = rememberPreference(\n        AutoLoadMoreKey,\n        defaultValue = true\n    )\n    val (disableLoadMoreWhenRepeatAll, onDisableLoadMoreWhenRepeatAllChange) = rememberPreference(\n        DisableLoadMoreWhenRepeatAllKey,\n        defaultValue = false\n    )\n    val (autoDownloadOnLike, onAutoDownloadOnLikeChange) = rememberPreference(\n        AutoDownloadOnLikeKey,\n        defaultValue = false\n    )\n    val (similarContentEnabled, similarContentEnabledChange) = rememberPreference(\n        key = SimilarContent,\n        defaultValue = true\n    )\n    val (autoSkipNextOnError, onAutoSkipNextOnErrorChange) = rememberPreference(\n        AutoSkipNextOnErrorKey,\n        defaultValue = false\n    )\n    val (persistentShuffleAcrossQueues, onPersistentShuffleAcrossQueuesChange) = rememberPreference(\n        PersistentShuffleAcrossQueuesKey,\n        defaultValue = false\n    )\n    val (rememberShuffleAndRepeat, onRememberShuffleAndRepeatChange) = rememberPreference(\n        RememberShuffleAndRepeatKey,\n        defaultValue = true\n    )\n    val (shufflePlaylistFirst, onShufflePlaylistFirstChange) = rememberPreference(\n        ShufflePlaylistFirstKey,\n        defaultValue = false\n    )\n    val (preventDuplicateTracksInQueue, onPreventDuplicateTracksInQueueChange) = rememberPreference(\n        PreventDuplicateTracksInQueueKey,\n        defaultValue = false\n    )\n    val (stopMusicOnTaskClear, onStopMusicOnTaskClearChange) = rememberPreference(\n        StopMusicOnTaskClearKey,\n        defaultValue = false\n    )\n    val (pauseOnMute, onPauseOnMuteChange) = rememberPreference(\n        PauseOnMute,\n        defaultValue = false\n    )\n    val (resumeOnBluetoothConnect, onResumeOnBluetoothConnectChange) = rememberPreference(\n        ResumeOnBluetoothConnectKey,\n        defaultValue = false\n    )\n    val (keepScreenOn, onKeepScreenOnChange) = rememberPreference(\n        KeepScreenOn,\n        defaultValue = false\n    )\n    val (historyDuration, onHistoryDurationChange) = rememberPreference(\n        HistoryDuration,\n        defaultValue = 30f\n    )\n\n    var showAudioQualityDialog by remember {\n        mutableStateOf(false)\n    }\n\n    if (showAudioQualityDialog) {\n        EnumDialog(\n            onDismiss = { showAudioQualityDialog = false },\n            onSelect = {\n                onAudioQualityChange(it)\n                showAudioQualityDialog = false\n            },\n            title = stringResource(R.string.audio_quality),\n            current = audioQuality,\n            values = AudioQuality.values().toList(),\n            valueText = {\n                when (it) {\n                    AudioQuality.AUTO -> stringResource(R.string.audio_quality_auto)\n                    AudioQuality.HIGH -> stringResource(R.string.audio_quality_high)\n                    AudioQuality.LOW -> stringResource(R.string.audio_quality_low)\n                }\n            }\n        )\n    }\n\n    Column(\n        Modifier\n            .windowInsetsPadding(\n                LocalPlayerAwareWindowInsets.current.only(\n                    WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom\n                )\n            )\n            .verticalScroll(rememberScrollState())\n            .padding(horizontal = 16.dp)\n    ) {\n        var showCrossfadeBetaDialog by remember { mutableStateOf(false) }\n\n        if (showCrossfadeBetaDialog) {\n            DefaultDialog(\n                onDismiss = { showCrossfadeBetaDialog = false },\n                title = { Text(stringResource(R.string.crossfade_beta_title)) },\n                buttons = {\n                    TextButton(onClick = { showCrossfadeBetaDialog = false }) {\n                        Text(stringResource(R.string.cancel))\n                    }\n                    TextButton(onClick = {\n                        showCrossfadeBetaDialog = false\n                        onCrossfadeEnabledChange(true)\n                    }) {\n                        Text(stringResource(R.string.enable))\n                    }\n                }\n            ) {\n                Text(stringResource(R.string.crossfade_beta_message))\n            }\n        }\n\n        Spacer(\n            Modifier.windowInsetsPadding(\n                LocalPlayerAwareWindowInsets.current.only(\n                    WindowInsetsSides.Top\n                )\n            )\n        )\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.player),\n            items = buildList {\n                add(Material3SettingsItem(\n                    icon = painterResource(R.drawable.graphic_eq),\n                    title = { Text(stringResource(R.string.audio_quality)) },\n                    description = {\n                        Text(\n                            when (audioQuality) {\n                                AudioQuality.AUTO -> stringResource(R.string.audio_quality_auto)\n                                AudioQuality.HIGH -> stringResource(R.string.audio_quality_high)\n                                AudioQuality.LOW -> stringResource(R.string.audio_quality_low)\n                            }\n                        )\n                    },\n                    onClick = { showAudioQualityDialog = true }\n                ))\n                add(Material3SettingsItem(\n                    icon = painterResource(R.drawable.linear_scale),\n                    title = { Text(stringResource(R.string.crossfade)) },\n                    description = { Text(stringResource(R.string.crossfade_desc)) },\n                    showBadge = true,\n                    trailingContent = {\n                        Switch(\n                            checked = crossfadeEnabled,\n                            onCheckedChange = {\n                                if (!crossfadeEnabled) {\n                                    showCrossfadeBetaDialog = true\n                                } else {\n                                    onCrossfadeEnabledChange(false)\n                                }\n                            },\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (crossfadeEnabled) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = {\n                        if (!crossfadeEnabled) {\n                            showCrossfadeBetaDialog = true\n                        } else {\n                            onCrossfadeEnabledChange(false)\n                        }\n                    }\n                ))\n                if (crossfadeEnabled) {\n                    add(Material3SettingsItem(\n                        icon = painterResource(R.drawable.timer),\n                        title = { Text(stringResource(R.string.crossfade_duration)) },\n                        description = {\n                            Column {\n                                Text(pluralStringResource(R.plurals.seconds, crossfadeDuration.toInt(), crossfadeDuration.toInt()))\n                                Slider(\n                                    value = crossfadeDuration,\n                                    onValueChange = onCrossfadeDurationChange,\n                                    valueRange = 1f..15f,\n                                    steps = 14\n                                )\n                            }\n                        }\n                    ))\n                    add(Material3SettingsItem(\n                        icon = painterResource(R.drawable.album),\n                        title = { Text(stringResource(R.string.crossfade_gapless)) },\n                        description = { Text(stringResource(R.string.crossfade_gapless_desc)) },\n                        trailingContent = {\n                            Switch(\n                                checked = crossfadeGapless,\n                                onCheckedChange = onCrossfadeGaplessChange,\n                                thumbContent = {\n                                    Icon(\n                                        painter = painterResource(\n                                            id = if (crossfadeGapless) R.drawable.check else R.drawable.close\n                                        ),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(SwitchDefaults.IconSize)\n                                    )\n                                }\n                            )\n                        },\n                        onClick = { onCrossfadeGaplessChange(!crossfadeGapless) }\n                    ))\n                }\n                add(Material3SettingsItem(\n                    icon = painterResource(R.drawable.history),\n                    title = { Text(stringResource(R.string.history_duration)) },\n                    description = {\n                        Column {\n                            Text(historyDuration.roundToInt().toString())\n                            Slider(\n                                value = historyDuration,\n                                onValueChange = onHistoryDurationChange,\n                                valueRange = 1f..100f\n                            )\n                        }\n                    }\n                ))\n                add(Material3SettingsItem(\n                    icon = painterResource(R.drawable.fast_forward),\n                    title = { Text(stringResource(R.string.skip_silence)) },\n                    description = { Text(stringResource(R.string.skip_silence_desc)) },\n                    trailingContent = {\n                        Switch(\n                            checked = skipSilence,\n                            onCheckedChange = onSkipSilenceChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (skipSilence) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onSkipSilenceChange(!skipSilence) }\n                ))\n                add(Material3SettingsItem(\n                    icon = painterResource(R.drawable.skip_next),\n                    title = { Text(stringResource(R.string.skip_silence_instant)) },\n                    description = { Text(stringResource(R.string.skip_silence_instant_desc)) },\n                    trailingContent = {\n                        Switch(\n                            checked = skipSilenceInstant,\n                            onCheckedChange = { onSkipSilenceInstantChange(it) },\n                            enabled = skipSilence,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (skipSilenceInstant) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { if (skipSilence) onSkipSilenceInstantChange(!skipSilenceInstant) }\n                ))\n                add(Material3SettingsItem(\n                    icon = painterResource(R.drawable.volume_up),\n                    title = { Text(stringResource(R.string.audio_normalization)) },\n                    trailingContent = {\n                        Switch(\n                            checked = audioNormalization,\n                            onCheckedChange = onAudioNormalizationChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (audioNormalization) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onAudioNormalizationChange(!audioNormalization) }\n                ))\n                add(Material3SettingsItem(\n                    icon = painterResource(R.drawable.graphic_eq),\n                    title = { Text(stringResource(R.string.audio_offload)) },\n                    description = {\n                        Text(\n                            if (crossfadeEnabled) stringResource(R.string.audio_offload_disabled_by_crossfade)\n                            else stringResource(R.string.audio_offload_description)\n                        )\n                    },\n                    trailingContent = {\n                        Switch(\n                            checked = if (crossfadeEnabled) false else audioOffload,\n                            onCheckedChange = onAudioOffloadChange,\n                            enabled = !crossfadeEnabled,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (!crossfadeEnabled && audioOffload) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { if (!crossfadeEnabled) onAudioOffloadChange(!audioOffload) }\n                ))\n                // Only show Cast setting in GMS builds (not in F-Droid/FOSS)\n                if (BuildConfig.CAST_AVAILABLE) {\n                    add(Material3SettingsItem(\n                        icon = painterResource(R.drawable.cast),\n                        title = { Text(stringResource(R.string.google_cast)) },\n                        description = { Text(stringResource(R.string.google_cast_description)) },\n                        trailingContent = {\n                            Switch(\n                                checked = enableGoogleCast,\n                                onCheckedChange = onEnableGoogleCastChange,\n                                thumbContent = {\n                                    Icon(\n                                        painter = painterResource(\n                                            id = if (enableGoogleCast) R.drawable.check else R.drawable.close\n                                        ),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(SwitchDefaults.IconSize)\n                                    )\n                                }\n                            )\n                        },\n                        onClick = { onEnableGoogleCastChange(!enableGoogleCast) }\n                    ))\n                }\n                add(Material3SettingsItem(\n                    icon = painterResource(R.drawable.arrow_forward),\n                    title = { Text(stringResource(R.string.seek_seconds_addup)) },\n                    description = { Text(stringResource(R.string.seek_seconds_addup_description)) },\n                    trailingContent = {\n                        Switch(\n                            checked = seekExtraSeconds,\n                            onCheckedChange = onSeekExtraSeconds,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (seekExtraSeconds) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onSeekExtraSeconds(!seekExtraSeconds) }\n                ))\n            }\n        )\n\n        Spacer(modifier = Modifier.height(27.dp))\n\n        var showSleepTimerDialog by remember { mutableStateOf(false) }\n\n        val (sleepTimerEnabled, onSleepTimerEnabledChange) = rememberPreference(\n            SleepTimerEnabledKey,\n            defaultValue = false\n        )\n        val (sleepTimerRepeat, onSleepTimerRepeatChange) = rememberPreference(\n            SleepTimerRepeatKey,\n            defaultValue = \"daily\"\n        )\n        val (sleepTimerStartTime, onSleepTimerStartTimeChange) = rememberPreference(\n            SleepTimerStartTimeKey,\n            defaultValue = \"22:00\"\n        )\n        val (sleepTimerEndTime, onSleepTimerEndTimeChange) = rememberPreference(\n            SleepTimerEndTimeKey,\n            defaultValue = \"06:00\"\n        )\n        val (sleepTimerCustomDays, onSleepTimerCustomDaysChange) = rememberPreference(\n            SleepTimerCustomDaysKey,\n            defaultValue = \"0,1,2,3,4\"\n        )\n        // Per-day time ranges used in custom mode\n        val (sleepTimerDayTimes, onSleepTimerDayTimesChange) = rememberPreference(\n            SleepTimerDayTimesKey,\n            defaultValue = \"\"\n        )\n\n        val (sleepTimerStopAfterCurrentSong, onSleepTimerStopAfterCurrentSongChange) = rememberPreference (\n        SleepTimerStopAfterCurrentSongKey,\n        defaultValue = false)\n        val (sleepTimerFadeOut, onSleepTimerFadeOutChange) = rememberPreference(\n            SleepTimerFadeOutKey,\n            false\n        )\n\n        if (showSleepTimerDialog) {\n            val customDays = sleepTimerCustomDays.split(\",\").mapNotNull { it.toIntOrNull() }\n            val dayTimesMap = decodeDayTimes(sleepTimerDayTimes)\n\n            SleepTimerDialog(\n                isVisible = true,\n                onDismiss = { showSleepTimerDialog = false },\n                onConfirm = { repeat, startTime, endTime, days, dayTimes ->\n                    onSleepTimerRepeatChange(repeat)\n                    onSleepTimerStartTimeChange(startTime)\n                    onSleepTimerEndTimeChange(endTime)\n                    onSleepTimerCustomDaysChange(days?.joinToString(\",\") ?: \"0,1,2,3,4\")\n                    onSleepTimerDayTimesChange(encodeDayTimes(dayTimes))\n                    showSleepTimerDialog = false\n                },\n                initialRepeat = sleepTimerRepeat,\n                initialStartTime = sleepTimerStartTime,\n                initialEndTime = sleepTimerEndTime,\n                initialCustomDays = customDays,\n                initialDayTimes = dayTimesMap\n            )\n        }\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.sleep_timer),\n            items = buildList {\n                add(\n                    Material3SettingsItem(\n                        icon = painterResource(R.drawable.time_auto),\n                        title = { Text(stringResource(R.string.enable_automatic_sleeptimer)) },\n                        description = { Text(stringResource(R.string.sleeptimer_description)) },\n                        trailingContent = {\n                            Switch(\n                                checked = sleepTimerEnabled,\n                                onCheckedChange = onSleepTimerEnabledChange,\n                                thumbContent = {\n                                    Icon(\n                                        painter = painterResource(\n                                            id = if (sleepTimerEnabled) R.drawable.check else R.drawable.close\n                                        ),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(SwitchDefaults.IconSize)\n                                    )\n                                }\n                            )\n                        },\n                        onClick = { onSleepTimerEnabledChange(!sleepTimerEnabled) }\n                    )\n                )\n\n                    add(\n                        Material3SettingsItem(\n                            icon = painterResource(R.drawable.baseline_event_repeat_24),\n                            title = { Text(stringResource(R.string.sleep_timer_repeat)) },\n                            description = {\n                                Text(\n                                    stringResource(R.string.sleep_timer_repeat_description)\n                                )\n                            },\n                            trailingContent = {\n                                Switch(\n                                    checked = sleepTimerEnabled,\n                                    onCheckedChange = {showSleepTimerDialog = true},\n                                    thumbContent = {\n                                        Icon(\n                                            painter = painterResource(\n                                                id = if (sleepTimerEnabled) R.drawable.check else R.drawable.close\n                                            ),\n                                            contentDescription = null,\n                                            modifier = Modifier.size(SwitchDefaults.IconSize)\n                                        )\n                                    }\n                                )\n                            },\n                            onClick = { showSleepTimerDialog = true }\n                        )\n                    )\n\n\n                add(\n                    Material3SettingsItem(\n                        icon = painterResource(R.drawable.more_time),\n                        title = { Text(stringResource(R.string.sleep_timer_stop_after_current_song_title)) },\n                        description = { Text(stringResource(R.string.sleep_timer_stop_after_current_song_description)) },\n                        trailingContent = {\n                            Switch(\n                                checked = sleepTimerStopAfterCurrentSong,\n                                onCheckedChange = onSleepTimerStopAfterCurrentSongChange,\n                                thumbContent = {\n                                    Icon(\n                                        painter = painterResource(\n                                            id = if (sleepTimerStopAfterCurrentSong) R.drawable.check else R.drawable.close\n                                        ),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(SwitchDefaults.IconSize)\n                                    )\n                                }\n                            )\n                        },\n                        onClick = { onSleepTimerStopAfterCurrentSongChange(!sleepTimerStopAfterCurrentSong) }\n                    )\n                )\n\n                add(\n                    Material3SettingsItem(\n                        icon = painterResource(R.drawable.timer_arrow_down),\n                        title = { Text(stringResource(R.string.sleep_timer_fade_out_title)) },\n                        description = { Text(stringResource(R.string.sleep_timer_fade_out_description)) },\n                        trailingContent = {\n                            Switch(\n                                checked = sleepTimerFadeOut,\n                                onCheckedChange = onSleepTimerFadeOutChange,\n                                thumbContent = {\n                                    Icon(\n                                        painter = painterResource(\n                                            id = if (sleepTimerFadeOut) R.drawable.check else R.drawable.close\n                                        ),\n                                        contentDescription = null,\n                                        modifier = Modifier.size(SwitchDefaults.IconSize)\n                                    )\n                                }\n                            )\n                        },\n                        onClick = { onSleepTimerFadeOutChange(!sleepTimerFadeOut) }\n                    )\n                )\n\n            }\n        )\n\n        AlarmSettingsSection(showTitle = false)\n\n        Spacer(modifier = Modifier.height(27.dp))\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.queue),\n            items = listOf(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.queue_music),\n                    title = { Text(stringResource(R.string.persistent_queue)) },\n                    description = { Text(stringResource(R.string.persistent_queue_desc)) },\n                    trailingContent = {\n                        Switch(\n                            checked = persistentQueue,\n                            onCheckedChange = onPersistentQueueChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (persistentQueue) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onPersistentQueueChange(!persistentQueue) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.playlist_add),\n                    title = { Text(stringResource(R.string.auto_load_more)) },\n                    description = { Text(stringResource(R.string.auto_load_more_desc)) },\n                    trailingContent = {\n                        Switch(\n                            checked = autoLoadMore,\n                            onCheckedChange = onAutoLoadMoreChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (autoLoadMore) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onAutoLoadMoreChange(!autoLoadMore) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.repeat),\n                    title = { Text(stringResource(R.string.disable_load_more_when_repeat_all)) },\n                    description = { Text(stringResource(R.string.disable_load_more_when_repeat_all_desc)) },\n                    trailingContent = {\n                        Switch(\n                            checked = disableLoadMoreWhenRepeatAll,\n                            onCheckedChange = onDisableLoadMoreWhenRepeatAllChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (disableLoadMoreWhenRepeatAll) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onDisableLoadMoreWhenRepeatAllChange(!disableLoadMoreWhenRepeatAll) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.download),\n                    title = { Text(stringResource(R.string.auto_download_on_like)) },\n                    description = { Text(stringResource(R.string.auto_download_on_like_desc)) },\n                    trailingContent = {\n                        Switch(\n                            checked = autoDownloadOnLike,\n                            onCheckedChange = onAutoDownloadOnLikeChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (autoDownloadOnLike) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onAutoDownloadOnLikeChange(!autoDownloadOnLike) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.similar),\n                    title = { Text(stringResource(R.string.enable_similar_content)) },\n                    description = { Text(stringResource(R.string.similar_content_desc)) },\n                    trailingContent = {\n                        Switch(\n                            checked = similarContentEnabled,\n                            onCheckedChange = similarContentEnabledChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (similarContentEnabled) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { similarContentEnabledChange(!similarContentEnabled) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.shuffle),\n                    title = { Text(stringResource(R.string.persistent_shuffle_title)) },\n                    description = { Text(stringResource(R.string.persistent_shuffle_desc)) },\n                    trailingContent = {\n                        Switch(\n                            checked = persistentShuffleAcrossQueues,\n                            onCheckedChange = onPersistentShuffleAcrossQueuesChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (persistentShuffleAcrossQueues) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onPersistentShuffleAcrossQueuesChange(!persistentShuffleAcrossQueues) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.shuffle),\n                    title = { Text(stringResource(R.string.remember_shuffle_and_repeat)) },\n                    description = { Text(stringResource(R.string.remember_shuffle_and_repeat_desc)) },\n                    trailingContent = {\n                        Switch(\n                            checked = rememberShuffleAndRepeat,\n                            onCheckedChange = onRememberShuffleAndRepeatChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (rememberShuffleAndRepeat) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onRememberShuffleAndRepeatChange(!rememberShuffleAndRepeat) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.shuffle),\n                    title = { Text(stringResource(R.string.shuffle_playlist_first)) },\n                    description = { Text(stringResource(R.string.shuffle_playlist_first_desc)) },\n                    trailingContent = {\n                        Switch(\n                            checked = shufflePlaylistFirst,\n                            onCheckedChange = onShufflePlaylistFirstChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (shufflePlaylistFirst) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onShufflePlaylistFirstChange(!shufflePlaylistFirst) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.queue_music),\n                    title = { Text(stringResource(R.string.prevent_duplicate_tracks_in_queue)) },\n                    description = { Text(stringResource(R.string.prevent_duplicate_tracks_in_queue_desc)) },\n                    trailingContent = {\n                        Switch(\n                            checked = preventDuplicateTracksInQueue,\n                            onCheckedChange = onPreventDuplicateTracksInQueueChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (preventDuplicateTracksInQueue) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onPreventDuplicateTracksInQueueChange(!preventDuplicateTracksInQueue) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.skip_next),\n                    title = { Text(stringResource(R.string.auto_skip_next_on_error)) },\n                    description = { Text(stringResource(R.string.auto_skip_next_on_error_desc)) },\n                    trailingContent = {\n                        Switch(\n                            checked = autoSkipNextOnError,\n                            onCheckedChange = onAutoSkipNextOnErrorChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (autoSkipNextOnError) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onAutoSkipNextOnErrorChange(!autoSkipNextOnError) }\n                )\n            )\n        )\n\n        Spacer(modifier = Modifier.height(27.dp))\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.misc),\n            items = listOf(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.clear_all),\n                    title = { Text(stringResource(R.string.stop_music_on_task_clear)) },\n                    trailingContent = {\n                        Switch(\n                            checked = stopMusicOnTaskClear,\n                            onCheckedChange = onStopMusicOnTaskClearChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (stopMusicOnTaskClear) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onStopMusicOnTaskClearChange(!stopMusicOnTaskClear) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.volume_off_pause),\n                    title = { Text(stringResource(R.string.pause_music_when_media_is_muted)) },\n                    trailingContent = {\n                        Switch(\n                            checked = pauseOnMute,\n                            onCheckedChange = onPauseOnMuteChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (pauseOnMute) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onPauseOnMuteChange(!pauseOnMute) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.bluetooth),\n                    title = { Text(stringResource(R.string.resume_on_bluetooth_connect)) },\n                    trailingContent = {\n                        Switch(\n                            checked = resumeOnBluetoothConnect,\n                            onCheckedChange = onResumeOnBluetoothConnectChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (resumeOnBluetoothConnect) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onResumeOnBluetoothConnectChange(!resumeOnBluetoothConnect) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.screenshot),\n                    title = { Text(stringResource(R.string.keep_screen_on_when_player_is_expanded)) },\n                    trailingContent = {\n                        Switch(\n                            checked = keepScreenOn,\n                            onCheckedChange = onKeepScreenOnChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (keepScreenOn) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onKeepScreenOnChange(!keepScreenOn) }\n                )\n            )\n        )\n        Spacer(modifier = Modifier.height(16.dp))\n    }\n\n    TopAppBar(\n        title = { Text(stringResource(R.string.player_and_audio)) },\n        navigationIcon = {\n            IconButton(\n                onClick = navController::navigateUp,\n                onLongClick = navController::backToMain\n            ) {\n                Icon(\n                    painterResource(R.drawable.arrow_back),\n                    contentDescription = null\n                )\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/settings/PrivacySettings.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.settings\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation.NavController\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.DisableScreenshotKey\nimport com.metrolist.music.constants.PauseListenHistoryKey\nimport com.metrolist.music.constants.PauseSearchHistoryKey\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.Material3SettingsGroup\nimport com.metrolist.music.ui.component.Material3SettingsItem\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.rememberPreference\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun PrivacySettings(\n    navController: NavController\n) {\n    val database = LocalDatabase.current\n    val (pauseListenHistory, onPauseListenHistoryChange) = rememberPreference(\n        key = PauseListenHistoryKey,\n        defaultValue = false\n    )\n    val (pauseSearchHistory, onPauseSearchHistoryChange) = rememberPreference(\n        key = PauseSearchHistoryKey,\n        defaultValue = false\n    )\n    val (disableScreenshot, onDisableScreenshotChange) = rememberPreference(\n        key = DisableScreenshotKey,\n        defaultValue = false\n    )\n\n    var showClearListenHistoryDialog by remember {\n        mutableStateOf(false)\n    }\n\n    if (showClearListenHistoryDialog) {\n        DefaultDialog(\n            onDismiss = { showClearListenHistoryDialog = false },\n            content = {\n                Text(\n                    text = stringResource(R.string.clear_listen_history_confirm),\n                    style = MaterialTheme.typography.bodyLarge,\n                    modifier = Modifier.padding(horizontal = 18.dp),\n                )\n            },\n            buttons = {\n                TextButton(\n                    onClick = { showClearListenHistoryDialog = false },\n                ) {\n                    Text(text = stringResource(android.R.string.cancel))\n                }\n\n                TextButton(\n                    onClick = {\n                        showClearListenHistoryDialog = false\n                        database.query {\n                            clearListenHistory()\n                        }\n                    },\n                ) {\n                    Text(text = stringResource(android.R.string.ok))\n                }\n            },\n        )\n    }\n\n    var showClearSearchHistoryDialog by remember {\n        mutableStateOf(false)\n    }\n\n    if (showClearSearchHistoryDialog) {\n        DefaultDialog(\n            onDismiss = { showClearSearchHistoryDialog = false },\n            content = {\n                Text(\n                    text = stringResource(R.string.clear_search_history_confirm),\n                    style = MaterialTheme.typography.bodyLarge,\n                    modifier = Modifier.padding(horizontal = 18.dp),\n                )\n            },\n            buttons = {\n                TextButton(\n                    onClick = { showClearSearchHistoryDialog = false },\n                ) {\n                    Text(text = stringResource(android.R.string.cancel))\n                }\n\n                TextButton(\n                    onClick = {\n                        showClearSearchHistoryDialog = false\n                        database.query {\n                            clearSearchHistory()\n                        }\n                    },\n                ) {\n                    Text(text = stringResource(android.R.string.ok))\n                }\n            },\n        )\n    }\n\n    Column(\n        Modifier\n            .windowInsetsPadding(\n                LocalPlayerAwareWindowInsets.current.only(\n                    WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom\n                )\n            )\n            .verticalScroll(rememberScrollState())\n            .padding(horizontal = 16.dp)\n    ) {\n        Spacer(\n            Modifier.windowInsetsPadding(\n                LocalPlayerAwareWindowInsets.current.only(\n                    WindowInsetsSides.Top\n                )\n            )\n        )\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.listen_history),\n            items = listOf(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.history),\n                    title = { Text(stringResource(R.string.pause_listen_history)) },\n                    trailingContent = {\n                        Switch(\n                            checked = pauseListenHistory,\n                            onCheckedChange = onPauseListenHistoryChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (pauseListenHistory) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(androidx.compose.material3.SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onPauseListenHistoryChange(!pauseListenHistory) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.delete_history),\n                    title = { Text(stringResource(R.string.clear_listen_history)) },\n                    onClick = { showClearListenHistoryDialog = true }\n                )\n            )\n        )\n\n        Spacer(modifier = Modifier.height(27.dp))\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.search_history),\n            items = listOf(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.search_off),\n                    title = { Text(stringResource(R.string.pause_search_history)) },\n                    trailingContent = {\n                        Switch(\n                            checked = pauseSearchHistory,\n                            onCheckedChange = onPauseSearchHistoryChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (pauseSearchHistory) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(androidx.compose.material3.SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onPauseSearchHistoryChange(!pauseSearchHistory) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.clear_all),\n                    title = { Text(stringResource(R.string.clear_search_history)) },\n                    onClick = { showClearSearchHistoryDialog = true }\n                )\n            )\n        )\n\n        Spacer(modifier = Modifier.height(27.dp))\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.misc),\n            items = listOf(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.screenshot),\n                    title = { Text(stringResource(R.string.disable_screenshot)) },\n                    description = { Text(stringResource(R.string.disable_screenshot_desc)) },\n                    trailingContent = {\n                        Switch(\n                            checked = disableScreenshot,\n                            onCheckedChange = onDisableScreenshotChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (disableScreenshot) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(androidx.compose.material3.SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onDisableScreenshotChange(!disableScreenshot) }\n                )\n            )\n        )\n        Spacer(modifier = Modifier.height(16.dp))\n    }\n\n    TopAppBar(\n        title = { Text(stringResource(R.string.privacy)) },\n        navigationIcon = {\n            IconButton(\n                onClick = navController::navigateUp,\n                onLongClick = navController::backToMain,\n            ) {\n                Icon(\n                    painterResource(R.drawable.arrow_back),\n                    contentDescription = null,\n                )\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/settings/RomanizationSettings.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.settings\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TriStateCheckbox\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.state.ToggleableState\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation.NavController\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.LyricsRomanizeAsMainKey\nimport com.metrolist.music.constants.LyricsRomanizeCyrillicByLineKey\nimport com.metrolist.music.constants.LyricsRomanizeList\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.Material3SettingsGroup\nimport com.metrolist.music.ui.component.Material3SettingsItem\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.rememberPreference\n\nval defaultList = mutableListOf(\n    \"Japanese\" to true,\n    \"Korean\" to true,\n    \"Chinese\" to true,\n    \"Hindi\" to true,\n    \"Punjabi\" to true,\n    \"Russian\" to true,\n    \"Ukrainian\" to true,\n    \"Serbian\" to true,\n    \"Bulgarian\" to true,\n    \"Belarusian\" to true,\n    \"Kyrgyz\" to true,\n    \"Macedonian\" to true,\n)\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun RomanizationSettings(\n    navController: NavController\n) {\n    val (pref, prefValue) = rememberPreference(LyricsRomanizeList, \"\")\n\n    val initialList = remember(pref) {\n        if (pref.isEmpty()) defaultList\n        else {\n            val savedMap = pref.split(\",\").associate { entry ->\n                val (lang, checked) = entry.split(\":\")\n                lang to checked.toBoolean()\n            }\n\n            defaultList.map { (lang, defaultChecked) ->\n                Pair(lang, savedMap[lang] ?: defaultChecked)\n            }\n        }\n    }\n\n    val states = remember(initialList) { mutableStateListOf(*initialList.toTypedArray()) }\n\n    val parentState = when {\n        states.all { it.component2() } -> ToggleableState.On\n        states.none { it.component2() } -> ToggleableState.Off\n        else -> ToggleableState.Indeterminate\n    }\n\n    val (lyricsRomanizeAsMain, onLyricsRomanizeAsMainChange) = rememberPreference(\n        LyricsRomanizeAsMainKey,\n        defaultValue = false\n    )\n\n    val (lyricsRomanizeCyrillicByLine, onLyricsRomanizeCyrillicByLineChange) = rememberPreference(\n        LyricsRomanizeCyrillicByLineKey,\n        defaultValue = false\n    )\n\n    val checkboxesList: MutableList<Material3SettingsItem> = mutableListOf()\n\n    Column(\n        Modifier\n            .windowInsetsPadding(LocalPlayerAwareWindowInsets.current)\n            .verticalScroll(rememberScrollState())\n            .padding(horizontal = 16.dp)\n    ) {\n        Material3SettingsGroup(\n            title = stringResource(R.string.options),\n            items = listOf(\n                Material3SettingsItem(\n                    title = { Text(stringResource(R.string.lyrics_romanize_as_main)) },\n                    trailingContent = {\n                        Switch(\n                            checked = lyricsRomanizeAsMain,\n                            onCheckedChange = onLyricsRomanizeAsMainChange,\n                        )\n                    },\n                    icon = painterResource(R.drawable.queue_music)\n                ),\n                Material3SettingsItem(\n                    title = { Text(stringResource(R.string.line_by_line_option_title)) },\n                    trailingContent = {\n                        Switch(\n                            checked = lyricsRomanizeCyrillicByLine,\n                            onCheckedChange = onLyricsRomanizeCyrillicByLineChange,\n                        )\n                    },\n                    icon = painterResource(R.drawable.info)\n                )\n            )\n        )\n\n        Spacer(modifier = Modifier.height(8.dp))\n\n        checkboxesList += Material3SettingsItem(\n            title = { Text(\"Play all\") },\n            trailingContent = {\n                TriStateCheckbox(\n                    state = parentState,\n                    onClick = {\n                        val newState = parentState != ToggleableState.On\n                        states.forEachIndexed { index, (language, _) ->\n                            states[index] = Pair(language, newState)\n                        }\n                        prefValue(states.joinToString(\",\") { (lang, c) -> \"$lang:$c\" })\n                    }\n                )\n            },\n            icon = painterResource(R.drawable.info)\n        )\n\n        states.forEachIndexed { index, (language, checked) ->\n            checkboxesList += Material3SettingsItem(\n                title = { Text(language) },\n                trailingContent = {\n                    Checkbox(\n                        checked = checked,\n                        onCheckedChange = { isChecked ->\n                            states[index] = Pair(language, isChecked)\n                            prefValue(states.joinToString(\",\") { (lang, c) -> \"$lang:$c\" })\n                        }\n                    )\n                },\n                icon = painterResource(R.drawable.language)\n            )\n        }\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.content_language),\n            items = checkboxesList\n        )\n    }\n\n    TopAppBar(\n        title = { Text(stringResource(R.string.lyrics_romanize_title)) },\n        navigationIcon = {\n            IconButton(\n                onClick = navController::navigateUp,\n                onLongClick = navController::backToMain,\n            ) {\n                Icon(\n                    painterResource(R.drawable.arrow_back),\n                    contentDescription = null,\n                )\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/settings/SettingsScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.settings\n\nimport android.content.ActivityNotFoundException\nimport android.content.Intent\nimport android.os.Build\nimport android.provider.Settings\nimport android.widget.Toast\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.*\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalUriHandler\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.core.net.toUri\nimport androidx.navigation.NavController\nimport com.metrolist.music.BuildConfig\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.Material3SettingsGroup\nimport com.metrolist.music.ui.component.Material3SettingsItem\nimport com.metrolist.music.ui.component.ReleaseNotesCard\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.Updater\nimport androidx.compose.runtime.remember\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun SettingsScreen(\n    navController: NavController,\n    latestVersionName: String,\n) {\n    val uriHandler = LocalUriHandler.current\n    val context = LocalContext.current\n    val isAndroid12OrLater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S\n    val hasAndroidAuto = remember {\n        try {\n            context.packageManager.getPackageInfo(\n                \"com.google.android.projection.gearhead\", 0\n            )\n            true\n        } catch (e: Exception) {\n            false\n        }\n    }\n\n    Column(\n        Modifier\n            .windowInsetsPadding(LocalPlayerAwareWindowInsets.current.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom))\n            .verticalScroll(rememberScrollState())\n            .padding(horizontal = 16.dp)\n    ) {\n        Spacer(\n            Modifier.windowInsetsPadding(\n                LocalPlayerAwareWindowInsets.current.only(\n                    WindowInsetsSides.Top\n                )\n            )\n        )\n\n        // User Interface Section\n        Material3SettingsGroup(\n            title = stringResource(R.string.settings_section_ui),\n            items = listOf(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.palette),\n                    title = { Text(stringResource(R.string.appearance)) },\n                    onClick = { navController.navigate(\"settings/appearance\") }\n                )\n            )\n        )\n\n        Spacer(modifier = Modifier.height(16.dp))\n\n        // Player & Content Section (moved up and combined with content)\n        Material3SettingsGroup(\n            title = stringResource(R.string.settings_section_player_content),\n            items = listOf(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.play),\n                    title = { Text(stringResource(R.string.player_and_audio)) },\n                    onClick = { navController.navigate(\"settings/player\") }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.language),\n                    title = { Text(stringResource(R.string.content)) },\n                    onClick = { navController.navigate(\"settings/content\") }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.translate),\n                    title = { Text(stringResource(R.string.ai_lyrics_translation)) },\n                    onClick = { navController.navigate(\"settings/ai\") }\n                )\n            )\n        )\n\n        Spacer(modifier = Modifier.height(16.dp))\n\n        // Android Auto Section — only shown if Android Auto is installed\n        if (hasAndroidAuto) {\n            Material3SettingsGroup(\n                title = \"Android Auto\",\n                items = listOf(\n                    Material3SettingsItem(\n                        icon = painterResource(R.drawable.ic_android_auto),\n                        title = { Text(stringResource(R.string.android_auto)) },\n                        onClick = { navController.navigate(\"settings/android_auto\") }\n                    )\n                )\n            )\n\n            Spacer(modifier = Modifier.height(16.dp))\n        }\n        \n        // Privacy & Security Section\n        Material3SettingsGroup(\n            title = stringResource(R.string.settings_section_privacy),\n            items = listOf(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.security),\n                    title = { Text(stringResource(R.string.privacy)) },\n                    onClick = { navController.navigate(\"settings/privacy\") }\n                )\n            )\n        )\n\n        Spacer(modifier = Modifier.height(16.dp))\n\n        // Storage & Data Section\n        Material3SettingsGroup(\n            title = stringResource(R.string.settings_section_storage),\n            items = listOf(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.storage),\n                    title = { Text(stringResource(R.string.storage)) },\n                    onClick = { navController.navigate(\"settings/storage\") }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.restore),\n                    title = { Text(stringResource(R.string.backup_restore)) },\n                    onClick = { navController.navigate(\"settings/backup_restore\") }\n                )\n            )\n        )\n\n        Spacer(modifier = Modifier.height(16.dp))\n\n        // System & About Section\n        Material3SettingsGroup(\n            title = stringResource(R.string.settings_section_system),\n            items = buildList {\n                if (isAndroid12OrLater) {\n                    add(\n                        Material3SettingsItem(\n                            icon = painterResource(R.drawable.link),\n                            title = { Text(stringResource(R.string.default_links)) },\n                            onClick = {\n                                try {\n                                    val intent = Intent(\n                                        Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS,\n                                        \"package:${context.packageName}\".toUri()\n                                    )\n                                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n                                    context.startActivity(intent)\n                                } catch (e: Exception) {\n                                    when (e) {\n                                        is ActivityNotFoundException -> {\n                                            Toast.makeText(\n                                                context,\n                                                R.string.open_app_settings_error,\n                                                Toast.LENGTH_LONG\n                                            ).show()\n                                        }\n\n                                        is SecurityException -> {\n                                            Toast.makeText(\n                                                context,\n                                                R.string.open_app_settings_error,\n                                                Toast.LENGTH_LONG\n                                            ).show()\n                                        }\n\n                                        else -> {\n                                            Toast.makeText(\n                                                context,\n                                                R.string.open_app_settings_error,\n                                                Toast.LENGTH_LONG\n                                            ).show()\n                                        }\n                                    }\n                                }\n                            }\n                        )\n                    )\n                }\n                if (BuildConfig.UPDATER_AVAILABLE) {\n                    add(\n                        Material3SettingsItem(\n                            icon = painterResource(R.drawable.update),\n                            title = { Text(stringResource(R.string.updater)) },\n                            onClick = { navController.navigate(\"settings/updater\") }\n                        )\n                    )\n                }\n                val showChangelog = com.metrolist.music.LocalChangelogState.current\n                add(\n                    Material3SettingsItem(\n                        icon = painterResource(R.drawable.newspaper),\n                        title = { Text(stringResource(R.string.changelog)) },\n                        onClick = { showChangelog.value = true }\n                    )\n                )\n                add(\n                    Material3SettingsItem(\n                        icon = painterResource(R.drawable.info),\n                        title = { Text(stringResource(R.string.about)) },\n                        onClick = { navController.navigate(\"settings/about\") }\n                    )\n                )\n                if (BuildConfig.UPDATER_AVAILABLE && latestVersionName != BuildConfig.VERSION_NAME) {\n                    val releaseInfo = Updater.getCachedLatestRelease()\n                    val downloadUrl = releaseInfo?.let { Updater.getDownloadUrlForCurrentVariant(it) }\n\n                    if (downloadUrl != null) {\n                        add(\n                            Material3SettingsItem(\n                                icon = painterResource(R.drawable.update),\n                                title = { \n                                    Text(\n                                        text = stringResource(R.string.new_version_available),\n                                    )\n                                },\n                                description = {\n                                    Text(\n                                        text = latestVersionName,\n                                        style = MaterialTheme.typography.bodyMedium,\n                                        color = MaterialTheme.colorScheme.onSurfaceVariant\n                                    )\n                                },\n                                showBadge = true,\n                                onClick = { uriHandler.openUri(downloadUrl) }\n                            )\n                        )\n                    }\n                }\n            }\n        )\n    if (BuildConfig.UPDATER_AVAILABLE && latestVersionName != BuildConfig.VERSION_NAME) {\n            Spacer(modifier = Modifier.height(16.dp))\n            ReleaseNotesCard()\n        }\n\n        Spacer(modifier = Modifier.height(16.dp))\n    }\n\n    TopAppBar(\n        title = { Text(stringResource(R.string.settings)) },\n        navigationIcon = {\n            IconButton(\n                onClick = navController::navigateUp,\n                onLongClick = navController::backToMain\n            ) {\n                Icon(\n                    painterResource(R.drawable.arrow_back),\n                    contentDescription = null\n                )\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/settings/StorageSettings.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.settings\n\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LinearProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Slider\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.SwitchDefaults\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.StrokeCap\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation.NavController\nimport coil3.SingletonImageLoader\nimport coil3.annotation.DelicateCoilApi\nimport coil3.annotation.ExperimentalCoilApi\nimport coil3.imageLoader\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.EnableSongCacheKey\nimport com.metrolist.music.constants.MaxImageCacheSizeKey\nimport com.metrolist.music.constants.MaxSongCacheSizeKey\nimport com.metrolist.music.extensions.tryOrNull\nimport com.metrolist.music.ui.component.ActionPromptDialog\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.Material3SettingsGroup\nimport com.metrolist.music.ui.component.Material3SettingsItem\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.ui.utils.formatFileSize\nimport com.metrolist.music.utils.rememberPreference\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\nimport okio.ByteString.Companion.encodeUtf8\nimport java.io.File\nimport kotlin.math.roundToInt\n\n@OptIn(ExperimentalCoilApi::class, ExperimentalMaterial3Api::class, DelicateCoilApi::class)\n@Composable\nfun StorageSettings(\n    navController: NavController\n) {\n    val context = LocalContext.current\n    val database = LocalDatabase.current\n    val imageDiskCache = context.imageLoader.diskCache ?: return\n    val playerCache = LocalPlayerConnection.current?.service?.playerCache ?: return\n    val downloadCache = LocalPlayerConnection.current?.service?.downloadCache ?: return\n\n    val coroutineScope = rememberCoroutineScope()\n    val songCacheString = stringResource(R.string.song_cache).lowercase()\n    val imageCacheString = stringResource(R.string.image_cache).lowercase()\n    val (maxImageCacheSize, onMaxImageCacheSizeChange) = rememberPreference(\n        key = MaxImageCacheSizeKey,\n        defaultValue = 512\n    )\n    val (maxSongCacheSize, onMaxSongCacheSizeChange) = rememberPreference(\n        key = MaxSongCacheSizeKey,\n        defaultValue = 1024\n    )\n    val (enableSongCache, onEnableSongCacheChange) = rememberPreference(\n        key = EnableSongCacheKey,\n        defaultValue = true\n    )\n\n    var clearDownloads by remember { mutableStateOf(false) }\n    var clearCacheDialog by remember { mutableStateOf(false) }\n    var clearImageCacheDialog by remember { mutableStateOf(false) }\n\n    // State for the confirmation dialog\n    var showCacheWarningDialog by remember { mutableStateOf(false) }\n    var cacheType by remember { mutableStateOf(\"\") }\n    var cacheUsage by remember { androidx.compose.runtime.mutableLongStateOf(0L) }\n    var onConfirmAction by remember { mutableStateOf<() -> Unit>({}) }\n\n    var imageCacheSize by remember {\n        androidx.compose.runtime.mutableLongStateOf(imageDiskCache.size)\n    }\n    var playerCacheSize by remember {\n        androidx.compose.runtime.mutableLongStateOf(tryOrNull { playerCache.cacheSpace } ?: 0)\n    }\n    var downloadCacheSize by remember {\n        mutableLongStateOf(tryOrNull { downloadCache.cacheSpace } ?: 0)\n    }\n    val imageCacheProgress by animateFloatAsState(\n        targetValue =\n            (imageCacheSize.toFloat() / (maxImageCacheSize * 1024 * 1024L)).coerceIn(\n                0f,\n                1f,\n            ),\n        label = \"imageCacheProgress\",\n    )\n    val playerCacheProgress by animateFloatAsState(\n        targetValue =\n            (playerCacheSize.toFloat() / (maxSongCacheSize * 1024 * 1024L)).coerceIn(\n                0f,\n                1f,\n            ),\n        label = \"playerCacheProgress\",\n    )\n\n    LaunchedEffect(maxImageCacheSize) {\n        SingletonImageLoader.reset()\n        if (maxImageCacheSize == 0) {\n            coroutineScope.launch(Dispatchers.IO) {\n                imageDiskCache.clear()\n            }\n        }\n    }\n    LaunchedEffect(maxSongCacheSize) {\n        if (maxSongCacheSize == 0) {\n            coroutineScope.launch(Dispatchers.IO) {\n                playerCache.keys.forEach { key ->\n                    playerCache.removeResource(key)\n                }\n            }\n        }\n    }\n\n    LaunchedEffect(imageDiskCache) {\n        while (isActive) {\n            delay(500)\n            imageCacheSize = imageDiskCache.size\n        }\n    }\n    LaunchedEffect(playerCache) {\n        while (isActive) {\n            delay(500)\n            playerCacheSize = tryOrNull { playerCache.cacheSpace } ?: 0\n        }\n    }\n    LaunchedEffect(downloadCache) {\n        while (isActive) {\n            delay(500)\n            downloadCacheSize = tryOrNull { downloadCache.cacheSpace } ?: 0\n        }\n    }\n\n    if (clearDownloads) {\n        ActionPromptDialog(\n            title = stringResource(R.string.clear_all_downloads),\n            onDismiss = { clearDownloads = false },\n            onConfirm = {\n                coroutineScope.launch(Dispatchers.IO) {\n                    downloadCache.keys.forEach { key ->\n                        downloadCache.removeResource(key)\n                    }\n                }\n                clearDownloads = false\n            },\n            onCancel = { clearDownloads = false },\n            content = {\n                Text(text = stringResource(R.string.clear_downloads_dialog))\n            },\n        )\n    }\n    if (clearCacheDialog) {\n        ActionPromptDialog(\n            title = stringResource(R.string.clear_song_cache),\n            onDismiss = { clearCacheDialog = false },\n            onConfirm = {\n                coroutineScope.launch(Dispatchers.IO) {\n                    playerCache.keys.forEach { key ->\n                        playerCache.removeResource(key)\n                    }\n                }\n                clearCacheDialog = false\n            },\n            onCancel = { clearCacheDialog = false },\n            content = {\n                Text(text = stringResource(R.string.clear_song_cache_dialog))\n            },\n        )\n    }\n    if (clearImageCacheDialog) {\n        ActionPromptDialog(\n            title = stringResource(R.string.clear_image_cache),\n            onDismiss = { clearImageCacheDialog = false },\n            onConfirm = {\n                coroutineScope.launch(Dispatchers.IO) {\n                    val urlsToPreserve = mutableSetOf<String>()\n                    val downloadedSongs =\n                        try {\n                            database.downloadedSongsByNameAsc().first()\n                        } catch (e: Exception) {\n                            emptyList()\n                        }\n                    downloadedSongs.forEach { song ->\n                        song.song.thumbnailUrl?.let { urlsToPreserve.add(it.encodeUtf8().sha256().hex()) }\n                        song.album?.thumbnailUrl?.let { urlsToPreserve.add(it.encodeUtf8().sha256().hex()) }\n                    }\n                    val directory = imageDiskCache.directory.toFile()\n                    if (directory.exists() && directory.isDirectory) {\n                        directory.listFiles()?.forEach { file ->\n                            if (file.isFile && !file.name.startsWith(\"journal\")) {\n                                val isPreserved = urlsToPreserve.any { hash -> file.name.startsWith(hash) }\n                                if (!isPreserved) {\n                                    file.delete()\n                                }\n                            }\n                        }\n                    }\n                    imageDiskCache.clear()\n                }\n                clearImageCacheDialog = false\n            },\n            onCancel = { clearImageCacheDialog = false },\n            content = {\n                Text(text = stringResource(R.string.clear_image_cache_dialog))\n            },\n        )\n    }\n\n    // Confirmation Dialog\n    if (showCacheWarningDialog) {\n        AlertDialog(\n            onDismissRequest = { showCacheWarningDialog = false },\n            title = { Text(stringResource(R.string.cache_size_warning_title)) },\n            text = {\n                Text(\n                    stringResource(\n                        R.string.cache_size_warning_message,\n                        formatFileSize(cacheUsage),\n                        cacheType,\n                    ),\n                )\n            },\n            confirmButton = {\n                TextButton(\n                    onClick = {\n                        onConfirmAction()\n                        showCacheWarningDialog = false\n                    },\n                ) {\n                    Text(\n                        stringResource(R.string.cache_size_warning_confirm),\n                        color = MaterialTheme.colorScheme.error,\n                    )\n                }\n            },\n            dismissButton = {\n                TextButton(onClick = { showCacheWarningDialog = false }) {\n                    Text(stringResource(id = android.R.string.cancel))\n                }\n            },\n        )\n    }\n\n    Column(\n        Modifier\n            .windowInsetsPadding(\n                LocalPlayerAwareWindowInsets.current.only(\n                    WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom,\n                ),\n            ).verticalScroll(rememberScrollState())\n            .padding(horizontal = 16.dp),\n    ) {\n        Spacer(\n            Modifier.windowInsetsPadding(\n                LocalPlayerAwareWindowInsets.current.only(\n                    WindowInsetsSides.Top,\n                ),\n            ),\n        )\n        Material3SettingsGroup(\n            title = stringResource(R.string.storage),\n            items =\n                listOf(\n                    Material3SettingsItem(\n                        icon = painterResource(R.drawable.storage),\n                        title = { Text(stringResource(R.string.downloaded_songs)) },\n                        description = {\n                            Text(text = formatFileSize(downloadCacheSize))\n                        },\n                    ),\n                    Material3SettingsItem(\n                        icon = painterResource(R.drawable.clear_all),\n                        title = { Text(stringResource(R.string.clear_all_downloads)) },\n                        onClick = {\n                            clearDownloads = true\n                        },\n                    ),\n                ),\n        )\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.song_cache),\n            items = listOf(\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.cached),\n                    title = { Text(stringResource(R.string.enable_song_cache)) },\n                    description = { Text(stringResource(R.string.enable_song_cache_desc)) },\n                    trailingContent = {\n                        Switch(\n                            checked = enableSongCache,\n                            onCheckedChange = onEnableSongCacheChange,\n                            thumbContent = {\n                                Icon(\n                                    painter = painterResource(\n                                        id = if (enableSongCache) R.drawable.check else R.drawable.close\n                                    ),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(SwitchDefaults.IconSize)\n                                )\n                            }\n                        )\n                    },\n                    onClick = { onEnableSongCacheChange(!enableSongCache) }\n                ),\n                Material3SettingsItem(\n                    icon = painterResource(R.drawable.cached),\n                    title = { Text(stringResource(R.string.max_song_cache_size)) },\n                    enabled = enableSongCache,\n                    description = {\n                        val songCacheValues =\n                            remember { listOf(0, 128, 256, 512, 1024, 2048, 4096, 8192, -1) }\n                        Column {\n                            Text(\n                                text = when (maxSongCacheSize) {\n                                    0 -> stringResource(R.string.disable)\n                                    -1 -> stringResource(R.string.unlimited)\n                                    else -> formatFileSize(maxSongCacheSize * 1024 * 1024L)\n                                }\n                            )\n                            Slider(\n                                value = songCacheValues.indexOf(maxSongCacheSize).toFloat(),\n                                enabled = enableSongCache,\n                                onValueChange = {\n                                    val newValue = songCacheValues[it.roundToInt()]\n                                    val newLimitInBytes = if (newValue == -1) {\n                                        Long.MAX_VALUE\n                                    } else {\n                                        newValue * 1024 * 1024L\n                                    }\n\n                                        if (newLimitInBytes < playerCacheSize) {\n                                            cacheUsage = playerCacheSize\n                                            cacheType = songCacheString\n                                            onConfirmAction = { onMaxSongCacheSizeChange(newValue) }\n                                            showCacheWarningDialog = true\n                                        } else {\n                                            onMaxSongCacheSizeChange(newValue)\n                                        }\n                                    },\n                                    steps = songCacheValues.size - 2,\n                                    valueRange = 0f..(songCacheValues.size - 1).toFloat(),\n                                )\n                                LinearProgressIndicator(\n                                    progress = { playerCacheProgress },\n                                    modifier = Modifier.fillMaxWidth(),\n                                    strokeCap = StrokeCap.Round,\n                                )\n                                Spacer(modifier = Modifier.padding(2.dp))\n                                Text(\n                                    text =\n                                        if (maxSongCacheSize == -1) {\n                                            formatFileSize(playerCacheSize)\n                                        } else {\n                                            \"${formatFileSize(playerCacheSize)} / ${\n                                                formatFileSize(\n                                                    maxSongCacheSize * 1024 * 1024L,\n                                                )\n                                            }\"\n                                        },\n                                    style = MaterialTheme.typography.bodyMedium,\n                                )\n                            }\n                        },\n                    ),\n                    Material3SettingsItem(\n                        icon = painterResource(R.drawable.clear_all),\n                        title = { Text(stringResource(R.string.clear_song_cache)) },\n                        onClick = {\n                            clearCacheDialog = true\n                        },\n                    ),\n                ),\n        )\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.image_cache),\n            items =\n                listOf(\n                    Material3SettingsItem(\n                        icon = painterResource(R.drawable.manage_search),\n                        title = { Text(stringResource(R.string.max_image_cache_size)) },\n                        description = {\n                            val imageCacheValues =\n                                remember { listOf(0, 128, 256, 512, 1024, 2048, 4096, 8192) }\n                            Column {\n                                Text(\n                                    text =\n                                        when (maxImageCacheSize) {\n                                            0 -> stringResource(R.string.disable)\n                                            else -> formatFileSize(maxImageCacheSize * 1024 * 1024L)\n                                        },\n                                )\n                                Slider(\n                                    value = imageCacheValues.indexOf(maxImageCacheSize).toFloat(),\n                                    onValueChange = {\n                                        val newValue = imageCacheValues[it.roundToInt()]\n                                        val newLimitInBytes = newValue * 1024 * 1024L\n\n                                        if (newLimitInBytes < imageCacheSize) {\n                                            cacheUsage = imageCacheSize\n                                            cacheType = imageCacheString\n                                            onConfirmAction = { onMaxImageCacheSizeChange(newValue) }\n                                            showCacheWarningDialog = true\n                                        } else {\n                                            onMaxImageCacheSizeChange(newValue)\n                                        }\n                                    },\n                                    steps = imageCacheValues.size - 2,\n                                    valueRange = 0f..(imageCacheValues.size - 1).toFloat(),\n                                )\n                                LinearProgressIndicator(\n                                    progress = { imageCacheProgress },\n                                    modifier = Modifier.fillMaxWidth(),\n                                    strokeCap = StrokeCap.Round,\n                                )\n                                Spacer(modifier = Modifier.padding(2.dp))\n                                Text(\n                                    text = \"${formatFileSize(imageCacheSize)} / ${\n                                        formatFileSize(\n                                            maxImageCacheSize * 1024 * 1024L,\n                                        )\n                                    }\",\n                                    style = MaterialTheme.typography.bodyMedium,\n                                )\n                            }\n                        },\n                    ),\n                    Material3SettingsItem(\n                        icon = painterResource(R.drawable.clear_all),\n                        title = { Text(stringResource(R.string.clear_image_cache)) },\n                        onClick = {\n                            clearImageCacheDialog = true\n                        },\n                    ),\n                ),\n        )\n    }\n\n    TopAppBar(\n        title = { Text(stringResource(R.string.storage)) },\n        navigationIcon = {\n            IconButton(\n                onClick = navController::navigateUp,\n                onLongClick = navController::backToMain,\n            ) {\n                Icon(\n                    painterResource(R.drawable.arrow_back),\n                    contentDescription = null,\n                )\n            }\n        },\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/settings/ThemeScreen.kt",
    "content": "package com.metrolist.music.ui.screens.settings\n\nimport android.content.res.Configuration\nimport android.os.Build\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.spring\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.scaleIn\nimport androidx.compose.animation.scaleOut\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.dynamicDarkColorScheme\nimport androidx.compose.material3.dynamicLightColorScheme\nimport androidx.compose.material3.ripple\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.semantics.contentDescription\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation.NavController\nimport com.materialkolor.PaletteStyle\nimport com.materialkolor.rememberDynamicColorScheme\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.DarkModeKey\nimport com.metrolist.music.constants.DynamicThemeKey\nimport com.metrolist.music.constants.PureBlackKey\nimport com.metrolist.music.constants.PureBlackMiniPlayerKey\nimport com.metrolist.music.constants.SelectedThemeColorKey\nimport com.metrolist.music.ui.theme.DefaultThemeColor\nimport com.metrolist.music.ui.theme.MetrolistTheme\nimport com.metrolist.music.utils.rememberEnumPreference\nimport com.metrolist.music.utils.rememberPreference\n\ndata class ThemePalette(\n    val nameRes: Int,\n    val seedColor: Color\n)\n\nval PaletteColors = listOf(\n    ThemePalette(R.string.palette_dynamic, Color.Transparent), // Sentinel for System/Dynamic colors\n    ThemePalette(R.string.palette_crimson, Color(0xFFEC5464)), // Slightly shifted from DefaultThemeColor (0xFFED5564) to avoid conflict\n    ThemePalette(R.string.palette_rose, Color(0xFFD81B60)),\n    ThemePalette(R.string.palette_purple, Color(0xFF8E24AA)),\n    ThemePalette(R.string.palette_deep_purple, Color(0xFF5E35B1)),\n    ThemePalette(R.string.palette_indigo, Color(0xFF3949AB)),\n    ThemePalette(R.string.palette_blue, Color(0xFF1E88E5)),\n    ThemePalette(R.string.palette_sky_blue, Color(0xFF039BE5)),\n    ThemePalette(R.string.palette_cyan, Color(0xFF00ACC1)),\n    ThemePalette(R.string.palette_teal, Color(0xFF00897B)),\n    ThemePalette(R.string.palette_green, Color(0xFF43A047)),\n    ThemePalette(R.string.palette_light_green, Color(0xFF7CB342)),\n    ThemePalette(R.string.palette_lime, Color(0xFFC0CA33)),\n    ThemePalette(R.string.palette_yellow, Color(0xFFFDD835)),\n    ThemePalette(R.string.palette_amber, Color(0xFFFFB300)),\n    ThemePalette(R.string.palette_orange, Color(0xFFFB8C00)),\n    ThemePalette(R.string.palette_deep_orange, Color(0xFFF4511E)),\n    ThemePalette(R.string.palette_brown, Color(0xFF6D4C41)),\n    ThemePalette(R.string.palette_grey, Color(0xFF757575)),\n    ThemePalette(R.string.palette_blue_grey, Color(0xFF546E7A)),\n)\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun ThemeScreen(\n    navController: NavController,\n) {\n    val (darkMode, onDarkModeChange) = rememberEnumPreference(DarkModeKey, DarkMode.AUTO)\n    val (pureBlack, onPureBlackChangeRaw) = rememberPreference(PureBlackKey, defaultValue = false)\n    val (_, onPureBlackMiniPlayerChange) = rememberPreference(\n        PureBlackMiniPlayerKey,\n        defaultValue = false\n    )\n\n    val onPureBlackChange: (Boolean) -> Unit = { enabled ->\n        onPureBlackChangeRaw(enabled)\n        onPureBlackMiniPlayerChange(enabled)\n    }\n    val (selectedThemeColorInt, onSelectedThemeColorChange) = rememberPreference(\n        SelectedThemeColorKey,\n        DefaultThemeColor.toArgb()\n    )\n    val (_, onDynamicThemeChange) = rememberPreference(DynamicThemeKey, defaultValue = true)\n\n    val selectedThemeColor = Color(selectedThemeColorInt)\n    val configuration = LocalConfiguration.current\n    val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE\n\n    // Helper function to handle color selection with dynamic theme toggle\n    val handleColorSelection: (Color) -> Unit = { color ->\n        onSelectedThemeColorChange(color.toArgb())\n        // Enable dynamic theme only when selecting the default/dynamic color\n        // Disable it when selecting any other color\n        val isDynamicColor = color == DefaultThemeColor\n        onDynamicThemeChange(isDynamicColor)\n    }\n\n    if (isLandscape) {\n        LandscapeThemeLayout(\n            innerPadding = PaddingValues(0.dp),\n            darkMode = darkMode,\n            onDarkModeChange = onDarkModeChange,\n            pureBlack = pureBlack,\n            onPureBlackChange = onPureBlackChange,\n            selectedThemeColor = selectedThemeColor,\n            onSelectedThemeColorChange = handleColorSelection\n        )\n    } else {\n        PortraitThemeLayout(\n            innerPadding = PaddingValues(0.dp),\n            darkMode = darkMode,\n            onDarkModeChange = onDarkModeChange,\n            pureBlack = pureBlack,\n            onPureBlackChange = onPureBlackChange,\n            selectedThemeColor = selectedThemeColor,\n            onSelectedThemeColorChange = handleColorSelection\n        )\n    }\n\n    TopAppBar(\n        title = { Text(stringResource(R.string.theme_colors)) },\n        navigationIcon = {\n            IconButton(onClick = { navController.navigateUp() }) {\n                Icon(\n                    painter = painterResource(R.drawable.arrow_back),\n                    contentDescription = stringResource(R.string.cd_back)\n                )\n            }\n        }\n    )\n}\n\n@Composable\nfun PortraitThemeLayout(\n    innerPadding: PaddingValues,\n    darkMode: DarkMode,\n    onDarkModeChange: (DarkMode) -> Unit,\n    pureBlack: Boolean,\n    onPureBlackChange: (Boolean) -> Unit,\n    selectedThemeColor: Color,\n    onSelectedThemeColorChange: (Color) -> Unit\n) {\n    Column(\n        modifier = Modifier\n            .fillMaxSize()\n            .padding(innerPadding),\n        horizontalAlignment = Alignment.CenterHorizontally\n    ) {\n        Spacer(modifier = Modifier.weight(1f))\n\n        Box(\n            modifier = Modifier\n                .width(120.dp)\n                .height(240.dp),\n            contentAlignment = Alignment.Center\n        ) {\n            ThemeMockupPortrait(\n                darkMode = darkMode,\n                pureBlack = pureBlack,\n                themeColor = selectedThemeColor\n            )\n        }\n\n        Spacer(modifier = Modifier.weight(1f))\n\n        ThemeControls(\n            darkMode = darkMode,\n            onDarkModeChange = onDarkModeChange,\n            pureBlack = pureBlack,\n            onPureBlackChange = onPureBlackChange,\n            selectedThemeColor = selectedThemeColor,\n            onSelectedThemeColorChange = onSelectedThemeColorChange\n        )\n\n        Spacer(modifier = Modifier.height(120.dp))\n    }\n}\n\n@Composable\nfun LandscapeThemeLayout(\n    innerPadding: PaddingValues,\n    darkMode: DarkMode,\n    onDarkModeChange: (DarkMode) -> Unit,\n    pureBlack: Boolean,\n    onPureBlackChange: (Boolean) -> Unit,\n    selectedThemeColor: Color,\n    onSelectedThemeColorChange: (Color) -> Unit\n) {\n    Row(\n        modifier = Modifier\n            .fillMaxSize()\n            .padding(innerPadding)\n    ) {\n        Column(\n            modifier = Modifier\n                .weight(0.4f)\n                .fillMaxHeight()\n                .padding(16.dp),\n            verticalArrangement = Arrangement.Center,\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            Box(\n                modifier = Modifier\n                    .fillMaxWidth(0.8f)\n                    .heightIn(max = 300.dp),\n                contentAlignment = Alignment.Center\n            ) {\n                ThemeMockup(\n                    darkMode = darkMode,\n                    pureBlack = pureBlack,\n                    themeColor = selectedThemeColor\n                )\n            }\n        }\n\n        Column(\n            modifier = Modifier\n                .weight(0.6f)\n                .fillMaxHeight()\n                .verticalScroll(rememberScrollState())\n                .padding(end = 16.dp, top = 16.dp, bottom = 16.dp)\n        ) {\n            ThemeControls(\n                darkMode = darkMode,\n                onDarkModeChange = onDarkModeChange,\n                pureBlack = pureBlack,\n                onPureBlackChange = onPureBlackChange,\n                selectedThemeColor = selectedThemeColor,\n                onSelectedThemeColorChange = onSelectedThemeColorChange\n            )\n\n            Spacer(modifier = Modifier.height(80.dp))\n        }\n    }\n}\n\n@Composable\nfun ThemeControls(\n    darkMode: DarkMode,\n    onDarkModeChange: (DarkMode) -> Unit,\n    pureBlack: Boolean,\n    onPureBlackChange: (Boolean) -> Unit,\n    selectedThemeColor: Color,\n    onSelectedThemeColorChange: (Color) -> Unit\n) {\n    Card(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(horizontal = 16.dp),\n        shape = RoundedCornerShape(24.dp),\n        colors = CardDefaults.cardColors(\n            containerColor = MaterialTheme.colorScheme.surfaceContainerHigh\n        ),\n        elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)\n    ) {\n        Column(\n            modifier = Modifier.padding(20.dp),\n            verticalArrangement = Arrangement.spacedBy(24.dp)\n        ) {\n            Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {\n                Text(\n                    text = stringResource(R.string.theme_mode),\n                    style = MaterialTheme.typography.titleMedium,\n                    color = MaterialTheme.colorScheme.onSurface\n                )\n                \n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally),\n                    verticalAlignment = Alignment.CenterVertically\n                ) {\n                    // System mode (AUTO)\n                    ModeCircle(\n                        darkMode = darkMode,\n                        pureBlack = pureBlack,\n                        targetMode = DarkMode.AUTO,\n                        targetPureBlack = pureBlack,\n                        onClick = {\n                            onDarkModeChange(DarkMode.AUTO)\n                        },\n                        showIcon = true\n                    )\n                    \n                    // Vertical divider to separate System from manual modes\n                    Box(\n                        modifier = Modifier\n                            .width(1.dp)\n                            .height(32.dp)\n                            .background(MaterialTheme.colorScheme.outlineVariant)\n                    )\n                    \n                    // Manual modes (Light, Dark, Pure Black)\n                    ModeCircle(\n                        darkMode = darkMode,\n                        pureBlack = pureBlack,\n                        targetMode = DarkMode.OFF,\n                        targetPureBlack = false,\n                        onClick = {\n                            onDarkModeChange(DarkMode.OFF)\n                            onPureBlackChange(false)\n                        },\n                        showIcon = false\n                    )\n                    \n                    ModeCircle(\n                        darkMode = darkMode,\n                        pureBlack = pureBlack,\n                        targetMode = DarkMode.ON,\n                        targetPureBlack = false,\n                        onClick = {\n                            onDarkModeChange(DarkMode.ON)\n                            onPureBlackChange(false)\n                        },\n                        showIcon = false\n                    )\n                    \n                    ModeCircle(\n                        darkMode = darkMode,\n                        pureBlack = pureBlack,\n                        targetMode = DarkMode.ON,\n                        targetPureBlack = true,\n                        onClick = {\n                            onDarkModeChange(DarkMode.ON)\n                            onPureBlackChange(true)\n                        },\n                        showIcon = false\n                    )\n                }\n            }\n\n            Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {\n                Text(\n                    text = stringResource(R.string.color_palette),\n                    style = MaterialTheme.typography.titleMedium,\n                    color = MaterialTheme.colorScheme.onSurface\n                )\n                \n                LazyRow(\n                    horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),\n                    contentPadding = PaddingValues(horizontal = 4.dp)\n                ) {\n                    items(PaletteColors) { palette ->\n                        val isDynamicPalette = palette.seedColor == Color.Transparent\n                        val isSelected = if (isDynamicPalette) {\n                            selectedThemeColor == DefaultThemeColor\n                        } else {\n                            selectedThemeColor == palette.seedColor\n                        }\n                        \n                        PaletteItem(\n                            palette = palette,\n                            isSelected = isSelected,\n                            onClick = { \n                                val colorToSave = if (isDynamicPalette) DefaultThemeColor else palette.seedColor\n                                onSelectedThemeColorChange(colorToSave) \n                            }\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun ModeCircle(\n    darkMode: DarkMode,\n    pureBlack: Boolean,\n    targetMode: DarkMode,\n    targetPureBlack: Boolean,\n    showIcon: Boolean,\n    onClick: () -> Unit\n) {\n    val context = LocalContext.current\n    val isSystemDark = isSystemInDarkTheme()\n    val isSelected = darkMode == targetMode && pureBlack == targetPureBlack\n    \n    val effectiveDark = when (targetMode) {\n        DarkMode.AUTO -> isSystemDark\n        DarkMode.ON -> true\n        DarkMode.OFF -> false\n    }\n    \n    // Use actual system colors for AUTO mode on Android 12+\n    val modeColorScheme = if (targetMode == DarkMode.AUTO && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {\n        if (effectiveDark) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)\n    } else {\n        rememberDynamicColorScheme(\n            seedColor = DefaultThemeColor,\n            isDark = effectiveDark,\n            style = PaletteStyle.TonalSpot\n        )\n    }\n    \n    val fillColor = when {\n        targetPureBlack -> Color.Black\n        effectiveDark -> modeColorScheme.surface\n        else -> modeColorScheme.surface\n    }\n    \n    // Animated border width\n    val borderWidth by animateDpAsState(\n        targetValue = if (isSelected) 3.dp else 0.dp,\n        animationSpec = spring(\n            dampingRatio = Spring.DampingRatioMediumBouncy,\n            stiffness = Spring.StiffnessMedium\n        ),\n        label = \"borderWidth\"\n    )\n    \n    // Animated scale for the entire circle\n    val scale by animateFloatAsState(\n        targetValue = if (isSelected) 1.05f else 1f,\n        animationSpec = spring(\n            dampingRatio = Spring.DampingRatioMediumBouncy,\n            stiffness = Spring.StiffnessMedium\n        ),\n        label = \"scale\"\n    )\n    \n    val interactionSource = remember { MutableInteractionSource() }\n    \n    val contentDesc = when {\n        targetPureBlack -> stringResource(R.string.cd_pure_black_mode)\n        targetMode == DarkMode.OFF -> stringResource(R.string.cd_light_mode)\n        targetMode == DarkMode.ON -> stringResource(R.string.cd_dark_mode)\n        else -> stringResource(R.string.cd_system_mode)\n    }\n    \n    Box(\n        modifier = Modifier\n            .size(48.dp)\n            .graphicsLayer {\n                scaleX = scale\n                scaleY = scale\n            }\n            .clip(CircleShape)\n            .background(fillColor)\n            .then(\n                if (borderWidth > 0.dp) {\n                    Modifier.border(\n                        width = borderWidth,\n                        color = MaterialTheme.colorScheme.inversePrimary,\n                        shape = CircleShape\n                    )\n                } else {\n                    Modifier\n                }\n            )\n            .clickable(\n                interactionSource = interactionSource,\n                indication = ripple(),\n                onClick = onClick\n            )\n            .semantics {\n                contentDescription = contentDesc\n            },\n        contentAlignment = Alignment.Center\n    ) {\n        when {\n            showIcon -> {\n                Icon(\n                    painter = painterResource(R.drawable.sync),\n                    contentDescription = null,\n                    tint = modeColorScheme.onSurface,\n                    modifier = Modifier.size(20.dp)\n                )\n            }\n            isSelected -> {\n                AnimatedVisibility(\n                    visible = isSelected,\n                    enter = fadeIn(animationSpec = tween(300)) + scaleIn(\n                        initialScale = 0.3f,\n                        animationSpec = spring(\n                            dampingRatio = Spring.DampingRatioMediumBouncy,\n                            stiffness = Spring.StiffnessMedium\n                        )\n                    ),\n                    exit = fadeOut(animationSpec = tween(150)) + scaleOut(\n                        targetScale = 0.3f,\n                        animationSpec = tween(150)\n                    )\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.check),\n                        contentDescription = null,\n                        tint = MaterialTheme.colorScheme.inversePrimary,\n                        modifier = Modifier.size(20.dp)\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun PaletteItem(\n    palette: ThemePalette,\n    isSelected: Boolean,\n    onClick: () -> Unit\n) {\n    val isSystemDark = isSystemInDarkTheme()\n    \n    val colorScheme = rememberDynamicColorScheme(\n        seedColor = palette.seedColor,\n        isDark = isSystemDark,\n        style = PaletteStyle.TonalSpot\n    )\n    \n    val cornerRadius by animateDpAsState(\n        targetValue = if (isSelected) 48.dp * 0.25f else 24.dp,\n        animationSpec = spring(\n            dampingRatio = Spring.DampingRatioLowBouncy,\n            stiffness = Spring.StiffnessMedium\n        ),\n        label = \"cornerRadius\"\n    )\n    \n    val borderWidth by animateDpAsState(\n        targetValue = if (isSelected) 3.dp else 0.dp,\n        animationSpec = spring(\n            dampingRatio = Spring.DampingRatioMediumBouncy,\n            stiffness = Spring.StiffnessMedium\n        ),\n        label = \"borderWidth\"\n    )\n    \n    val scale by animateFloatAsState(\n        targetValue = if (isSelected) 1.08f else 1f,\n        animationSpec = spring(\n            dampingRatio = Spring.DampingRatioMediumBouncy,\n            stiffness = Spring.StiffnessMedium\n        ),\n        label = \"scale\"\n    )\n    \n    val shape = RoundedCornerShape(cornerRadius)\n    val interactionSource = remember { MutableInteractionSource() }\n    \n    val paletteName = stringResource(palette.nameRes)\n    val contentDesc = stringResource(R.string.cd_palette_item, paletteName)\n    \n    Box(\n        modifier = Modifier\n            .size(48.dp)\n            .graphicsLayer {\n                scaleX = scale\n                scaleY = scale\n            }\n            .clip(shape)\n            .then(\n                if (borderWidth > 0.dp) {\n                    Modifier.border(\n                        width = borderWidth,\n                        color = MaterialTheme.colorScheme.inversePrimary,\n                        shape = shape\n                    )\n                } else {\n                    Modifier\n                }\n            )\n            .clickable(\n                interactionSource = interactionSource,\n                indication = ripple(),\n                onClick = onClick\n            )\n            .semantics {\n                contentDescription = contentDesc\n            }\n    ) {\n        if (palette.seedColor == Color.Transparent) {\n            // Draw Dynamic/System icon using Material Design icon\n            Box(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .background(MaterialTheme.colorScheme.surfaceVariant),\n                contentAlignment = Alignment.Center\n            ) {\n                Icon(\n                    painter = painterResource(R.drawable.palette),\n                    contentDescription = null,\n                    tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                    modifier = Modifier.size(24.dp)\n                )\n            }\n        } else {\n            Canvas(modifier = Modifier.fillMaxSize()) {\n                val width = size.width\n                val height = size.height\n                \n                drawRect(\n                    color = colorScheme.onPrimary,\n                    topLeft = Offset(0f, 0f),\n                    size = Size(width, height / 2)\n                )\n                \n                drawRect(\n                    color = colorScheme.secondary,\n                    topLeft = Offset(0f, height / 2),\n                    size = Size(width / 2, height / 2)\n                )\n                \n                drawRect(\n                    color = colorScheme.tertiary,\n                    topLeft = Offset(width / 2, height / 2),\n                    size = Size(width / 2, height / 2)\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun ThemeMockup(\n    darkMode: DarkMode,\n    pureBlack: Boolean,\n    themeColor: Color\n) {\n    val isSystemDark = isSystemInDarkTheme()\n    val useDark = when (darkMode) {\n        DarkMode.AUTO -> isSystemDark\n        DarkMode.ON -> true\n        DarkMode.OFF -> false\n    }\n\n    MetrolistTheme(\n        darkTheme = useDark,\n        pureBlack = pureBlack,\n        themeColor = themeColor\n    ) {\n        Card(\n            modifier = Modifier\n                .fillMaxSize()\n                .aspectRatio(9f / 18f),\n            shape = RoundedCornerShape(16.dp),\n            colors = CardDefaults.cardColors(\n                containerColor = MaterialTheme.colorScheme.surface\n            ),\n            border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),\n            elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)\n        ) {\n            Column(\n                modifier = Modifier.fillMaxSize()\n            ) {\n                Box(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .height(40.dp)\n                        .background(MaterialTheme.colorScheme.surfaceContainer)\n                        .padding(10.dp),\n                    contentAlignment = Alignment.CenterStart\n                ) {\n                    Row(\n                        modifier = Modifier.fillMaxWidth(),\n                        horizontalArrangement = Arrangement.SpaceBetween,\n                        verticalAlignment = Alignment.CenterVertically\n                    ) {\n                        Box(\n                            modifier = Modifier\n                                .size(18.dp)\n                                .background(MaterialTheme.colorScheme.primary, CircleShape)\n                        )\n                        Box(\n                            modifier = Modifier\n                                .size(18.dp)\n                                .background(MaterialTheme.colorScheme.secondary, CircleShape)\n                        )\n                    }\n                }\n\n                Column(\n                    modifier = Modifier\n                        .weight(1f)\n                        .padding(10.dp),\n                    verticalArrangement = Arrangement.spacedBy(6.dp)\n                ) {\n                    Box(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .height(32.dp)\n                            .background(MaterialTheme.colorScheme.primary, RoundedCornerShape(6.dp))\n                    )\n                    \n                    Row(\n                        modifier = Modifier.fillMaxWidth(),\n                        horizontalArrangement = Arrangement.spacedBy(6.dp)\n                    ) {\n                        Box(\n                            modifier = Modifier\n                                .weight(1f)\n                                .height(40.dp)\n                                .background(MaterialTheme.colorScheme.secondary, RoundedCornerShape(6.dp))\n                        )\n                        \n                        Box(\n                            modifier = Modifier\n                                .weight(1f)\n                                .height(40.dp)\n                                .background(MaterialTheme.colorScheme.tertiary, RoundedCornerShape(6.dp))\n                        )\n                    }\n                }\n\n                Box(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(10.dp),\n                    contentAlignment = Alignment.BottomEnd\n                ) {\n                    Box(\n                        modifier = Modifier\n                            .size(30.dp)\n                            .background(MaterialTheme.colorScheme.primaryContainer, CircleShape)\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun ThemeMockupPortrait(\n    darkMode: DarkMode,\n    pureBlack: Boolean,\n    themeColor: Color\n) {\n    val isSystemDark = isSystemInDarkTheme()\n    val useDark = when (darkMode) {\n        DarkMode.AUTO -> isSystemDark\n        DarkMode.ON -> true\n        DarkMode.OFF -> false\n    }\n\n    MetrolistTheme(\n        darkTheme = useDark,\n        pureBlack = pureBlack,\n        themeColor = themeColor\n    ) {\n        Card(\n            modifier = Modifier\n                .fillMaxSize(),\n            shape = RoundedCornerShape(12.dp),\n            colors = CardDefaults.cardColors(\n                containerColor = MaterialTheme.colorScheme.surface\n            ),\n            border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),\n            elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)\n        ) {\n            Column(\n                modifier = Modifier.fillMaxSize()\n            ) {\n                // Header (20% of height)\n                Box(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .weight(0.2f)\n                        .background(MaterialTheme.colorScheme.surfaceContainer)\n                        .padding(6.dp),\n                    contentAlignment = Alignment.CenterStart\n                ) {\n                    Row(\n                        modifier = Modifier.fillMaxWidth(),\n                        horizontalArrangement = Arrangement.SpaceBetween,\n                        verticalAlignment = Alignment.CenterVertically\n                    ) {\n                        Box(\n                            modifier = Modifier\n                                .size(12.dp)\n                                .background(MaterialTheme.colorScheme.primary, CircleShape)\n                        )\n                        Box(\n                            modifier = Modifier\n                                .size(12.dp)\n                                .background(MaterialTheme.colorScheme.secondary, CircleShape)\n                        )\n                    }\n                }\n\n                // Main Content (60% of height)\n                Column(\n                    modifier = Modifier\n                        .weight(0.6f)\n                        .padding(6.dp),\n                    verticalArrangement = Arrangement.spacedBy(4.dp)\n                ) {\n                    Box(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .weight(1f)\n                            .background(MaterialTheme.colorScheme.primary, RoundedCornerShape(4.dp))\n                    )\n                    \n                    Row(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .weight(1.2f),\n                        horizontalArrangement = Arrangement.spacedBy(4.dp)\n                    ) {\n                        Box(\n                            modifier = Modifier\n                                .weight(1f)\n                                .fillMaxHeight()\n                                .background(MaterialTheme.colorScheme.secondary, RoundedCornerShape(4.dp))\n                        )\n                        \n                        Box(\n                            modifier = Modifier\n                                .weight(1f)\n                                .fillMaxHeight()\n                                .background(MaterialTheme.colorScheme.tertiary, RoundedCornerShape(4.dp))\n                        )\n                    }\n                }\n\n                // FAB Area (20% of height)\n                Box(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .weight(0.2f)\n                        .padding(6.dp),\n                    contentAlignment = Alignment.BottomEnd\n                ) {\n                    Box(\n                        modifier = Modifier\n                            .size(18.dp)\n                            .background(MaterialTheme.colorScheme.primaryContainer, CircleShape)\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/settings/UpdaterSettings.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.settings\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation.NavController\nimport com.metrolist.music.BuildConfig\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.CheckForUpdatesKey\nimport com.metrolist.music.constants.UpdateNotificationsEnabledKey\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.Material3SettingsGroup\nimport com.metrolist.music.ui.component.Material3SettingsItem\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.Updater\nimport com.metrolist.music.utils.rememberPreference\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun UpdaterScreen(\n    navController: NavController\n) {\n    val (checkForUpdates, onCheckForUpdatesChange) = rememberPreference(CheckForUpdatesKey, true)\n    val (updateNotifications, onUpdateNotificationsChange) = rememberPreference(UpdateNotificationsEnabledKey, true)\n\n    val context = LocalContext.current\n    var isChecking by remember { mutableStateOf(false) }\n    var updateAvailable by remember { mutableStateOf(false) }\n    var latestVersion by remember { mutableStateOf<String?>(null) }\n    var showChangelog by remember { mutableStateOf(false) }\n    var changelogContent by remember { mutableStateOf<String?>(null) }\n    var checkError by remember { mutableStateOf<String?>(null) }\n    val failedToCheckUpdatesTemplate = stringResource(R.string.failed_to_check_updates)\n\n    val coroutineScope = rememberCoroutineScope()\n\n    fun performManualCheck() {\n        coroutineScope.launch {\n            isChecking = true\n            checkError = null\n            withContext(Dispatchers.IO) {\n                Updater\n                    .checkForUpdate(forceRefresh = true)\n                    .onSuccess { (releaseInfo, hasUpdate) ->\n                        if (releaseInfo != null) {\n                            latestVersion = releaseInfo.versionName\n                            updateAvailable = hasUpdate\n                            changelogContent = releaseInfo.description\n                        }\n                    }.onFailure {\n                        checkError = String.format(failedToCheckUpdatesTemplate, it.message ?: \"Unknown error\")\n                    }\n            }\n            isChecking = false\n        }\n    }\n\n    Column(\n        modifier =\n            Modifier\n                .fillMaxWidth()\n                .windowInsetsPadding(\n                    LocalPlayerAwareWindowInsets.current.only(\n                        WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom,\n                    ),\n                ).verticalScroll(rememberScrollState())\n                .padding(horizontal = 16.dp),\n        horizontalAlignment = Alignment.CenterHorizontally,\n    ) {\n        Spacer(\n            Modifier.windowInsetsPadding(\n                LocalPlayerAwareWindowInsets.current.only(\n                    WindowInsetsSides.Top,\n                ),\n            ),\n        )\n\n        Spacer(Modifier.height(4.dp))\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.current_version),\n            items =\n                listOf(\n                    Material3SettingsItem(\n                        title = {\n                            Text(stringResource(R.string.version_format, BuildConfig.VERSION_NAME))\n                        },\n                        description = {\n                            val arch = BuildConfig.ARCHITECTURE\n                            val variant = if (BuildConfig.CAST_AVAILABLE) \"GMS\" else \"FOSS\"\n                            Text(\"$arch - $variant\")\n                        },\n                    ),\n                ),\n        )\n\n        Spacer(Modifier.height(16.dp))\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.update_settings),\n            items =\n                buildList {\n                    add(\n                        Material3SettingsItem(\n                            title = { Text(stringResource(R.string.check_for_updates)) },\n                            icon = painterResource(R.drawable.update),\n                            trailingContent = {\n                                Switch(\n                                    checked = checkForUpdates,\n                                    onCheckedChange = onCheckForUpdatesChange,\n                                )\n                            },\n                            onClick = { onCheckForUpdatesChange(!checkForUpdates) },\n                        ),\n                    )\n\n                    if (checkForUpdates) {\n                        add(\n                            Material3SettingsItem(\n                                title = { Text(stringResource(R.string.update_notifications)) },\n                                icon = painterResource(R.drawable.notification),\n                                trailingContent = {\n                                    Switch(\n                                        checked = updateNotifications,\n                                        onCheckedChange = onUpdateNotificationsChange,\n                                    )\n                                },\n                                onClick = { onUpdateNotificationsChange(!updateNotifications) },\n                            ),\n                        )\n                    }\n                },\n        )\n\n        Spacer(Modifier.height(16.dp))\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.check_for_updates_title),\n            items =\n                listOf(\n                    Material3SettingsItem(\n                        icon = painterResource(R.drawable.refresh),\n                        title = {\n                            if (isChecking) {\n                                Text(stringResource(R.string.checking_for_updates))\n                            } else if (latestVersion != null) {\n                                Text(stringResource(R.string.latest_version_format, latestVersion!!))\n                            } else {\n                                Text(stringResource(R.string.check_for_updates_button))\n                            }\n                        },\n                        trailingContent = {\n                            if (isChecking) {\n                                CircularProgressIndicator(\n                                    modifier = Modifier.padding(end = 16.dp),\n                                    strokeWidth = 2.dp,\n                                )\n                            } else if (updateAvailable) {\n                                Icon(\n                                    painter = painterResource(R.drawable.download),\n                                    contentDescription = stringResource(R.string.update_available_title),\n                                    tint = MaterialTheme.colorScheme.primary,\n                                )\n                            }\n                        },\n                        onClick = { if (!isChecking) performManualCheck() },\n                    ),\n                ),\n        )\n\n        checkError?.let {\n            Spacer(Modifier.height(12.dp))\n            Text(\n                text = it,\n                style = MaterialTheme.typography.bodySmall,\n                color = MaterialTheme.colorScheme.error,\n                modifier = Modifier.padding(horizontal = 16.dp),\n            )\n        }\n\n        if (updateAvailable && latestVersion != null) {\n            Spacer(Modifier.height(16.dp))\n            Button(\n                onClick = { showChangelog = !showChangelog },\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 16.dp),\n            ) {\n                Text(if (showChangelog) stringResource(R.string.hide_changelog) else stringResource(R.string.view_changelog))\n            }\n\n            if (showChangelog && changelogContent != null) {\n                Spacer(Modifier.height(12.dp))\n                Text(\n                    text = changelogContent!!,\n                    style = MaterialTheme.typography.bodySmall,\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(16.dp),\n                )\n            }\n        }\n\n        Spacer(Modifier.height(32.dp))\n    }\n\n    TopAppBar(\n        title = { Text(stringResource(R.string.updater)) },\n        navigationIcon = {\n            IconButton(\n                onClick = navController::navigateUp,\n                onLongClick = navController::backToMain,\n            ) {\n                Icon(\n                    painter = painterResource(R.drawable.arrow_back),\n                    contentDescription = null,\n                )\n            }\n        },\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/settings/integrations/DiscordSettings.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.settings.integrations\n\nimport android.content.Intent\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.animateContentSize\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LinearProgressIndicator\nimport androidx.compose.material3.LinearWavyProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedButton\nimport androidx.compose.material3.SnackbarDuration\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.material3.SnackbarResult\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.input.TextFieldValue\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.core.net.toUri\nimport androidx.media3.common.Player.STATE_READY\nimport androidx.navigation.NavController\nimport coil3.compose.AsyncImage\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.DiscordActivityNameKey\nimport com.metrolist.music.constants.DiscordActivityTypeKey\nimport com.metrolist.music.constants.DiscordAdvancedModeKey\nimport com.metrolist.music.constants.DiscordAvatarKey\nimport com.metrolist.music.constants.DiscordButton1TextKey\nimport com.metrolist.music.constants.DiscordButton1VisibleKey\nimport com.metrolist.music.constants.DiscordButton2TextKey\nimport com.metrolist.music.constants.DiscordButton2VisibleKey\nimport com.metrolist.music.constants.DiscordInfoDismissedKey\nimport com.metrolist.music.constants.DiscordNameKey\nimport com.metrolist.music.constants.DiscordStatusKey\nimport com.metrolist.music.constants.DiscordTokenKey\nimport com.metrolist.music.constants.DiscordUseDetailsKey\nimport com.metrolist.music.constants.DiscordUsernameKey\nimport com.metrolist.music.constants.EnableDiscordRPCKey\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.ui.component.EnumDialog\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.InfoLabel\nimport com.metrolist.music.ui.component.Material3SettingsGroup\nimport com.metrolist.music.ui.component.Material3SettingsItem\nimport com.metrolist.music.ui.component.TextFieldDialog\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.DiscordRPC\nimport com.metrolist.music.utils.SuperProperties\nimport com.metrolist.music.utils.makeTimeString\nimport com.metrolist.music.utils.rememberPreference\nimport com.my.kizzy.rpc.KizzyRPC\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\n\nprivate enum class DiscordStatus { ONLINE, IDLE, DND }\n\nprivate enum class DiscordActivityType { LISTENING, PLAYING, WATCHING, COMPETING }\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun DiscordSettings(\n    navController: NavController,\n    snackbarHostState: SnackbarHostState,\n) {\n    val playerConnection = LocalPlayerConnection.current ?: return\n    val song by playerConnection.currentSong.collectAsState(null)\n    val playbackState by playerConnection.playbackState.collectAsState()\n\n    var position by rememberSaveable(playbackState) {\n        mutableLongStateOf(playerConnection.player.currentPosition)\n    }\n\n    val coroutineScope = rememberCoroutineScope()\n    val loginSuccessfulStr = stringResource(R.string.login_successful)\n\n    // Preferences\n    var discordToken by rememberPreference(DiscordTokenKey, \"\")\n    var discordUsername by rememberPreference(DiscordUsernameKey, \"\")\n    var discordName by rememberPreference(DiscordNameKey, \"\")\n    var discordAvatar by rememberPreference(DiscordAvatarKey, \"\")\n    var infoDismissed by rememberPreference(DiscordInfoDismissedKey, false)\n\n    val (discordRPC, onDiscordRPCChange) = rememberPreference(EnableDiscordRPCKey, true)\n    val (useDetails, onUseDetailsChange) = rememberPreference(DiscordUseDetailsKey, false)\n    val (advancedMode, onAdvancedModeChange) = rememberPreference(DiscordAdvancedModeKey, false)\n\n    var discordStatus by rememberPreference(DiscordStatusKey, \"online\")\n    var button1Text by rememberPreference(DiscordButton1TextKey, \"\")\n    var button1Visible by rememberPreference(DiscordButton1VisibleKey, true)\n    var button2Text by rememberPreference(DiscordButton2TextKey, \"\")\n    var button2Visible by rememberPreference(DiscordButton2VisibleKey, true)\n    var activityType by rememberPreference(DiscordActivityTypeKey, \"listening\")\n    var activityName by rememberPreference(DiscordActivityNameKey, \"\")\n\n    val isLoggedIn = remember(discordToken) { discordToken.isNotEmpty() }\n\n    var showTokenDialog by rememberSaveable { mutableStateOf(false) }\n    var showStatusDialog by rememberSaveable { mutableStateOf(false) }\n    var showActivityTypeDialog by rememberSaveable { mutableStateOf(false) }\n    var showButton1TextDialog by rememberSaveable { mutableStateOf(false) }\n    var showButton2TextDialog by rememberSaveable { mutableStateOf(false) }\n    var showActivityNameDialog by rememberSaveable { mutableStateOf(false) }\n\n    // Map string prefs to enums for dialogs\n    val currentStatus =\n        when (discordStatus) {\n            \"idle\" -> DiscordStatus.IDLE\n            \"dnd\" -> DiscordStatus.DND\n            else -> DiscordStatus.ONLINE\n        }\n    val currentActivityType =\n        when (activityType) {\n            \"playing\" -> DiscordActivityType.PLAYING\n            \"watching\" -> DiscordActivityType.WATCHING\n            \"competing\" -> DiscordActivityType.COMPETING\n            else -> DiscordActivityType.LISTENING\n        }\n\n    // Fetch user info when token changes\n    LaunchedEffect(discordToken) {\n        val token = discordToken\n        if (token.isEmpty()) {\n            discordUsername = \"\"\n            discordName = \"\"\n            discordAvatar = \"\"\n            return@LaunchedEffect\n        }\n        launch(Dispatchers.IO) {\n            KizzyRPC\n                .getUserInfo(\n                    token,\n                    SuperProperties.userAgent,\n                    SuperProperties.superPropertiesBase64,\n                ).onSuccess {\n                    discordUsername = it.username\n                    discordName = it.name\n                    discordAvatar = it.avatar ?: \"\"\n                }.onFailure {\n                    discordUsername = \"\"\n                    discordName = \"\"\n                    discordAvatar = \"\"\n                }\n        }\n    }\n\n    // Update playback position\n    LaunchedEffect(playbackState) {\n        if (playbackState == STATE_READY) {\n            while (isActive) {\n                delay(100)\n                position = playerConnection.player.currentPosition\n            }\n        }\n    }\n\n    // Dialogs\n    if (showTokenDialog) {\n        var isVerifying by remember { mutableStateOf(false) }\n        var error by remember { mutableStateOf<String?>(null) }\n\n        TextFieldDialog(\n            onDismiss = { showTokenDialog = false },\n            icon = { Icon(painterResource(R.drawable.token), null) },\n            autoDismiss = false,\n            onDone = { token ->\n                isVerifying = true\n                error = null\n                coroutineScope.launch(Dispatchers.IO) {\n                    KizzyRPC\n                        .getUserInfo(\n                            token,\n                            SuperProperties.userAgent,\n                            SuperProperties.superPropertiesBase64,\n                        ).onSuccess {\n                            discordToken = token\n                            showTokenDialog = false\n                            snackbarHostState.showSnackbar(loginSuccessfulStr)\n                        }.onFailure {\n                            error = \"Invalid token\"\n                            isVerifying = false\n                        }\n                }\n            },\n            singleLine = true,\n            isInputValid = { it.isNotEmpty() },\n            extraContent = {\n                if (isVerifying) {\n                    LinearProgressIndicator(\n                        modifier =\n                            Modifier\n                                .fillMaxWidth()\n                                .padding(bottom = 8.dp),\n                    )\n                }\n                if (error != null) {\n                    Text(\n                        text = error!!,\n                        color = MaterialTheme.colorScheme.error,\n                        style = MaterialTheme.typography.bodySmall,\n                        modifier = Modifier.padding(bottom = 8.dp),\n                    )\n                }\n                InfoLabel(text = stringResource(R.string.token_adv_login_description))\n            },\n        )\n    }\n\n    if (showStatusDialog) {\n        EnumDialog(\n            onDismiss = { showStatusDialog = false },\n            onSelect = { selected ->\n                discordStatus =\n                    when (selected) {\n                        DiscordStatus.IDLE -> \"idle\"\n                        DiscordStatus.DND -> \"dnd\"\n                        DiscordStatus.ONLINE -> \"online\"\n                    }\n                showStatusDialog = false\n            },\n            title = stringResource(R.string.discord_status),\n            current = currentStatus,\n            values = DiscordStatus.entries.toList(),\n            valueText = {\n                when (it) {\n                    DiscordStatus.ONLINE -> stringResource(R.string.discord_status_online)\n                    DiscordStatus.IDLE -> stringResource(R.string.discord_status_idle)\n                    DiscordStatus.DND -> stringResource(R.string.discord_status_dnd)\n                }\n            },\n        )\n    }\n\n    if (showActivityTypeDialog) {\n        EnumDialog(\n            onDismiss = { showActivityTypeDialog = false },\n            onSelect = { selected ->\n                activityType =\n                    when (selected) {\n                        DiscordActivityType.PLAYING -> \"playing\"\n                        DiscordActivityType.WATCHING -> \"watching\"\n                        DiscordActivityType.COMPETING -> \"competing\"\n                        DiscordActivityType.LISTENING -> \"listening\"\n                    }\n                showActivityTypeDialog = false\n            },\n            title = stringResource(R.string.discord_activity_type),\n            current = currentActivityType,\n            values = DiscordActivityType.entries.toList(),\n            valueText = {\n                when (it) {\n                    DiscordActivityType.LISTENING -> stringResource(R.string.discord_activity_listening)\n                    DiscordActivityType.PLAYING -> stringResource(R.string.discord_activity_playing)\n                    DiscordActivityType.WATCHING -> stringResource(R.string.discord_activity_watching)\n                    DiscordActivityType.COMPETING -> stringResource(R.string.discord_activity_competing)\n                }\n            },\n        )\n    }\n\n    if (showButton1TextDialog) {\n        TextFieldDialog(\n            onDismiss = { showButton1TextDialog = false },\n            onDone = {\n                button1Text = it\n                showButton1TextDialog = false\n            },\n            singleLine = true,\n            initialTextFieldValue = TextFieldValue(button1Text),\n            extraContent = {\n                InfoLabel(text = stringResource(R.string.discord_button_text_variables))\n            },\n        )\n    }\n\n    if (showButton2TextDialog) {\n        TextFieldDialog(\n            onDismiss = { showButton2TextDialog = false },\n            onDone = {\n                button2Text = it\n                showButton2TextDialog = false\n            },\n            singleLine = true,\n            initialTextFieldValue = TextFieldValue(button2Text),\n            extraContent = {\n                InfoLabel(text = stringResource(R.string.discord_button_text_variables))\n            },\n        )\n    }\n\n    if (showActivityNameDialog) {\n        TextFieldDialog(\n            onDismiss = { showActivityNameDialog = false },\n            onDone = {\n                activityName = it\n                showActivityNameDialog = false\n            },\n            singleLine = true,\n            initialTextFieldValue = TextFieldValue(activityName),\n            extraContent = {\n                InfoLabel(text = stringResource(R.string.discord_activity_name_description))\n            },\n        )\n    }\n\n    Column(\n        modifier =\n            Modifier\n                .windowInsetsPadding(\n                    LocalPlayerAwareWindowInsets.current.only(\n                        WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom,\n                    ),\n                ).verticalScroll(rememberScrollState())\n                .padding(horizontal = 16.dp),\n    ) {\n        Spacer(\n            Modifier.windowInsetsPadding(\n                LocalPlayerAwareWindowInsets.current.only(WindowInsetsSides.Top),\n            ),\n        )\n\n        // Warning Card\n        AnimatedVisibility(visible = !infoDismissed) {\n            Card(\n                colors =\n                    CardDefaults.cardColors(\n                        containerColor = MaterialTheme.colorScheme.surfaceContainerHighest,\n                    ),\n                modifier =\n                    Modifier\n                        .fillMaxWidth()\n                        .padding(bottom = 16.dp),\n            ) {\n                Row(\n                    modifier = Modifier.padding(16.dp),\n                    verticalAlignment = Alignment.Top,\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.warning),\n                        contentDescription = null,\n                        tint = MaterialTheme.colorScheme.onSurface,\n                        modifier = Modifier.size(24.dp),\n                    )\n                    Spacer(Modifier.width(12.dp))\n                    Column(modifier = Modifier.weight(1f)) {\n                        Text(\n                            text = stringResource(R.string.discord_information_warning),\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.onSurface,\n                        )\n                        Spacer(Modifier.height(8.dp))\n                        TextButton(\n                            onClick = { infoDismissed = true },\n                            modifier = Modifier.align(Alignment.End),\n                        ) {\n                            Text(stringResource(R.string.dismiss))\n                        }\n                    }\n                }\n            }\n        }\n\n        // Profile Card (fully rounded)\n        Card(\n            shape = RoundedCornerShape(28.dp),\n            colors =\n                CardDefaults.cardColors(\n                    containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,\n                ),\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .padding(bottom = 16.dp),\n        ) {\n            Row(\n                modifier =\n                    Modifier\n                        .padding(\n                            start = 20.dp,\n                            end = 20.dp,\n                            top = 20.dp,\n                            bottom = if (isLoggedIn) 20.dp else 8.dp,\n                        ).fillMaxWidth(),\n                verticalAlignment = Alignment.CenterVertically,\n            ) {\n                // Avatar with status dot\n                Box(modifier = Modifier.size(56.dp)) {\n                    if (isLoggedIn && discordAvatar.isNotEmpty()) {\n                        AsyncImage(\n                            model = discordAvatar,\n                            contentDescription = null,\n                            modifier =\n                                Modifier\n                                    .size(56.dp)\n                                    .clip(CircleShape),\n                        )\n                    } else {\n                        Icon(\n                            painter = painterResource(R.drawable.discord),\n                            contentDescription = null,\n                            modifier =\n                                Modifier\n                                    .size(36.dp)\n                                    .align(Alignment.Center)\n                                    .alpha(0.4f),\n                        )\n                    }\n                    if (isLoggedIn) {\n                        val statusColor =\n                            when (discordStatus) {\n                                \"idle\" -> MaterialTheme.colorScheme.tertiary\n                                \"dnd\" -> MaterialTheme.colorScheme.error\n                                else -> MaterialTheme.colorScheme.primary\n                            }\n                        Surface(\n                            color = statusColor,\n                            shape = CircleShape,\n                            modifier =\n                                Modifier\n                                    .size(16.dp)\n                                    .align(Alignment.BottomEnd)\n                                    .border(\n                                        2.dp,\n                                        MaterialTheme.colorScheme.surfaceContainerHigh,\n                                        CircleShape,\n                                    ),\n                            content = {},\n                        )\n                    }\n                }\n\n                Spacer(Modifier.width(16.dp))\n\n                Column(modifier = Modifier.weight(1f)) {\n                    Text(\n                        text =\n                            if (isLoggedIn) {\n                                discordName\n                            } else {\n                                stringResource(R.string.not_logged_in)\n                            },\n                        style = MaterialTheme.typography.titleMedium,\n                        fontWeight = FontWeight.Bold,\n                        modifier = Modifier.alpha(if (isLoggedIn) 1f else 0.5f),\n                    )\n                    if (discordUsername.isNotEmpty()) {\n                        Text(\n                            text = \"@$discordUsername\",\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        )\n                    }\n                    if (!isLoggedIn) {\n                        Text(\n                            text = stringResource(R.string.discord_connect_description),\n                            style = MaterialTheme.typography.bodySmall,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        )\n                    }\n                }\n\n                // Only show logout inline when logged in\n                if (isLoggedIn) {\n                    OutlinedButton(onClick = {\n                        discordName = \"\"\n                        discordToken = \"\"\n                        discordUsername = \"\"\n                        discordAvatar = \"\"\n                    }) {\n                        Text(stringResource(R.string.action_logout))\n                    }\n                }\n            }\n\n            // Login buttons below when not logged in\n            if (!isLoggedIn) {\n                Row(\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(start = 20.dp, end = 20.dp, bottom = 16.dp),\n                    horizontalArrangement = Arrangement.spacedBy(12.dp),\n                ) {\n                    OutlinedButton(\n                        onClick = { navController.navigate(\"settings/discord/login\") },\n                    ) {\n                        Text(stringResource(R.string.action_login))\n                    }\n                    OutlinedButton(\n                        onClick = { showTokenDialog = true },\n                    ) {\n                        Icon(\n                            painterResource(R.drawable.token),\n                            contentDescription = null,\n                            modifier = Modifier.size(18.dp),\n                        )\n                        Spacer(Modifier.width(6.dp))\n                        Text(stringResource(R.string.advanced_login))\n                    }\n                }\n            }\n        }\n\n        // Options section (card-based)\n        Material3SettingsGroup(\n            title = stringResource(R.string.options),\n            items =\n                listOf(\n                    Material3SettingsItem(\n                        title = { Text(stringResource(R.string.enable_discord_rpc)) },\n                        trailingContent = {\n                            Switch(\n                                checked = discordRPC,\n                                onCheckedChange = onDiscordRPCChange,\n                                enabled = isLoggedIn,\n                            )\n                        },\n                        enabled = isLoggedIn,\n                        onClick = { if (isLoggedIn) onDiscordRPCChange(!discordRPC) },\n                    ),\n                    Material3SettingsItem(\n                        title = { Text(stringResource(R.string.discord_use_details)) },\n                        description = {\n                            Text(stringResource(R.string.discord_use_details_description))\n                        },\n                        trailingContent = {\n                            Switch(\n                                checked = useDetails,\n                                onCheckedChange = onUseDetailsChange,\n                                enabled = isLoggedIn && discordRPC,\n                            )\n                        },\n                        enabled = isLoggedIn && discordRPC,\n                        onClick = {\n                            if (isLoggedIn && discordRPC) onUseDetailsChange(!useDetails)\n                        },\n                    ),\n                    Material3SettingsItem(\n                        title = { Text(stringResource(R.string.discord_advanced_mode)) },\n                        description = {\n                            Text(stringResource(R.string.discord_advanced_mode_description))\n                        },\n                        trailingContent = {\n                            Switch(\n                                checked = advancedMode,\n                                onCheckedChange = onAdvancedModeChange,\n                                enabled = isLoggedIn && discordRPC,\n                            )\n                        },\n                        enabled = isLoggedIn && discordRPC,\n                        onClick = {\n                            if (isLoggedIn && discordRPC) onAdvancedModeChange(!advancedMode)\n                        },\n                    ),\n                ),\n        )\n\n        Spacer(Modifier.height(8.dp))\n\n        // Advanced customization section\n        AnimatedVisibility(visible = isLoggedIn && discordRPC && advancedMode) {\n            Column(modifier = Modifier.animateContentSize()) {\n                // Presence settings\n                Material3SettingsGroup(\n                    title = stringResource(R.string.discord_presence),\n                    items =\n                        listOf(\n                            Material3SettingsItem(\n                                title = { Text(stringResource(R.string.discord_status)) },\n                                description = {\n                                    Text(\n                                        when (currentStatus) {\n                                            DiscordStatus.ONLINE -> {\n                                                stringResource(R.string.discord_status_online)\n                                            }\n\n                                            DiscordStatus.IDLE -> {\n                                                stringResource(R.string.discord_status_idle)\n                                            }\n\n                                            DiscordStatus.DND -> {\n                                                stringResource(R.string.discord_status_dnd)\n                                            }\n                                        },\n                                    )\n                                },\n                                onClick = { showStatusDialog = true },\n                            ),\n                            Material3SettingsItem(\n                                title = { Text(stringResource(R.string.discord_activity_type)) },\n                                description = {\n                                    Text(\n                                        when (currentActivityType) {\n                                            DiscordActivityType.LISTENING -> {\n                                                stringResource(R.string.discord_activity_listening)\n                                            }\n\n                                            DiscordActivityType.PLAYING -> {\n                                                stringResource(R.string.discord_activity_playing)\n                                            }\n\n                                            DiscordActivityType.WATCHING -> {\n                                                stringResource(R.string.discord_activity_watching)\n                                            }\n\n                                            DiscordActivityType.COMPETING -> {\n                                                stringResource(R.string.discord_activity_competing)\n                                            }\n                                        },\n                                    )\n                                },\n                                onClick = { showActivityTypeDialog = true },\n                            ),\n                            Material3SettingsItem(\n                                title = { Text(stringResource(R.string.discord_activity_name)) },\n                                description = {\n                                    Text(\n                                        activityName.ifEmpty {\n                                            stringResource(R.string.discord_activity_name_description)\n                                        },\n                                    )\n                                },\n                                onClick = { showActivityNameDialog = true },\n                            ),\n                        ),\n                )\n\n                Spacer(Modifier.height(8.dp))\n\n                // Button customization\n                Material3SettingsGroup(\n                    title = stringResource(R.string.discord_buttons),\n                    items =\n                        listOf(\n                            Material3SettingsItem(\n                                title = { Text(stringResource(R.string.discord_button_1)) },\n                                description = {\n                                    Text(button1Text.ifEmpty { \"Listen on YouTube Music\" })\n                                },\n                                trailingContent = {\n                                    Switch(\n                                        checked = button1Visible,\n                                        onCheckedChange = { button1Visible = it },\n                                    )\n                                },\n                                onClick = { showButton1TextDialog = true },\n                            ),\n                            Material3SettingsItem(\n                                title = { Text(stringResource(R.string.discord_button_2)) },\n                                description = {\n                                    Text(button2Text.ifEmpty { \"Visit Metrolist\" })\n                                },\n                                trailingContent = {\n                                    Switch(\n                                        checked = button2Visible,\n                                        onCheckedChange = { button2Visible = it },\n                                    )\n                                },\n                                onClick = { showButton2TextDialog = true },\n                            ),\n                        ),\n                )\n\n                // Variable hint\n                Card(\n                    colors =\n                        CardDefaults.cardColors(\n                            containerColor = MaterialTheme.colorScheme.secondaryContainer,\n                        ),\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .padding(top = 8.dp),\n                ) {\n                    Row(\n                        modifier = Modifier.padding(12.dp),\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        Icon(\n                            painter = painterResource(R.drawable.info),\n                            contentDescription = null,\n                            tint = MaterialTheme.colorScheme.onSecondaryContainer,\n                            modifier = Modifier.size(20.dp),\n                        )\n                        Spacer(Modifier.width(8.dp))\n                        Text(\n                            text = stringResource(R.string.discord_button_text_variables),\n                            style = MaterialTheme.typography.bodySmall,\n                            color = MaterialTheme.colorScheme.onSecondaryContainer,\n                        )\n                    }\n                }\n\n                Spacer(Modifier.height(8.dp))\n            }\n        }\n\n        // Preview section\n        Spacer(Modifier.height(8.dp))\n\n        Text(\n            text = stringResource(R.string.discord_rpc_preview),\n            style = MaterialTheme.typography.labelLarge,\n            color = MaterialTheme.colorScheme.primary,\n            modifier = Modifier.padding(bottom = 8.dp, top = 8.dp),\n        )\n\n        RichPresence(\n            song = song,\n            currentPlaybackTimeMillis = position,\n            activityType = activityType,\n            activityName = activityName,\n            button1Text = button1Text,\n            button1Visible = button1Visible,\n            button2Text = button2Text,\n            button2Visible = button2Visible,\n        )\n\n        // Bottom padding for mini player\n        Spacer(Modifier.height(24.dp))\n    }\n\n    TopAppBar(\n        title = { Text(stringResource(R.string.discord_integration)) },\n        navigationIcon = {\n            IconButton(\n                onClick = navController::navigateUp,\n                onLongClick = navController::backToMain,\n            ) {\n                Icon(\n                    painterResource(R.drawable.arrow_back),\n                    contentDescription = null,\n                )\n            }\n        },\n    )\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun RichPresence(\n    song: Song?,\n    currentPlaybackTimeMillis: Long = 0L,\n    activityType: String = \"listening\",\n    activityName: String = \"\",\n    button1Text: String = \"\",\n    button1Visible: Boolean = true,\n    button2Text: String = \"\",\n    button2Visible: Boolean = true,\n) {\n    val context = LocalContext.current\n\n    val activityLabel =\n        when (activityType) {\n            \"playing\" -> stringResource(R.string.discord_playing_metrolist)\n            \"watching\" -> stringResource(R.string.discord_watching_metrolist)\n            \"competing\" -> stringResource(R.string.discord_competing_metrolist)\n            else -> stringResource(R.string.listening_to_metrolist)\n        }\n\n    Surface(\n        color = MaterialTheme.colorScheme.surfaceContainer,\n        shape = MaterialTheme.shapes.medium,\n        shadowElevation = 6.dp,\n        modifier = Modifier.fillMaxWidth(),\n    ) {\n        Column(\n            modifier = Modifier.padding(16.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n        ) {\n            Text(\n                text = if (activityName.isNotEmpty()) activityName else activityLabel,\n                style = MaterialTheme.typography.labelLarge,\n                textAlign = TextAlign.Start,\n                fontWeight = FontWeight.ExtraBold,\n                modifier = Modifier.fillMaxWidth(),\n            )\n\n            Spacer(Modifier.height(16.dp))\n\n            Row(verticalAlignment = Alignment.Top) {\n                Box(Modifier.size(108.dp)) {\n                    AsyncImage(\n                        model = song?.song?.thumbnailUrl,\n                        contentDescription = null,\n                        modifier =\n                            Modifier\n                                .size(96.dp)\n                                .clip(RoundedCornerShape(3.dp))\n                                .align(Alignment.TopStart)\n                                .run {\n                                    if (song == null) {\n                                        border(\n                                            2.dp,\n                                            MaterialTheme.colorScheme.onSurface,\n                                            RoundedCornerShape(3.dp),\n                                        )\n                                    } else {\n                                        this\n                                    }\n                                },\n                    )\n\n                    song?.artists?.firstOrNull()?.thumbnailUrl?.let {\n                        Box(\n                            modifier =\n                                Modifier\n                                    .border(\n                                        2.dp,\n                                        MaterialTheme.colorScheme.surfaceContainer,\n                                        CircleShape,\n                                    ).padding(2.dp)\n                                    .align(Alignment.BottomEnd),\n                        ) {\n                            AsyncImage(\n                                model = it,\n                                contentDescription = null,\n                                modifier =\n                                    Modifier\n                                        .size(32.dp)\n                                        .clip(CircleShape),\n                            )\n                        }\n                    }\n                }\n\n                Column(\n                    modifier =\n                        Modifier\n                            .weight(1f)\n                            .padding(horizontal = 6.dp),\n                ) {\n                    Text(\n                        text = song?.song?.title ?: \"Song Title\",\n                        color = MaterialTheme.colorScheme.onSurface,\n                        fontSize = 20.sp,\n                        fontWeight = FontWeight.ExtraBold,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis,\n                    )\n\n                    Text(\n                        text = song?.artists?.joinToString { it.name } ?: \"Artist\",\n                        color = MaterialTheme.colorScheme.secondary,\n                        fontSize = 16.sp,\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis,\n                    )\n\n                    song?.album?.title?.let {\n                        Text(\n                            text = it,\n                            color = MaterialTheme.colorScheme.secondary,\n                            fontSize = 16.sp,\n                            maxLines = 1,\n                            overflow = TextOverflow.Ellipsis,\n                        )\n                    }\n\n                    if (song != null) {\n                        SongProgressBar(\n                            currentTimeMillis = currentPlaybackTimeMillis,\n                            durationMillis = song.song.duration.times(1000L),\n                        )\n                    }\n                }\n            }\n\n            Spacer(modifier = Modifier.height(16.dp))\n\n            if (button1Visible) {\n                val resolvedButton1 =\n                    if (song != null) {\n                        DiscordRPC.resolveVariables(\n                            button1Text.ifEmpty { \"Listen on YouTube Music\" },\n                            song,\n                        )\n                    } else {\n                        button1Text.ifEmpty { \"Listen on YouTube Music\" }\n                    }\n                OutlinedButton(\n                    enabled = song != null,\n                    onClick = {\n                        val intent =\n                            Intent(\n                                Intent.ACTION_VIEW,\n                                \"https://music.youtube.com/watch?v=${song?.id}\".toUri(),\n                            )\n                        context.startActivity(intent)\n                    },\n                    modifier = Modifier.fillMaxWidth(),\n                ) {\n                    Text(resolvedButton1)\n                }\n            }\n\n            if (button2Visible) {\n                val resolvedButton2 =\n                    if (song != null) {\n                        DiscordRPC.resolveVariables(\n                            button2Text.ifEmpty { \"Visit Metrolist\" },\n                            song,\n                        )\n                    } else {\n                        button2Text.ifEmpty { \"Visit Metrolist\" }\n                    }\n                OutlinedButton(\n                    onClick = {\n                        val intent =\n                            Intent(\n                                Intent.ACTION_VIEW,\n                                \"https://github.com/MetrolistGroup/Metrolist\".toUri(),\n                            )\n                        context.startActivity(intent)\n                    },\n                    modifier = Modifier.fillMaxWidth(),\n                ) {\n                    Text(resolvedButton2)\n                }\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun SongProgressBar(\n    currentTimeMillis: Long,\n    durationMillis: Long,\n) {\n    val progress = if (durationMillis > 0) currentTimeMillis.toFloat() / durationMillis else 0f\n\n    Column(modifier = Modifier.fillMaxWidth()) {\n        Spacer(modifier = Modifier.height(16.dp))\n\n        LinearWavyProgressIndicator(\n            progress = { progress },\n            amplitude = { 1f },\n            wavelength = 16.dp,\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .height(6.dp),\n        )\n        Row(\n            modifier = Modifier.fillMaxWidth(),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            Text(\n                text = makeTimeString(currentTimeMillis),\n                modifier = Modifier.weight(1f),\n                textAlign = TextAlign.Start,\n                fontSize = 12.sp,\n            )\n            Text(\n                text = makeTimeString(durationMillis),\n                modifier = Modifier.weight(1f),\n                textAlign = TextAlign.End,\n                fontSize = 12.sp,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/settings/integrations/IntegrationScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.settings.integrations\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.navigation.NavController\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.IntegrationCard\nimport com.metrolist.music.ui.component.IntegrationCardItem\nimport com.metrolist.music.ui.utils.backToMain\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun IntegrationScreen(\n    navController: NavController\n) {\n    Column(\n        Modifier\n            .windowInsetsPadding(LocalPlayerAwareWindowInsets.current)\n            .verticalScroll(rememberScrollState())\n            .padding(horizontal = 16.dp),\n    ) {\n        IntegrationCard(\n            title = stringResource(R.string.general),\n            items = listOf(\n                IntegrationCardItem(\n                    icon = painterResource(R.drawable.discord),\n                    title = { Text(stringResource(R.string.discord_integration)) },\n                    onClick = {\n                        navController.navigate(\"settings/integrations/discord\")\n                    }\n                ),\n                IntegrationCardItem(\n                    icon = painterResource(R.drawable.music_note),\n                    title = { Text(stringResource(R.string.lastfm_integration)) },\n                    onClick = {\n                        navController.navigate(\"settings/integrations/lastfm\")\n                    }\n                )\n            )\n        )\n    }\n\n    TopAppBar(\n        title = { Text(stringResource(R.string.integrations)) },\n        navigationIcon = {\n            IconButton(\n                onClick = navController::navigateUp,\n                onLongClick = navController::backToMain,\n            ) {\n                Icon(\n                    painterResource(R.drawable.arrow_back),\n                    contentDescription = null,\n                )\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/settings/integrations/LastFMSettings.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.settings.integrations\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedButton\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Slider\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.text.input.PasswordVisualTransformation\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.DialogProperties\nimport androidx.navigation.NavController\nimport com.metrolist.lastfm.LastFM\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.EnableLastFMScrobblingKey\nimport com.metrolist.music.constants.LastFMSessionKey\nimport com.metrolist.music.constants.LastFMUseNowPlaying\nimport com.metrolist.music.constants.LastFMUseSendLikes\nimport com.metrolist.music.constants.LastFMUsernameKey\nimport com.metrolist.music.constants.ScrobbleDelayPercentKey\nimport com.metrolist.music.constants.ScrobbleDelaySecondsKey\nimport com.metrolist.music.constants.ScrobbleMinSongDurationKey\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.Material3SettingsGroup\nimport com.metrolist.music.ui.component.Material3SettingsItem\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.makeTimeString\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.utils.reportException\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlin.math.roundToInt\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun LastFMSettings(\n    navController: NavController\n) {\n    val coroutineScope = rememberCoroutineScope()\n\n    var lastfmUsername by rememberPreference(LastFMUsernameKey, \"\")\n    var lastfmSession by rememberPreference(LastFMSessionKey, \"\")\n\n    val isLoggedIn =\n        remember(lastfmSession) {\n            lastfmSession != \"\"\n        }\n\n    val (useNowPlaying, onUseNowPlayingChange) = rememberPreference(\n        key = LastFMUseNowPlaying,\n        defaultValue = false\n    )\n\n    val (useSendLikes, onUseSendLikes) = rememberPreference(\n        key = LastFMUseSendLikes,\n        defaultValue = false\n    )\n\n    val (lastfmScrobbling, onlastfmScrobblingChange) = rememberPreference(\n        key = EnableLastFMScrobblingKey,\n        defaultValue = false\n    )\n\n    val (scrobbleDelayPercent, onScrobbleDelayPercentChange) = rememberPreference(\n        ScrobbleDelayPercentKey,\n        defaultValue = LastFM.DEFAULT_SCROBBLE_DELAY_PERCENT\n    )\n\n    val (minTrackDuration, onMinTrackDurationChange) = rememberPreference(\n        ScrobbleMinSongDurationKey,\n        defaultValue = LastFM.DEFAULT_SCROBBLE_MIN_SONG_DURATION\n    )\n\n    val (scrobbleDelaySeconds, onScrobbleDelaySecondsChange) = rememberPreference(\n        ScrobbleDelaySecondsKey,\n        defaultValue = LastFM.DEFAULT_SCROBBLE_DELAY_SECONDS\n    )\n\n    var showLoginDialog by rememberSaveable { mutableStateOf(false) }\n    var isLoggingIn by rememberSaveable { mutableStateOf(false) }\n    var loginError by rememberSaveable { mutableStateOf<String?>(null) }\n\n    if (showLoginDialog) {\n        var tempUsername by rememberSaveable { mutableStateOf(\"\") }\n        var tempPassword by rememberSaveable { mutableStateOf(\"\") }\n\n        AlertDialog(\n            properties = DialogProperties(usePlatformDefaultWidth = false),\n            onDismissRequest = {\n                if (!isLoggingIn) {\n                    showLoginDialog = false\n                    loginError = null\n                }\n            },\n            title = { Text(stringResource(R.string.login)) },\n            text = {\n                Column(\n                    modifier = Modifier\n                        .verticalScroll(rememberScrollState()),\n                    verticalArrangement = Arrangement.spacedBy(12.dp)\n                ) {\n                    OutlinedTextField(\n                        value = tempUsername,\n                        onValueChange = {\n                            tempUsername = it\n                            loginError = null\n                        },\n                        label = { Text(stringResource(R.string.username)) },\n                        singleLine = true,\n                        enabled = !isLoggingIn,\n                    )\n                    OutlinedTextField(\n                        value = tempPassword,\n                        onValueChange = {\n                            tempPassword = it\n                            loginError = null\n                        },\n                        label = { Text(stringResource(R.string.password)) },\n                        singleLine = true,\n                        visualTransformation = PasswordVisualTransformation(),\n                        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),\n                        enabled = !isLoggingIn,\n                    )\n\n                    // Show error message if login failed\n                    loginError?.let { error ->\n                        Text(\n                            text = error,\n                            color = MaterialTheme.colorScheme.error,\n                            style = MaterialTheme.typography.bodySmall,\n                            modifier = Modifier.padding(top = 8.dp)\n                        )\n                    }\n\n                    // Show loading indicator\n                    if (isLoggingIn) {\n                        Row(\n                            modifier = Modifier.fillMaxWidth(),\n                            horizontalArrangement = Arrangement.Center,\n                            verticalAlignment = Alignment.CenterVertically\n                        ) {\n                            CircularProgressIndicator(\n                                modifier = Modifier.size(16.dp)\n                            )\n                            Spacer(modifier = Modifier.size(8.dp))\n                            Text(\n                                text = stringResource(R.string.logging_in),\n                                style = MaterialTheme.typography.bodySmall\n                            )\n                        }\n                    }\n                }\n            },\n            confirmButton = {\n                TextButton(\n                    onClick = {\n                        if (tempUsername.isBlank() || tempPassword.isBlank()) {\n                            loginError = \"Please enter both username and password\"\n                            return@TextButton\n                        }\n\n                        isLoggingIn = true\n                        loginError = null\n\n                        coroutineScope.launch(Dispatchers.IO) {\n                            try {\n                                LastFM.getMobileSession(tempUsername, tempPassword)\n                                    .onSuccess { auth ->\n                                        lastfmUsername = auth.session.name\n                                        lastfmSession = auth.session.key\n                                        LastFM.sessionKey = auth.session.key\n\n                                        // Switch back to main thread to update UI\n                                        coroutineScope.launch(Dispatchers.Main) {\n                                            isLoggingIn = false\n                                            showLoginDialog = false\n                                            loginError = null\n                                        }\n                                    }\n                                    .onFailure { exception ->\n                                        coroutineScope.launch(Dispatchers.Main) {\n                                            isLoggingIn = false\n                                            loginError = when (exception) {\n                                                is LastFM.LastFmException -> {\n                                                    when (exception.code) {\n                                                        4 -> \"Invalid username or password\"\n                                                        6 -> \"Invalid parameters\"\n                                                        9 -> \"Invalid session key\"\n                                                        10 -> \"Invalid API key\"\n                                                        13 -> \"Invalid method signature\"\n                                                        14 -> \"Unauthorized token\"\n                                                        15 -> \"Service temporarily unavailable\"\n                                                        else -> \"Login failed: ${exception.message}\"\n                                                    }\n                                                }\n                                                else -> \"Network error. Please check your connection.\"\n                                            }\n                                        }\n                                        reportException(exception)\n                                    }\n                            } catch (e: Exception) {\n                                coroutineScope.launch(Dispatchers.Main) {\n                                    isLoggingIn = false\n                                    loginError = \"Unexpected error occurred\"\n                                }\n                                reportException(e)\n                            }\n                        }\n                    },\n                    enabled = !isLoggingIn\n                ) {\n                    Text(stringResource(R.string.login))\n                }\n            },\n            dismissButton = {\n                TextButton(\n                    onClick = {\n                        if (!isLoggingIn) {\n                            showLoginDialog = false\n                            loginError = null\n                        }\n                    },\n                    enabled = !isLoggingIn\n                ) {\n                    Text(stringResource(R.string.cancel))\n                }\n            }\n        )\n    }\n\n    Column(\n        modifier = Modifier\n            .windowInsetsPadding(\n                LocalPlayerAwareWindowInsets.current.only(\n                    WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom\n                )\n            )\n            .verticalScroll(rememberScrollState())\n            .padding(horizontal = 16.dp)\n    ) {\n        Spacer(\n            Modifier.windowInsetsPadding(\n                LocalPlayerAwareWindowInsets.current.only(\n                    WindowInsetsSides.Top\n                )\n            )\n        )\n\n        // Options section (card-based)\n        Material3SettingsGroup(\n            title = stringResource(R.string.account),\n            items = listOf(\n                Material3SettingsItem(\n                    title = {\n                        Text(\n                            text = if (isLoggedIn) lastfmUsername else stringResource(R.string.not_logged_in),\n                            modifier = Modifier.alpha(if (isLoggedIn) 1f else 0.5f),\n                        )\n                    },\n                    trailingContent = {\n                        if (isLoggedIn) {\n                            OutlinedButton(onClick = {\n                                lastfmSession = \"\"\n                                lastfmUsername = \"\"\n                            }) {\n                                Text(stringResource(R.string.action_logout))\n                            }\n                        } else {\n                            OutlinedButton(onClick = { showLoginDialog = true }) {\n                                Text(stringResource(R.string.action_login))\n                            }\n                        }\n                    },\n                    icon = painterResource(R.drawable.music_note)\n                ),\n            )\n        )\n\n        Spacer(Modifier.height(8.dp))\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.options),\n            items = listOf(\n                Material3SettingsItem(\n                    title = { Text(stringResource(R.string.enable_scrobbling)) },\n                    trailingContent = {\n                        Switch(\n                            checked = lastfmScrobbling,\n                            onCheckedChange = onlastfmScrobblingChange,\n                            enabled = isLoggedIn,\n                        )\n                    },\n                    enabled = isLoggedIn,\n                    icon = painterResource(R.drawable.queue_music)\n                ),\n                Material3SettingsItem(\n                    title = { Text(stringResource(R.string.lastfm_now_playing)) },\n                    trailingContent = {\n                        Switch(\n                            checked = useNowPlaying,\n                            onCheckedChange = onUseNowPlayingChange,\n                            enabled = isLoggedIn && lastfmScrobbling,\n                        )\n                    },\n                    enabled = isLoggedIn && lastfmScrobbling,\n                    icon = painterResource(R.drawable.play)\n                ),\n                Material3SettingsItem(\n                    title = { Text(stringResource(R.string.last_fm_send_likes)) },\n                    description = { stringResource(R.string.last_fm_send_likes_description) },\n                    trailingContent = {\n                        Switch(\n                            checked = useSendLikes,\n                            onCheckedChange = onUseSendLikes,\n                            enabled = isLoggedIn,\n                        )\n                    },\n                    enabled = isLoggedIn,\n                    icon = painterResource(R.drawable.media3_icon_thumb_up_unfilled)\n                )\n            )\n        )\n\n        var showMinTrackDurationDialog by rememberSaveable { mutableStateOf(false) }\n\n        if (showMinTrackDurationDialog) {\n            var tempMinTrackDuration by remember { mutableIntStateOf(minTrackDuration) }\n\n            DefaultDialog(\n                onDismiss = {\n                    tempMinTrackDuration = minTrackDuration\n                    showMinTrackDurationDialog = false\n                },\n                buttons = {\n                    TextButton(\n                        onClick = {\n                            tempMinTrackDuration = LastFM.DEFAULT_SCROBBLE_MIN_SONG_DURATION\n                        }\n                    ) {\n                        Text(stringResource(R.string.reset))\n                    }\n\n                    Spacer(modifier = Modifier.weight(1f))\n\n                    TextButton(\n                        onClick = {\n                            tempMinTrackDuration = minTrackDuration\n                            showMinTrackDurationDialog = false\n                        }\n                    ) {\n                        Text(stringResource(android.R.string.cancel))\n                    }\n                    TextButton(\n                        onClick = {\n                            onMinTrackDurationChange(tempMinTrackDuration)\n                            showMinTrackDurationDialog = false\n                        }\n                    ) {\n                        Text(stringResource(android.R.string.ok))\n                    }\n                }\n            ) {\n                Column(\n                    horizontalAlignment = Alignment.CenterHorizontally,\n                    modifier = Modifier.padding(16.dp)\n                ) {\n                    Text(\n                        text = stringResource(R.string.scrobble_min_track_duration),\n                        style = MaterialTheme.typography.headlineSmall,\n                        modifier = Modifier.padding(bottom = 16.dp)\n                    )\n\n                    Text(\n                        text = makeTimeString((tempMinTrackDuration * 1000).toLong()),\n                        style = MaterialTheme.typography.bodyLarge,\n                        modifier = Modifier.padding(bottom = 16.dp)\n                    )\n\n                    Slider(\n                        value = tempMinTrackDuration.toFloat(),\n                        onValueChange = { tempMinTrackDuration = it.toInt() },\n                        valueRange = 10f..60f,\n                        modifier = Modifier.fillMaxWidth()\n                    )\n                }\n            }\n        }\n\n        var showScrobbleDelayPercentDialog by rememberSaveable { mutableStateOf(false) }\n\n        if (showScrobbleDelayPercentDialog) {\n            var tempScrobbleDelayPercent by remember { mutableFloatStateOf(scrobbleDelayPercent) }\n\n            DefaultDialog(\n                onDismiss = {\n                    tempScrobbleDelayPercent = scrobbleDelayPercent\n                    showScrobbleDelayPercentDialog = false\n                },\n                buttons = {\n                    TextButton(\n                        onClick = {\n                            tempScrobbleDelayPercent = LastFM.DEFAULT_SCROBBLE_DELAY_PERCENT\n                        }\n                    ) {\n                        Text(stringResource(R.string.reset))\n                    }\n\n                    Spacer(modifier = Modifier.weight(1f))\n\n                    TextButton(\n                        onClick = {\n                            tempScrobbleDelayPercent = scrobbleDelayPercent\n                            showScrobbleDelayPercentDialog = false\n                        }\n                    ) {\n                        Text(stringResource(android.R.string.cancel))\n                    }\n                    TextButton(\n                        onClick = {\n                            onScrobbleDelayPercentChange(tempScrobbleDelayPercent)\n                            showScrobbleDelayPercentDialog = false\n                        }\n                    ) {\n                        Text(stringResource(android.R.string.ok))\n                    }\n                }\n            ) {\n                Column(\n                    horizontalAlignment = Alignment.CenterHorizontally,\n                    modifier = Modifier.padding(16.dp)\n                ) {\n                    Text(\n                        text = stringResource(R.string.scrobble_delay_percent),\n                        style = MaterialTheme.typography.headlineSmall,\n                        modifier = Modifier.padding(bottom = 16.dp)\n                    )\n\n                    Text(\n                        text = stringResource(R.string.sensitivity_percentage, (tempScrobbleDelayPercent * 100).roundToInt()),\n                        style = MaterialTheme.typography.bodyLarge,\n                        modifier = Modifier.padding(bottom = 16.dp)\n                    )\n\n                    Slider(\n                        value = tempScrobbleDelayPercent,\n                        onValueChange = { tempScrobbleDelayPercent = it },\n                        valueRange = 0.3f..0.95f,\n                        modifier = Modifier.fillMaxWidth()\n                    )\n                }\n            }\n        }\n\n        var showScrobbleDelaySecondsDialog by rememberSaveable { mutableStateOf(false) }\n\n        if (showScrobbleDelaySecondsDialog) {\n            var tempScrobbleDelaySeconds by remember { mutableIntStateOf(scrobbleDelaySeconds) }\n\n            DefaultDialog(\n                onDismiss = {\n                    tempScrobbleDelaySeconds = scrobbleDelaySeconds\n                    showScrobbleDelaySecondsDialog = false\n                },\n                buttons = {\n                    TextButton(\n                        onClick = {\n                            tempScrobbleDelaySeconds = LastFM.DEFAULT_SCROBBLE_DELAY_SECONDS\n                        }\n                    ) {\n                        Text(stringResource(R.string.reset))\n                    }\n\n                    Spacer(modifier = Modifier.weight(1f))\n\n                    TextButton(\n                        onClick = {\n                            tempScrobbleDelaySeconds = scrobbleDelaySeconds\n                            showScrobbleDelaySecondsDialog = false\n                        }\n                    ) {\n                        Text(stringResource(android.R.string.cancel))\n                    }\n                    TextButton(\n                        onClick = {\n                            onScrobbleDelaySecondsChange(tempScrobbleDelaySeconds)\n                            showScrobbleDelaySecondsDialog = false\n                        }\n                    ) {\n                        Text(stringResource(android.R.string.ok))\n                    }\n                }\n            ) {\n                Column(\n                    horizontalAlignment = Alignment.CenterHorizontally,\n                    modifier = Modifier.padding(16.dp)\n                ) {\n                    Text(\n                        text = stringResource(R.string.scrobble_delay_minutes),\n                        style = MaterialTheme.typography.headlineSmall,\n                        modifier = Modifier.padding(bottom = 16.dp)\n                    )\n\n                    Text(\n                        text = makeTimeString((tempScrobbleDelaySeconds * 1000).toLong()),\n                        style = MaterialTheme.typography.bodyLarge,\n                        modifier = Modifier.padding(bottom = 16.dp)\n                    )\n\n                    Slider(\n                        value = tempScrobbleDelaySeconds.toFloat(),\n                        onValueChange = { tempScrobbleDelaySeconds = it.toInt() },\n                        valueRange = 30f..360f,\n                        modifier = Modifier.fillMaxWidth()\n                    )\n                }\n            }\n        }\n\n        Spacer(Modifier.height(8.dp))\n\n        Material3SettingsGroup(\n            title = stringResource(R.string.scrobbling_configuration),\n            items = listOf(\n                Material3SettingsItem(\n                    title = { Text(stringResource(R.string.scrobble_min_track_duration)) },\n                    description = { Text(makeTimeString((minTrackDuration * 1000).toLong())) },\n                    onClick = { showMinTrackDurationDialog = true },\n                    icon = painterResource(R.drawable.timer)\n                ),\n                Material3SettingsItem(\n                    title = { Text(stringResource(R.string.scrobble_delay_percent)) },\n                    description = { Text(stringResource(R.string.sensitivity_percentage, (scrobbleDelayPercent * 100).roundToInt())) },\n                    onClick = { showScrobbleDelayPercentDialog = true },\n                    icon = painterResource(R.drawable.timer)\n                ),\n                Material3SettingsItem(\n                    title = { Text(stringResource(R.string.scrobble_delay_minutes)) },\n                    description = { Text(makeTimeString((scrobbleDelaySeconds * 1000).toLong())) },\n                    onClick = { showScrobbleDelaySecondsDialog = true },\n                    icon = painterResource(R.drawable.timer)\n                ),\n            )\n        )\n    }\n\n    TopAppBar(\n        title = { Text(stringResource(R.string.lastfm_integration)) },\n        navigationIcon = {\n            IconButton(\n                onClick = navController::navigateUp,\n                onLongClick = navController::backToMain,\n            ) {\n                Icon(\n                    painterResource(R.drawable.arrow_back),\n                    contentDescription = null,\n                )\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/settings/integrations/ListenTogetherSettings.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.settings.integrations\n\nimport android.widget.Toast\nimport androidx.compose.animation.animateContentSize\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.animation.core.spring\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.horizontalScroll\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LinearProgressIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.OutlinedButton\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.SwitchDefaults\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel\nimport androidx.navigation.NavController\nimport com.metrolist.music.LocalPlayerAwareWindowInsets\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.ListenTogetherAutoApprovalKey\nimport com.metrolist.music.constants.ListenTogetherAutoApproveSuggestionsKey\nimport com.metrolist.music.constants.ListenTogetherServerUrlKey\nimport com.metrolist.music.constants.ListenTogetherSyncVolumeKey\nimport com.metrolist.music.constants.ListenTogetherUsernameKey\nimport com.metrolist.music.listentogether.ConnectionState\nimport com.metrolist.music.listentogether.ListenTogetherEvent\nimport com.metrolist.music.listentogether.ListenTogetherServer\nimport com.metrolist.music.listentogether.ListenTogetherServers\nimport com.metrolist.music.listentogether.LogEntry\nimport com.metrolist.music.listentogether.LogLevel\nimport com.metrolist.music.listentogether.RoomRole\nimport com.metrolist.music.ui.component.DefaultDialog\nimport com.metrolist.music.ui.component.IconButton\nimport com.metrolist.music.ui.component.IntegrationCard\nimport com.metrolist.music.ui.component.IntegrationCardItem\nimport com.metrolist.music.ui.utils.backToMain\nimport com.metrolist.music.utils.rememberPreference\nimport com.metrolist.music.viewmodels.ListenTogetherViewModel\nimport kotlinx.coroutines.flow.collectLatest\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun ListenTogetherSettings(\n    navController: NavController,\n    viewModel: ListenTogetherViewModel = hiltViewModel(),\n) {\n    val context = LocalContext.current\n    val cannotEditUsernameInRoomStr = stringResource(R.string.listen_together_cannot_edit_username_in_room)\n    val coroutineScope = rememberCoroutineScope()\n\n    val connectionState by viewModel.connectionState.collectAsState()\n    val roomState by viewModel.roomState.collectAsState()\n    val role by viewModel.role.collectAsState()\n    val pendingJoinRequests by viewModel.pendingJoinRequests.collectAsState()\n    val logs by viewModel.logs.collectAsState()\n    val blockedUsernames by viewModel.blockedUsernames.collectAsState()\n\n    val servers = remember { ListenTogetherServers.servers }\n    var serverUrl by rememberPreference(ListenTogetherServerUrlKey, ListenTogetherServers.defaultServerUrl)\n    var username by rememberPreference(ListenTogetherUsernameKey, \"\")\n    var autoApprovalJoins by rememberPreference(ListenTogetherAutoApprovalKey, false)\n    var autoApproveSuggestions by rememberPreference(ListenTogetherAutoApproveSuggestionsKey, false)\n    var syncHostVolume by rememberPreference(ListenTogetherSyncVolumeKey, true)\n\n    var showServerUrlDialog by rememberSaveable { mutableStateOf(false) }\n    var showUsernameDialog by rememberSaveable { mutableStateOf(false) }\n    var showCreateRoomDialog by rememberSaveable { mutableStateOf(false) }\n    var showJoinRoomDialog by rememberSaveable { mutableStateOf(false) }\n    var showLogsDialog by rememberSaveable { mutableStateOf(false) }\n    var showBlockedUsersDialog by rememberSaveable { mutableStateOf(false) }\n    var roomCodeInput by rememberSaveable { mutableStateOf(\"\") }\n\n    // Handle events\n    LaunchedEffect(Unit) {\n        viewModel.events.collectLatest { event ->\n            when (event) {\n                is ListenTogetherEvent.RoomCreated -> {\n                    // Room created toast is shown globally by the client\n                }\n\n                is ListenTogetherEvent.JoinApproved -> {\n                    Toast.makeText(context, \"Joined room: ${event.roomCode}\", Toast.LENGTH_SHORT).show()\n                }\n\n                is ListenTogetherEvent.JoinRejected -> {\n                    Toast.makeText(context, \"Join rejected: ${event.reason}\", Toast.LENGTH_SHORT).show()\n                }\n\n                is ListenTogetherEvent.JoinRequestReceived -> {\n                    Toast.makeText(context, \"${event.username} wants to join\", Toast.LENGTH_SHORT).show()\n                }\n\n                is ListenTogetherEvent.Kicked -> {\n                    Toast.makeText(context, \"Kicked: ${event.reason}\", Toast.LENGTH_SHORT).show()\n                }\n\n                is ListenTogetherEvent.ConnectionError -> {\n                    Toast.makeText(context, \"Connection error: ${event.error}\", Toast.LENGTH_SHORT).show()\n                }\n\n                is ListenTogetherEvent.ServerError -> {\n                    Toast.makeText(context, \"Error: ${event.message}\", Toast.LENGTH_SHORT).show()\n                }\n\n                else -> {}\n            }\n        }\n    }\n\n    // Dialogs\n    if (showServerUrlDialog) {\n        ServerChooserDialog(\n            servers = servers,\n            currentUrl = serverUrl,\n            onSelect = { server ->\n                serverUrl = server.url\n                showServerUrlDialog = false\n            },\n            onUseCustom = { customUrl ->\n                serverUrl = customUrl\n                showServerUrlDialog = false\n            },\n            onDismiss = { showServerUrlDialog = false },\n        )\n    }\n\n    if (showUsernameDialog) {\n        var tempUsername by rememberSaveable(showUsernameDialog) { mutableStateOf(username) }\n\n        DefaultDialog(\n            onDismiss = { showUsernameDialog = false },\n            icon = { Icon(painterResource(R.drawable.person), contentDescription = null) },\n            title = { Text(stringResource(R.string.listen_together_username)) },\n            buttons = {\n                TextButton(onClick = {\n                    username = \"\"\n                    showUsernameDialog = false\n                }) {\n                    Text(stringResource(R.string.reset))\n                }\n                Spacer(modifier = Modifier.width(8.dp))\n                Button(onClick = {\n                    username = tempUsername.trim()\n                    showUsernameDialog = false\n                }) {\n                    Text(stringResource(android.R.string.ok))\n                }\n            },\n        ) {\n            OutlinedTextField(\n                value = tempUsername,\n                onValueChange = { tempUsername = it },\n                label = { Text(stringResource(R.string.listen_together_username)) },\n                leadingIcon = {\n                    Icon(painterResource(R.drawable.person), contentDescription = null)\n                },\n                trailingIcon = {\n                    if (tempUsername.isNotBlank()) {\n                        IconButton(onClick = { tempUsername = \"\" }, onLongClick = {}) {\n                            Icon(painterResource(R.drawable.close), contentDescription = null)\n                        }\n                    }\n                },\n                singleLine = true,\n                modifier = Modifier.fillMaxWidth(),\n            )\n        }\n    }\n\n    if (showCreateRoomDialog) {\n        var createUsername by rememberSaveable(showCreateRoomDialog) { mutableStateOf(username) }\n\n        DefaultDialog(\n            onDismiss = { showCreateRoomDialog = false },\n            icon = { Icon(painterResource(R.drawable.add), contentDescription = null) },\n            title = { Text(stringResource(R.string.listen_together_create_room)) },\n            buttons = {\n                TextButton(onClick = { showCreateRoomDialog = false }) {\n                    Text(stringResource(android.R.string.cancel))\n                }\n                Spacer(modifier = Modifier.width(8.dp))\n                Button(\n                    onClick = {\n                        val finalUsername = createUsername.trim()\n                        if (finalUsername.isNotBlank()) {\n                            username = finalUsername\n                            viewModel.createRoom(finalUsername)\n                            showCreateRoomDialog = false\n                        } else {\n                            Toast.makeText(context, R.string.error_username_empty, Toast.LENGTH_SHORT).show()\n                        }\n                    },\n                    enabled = createUsername.trim().isNotBlank(),\n                ) {\n                    Text(stringResource(R.string.create))\n                }\n            },\n        ) {\n            Column(\n                verticalArrangement = Arrangement.spacedBy(12.dp),\n            ) {\n                Text(\n                    text = stringResource(R.string.listen_together_create_room_desc),\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.onSurfaceVariant,\n                )\n                OutlinedTextField(\n                    value = createUsername,\n                    onValueChange = { createUsername = it },\n                    label = { Text(stringResource(R.string.listen_together_username)) },\n                    leadingIcon = {\n                        Icon(painterResource(R.drawable.person), contentDescription = null)\n                    },\n                    singleLine = true,\n                    modifier = Modifier.fillMaxWidth(),\n                )\n            }\n        }\n    }\n\n    if (showJoinRoomDialog) {\n        var joinUsername by rememberSaveable(showJoinRoomDialog) { mutableStateOf(username) }\n\n        DefaultDialog(\n            onDismiss = { showJoinRoomDialog = false },\n            icon = { Icon(painterResource(R.drawable.group_add), contentDescription = null) },\n            title = { Text(stringResource(R.string.listen_together_join_room)) },\n            buttons = {\n                TextButton(onClick = { showJoinRoomDialog = false }) {\n                    Text(stringResource(android.R.string.cancel))\n                }\n                Spacer(modifier = Modifier.width(8.dp))\n                Button(\n                    onClick = {\n                        val finalUsername = joinUsername.trim()\n                        if (finalUsername.isNotBlank() && roomCodeInput.length == 8) {\n                            username = finalUsername\n                            viewModel.joinRoom(roomCodeInput, finalUsername)\n                            showJoinRoomDialog = false\n                            roomCodeInput = \"\"\n                        } else {\n                            Toast.makeText(context, R.string.error_username_empty, Toast.LENGTH_SHORT).show()\n                        }\n                    },\n                    enabled = joinUsername.trim().isNotBlank() && roomCodeInput.length == 8,\n                ) {\n                    Text(stringResource(R.string.join))\n                }\n            },\n        ) {\n            Column(\n                verticalArrangement = Arrangement.spacedBy(12.dp),\n            ) {\n                OutlinedTextField(\n                    value = joinUsername,\n                    onValueChange = { joinUsername = it },\n                    label = { Text(stringResource(R.string.listen_together_username)) },\n                    leadingIcon = {\n                        Icon(painterResource(R.drawable.person), contentDescription = null)\n                    },\n                    singleLine = true,\n                    modifier = Modifier.fillMaxWidth(),\n                )\n                OutlinedTextField(\n                    value = roomCodeInput,\n                    onValueChange = { roomCodeInput = it.uppercase().filter { c -> c.isLetterOrDigit() }.take(8) },\n                    label = { Text(stringResource(R.string.listen_together_room_code)) },\n                    leadingIcon = {\n                        Icon(painterResource(R.drawable.key), contentDescription = null)\n                    },\n                    singleLine = true,\n                    modifier = Modifier.fillMaxWidth(),\n                )\n            }\n        }\n    }\n\n    if (showLogsDialog) {\n        LogsDialog(\n            logs = logs,\n            onClear = { viewModel.clearLogs() },\n            onDismiss = { showLogsDialog = false },\n        )\n    }\n\n    if (showBlockedUsersDialog) {\n        BlockedUsersDialog(\n            blockedUsernames = blockedUsernames,\n            onUnblock = { viewModel.unblockUser(it) },\n            onDismiss = { showBlockedUsersDialog = false },\n        )\n    }\n\n    Column(\n        Modifier\n            .windowInsetsPadding(LocalPlayerAwareWindowInsets.current.only(WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom))\n            .verticalScroll(rememberScrollState()),\n    ) {\n        Spacer(\n            Modifier.windowInsetsPadding(\n                LocalPlayerAwareWindowInsets.current.only(WindowInsetsSides.Top),\n            ),\n        )\n\n        // Settings section using IntegrationCard\n        val selectedServer = remember(serverUrl) { ListenTogetherServers.findByUrl(serverUrl) }\n\n        Column(modifier = Modifier.padding(horizontal = 16.dp)) {\n            IntegrationCard(\n                title = stringResource(R.string.settings),\n                items =\n                    listOf(\n                        IntegrationCardItem(\n                            icon = painterResource(R.drawable.person),\n                            title = { Text(stringResource(R.string.listen_together_blocked_users)) },\n                            description = {\n                                Text(\n                                    if (blockedUsernames.isNotEmpty()) {\n                                        stringResource(R.string.listen_together_blocked_users_count, blockedUsernames.size)\n                                    } else {\n                                        stringResource(R.string.listen_together_no_blocked_users)\n                                    },\n                                )\n                            },\n                            onClick =\n                                if (blockedUsernames.isNotEmpty()) {\n                                    { showBlockedUsersDialog = true }\n                                } else {\n                                    null\n                                },\n                        ),\n                        IntegrationCardItem(\n                            icon = painterResource(R.drawable.cloud),\n                            title = { Text(stringResource(R.string.listen_together_server_url)) },\n                            description = {\n                                Text(\n                                    selectedServer?.let { server ->\n                                        \"${server.name} - ${server.location}\"\n                                    } ?: serverUrl,\n                                    maxLines = 1,\n                                    overflow = TextOverflow.Ellipsis,\n                                )\n                            },\n                            onClick = { showServerUrlDialog = true },\n                        ),\n                        IntegrationCardItem(\n                            icon = painterResource(R.drawable.person),\n                            title = { Text(stringResource(R.string.listen_together_username)) },\n                            description = {\n                                Text(username.ifEmpty { stringResource(R.string.not_set) })\n                            },\n                            onClick =\n                                if (roomState == null) {\n                                    { showUsernameDialog = true }\n                                } else {\n                                    {\n                                        Toast\n                                            .makeText(\n                                                context,\n                                                cannotEditUsernameInRoomStr,\n                                                Toast.LENGTH_SHORT,\n                                            ).show()\n                                    }\n                                },\n                        ),\n                        IntegrationCardItem(\n                            icon = painterResource(R.drawable.done),\n                            title = { Text(stringResource(R.string.listen_together_auto_approval_joins)) },\n                            description = {\n                                Text(stringResource(R.string.listen_together_auto_approval_joins_desc))\n                            },\n                            trailingContent = {\n                                Switch(\n                                    checked = autoApprovalJoins,\n                                    onCheckedChange = { autoApprovalJoins = it },\n                                    // Only disable for guests in a room (hosts can always change)\n                                    enabled = roomState == null || role != RoomRole.GUEST,\n                                    thumbContent = {\n                                        Icon(\n                                            painter =\n                                                painterResource(\n                                                    id = if (autoApprovalJoins) R.drawable.check else R.drawable.close,\n                                                ),\n                                            contentDescription = null,\n                                            modifier = Modifier.size(SwitchDefaults.IconSize),\n                                        )\n                                    },\n                                )\n                            },\n                            // Allow clicking to see disabled state, but only change if enabled\n                            onClick = { if (roomState == null || role != RoomRole.GUEST) autoApprovalJoins = !autoApprovalJoins },\n                        ),\n                        IntegrationCardItem(\n                            icon = painterResource(R.drawable.done),\n                            title = { Text(stringResource(R.string.listen_together_auto_approval_suggestions)) },\n                            description = {\n                                Text(stringResource(R.string.listen_together_auto_approval_suggestions_desc))\n                            },\n                            trailingContent = {\n                                Switch(\n                                    checked = autoApproveSuggestions,\n                                    onCheckedChange = { autoApproveSuggestions = it },\n                                    // Only disable for guests in a room (hosts can always change)\n                                    enabled = roomState == null || role != RoomRole.GUEST,\n                                    thumbContent = {\n                                        Icon(\n                                            painter =\n                                                painterResource(\n                                                    id = if (autoApproveSuggestions) R.drawable.check else R.drawable.close,\n                                                ),\n                                            contentDescription = null,\n                                            modifier = Modifier.size(SwitchDefaults.IconSize),\n                                        )\n                                    },\n                                )\n                            },\n                            // Allow clicking to see disabled state, but only change if enabled\n                            onClick = { if (roomState == null || role != RoomRole.GUEST) autoApproveSuggestions = !autoApproveSuggestions },\n                        ),\n                        IntegrationCardItem(\n                            icon = painterResource(R.drawable.volume_up),\n                            title = { Text(stringResource(R.string.listen_together_sync_volume)) },\n                            description = {\n                                Text(stringResource(R.string.listen_together_sync_volume_desc))\n                            },\n                            trailingContent = {\n                                Switch(\n                                    checked = syncHostVolume,\n                                    onCheckedChange = { syncHostVolume = it },\n                                    thumbContent = {\n                                        Icon(\n                                            painter =\n                                                painterResource(\n                                                    id = if (syncHostVolume) R.drawable.check else R.drawable.close,\n                                                ),\n                                            contentDescription = null,\n                                            modifier = Modifier.size(SwitchDefaults.IconSize),\n                                        )\n                                    },\n                                )\n                            },\n                            onClick = { syncHostVolume = !syncHostVolume },\n                        ),\n                        IntegrationCardItem(\n                            icon = painterResource(R.drawable.bug_report),\n                            title = { Text(stringResource(R.string.listen_together_view_logs)) },\n                            description = {\n                                Text(stringResource(R.string.listen_together_view_logs_desc))\n                            },\n                            onClick = { showLogsDialog = true },\n                        ),\n                    ),\n            )\n        }\n\n        Spacer(modifier = Modifier.height(16.dp))\n    }\n\n    TopAppBar(\n        title = { Text(stringResource(R.string.listen_together)) },\n        navigationIcon = {\n            IconButton(\n                onClick = navController::navigateUp,\n                onLongClick = navController::backToMain,\n            ) {\n                Icon(\n                    painterResource(R.drawable.arrow_back),\n                    contentDescription = null,\n                )\n            }\n        },\n    )\n}\n\n@Composable\nfun LogsDialog(\n    logs: List<LogEntry>,\n    onClear: () -> Unit,\n    onDismiss: () -> Unit,\n) {\n    val listState = rememberLazyListState()\n\n    LaunchedEffect(logs.size) {\n        if (logs.isNotEmpty()) {\n            listState.animateScrollToItem(logs.size - 1)\n        }\n    }\n\n    val context = LocalContext.current\n\n    DefaultDialog(\n        onDismiss = onDismiss,\n        icon = { Icon(painterResource(R.drawable.bug_report), contentDescription = null) },\n        title = { Text(stringResource(R.string.listen_together_logs)) },\n        buttons = {\n            TextButton(\n                onClick = {\n                    val textToCopy =\n                        logs.joinToString(\"\\n\") { log ->\n                            buildString {\n                                append(log.timestamp)\n                                append(\" [\")\n                                append(log.level.name)\n                                append(\"] \")\n                                append(log.message)\n                                log.details?.let { d -> append(\" -- $d\") }\n                            }\n                        }\n                    val cm = context.getSystemService(android.content.Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager\n                    val clip = android.content.ClipData.newPlainText(\"ListenTogetherLogs\", textToCopy)\n                    cm.setPrimaryClip(clip)\n                    Toast.makeText(context, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show()\n                },\n                enabled = logs.isNotEmpty(),\n            ) {\n                Text(stringResource(R.string.copy))\n            }\n            TextButton(onClick = onClear) {\n                Text(stringResource(R.string.clear))\n            }\n            Spacer(modifier = Modifier.width(8.dp))\n            Button(onClick = onDismiss) {\n                Text(stringResource(android.R.string.ok))\n            }\n        },\n    ) {\n        Box(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .height(350.dp),\n        ) {\n            if (logs.isEmpty()) {\n                Box(\n                    modifier = Modifier.fillMaxSize(),\n                    contentAlignment = Alignment.Center,\n                ) {\n                    Text(\n                        text = stringResource(R.string.listen_together_no_logs),\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n                }\n            } else {\n                LazyColumn(\n                    state = listState,\n                    modifier = Modifier.fillMaxSize(),\n                ) {\n                    items(logs) { log ->\n                        LogEntryItem(log)\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ServerChooserDialog(\n    servers: List<ListenTogetherServer>,\n    currentUrl: String,\n    onSelect: (ListenTogetherServer) -> Unit,\n    onUseCustom: (String) -> Unit,\n    onDismiss: () -> Unit,\n) {\n    var customUrl by rememberSaveable(currentUrl) { mutableStateOf(currentUrl) }\n    val trimmedCustomUrl = customUrl.trim()\n\n    DefaultDialog(\n        onDismiss = onDismiss,\n        icon = { Icon(painterResource(R.drawable.cloud), contentDescription = null) },\n        title = { Text(stringResource(R.string.listen_together_choose_server)) },\n        buttons = {\n            TextButton(onClick = onDismiss) {\n                Text(stringResource(android.R.string.cancel))\n            }\n        },\n    ) {\n        Column(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .verticalScroll(rememberScrollState()),\n            verticalArrangement = Arrangement.spacedBy(12.dp),\n        ) {\n            servers.forEach { server ->\n                val isSelected = server.url == currentUrl\n                Card(\n                    modifier =\n                        Modifier\n                            .fillMaxWidth()\n                            .clickable { onSelect(server) },\n                    shape = RoundedCornerShape(16.dp),\n                    colors =\n                        CardDefaults.cardColors(\n                            containerColor =\n                                if (isSelected) {\n                                    MaterialTheme.colorScheme.primaryContainer\n                                } else {\n                                    MaterialTheme.colorScheme.surfaceVariant\n                                },\n                        ),\n                ) {\n                    Row(\n                        modifier =\n                            Modifier\n                                .fillMaxWidth()\n                                .padding(12.dp),\n                        verticalAlignment = Alignment.CenterVertically,\n                    ) {\n                        Column(modifier = Modifier.weight(1f)) {\n                            Text(\n                                text = server.name,\n                                style = MaterialTheme.typography.titleSmall,\n                                fontWeight = FontWeight.SemiBold,\n                            )\n                            Text(\n                                text = \"${server.location} - ${server.operator}\",\n                                style = MaterialTheme.typography.bodySmall,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                            )\n                            Text(\n                                text = server.url,\n                                style = MaterialTheme.typography.labelSmall,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                                maxLines = 1,\n                                overflow = TextOverflow.Ellipsis,\n                            )\n                        }\n                        if (isSelected) {\n                            Icon(\n                                painter = painterResource(R.drawable.done),\n                                contentDescription = null,\n                                tint = MaterialTheme.colorScheme.primary,\n                            )\n                        }\n                    }\n                }\n            }\n\n            HorizontalDivider()\n\n            Text(\n                text = stringResource(R.string.listen_together_custom_server),\n                style = MaterialTheme.typography.titleSmall,\n                fontWeight = FontWeight.SemiBold,\n            )\n            OutlinedTextField(\n                value = customUrl,\n                onValueChange = { customUrl = it },\n                label = { Text(stringResource(R.string.listen_together_server_url)) },\n                leadingIcon = {\n                    Icon(painterResource(R.drawable.link), contentDescription = null)\n                },\n                singleLine = true,\n                modifier = Modifier.fillMaxWidth(),\n            )\n            Button(\n                onClick = { onUseCustom(trimmedCustomUrl) },\n                enabled = trimmedCustomUrl.isNotBlank(),\n                modifier = Modifier.fillMaxWidth(),\n                shape = RoundedCornerShape(12.dp),\n            ) {\n                Text(stringResource(R.string.listen_together_use_custom_server))\n            }\n        }\n    }\n}\n\n@Composable\nfun LogEntryItem(log: LogEntry) {\n    val context = LocalContext.current\n\n    Column(\n        modifier =\n            Modifier\n                .fillMaxWidth()\n                .padding(vertical = 2.dp),\n    ) {\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n            modifier = Modifier.fillMaxWidth(),\n        ) {\n            Text(\n                text = log.timestamp,\n                style = MaterialTheme.typography.labelSmall,\n                fontFamily = FontFamily.Monospace,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n            )\n            Spacer(modifier = Modifier.width(8.dp))\n            Surface(\n                shape = RoundedCornerShape(8.dp),\n                color =\n                    when (log.level) {\n                        LogLevel.ERROR -> MaterialTheme.colorScheme.errorContainer\n                        LogLevel.WARNING -> Color(0xFFFFF3CD)\n                        LogLevel.DEBUG -> MaterialTheme.colorScheme.surfaceVariant\n                        LogLevel.INFO -> MaterialTheme.colorScheme.primaryContainer\n                    },\n            ) {\n                Text(\n                    text = log.level.name,\n                    style = MaterialTheme.typography.labelSmall,\n                    modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp),\n                    color =\n                        when (log.level) {\n                            LogLevel.ERROR -> MaterialTheme.colorScheme.onErrorContainer\n                            LogLevel.WARNING -> Color(0xFF856404)\n                            LogLevel.DEBUG -> MaterialTheme.colorScheme.onSurfaceVariant\n                            LogLevel.INFO -> MaterialTheme.colorScheme.onPrimaryContainer\n                        },\n                )\n            }\n        }\n\n        Text(\n            text = log.message,\n            style = MaterialTheme.typography.bodySmall,\n            fontFamily = FontFamily.Monospace,\n        )\n        log.details?.let { details ->\n            Text(\n                text = details,\n                style = MaterialTheme.typography.bodySmall,\n                fontFamily = FontFamily.Monospace,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                maxLines = 2,\n                overflow = TextOverflow.Ellipsis,\n            )\n        }\n    }\n}\n\n@Composable\nfun BlockedUsersDialog(\n    blockedUsernames: Set<String>,\n    onUnblock: (String) -> Unit,\n    onDismiss: () -> Unit,\n) {\n    val listState = rememberLazyListState()\n\n    DefaultDialog(\n        onDismiss = onDismiss,\n        icon = { Icon(painterResource(R.drawable.person), contentDescription = null) },\n        title = { Text(stringResource(R.string.listen_together_blocked_users)) },\n        buttons = {\n            Button(onClick = onDismiss) {\n                Text(stringResource(android.R.string.ok))\n            }\n        },\n    ) {\n        Box(\n            modifier =\n                Modifier\n                    .fillMaxWidth()\n                    .height(280.dp),\n        ) {\n            if (blockedUsernames.isEmpty()) {\n                Box(\n                    modifier = Modifier.fillMaxSize(),\n                    contentAlignment = Alignment.Center,\n                ) {\n                    Text(\n                        text = stringResource(R.string.listen_together_no_blocked_users),\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n                }\n            } else {\n                LazyColumn(\n                    state = listState,\n                    modifier = Modifier.fillMaxSize(),\n                    verticalArrangement = Arrangement.spacedBy(8.dp),\n                ) {\n                    items(blockedUsernames.toList()) { username ->\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            modifier =\n                                Modifier\n                                    .fillMaxWidth()\n                                    .clip(RoundedCornerShape(12.dp))\n                                    .background(MaterialTheme.colorScheme.surfaceVariant)\n                                    .padding(12.dp),\n                            horizontalArrangement = Arrangement.SpaceBetween,\n                        ) {\n                            Row(\n                                verticalAlignment = Alignment.CenterVertically,\n                                modifier = Modifier.weight(1f),\n                            ) {\n                                Icon(\n                                    painter = painterResource(R.drawable.person),\n                                    contentDescription = null,\n                                    modifier = Modifier.size(20.dp),\n                                    tint = MaterialTheme.colorScheme.onSurfaceVariant,\n                                )\n                                Spacer(modifier = Modifier.width(8.dp))\n                                Text(\n                                    text = username,\n                                    style = MaterialTheme.typography.bodyMedium,\n                                )\n                            }\n                            TextButton(\n                                onClick = { onUnblock(username) },\n                            ) {\n                                Text(stringResource(R.string.unblock))\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/WrappedAudioService.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.wrapped\n\nimport android.content.Context\nimport android.net.ConnectivityManager\nimport android.net.Uri\nimport androidx.core.net.toUri\nimport androidx.media3.common.MediaItem\nimport androidx.media3.common.Player\nimport androidx.media3.exoplayer.ExoPlayer\nimport com.metrolist.music.R\nimport com.metrolist.music.constants.AudioQuality\nimport com.metrolist.music.utils.YTPlayerUtils\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport timber.log.Timber\n\nclass WrappedAudioService(\n    private val context: Context,\n) {\n    private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())\n    private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager\n    private var player: ExoPlayer? = null\n    private var playbackJob: Job? = null\n\n    private val _isMuted = MutableStateFlow(false)\n    val isMuted = _isMuted.asStateFlow()\n\n    private fun initPlayer() {\n        if (player == null) {\n            player = ExoPlayer.Builder(context).build().apply {\n                addListener(object : Player.Listener {\n                    override fun onPlayerError(error: androidx.media3.common.PlaybackException) {\n                        Timber.tag(\"WrappedAudioService\").e(error, \"Player error\")\n                        playbackJob?.cancel()\n                    }\n                })\n            }\n        }\n    }\n\n    fun toggleMute() {\n        _isMuted.value = !_isMuted.value\n        player?.volume = if (_isMuted.value) 0f else 1f\n    }\n\n    suspend fun prepareTrack(songId: String?) {\n        initPlayer()\n        val songUri = getSongUri(songId)\n        withContext(Dispatchers.Main) {\n            val mediaItem = MediaItem.Builder()\n                .setUri(songUri)\n                .setMediaId(songId ?: \"fallback\")\n                .build()\n            player?.setMediaItem(mediaItem)\n            player?.prepare()\n        }\n    }\n\n    fun playTrack(songId: String?) {\n        if (player?.currentMediaItem?.mediaId == songId) {\n            Timber.tag(\"WrappedAudioService\").d(\"Track $songId is already loaded or playing.\")\n            if (player?.isPlaying == false) player?.play()\n            return\n        }\n        playbackJob?.cancel()\n\n        playbackJob = scope.launch {\n            try {\n                prepareTrack(songId)\n                withContext(Dispatchers.Main) {\n                    if (songId != null && songId != \"2-p9DM2Xvsc\") {\n                        player?.seekTo(30_000)\n                    } else {\n                        player?.seekTo(0)\n                    }\n                    player?.play()\n                    player?.volume = if (_isMuted.value) 0f else 1f\n                }\n            } catch (e: Exception) {\n                Timber.tag(\"WrappedAudioService\").e(e, \"Error during playback preparation\")\n            }\n        }\n    }\n\n    private suspend fun getSongUri(songId: String?): Uri {\n        val fallbackUri = \"android.resource://${context.packageName}/${R.raw.wrapped_theme}\".toUri()\n        if (songId == null) {\n            Timber.tag(\"WrappedAudio\").i(\"No song ID provided, using fallback audio.\")\n            return fallbackUri\n        }\n\n        return try {\n            val audioQuality = context.dataStore.get(com.metrolist.music.constants.AudioQualityKey).let {\n                AudioQuality.valueOf(it ?: AudioQuality.AUTO.name)\n            }\n            val playbackData = withContext(Dispatchers.IO) {\n                YTPlayerUtils.playerResponseForPlayback(\n                    videoId = songId,\n                    audioQuality = audioQuality,\n                    connectivityManager = connectivityManager,\n                ).getOrNull()\n            }\n            val streamUrl = playbackData?.streamUrl\n            if (streamUrl.isNullOrBlank()) {\n                Timber.tag(\"WrappedAudio\")\n                    .w(\"Resolved URL for $songId is null or blank. Using fallback.\")\n                fallbackUri\n            } else {\n                streamUrl.toUri()\n            }\n        } catch (e: Exception) {\n            Timber.tag(\"WrappedAudio\").e(e, \"Failed to resolve URL for $songId. Using fallback.\")\n            fallbackUri\n        }\n    }\n\n    fun pause() {\n        player?.pause()\n    }\n\n    fun resume() {\n        player?.play()\n    }\n\n    fun release() {\n        playbackJob?.cancel()\n        player?.release()\n        player = null\n        Timber.tag(\"WrappedAudioService\").d(\"Player released.\")\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/WrappedConstants.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.wrapped\n\nobject WrappedConstants {\n    // This is intentionally hardcoded to 2025 and should not be changed.\n    const val YEAR = 2025\n    const val PLAYLIST_NAME = \"Metrolist 2025\"\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/WrappedData.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.wrapped\n\ndata class MessagePair(val range: LongRange, val tease: String, val reveal: String)\n\nobject WrappedRepository {\n    private val messages = listOf(\n        MessagePair(0L..999L, \"I really hope you are not dissapointed...\", \"That's **%d minutes**. Just warming up?\"),\n        MessagePair(0L..999L, \"Testing the waters, are we?\", \"**%d minutes** is a quick dip in the musical ocean.\"),\n        MessagePair(0L..999L, \"Busy schedule this year?\", \"**%d minutes** is short, sweet, and to the point.\"),\n        MessagePair(0L..999L, \"Silence is golden, they say...\", \"But you preferred **%d minutes** of noise.\"),\n\n        MessagePair(1000L..4999L, \"It seems like you found Metrolist recently...\", \"And you dedicated **%d minutes** to the tunes.\"),\n        MessagePair(1000L..4999L, \"You have a life outside of music.\", \"**%d minutes** is a healthy balance. We respect that.\"),\n        MessagePair(1000L..4999L, \"Not too quiet, not too loud.\", \"Just the right amount of vibes for **%d minutes**.\"),\n        MessagePair(1000L..4999L, \"A casual stop on your journey.\", \"Thanks for dropping by for **%d minutes**.\"),\n\n        MessagePair(5000L..14999L, \"Music is definitely your thing.\", \"**%d minutes** is a solid soundtrack for your year.\"),\n        MessagePair(5000L..14999L, \"We saw you here quite a bit.\", \"Always setting the mood for **%d minutes**.\"),\n        MessagePair(5000L..14999L, \"Your commute must be fun.\", \"**%d minutes** of melodies.\"),\n        MessagePair(5000L..14999L, \"Consistent. Reliable. Rhythmic.\", \"You know what you like, for **%d minutes**.\"),\n\n        MessagePair(15000L..39999L, \"Do you ever take your headphones off?\", \"**%d minutes** suggests music is your oxygen.\"),\n        MessagePair(15000L..39999L, \"Your battery is begging for mercy.\", \"But your ears absolutely love those **%d minutes**.\"),\n        MessagePair(15000L..39999L, \"Main Character Energy detected.\", \"Your life was a movie for **%d minutes**.\"),\n        MessagePair(15000L..39999L, \"Walking, working, sleeping...\", \"There was always a song playing during those **%d minutes**.\"),\n\n        MessagePair(40000L..Long.MAX_VALUE, \"Are you... okay?\", \"You literally lived here for **%d minutes**.\"),\n        MessagePair(40000L..Long.MAX_VALUE, \"We are worried about your eardrums.\", \"Top 1% behavior. **%d minutes** is legendary.\"),\n        MessagePair(40000L..Long.MAX_VALUE, \"Silence scares you, doesn't it?\", \"A wall of sound, all year long, for **%d minutes**.\"),\n        MessagePair(40000L..Long.MAX_VALUE, \"Certified Stress Tester.\", \"You made those extractors work overtime for **%d minutes**.\")\n    )\n\n    fun getMessage(minutes: Long): MessagePair {\n        val possibleMessages = messages.filter { minutes in it.range }\n        val chosenMessage = if (possibleMessages.isNotEmpty()) {\n            possibleMessages.random()\n        } else {\n            // Fallback for safety\n            MessagePair(0L..Long.MAX_VALUE, \"Looks like we lost count!\", \"But you definitely listened to **%d minutes** of music.\")\n        }\n        return chosenMessage.copy(\n            reveal = chosenMessage.reveal.format(minutes)\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/WrappedEntryPoint.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.wrapped\n\nimport android.content.Context\nimport androidx.compose.runtime.compositionLocalOf\nimport com.metrolist.music.db.DatabaseDao\nimport dagger.hilt.EntryPoint\nimport dagger.hilt.InstallIn\nimport dagger.hilt.android.EntryPointAccessors\nimport dagger.hilt.components.SingletonComponent\n\ninternal val LocalWrappedManager = compositionLocalOf<WrappedManager> { error(\"No WrappedManager found!\") }\n\n@EntryPoint\n@InstallIn(SingletonComponent::class)\ninternal interface WrappedEntryPoint {\n    fun databaseDao(): DatabaseDao\n}\n\ninternal fun provideWrappedManager(context: Context): WrappedManager {\n    val entryPoint = EntryPointAccessors.fromApplication(\n        context.applicationContext,\n        WrappedEntryPoint::class.java\n    )\n    return WrappedManager(\n        databaseDao = entryPoint.databaseDao(),\n        context = context.applicationContext\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/WrappedManager.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.wrapped\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.AccountInfo\nimport com.metrolist.music.constants.ArtistSongSortType\nimport com.metrolist.music.db.DatabaseDao\nimport com.metrolist.music.db.entities.Artist\nimport com.metrolist.music.db.entities.PlaylistEntity\nimport com.metrolist.music.db.entities.SongWithStats\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport timber.log.Timber\nimport java.io.File\nimport java.io.FileOutputStream\nimport java.time.LocalDateTime\nimport java.util.Calendar\nimport java.util.UUID\n\nsealed class PlaylistCreationState {\n    object Idle : PlaylistCreationState()\n    object Creating : PlaylistCreationState()\n    object Success : PlaylistCreationState()\n}\n\nclass WrappedManager(\n    private val databaseDao: DatabaseDao,\n    private val context: Context\n) {\n    private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())\n\n    private val _state = MutableStateFlow(WrappedState())\n    val state = _state.asStateFlow()\n\n    fun createPlaylist(imageResName: String) {\n        if (_state.value.playlistCreationState != PlaylistCreationState.Idle) return\n\n        _state.update { it.copy(playlistCreationState = PlaylistCreationState.Creating) }\n        scope.launch {\n            try {\n                withContext(Dispatchers.IO) {\n                    val fromTimestamp = Calendar.getInstance().apply {\n                        set(WrappedConstants.YEAR, Calendar.JANUARY, 1, 0, 0, 0)\n                    }.timeInMillis\n                    val toTimestamp = Calendar.getInstance().apply {\n                        set(WrappedConstants.YEAR, Calendar.DECEMBER, 31, 23, 59, 59)\n                    }.timeInMillis\n                    val allSongs = databaseDao.mostPlayedSongsStats(fromTimestamp, toTimeStamp = toTimestamp, limit = -1).first()\n\n                    val playlistId = UUID.randomUUID().toString()\n\n                    val drawableId = context.resources.getIdentifier(imageResName, \"drawable\", context.packageName)\n                    val bitmap = BitmapFactory.decodeResource(context.resources, drawableId)\n                    val file = File(context.cacheDir, \"$playlistId.png\")\n                    FileOutputStream(file).use {\n                        bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)\n                    }\n\n                    val newPlaylist = PlaylistEntity(\n                        id = playlistId,\n                        name = WrappedConstants.PLAYLIST_NAME,\n                        thumbnailUrl = file.toURI().toString(),\n                        bookmarkedAt = LocalDateTime.now(),\n                        isEditable = true\n                    )\n                    databaseDao.insert(newPlaylist)\n\n                    val createdPlaylist = databaseDao.playlist(playlistId).first()\n                    if (createdPlaylist != null) {\n                        val songIds = allSongs.map { it.id }\n                        databaseDao.addSongToPlaylist(createdPlaylist, songIds)\n                    } else {\n                        Timber.tag(\"WrappedManager\")\n                            .e(\"Failed to retrieve created playlist with id: $playlistId\")\n                    }\n                }\n                _state.update { it.copy(playlistCreationState = PlaylistCreationState.Success) }\n            } catch (e: Exception) {\n                Timber.tag(\"WrappedManager\").e(e, \"Error saving wrapped playlist\")\n                _state.update { it.copy(playlistCreationState = PlaylistCreationState.Idle) }\n            }\n        }\n    }\n\n    private suspend fun generatePlaylistMap() {\n        val topSongs = _state.value.topSongs\n        val topArtists = _state.value.topArtists\n        if (topSongs.isEmpty()) {\n            Timber.tag(\"WrappedManager\").w(\"Cannot generate playlist map, top songs list is empty.\")\n            _state.update { it.copy(trackMap = emptyMap()) }\n            return\n        }\n\n        withContext(Dispatchers.IO) {\n            val playlistMap = mutableMapOf<WrappedScreenType, String>()\n\n            // Intro Part: Random song from top 6-30\n            val introSongPool = topSongs.subList(5, topSongs.size)\n            val introSong = introSongPool.randomOrNull()?.id ?: topSongs.last().id\n            playlistMap[WrappedScreenType.Welcome] = introSong\n            playlistMap[WrappedScreenType.MinutesTease] = introSong\n            playlistMap[WrappedScreenType.MinutesReveal] = introSong\n\n            // Music Part: Top 1 song\n            val topSong = topSongs.first()\n            playlistMap[WrappedScreenType.TotalSongs] = topSong.id\n            playlistMap[WrappedScreenType.TopSongReveal] = topSong.id\n            playlistMap[WrappedScreenType.Top5Songs] = topSong.id\n\n            // Album Part: Random song from top album\n            val topAlbum = _state.value.topAlbum\n            val albumSong = topAlbum?.let { album ->\n                val albumSongs = databaseDao.albumSongs(album.id).first()\n                albumSongs.randomOrNull()?.id\n            } ?: topSong.id // Fallback to top song if no album songs\n            playlistMap[WrappedScreenType.TotalAlbums] = albumSong\n            playlistMap[WrappedScreenType.TopAlbumReveal] = albumSong\n            playlistMap[WrappedScreenType.Top5Albums] = albumSong\n\n            // Artist Part: Top artist's song with specific rule\n            val topArtist = topArtists.firstOrNull()\n            val fromTimestamp = Calendar.getInstance().apply {\n                set(WrappedConstants.YEAR, Calendar.JANUARY, 1, 0, 0, 0)\n            }.timeInMillis\n            val toTimestamp = Calendar.getInstance().apply {\n                set(WrappedConstants.YEAR, Calendar.DECEMBER, 31, 23, 59, 59)\n            }.timeInMillis\n\n            val artistSong = topArtist?.let { artist ->\n                val artistTopSongs = databaseDao.artistSongs(\n                    artistId = artist.id,\n                    sortType = ArtistSongSortType.PLAY_TIME,\n                    descending = true,\n                    fromTimeStamp = fromTimestamp,\n                    toTimeStamp = toTimestamp\n                ).first()\n                if (artistTopSongs.isNotEmpty()) {\n                    val artistTopSong = artistTopSongs.first()\n                    if (artistTopSong.id == topSong.id) {\n                        // Overlap: Use the artist's second song.\n                        // If a second song doesn't exist, use a random song from their list.\n                        artistTopSongs.getOrNull(1)?.id ?: artistTopSongs.filter { it.id != topSong.id }.randomOrNull()?.id ?: artistTopSong.id\n                    } else {\n                        artistTopSong.id\n                    }\n                } else {\n                    // Data anomaly: Fallback to the user's top song.\n                    topSong.id\n                }\n            } ?: topSong.id // Fallback if no top artist.\n            playlistMap[WrappedScreenType.TotalArtists] = artistSong\n            playlistMap[WrappedScreenType.TopArtistReveal] = artistSong\n            playlistMap[WrappedScreenType.Top5Artists] = artistSong\n\n            // End Part\n            val endSongPool = topSongs.subList(2, 5)\n            val endSong = endSongPool.randomOrNull()?.id ?: topSongs[2].id\n            playlistMap[WrappedScreenType.Playlist] = endSong\n            playlistMap[WrappedScreenType.Conclusion] = \"2-p9DM2Xvsc\"\n\n            Timber.tag(\"WrappedManager\").d(\"Generated Playlist Map: $playlistMap\")\n            _state.update { it.copy(trackMap = playlistMap) }\n        }\n    }\n\n    suspend fun prepare() {\n        if (_state.value.isDataReady) return\n        Timber.tag(\"WrappedManager\").d(\"Starting Wrapped data preparation\")\n\n        val fromTimestamp = Calendar.getInstance().apply {\n            set(WrappedConstants.YEAR, Calendar.JANUARY, 1, 0, 0, 0)\n        }.timeInMillis\n\n        val toTimestamp = Calendar.getInstance().apply {\n            set(WrappedConstants.YEAR, Calendar.DECEMBER, 31, 23, 59, 59)\n        }.timeInMillis\n\n        withContext(Dispatchers.IO) {\n            val accountInfoDeferred = async { YouTube.accountInfo().getOrNull() }\n            val topSongsDeferred = async { databaseDao.mostPlayedSongsStats(fromTimestamp, toTimeStamp = toTimestamp, limit = 30).first() }\n            val topArtistsDeferred = async { databaseDao.mostPlayedArtists(fromTimestamp, toTimeStamp = toTimestamp, limit = 5).first() }\n            val topAlbumsDeferred = async { databaseDao.mostPlayedAlbums(fromTimestamp, toTimeStamp = toTimestamp, limit = 5).first() }\n            val uniqueSongCountDeferred = async { databaseDao.getUniqueSongCountInRange(fromTimestamp, toTimestamp).first() }\n            val uniqueArtistCountDeferred = async { databaseDao.getUniqueArtistCountInRange(fromTimestamp, toTimestamp).first() }\n            val uniqueAlbumCountDeferred = async { databaseDao.getUniqueAlbumCountInRange(fromTimestamp, toTimestamp).first() }\n            val totalPlayTimeMsDeferred = async { databaseDao.getTotalPlayTimeInRange(fromTimestamp, toTimestamp).first() ?: 0L }\n\n            val results = awaitAll(\n                accountInfoDeferred,\n                topSongsDeferred,\n                topArtistsDeferred,\n                topAlbumsDeferred,\n                uniqueSongCountDeferred,\n                uniqueArtistCountDeferred,\n                uniqueAlbumCountDeferred,\n                totalPlayTimeMsDeferred\n            )\n\n            @Suppress(\"UNCHECKED_CAST\")\n            val topSongsResult = results[1] as List<SongWithStats>\n            @Suppress(\"UNCHECKED_CAST\")\n            val topAlbumsResult = results[3] as List<com.metrolist.music.db.entities.Album>\n            @Suppress(\"UNCHECKED_CAST\")\n            val topArtistsResult = results[2] as List<Artist>\n            _state.update {\n                it.copy(\n                    accountInfo = results[0] as AccountInfo?,\n                    topSongs = topSongsResult,\n                    topArtists = topArtistsResult,\n                    top5Albums = topAlbumsResult,\n                    topAlbum = topAlbumsResult.firstOrNull(),\n                    uniqueSongCount = results[4] as Int,\n                    uniqueArtistCount = results[5] as Int,\n                    totalAlbums = results[6] as Int,\n                    totalMinutes = (results[7] as Long) / 1000 / 60\n                )\n            }\n        }\n\n        generatePlaylistMap()\n        _state.update { it.copy(isDataReady = true) }\n        Timber.tag(\"WrappedManager\").d(\"Wrapped data preparation finished\")\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/WrappedScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.wrapped\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.pager.VerticalPager\nimport androidx.compose.foundation.pager.rememberPagerState\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.Saver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.snapshotFlow\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalView\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.core.view.WindowCompat\nimport androidx.core.view.WindowInsetsCompat\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.LifecycleEventObserver\nimport androidx.lifecycle.compose.LocalLifecycleOwner\nimport androidx.navigation.NavController\nimport com.metrolist.music.R\nimport com.metrolist.music.ui.screens.wrapped.pages.ConclusionPage\nimport com.metrolist.music.ui.screens.wrapped.pages.PlaylistPage\nimport com.metrolist.music.ui.screens.wrapped.pages.WrappedIntro\nimport com.metrolist.music.ui.screens.wrapped.pages.WrappedMinutesScreen\nimport com.metrolist.music.ui.screens.wrapped.pages.WrappedMinutesTease\nimport com.metrolist.music.ui.screens.wrapped.pages.WrappedTop5AlbumsScreen\nimport com.metrolist.music.ui.screens.wrapped.pages.WrappedTop5ArtistsScreen\nimport com.metrolist.music.ui.screens.wrapped.pages.WrappedTop5SongsScreen\nimport com.metrolist.music.ui.screens.wrapped.pages.WrappedTopAlbumScreen\nimport com.metrolist.music.ui.screens.wrapped.pages.WrappedTopArtistScreen\nimport com.metrolist.music.ui.screens.wrapped.pages.WrappedTopSongScreen\nimport com.metrolist.music.ui.screens.wrapped.pages.WrappedTotalAlbumsScreen\nimport com.metrolist.music.ui.screens.wrapped.pages.WrappedTotalArtistsScreen\nimport com.metrolist.music.ui.screens.wrapped.pages.WrappedTotalSongsScreen\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.launch\n\nsealed class WrappedScreenType {\n    object Welcome : WrappedScreenType()\n\n    object MinutesTease : WrappedScreenType()\n\n    object MinutesReveal : WrappedScreenType()\n\n    object TotalSongs : WrappedScreenType()\n\n    object TopSongReveal : WrappedScreenType()\n\n    object Top5Songs : WrappedScreenType()\n\n    object TotalAlbums : WrappedScreenType()\n\n    object TopAlbumReveal : WrappedScreenType()\n\n    object Top5Albums : WrappedScreenType()\n\n    object TotalArtists : WrappedScreenType()\n\n    object TopArtistReveal : WrappedScreenType()\n\n    object Top5Artists : WrappedScreenType()\n\n    object Playlist : WrappedScreenType()\n\n    object Conclusion : WrappedScreenType()\n}\n\n@Composable\nfun WrappedScreen(navController: NavController) {\n    val context = LocalContext.current\n    val manager = remember { provideWrappedManager(context) }\n\n    CompositionLocalProvider(LocalWrappedManager provides manager) {\n        WrappedScreenContent(navController = navController)\n    }\n}\n\n@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun WrappedScreenContent(navController: NavController) {\n    val onClose: () -> Unit = {\n        navController.previousBackStackEntry?.savedStateHandle?.set(\"wrapped_seen\", true)\n        navController.popBackStack()\n    }\n    BackHandler(onBack = onClose)\n\n    val messagePairSaver =\n        Saver<MessagePair, List<Any>>(\n            save = { listOf(it.range.first, it.range.last, it.tease, it.reveal) },\n            restore = {\n                MessagePair(\n                    range = (it[0] as Long)..(it[1] as Long),\n                    tease = it[2] as String,\n                    reveal = it[3] as String,\n                )\n            },\n        )\n    val view = LocalView.current\n    val scope = rememberCoroutineScope()\n    val manager = LocalWrappedManager.current\n    val audioService = remember { WrappedAudioService(view.context) }\n    val lifecycleOwner = LocalLifecycleOwner.current\n\n    DisposableEffect(Unit) {\n        val window = (view.context as android.app.Activity).window\n        val insetsController = WindowCompat.getInsetsController(window, view)\n        insetsController.hide(WindowInsetsCompat.Type.systemBars())\n\n        val observer =\n            LifecycleEventObserver { _, event ->\n                when (event) {\n                    Lifecycle.Event.ON_PAUSE -> {\n                        audioService.pause()\n                    }\n\n                    Lifecycle.Event.ON_RESUME -> {\n                        audioService.resume()\n                    }\n\n                    else -> {}\n                }\n            }\n        lifecycleOwner.lifecycle.addObserver(observer)\n\n        onDispose {\n            insetsController.show(WindowInsetsCompat.Type.systemBars())\n            lifecycleOwner.lifecycle.removeObserver(observer)\n            audioService.release()\n        }\n    }\n\n    val screens =\n        remember {\n            listOf(\n                WrappedScreenType.Welcome,\n                WrappedScreenType.MinutesTease,\n                WrappedScreenType.MinutesReveal,\n                WrappedScreenType.TotalSongs,\n                WrappedScreenType.TopSongReveal,\n                WrappedScreenType.Top5Songs,\n                WrappedScreenType.TotalAlbums,\n                WrappedScreenType.TopAlbumReveal,\n                WrappedScreenType.Top5Albums,\n                WrappedScreenType.TotalArtists,\n                WrappedScreenType.TopArtistReveal,\n                WrappedScreenType.Top5Artists,\n                WrappedScreenType.Playlist,\n                WrappedScreenType.Conclusion,\n            )\n        }\n    val pagerState = rememberPagerState(pageCount = { screens.size })\n    val state by manager.state.collectAsState()\n    val isMuted by audioService.isMuted.collectAsState()\n    val messagePair =\n        rememberSaveable(state.totalMinutes, saver = messagePairSaver) {\n            WrappedRepository.getMessage(state.totalMinutes)\n        }\n\n    LaunchedEffect(Unit) {\n        manager.prepare()\n    }\n\n    LaunchedEffect(pagerState, state.trackMap) {\n        if (state.trackMap.isEmpty()) return@LaunchedEffect\n\n        snapshotFlow { pagerState.currentPage }.distinctUntilChanged().collect { page ->\n            val screen = screens.getOrNull(page)\n            audioService.playTrack(state.trackMap[screen])\n        }\n    }\n\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { },\n                navigationIcon = {\n                    IconButton(onClick = onClose) {\n                        Icon(painterResource(R.drawable.arrow_back), stringResource(R.string.back_button_desc), tint = Color.White)\n                    }\n                },\n                actions = {\n                    IconButton(onClick = { audioService.toggleMute() }) {\n                        val icon = if (isMuted) R.drawable.volume_off else R.drawable.volume_up\n                        Icon(painterResource(icon), \"Mute\", tint = Color.White)\n                    }\n                },\n                colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),\n            )\n        },\n        containerColor = Color.Black,\n    ) { paddingValues ->\n        VerticalPager(\n            state = pagerState,\n            modifier =\n                Modifier\n                    .fillMaxSize()\n                    .padding(paddingValues),\n        ) { page ->\n            when (screens[page]) {\n                is WrappedScreenType.Welcome -> {\n                    WrappedIntro { scope.launch { pagerState.animateScrollToPage(page = 1) } }\n                }\n\n                is WrappedScreenType.MinutesTease -> {\n                    WrappedMinutesTease(\n                        messagePair = messagePair,\n                        onNavigateForward = { scope.launch { pagerState.animateScrollToPage(page = 2) } },\n                        isDataReady = state.isDataReady,\n                    )\n                }\n\n                is WrappedScreenType.MinutesReveal -> {\n                    WrappedMinutesScreen(\n                        messagePair = messagePair,\n                        totalMinutes = state.totalMinutes,\n                        isVisible = pagerState.currentPage == screens.indexOf(WrappedScreenType.MinutesReveal),\n                    )\n                }\n\n                is WrappedScreenType.TotalSongs -> {\n                    WrappedTotalSongsScreen(\n                        uniqueSongCount = state.uniqueSongCount,\n                        isVisible = pagerState.currentPage == screens.indexOf(WrappedScreenType.TotalSongs),\n                    )\n                }\n\n                is WrappedScreenType.TopSongReveal -> {\n                    WrappedTopSongScreen(\n                        topSong = state.topSongs.firstOrNull(),\n                        isVisible = pagerState.currentPage == screens.indexOf(WrappedScreenType.TopSongReveal),\n                    )\n                }\n\n                is WrappedScreenType.Top5Songs -> {\n                    WrappedTop5SongsScreen(\n                        topSongs = state.topSongs.take(5),\n                        isVisible = pagerState.currentPage == screens.indexOf(WrappedScreenType.Top5Songs),\n                    )\n                }\n\n                is WrappedScreenType.TotalAlbums -> {\n                    WrappedTotalAlbumsScreen(\n                        uniqueAlbumCount = state.totalAlbums,\n                        isVisible = pagerState.currentPage == screens.indexOf(WrappedScreenType.TotalAlbums),\n                    )\n                }\n\n                is WrappedScreenType.TopAlbumReveal -> {\n                    WrappedTopAlbumScreen(\n                        topAlbum = state.topAlbum,\n                        isVisible = pagerState.currentPage == screens.indexOf(WrappedScreenType.TopAlbumReveal),\n                    )\n                }\n\n                is WrappedScreenType.Top5Albums -> {\n                    WrappedTop5AlbumsScreen(\n                        topAlbums = state.top5Albums,\n                        isVisible = pagerState.currentPage == screens.indexOf(WrappedScreenType.Top5Albums),\n                    )\n                }\n\n                is WrappedScreenType.TotalArtists -> {\n                    WrappedTotalArtistsScreen(\n                        uniqueArtistCount = state.uniqueArtistCount,\n                        isVisible = pagerState.currentPage == screens.indexOf(WrappedScreenType.TotalArtists),\n                    )\n                }\n\n                is WrappedScreenType.TopArtistReveal -> {\n                    WrappedTopArtistScreen(\n                        topArtist = state.topArtists.firstOrNull(),\n                        isVisible = pagerState.currentPage == screens.indexOf(WrappedScreenType.TopArtistReveal),\n                    )\n                }\n\n                is WrappedScreenType.Top5Artists -> {\n                    WrappedTop5ArtistsScreen(\n                        topArtists = state.topArtists,\n                        isVisible = pagerState.currentPage == screens.indexOf(WrappedScreenType.Top5Artists),\n                    )\n                }\n\n                is WrappedScreenType.Playlist -> {\n                    PlaylistPage()\n                }\n\n                is WrappedScreenType.Conclusion -> {\n                    ConclusionPage(onClose = onClose)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/WrappedState.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.wrapped\n\nimport com.metrolist.innertube.models.AccountInfo\nimport com.metrolist.music.db.entities.Album\nimport com.metrolist.music.db.entities.Artist\nimport com.metrolist.music.db.entities.SongWithStats\n\ndata class WrappedState(\n    val accountInfo: AccountInfo? = null,\n    val totalMinutes: Long = 0,\n    val topSongs: List<SongWithStats> = emptyList(),\n    val topArtists: List<Artist> = emptyList(),\n    val top5Albums: List<Album> = emptyList(),\n    val topAlbum: Album? = null,\n    val uniqueSongCount: Int = 0,\n    val uniqueArtistCount: Int = 0,\n    val totalAlbums: Int = 0,\n    val isDataReady: Boolean = false,\n    val trackMap: Map<WrappedScreenType, String?> = emptyMap(),\n    val playlistCreationState: PlaylistCreationState = PlaylistCreationState.Idle\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/WrappedViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.wrapped\n\nimport androidx.lifecycle.ViewModel\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport javax.inject.Inject\n\n@HiltViewModel\nclass WrappedViewModel @Inject constructor() : ViewModel()\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/components/AnimatedBackground.kt",
    "content": "package com.metrolist.music.ui.screens.wrapped.components\n\nimport androidx.compose.animation.core.LinearEasing\nimport androidx.compose.animation.core.RepeatMode\nimport androidx.compose.animation.core.animateFloat\nimport androidx.compose.animation.core.infiniteRepeatable\nimport androidx.compose.animation.core.rememberInfiniteTransition\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.Color\nimport kotlin.random.Random\n\nenum class ShapeType {\n    Circle, Rect, Line\n}\n\nprivate data class AnimatedElement(\n    val shapeType: ShapeType,\n    val initialX: Float,\n    val initialY: Float,\n    val targetX: Float,\n    val targetY: Float,\n    val size: Float, // radius for circle, width/height for rect, length multiplier for line\n    val alpha: Float,\n    val duration: Int\n)\n\n@Composable\ninternal fun AnimatedBackground(\n    elementCount: Int = 20,\n    shapeTypes: List<ShapeType> = listOf(ShapeType.Circle)\n) {\n    val random = remember { Random(System.currentTimeMillis()) }\n    val elements = remember {\n        List(elementCount) {\n            val shapeType = shapeTypes.random(random)\n            AnimatedElement(\n                shapeType = shapeType,\n                initialX = random.nextFloat(),\n                initialY = random.nextFloat(),\n                targetX = random.nextFloat(),\n                targetY = random.nextFloat(),\n                size = if (shapeType == ShapeType.Circle) random.nextFloat() * 15f + 5f else random.nextFloat() * 50f + 10f,\n                alpha = random.nextFloat() * 0.3f + 0.1f,\n                duration = random.nextInt(4000, 10000)\n            )\n        }\n    }\n\n    val infiniteTransition = rememberInfiniteTransition(label = \"animated_bg\")\n    val progressAnims = elements.map {\n        infiniteTransition.animateFloat(\n            initialValue = 0f,\n            targetValue = 1f,\n            animationSpec = infiniteRepeatable(\n                animation = tween(it.duration, easing = LinearEasing),\n                repeatMode = RepeatMode.Reverse\n            ),\n            label = \"element_progress\"\n        )\n    }\n\n    Canvas(modifier = Modifier.fillMaxSize()) {\n        elements.forEachIndexed { index, element ->\n            val progress = progressAnims[index].value\n            val currentX = element.initialX + (element.targetX - element.initialX) * progress\n            val currentY = element.initialY + (element.targetY - element.initialY) * progress\n\n            when (element.shapeType) {\n                ShapeType.Circle -> {\n                    drawCircle(\n                        color = Color.White.copy(alpha = element.alpha),\n                        radius = element.size,\n                        center = Offset(currentX * size.width, currentY * size.height)\n                    )\n                }\n                ShapeType.Rect -> {\n                    drawRect(\n                        color = Color.White.copy(alpha = element.alpha),\n                        topLeft = Offset(currentX * size.width, currentY * size.height),\n                        size = Size(element.size, element.size)\n                    )\n                }\n                ShapeType.Line -> {\n                    val endX = currentX + (element.targetX - element.initialX) * 0.1f\n                    val endY = currentY + (element.targetY - element.initialY) * 0.1f\n                    drawLine(\n                        color = Color.White.copy(alpha = element.alpha),\n                        start = Offset(currentX * size.width, currentY * size.height),\n                        end = Offset(endX * size.width, endY * size.height),\n                        strokeWidth = 2f\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/components/AnimatedDecorativeElement.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.wrapped.components\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.RepeatMode\nimport androidx.compose.animation.core.infiniteRepeatable\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.delay\nimport kotlin.random.Random\n\n@Composable\nfun AnimatedDecorativeElement(modifier: Modifier = Modifier, isVisible: Boolean) {\n    val rotation = remember { Animatable(0f) }\n    val shapeType = remember { Random.nextInt(3) }\n    LaunchedEffect(isVisible) {\n        if (isVisible) {\n            delay(Random.nextLong(500))\n            rotation.animateTo(\n                targetValue = 360f,\n                animationSpec = infiniteRepeatable(\n                    animation = tween(Random.nextInt(1000, 3000)),\n                    repeatMode = RepeatMode.Restart\n                )\n            )\n        }\n    }\n    Canvas(modifier.graphicsLayer { rotationZ = rotation.value }) {\n        val strokeWidth = 2.dp.toPx()\n        when (shapeType) {\n            0 -> drawArc(Color.White.copy(0.2f), 0f, 90f, false, style = Stroke(strokeWidth))\n            1 -> drawCircle(Color.White.copy(0.2f), style = Stroke(strokeWidth))\n            2 -> drawRect(Color.White.copy(0.2f), style = Stroke(strokeWidth))\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/components/AutoResizingText.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.wrapped.components\n\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.drawWithContent\nimport androidx.compose.ui.text.TextStyle\n\n@Composable\nfun AutoResizingText(\n    text: String,\n    modifier: Modifier = Modifier,\n    style: TextStyle\n) {\n    var scaledTextStyle by remember { mutableStateOf(style) }\n    var readyToDraw by remember { mutableStateOf(false) }\n\n    Text(\n        text = text,\n        style = scaledTextStyle,\n        maxLines = 1,\n        softWrap = false,\n        modifier = modifier.drawWithContent {\n            if (readyToDraw) {\n                drawContent()\n            }\n        },\n        onTextLayout = { textLayoutResult ->\n            if (textLayoutResult.didOverflowWidth) {\n                scaledTextStyle =\n                    scaledTextStyle.copy(fontSize = scaledTextStyle.fontSize * 0.9)\n            } else {\n                readyToDraw = true\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/pages/AlbumPages.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.wrapped.pages\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.FastOutSlowInEasing\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.slideInVertically\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.rememberTextMeasurer\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport coil3.compose.AsyncImage\nimport com.metrolist.music.R\nimport com.metrolist.music.db.entities.Album\nimport com.metrolist.music.ui.screens.wrapped.components.AnimatedBackground\nimport com.metrolist.music.ui.screens.wrapped.components.ShapeType\nimport com.metrolist.music.ui.theme.bbh_bartle\nimport com.metrolist.music.ui.utils.resize\nimport kotlinx.coroutines.delay\n\n@Composable\nfun WrappedTotalAlbumsScreen(uniqueAlbumCount: Int, isVisible: Boolean) {\n    val animatedAlbums = remember { Animatable(0f) }\n    val textMeasurer = rememberTextMeasurer()\n    var visible by remember { mutableStateOf(false) }\n\n    LaunchedEffect(isVisible, uniqueAlbumCount) {\n        if (isVisible) {\n            visible = true\n            if (uniqueAlbumCount > 0) {\n                animatedAlbums.animateTo(\n                    targetValue = uniqueAlbumCount.toFloat(),\n                    animationSpec = tween(1500, easing = FastOutSlowInEasing)\n                )\n            }\n        }\n    }\n\n    Box(modifier = Modifier.fillMaxSize()) {\n        AnimatedBackground(shapeTypes = listOf(ShapeType.Circle))\n        Column(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(vertical = 32.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.Center\n        ) {\n            AnimatedVisibility(\n                visible = visible,\n                enter = fadeIn(animationSpec = tween(1000, delayMillis = 200)) + slideInVertically(animationSpec = tween(1000, delayMillis = 200))\n            ) {\n                Text(\n                    text = stringResource(R.string.wrapped_total_albums_title),\n                    modifier = Modifier.padding(horizontal = 24.dp),\n                    style = MaterialTheme.typography.headlineSmall.copy(\n                        color = Color.White,\n                        textAlign = TextAlign.Center\n                    )\n                )\n            }\n            Spacer(Modifier.height(32.dp))\n\n            BoxWithConstraints(Modifier.fillMaxWidth().padding(horizontal = 16.dp)) {\n                val density = LocalDensity.current\n                val baseStyle = MaterialTheme.typography.displayLarge.copy(\n                    color = Color.White,\n                    fontWeight = FontWeight.Bold,\n                    textAlign = TextAlign.Center,\n                    fontFamily = bbh_bartle,\n                    drawStyle = Stroke(with(density) { 2.dp.toPx() })\n                )\n\n                val textStyle = remember(uniqueAlbumCount, maxWidth) {\n                    val finalText = uniqueAlbumCount.toString()\n                    var style = baseStyle.copy(fontSize = 96.sp)\n                    var textWidth = textMeasurer.measure(finalText, style).size.width\n                    while (textWidth > constraints.maxWidth) {\n                        style = style.copy(fontSize = style.fontSize * 0.95f)\n                        textWidth = textMeasurer.measure(finalText, style).size.width\n                    }\n                    style.copy(lineHeight = style.fontSize * 1.08f)\n                }\n\n                Text(\n                    text = animatedAlbums.value.toInt().toString(),\n                    style = textStyle,\n                    maxLines = 1,\n                    modifier = Modifier.fillMaxWidth(),\n                    textAlign = TextAlign.Center\n                )\n            }\n\n            Spacer(Modifier.height(16.dp))\n\n            AnimatedVisibility(\n                visible = visible,\n                enter = fadeIn(animationSpec = tween(1000, delayMillis = 600)) + slideInVertically(animationSpec = tween(1000, delayMillis = 600))\n            ) {\n                Text(\n                    text = stringResource(R.string.wrapped_total_albums_subtitle),\n                    modifier = Modifier.padding(horizontal = 24.dp),\n                    style = MaterialTheme.typography.bodyLarge.copy(\n                        color = Color.White.copy(alpha = 0.8f),\n                        textAlign = TextAlign.Center\n                    )\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun WrappedTopAlbumScreen(topAlbum: Album?, isVisible: Boolean) {\n    var visible by remember { mutableStateOf(false) }\n    LaunchedEffect(isVisible) {\n        if (isVisible) {\n            visible = true\n        }\n    }\n\n    Box(modifier = Modifier.fillMaxSize()) {\n        AnimatedBackground(shapeTypes = listOf(ShapeType.Rect))\n        Column(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(32.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.Center\n        ) {\n            AnimatedVisibility(\n                visible = visible,\n                enter = fadeIn(animationSpec = tween(1000, delayMillis = 200)) + slideInVertically(animationSpec = tween(1000, delayMillis = 200))\n            ) {\n                Text(\n                    text = stringResource(R.string.wrapped_top_album_title),\n                    style = TextStyle(\n                        fontFamily = bbh_bartle,\n                        fontSize = 40.sp,\n                        color = Color.White,\n                        textAlign = TextAlign.Center,\n                        lineHeight = 48.sp\n                    )\n                )\n            }\n            Spacer(modifier = Modifier.height(32.dp))\n            AnimatedVisibility(\n                visible = visible,\n                enter = fadeIn(animationSpec = tween(1000, delayMillis = 400)) + slideInVertically(animationSpec = tween(1000, delayMillis = 400))\n            ) {\n                AsyncImage(\n                    model = topAlbum?.thumbnailUrl?.resize(512, 512),\n                    contentDescription = stringResource(R.string.album_art_for, topAlbum?.title ?: \"\"),\n                    modifier = Modifier\n                        .size(200.dp)\n                        .clip(RoundedCornerShape(3.dp)),\n                    contentScale = ContentScale.Crop\n                )\n            }\n            Spacer(modifier = Modifier.height(16.dp))\n            AnimatedVisibility(\n                visible = visible,\n                enter = fadeIn(animationSpec = tween(1000, delayMillis = 600)) + slideInVertically(animationSpec = tween(1000, delayMillis = 600))\n            ) {\n                Text(\n                    text = topAlbum?.title ?: stringResource(id = R.string.wrapped_no_data),\n                    fontSize = 24.sp,\n                    color = Color.White,\n                    fontWeight = FontWeight.Bold,\n                    textAlign = TextAlign.Center\n                )\n            }\n            Spacer(modifier = Modifier.height(8.dp))\n            AnimatedVisibility(\n                visible = visible,\n                enter = fadeIn(animationSpec = tween(1000, delayMillis = 800)) + slideInVertically(animationSpec = tween(1000, delayMillis = 800))\n            ) {\n                Text(\n                    text = stringResource(R.string.wrapped_album_listening_time, topAlbum?.timeListened?.div(60000) ?: 0),\n                    fontSize = 16.sp,\n                    color = Color.White.copy(alpha = 0.8f),\n                    textAlign = TextAlign.Center\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun WrappedTop5AlbumsScreen(topAlbums: List<Album>, isVisible: Boolean) {\n    var visible by remember { mutableStateOf(false) }\n\n    LaunchedEffect(isVisible) {\n        if (isVisible) {\n            delay(200)\n            visible = true\n        }\n    }\n\n    Box(modifier = Modifier.fillMaxSize()) {\n        AnimatedBackground(shapeTypes = listOf(ShapeType.Circle))\n        Column(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(32.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.Center\n        ) {\n            AnimatedVisibility(\n                visible = visible,\n                enter = fadeIn(animationSpec = tween(1000, delayMillis = 200)) + slideInVertically(animationSpec = tween(1000, delayMillis = 200))\n            ) {\n                Text(\n                    text = stringResource(R.string.wrapped_top_5_albums_title),\n                    style = TextStyle(\n                        fontFamily = bbh_bartle,\n                        fontSize = 48.sp,\n                        color = Color.White,\n                        textAlign = TextAlign.Center,\n                        lineHeight = 56.sp\n                    )\n                )\n            }\n\n            Spacer(modifier = Modifier.height(32.dp))\n\n            Column {\n                topAlbums.forEachIndexed { index, album ->\n                    AnimatedVisibility(\n                        visible = visible,\n                        enter = fadeIn(animationSpec = tween(600, delayMillis = 400 + (index * 200))) + slideInVertically(animationSpec = tween(600, delayMillis = 400 + (index * 200)))\n                    ) {\n                        Row(\n                            modifier = Modifier\n                                .padding(vertical = 8.dp),\n                            verticalAlignment = Alignment.CenterVertically\n                        ) {\n                            Text(\n                                text = \"${index + 1}\",\n                                fontFamily = bbh_bartle,\n                                fontSize = 36.sp,\n                                color = Color.White.copy(alpha = 0.8f),\n                                modifier = Modifier.width(40.dp)\n                            )\n                            Spacer(modifier = Modifier.width(16.dp))\n                            AsyncImage(\n                                model = album.thumbnailUrl?.resize(128, 128),\n                                contentDescription = stringResource(R.string.album_art_for, album.title),\n                                modifier = Modifier\n                                    .size(64.dp)\n                                    .clip(RoundedCornerShape(3.dp)),\n                                contentScale = ContentScale.Crop\n                            )\n                            Spacer(modifier = Modifier.width(16.dp))\n                            Column {\n                                Text(\n                                    text = album.title,\n                                    color = Color.White,\n                                    fontWeight = FontWeight.Bold,\n                                    fontSize = 16.sp,\n                                    maxLines = 1\n                                )\n                                Text(\n                                    text = stringResource(R.string.wrapped_album_listening_time_minutes, album.timeListened?.div(60000) ?: 0),\n                                    color = Color.White.copy(alpha = 0.7f),\n                                    fontSize = 14.sp\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/pages/ConclusionPage.kt",
    "content": "package com.metrolist.music.ui.screens.wrapped.pages\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.metrolist.music.R\nimport com.metrolist.music.ui.screens.wrapped.components.AnimatedBackground\nimport com.metrolist.music.ui.screens.wrapped.components.ShapeType\n\n@Composable\nfun ConclusionPage(onClose: () -> Unit) {\n    Box(modifier = Modifier.fillMaxSize()) {\n        AnimatedBackground(elementCount = 30, shapeTypes = listOf(ShapeType.Circle, ShapeType.Line))\n        Column(\n            modifier = Modifier.fillMaxSize(),\n            verticalArrangement = Arrangement.Center,\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            Icon(\n                painter = painterResource(id = R.drawable.ic_launcher_foreground),\n                contentDescription = stringResource(R.string.wrapped_logo_content_description),\n                modifier = Modifier.size(96.dp),\n                tint = Color.White\n            )\n            Spacer(modifier = Modifier.height(24.dp))\n            Text(\n                text = stringResource(R.string.wrapped_thank_you),\n                style = TextStyle(\n                    fontSize = 28.sp,\n                    fontWeight = FontWeight.Bold,\n                    color = Color.White\n                )\n            )\n            Spacer(modifier = Modifier.height(8.dp))\n            Text(\n                text = stringResource(R.string.wrapped_special_thanks),\n                style = TextStyle(\n                    fontSize = 16.sp,\n                    color = Color.Gray\n                )\n            )\n            Spacer(modifier = Modifier.height(48.dp))\n            Button(\n                onClick = onClose,\n                shape = CircleShape,\n                colors = ButtonDefaults.buttonColors(containerColor = Color.White)\n            ) {\n                Text(\n                    text = stringResource(R.string.wrapped_close),\n                    style = TextStyle(\n                        color = Color.Black,\n                        fontWeight = FontWeight.Bold\n                    )\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/pages/PlaylistPage.kt",
    "content": "package com.metrolist.music.ui.screens.wrapped.pages\n\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.metrolist.music.R\nimport com.metrolist.music.ui.screens.wrapped.LocalWrappedManager\nimport com.metrolist.music.ui.screens.wrapped.PlaylistCreationState\nimport com.metrolist.music.ui.screens.wrapped.WrappedConstants\nimport com.metrolist.music.ui.screens.wrapped.components.AnimatedBackground\nimport com.metrolist.music.ui.screens.wrapped.components.AutoResizingText\nimport com.metrolist.music.ui.screens.wrapped.components.ShapeType\nimport com.metrolist.music.ui.theme.bbh_bartle\nimport kotlinx.coroutines.delay\nimport kotlin.random.Random\n\n@Composable\nfun PlaylistPage() {\n    val manager = LocalWrappedManager.current\n    val state by manager.state.collectAsState()\n    val playlistCreationState = state.playlistCreationState\n\n    val (playlistImageRes, playlistImageName) = remember {\n        if (Random.nextBoolean()) {\n            Pair(R.drawable.wrapped_playlistv1, \"wrapped_playlistv1\")\n        } else {\n            Pair(R.drawable.wrapped_playlistv2, \"wrapped_playlistv2\")\n        }\n    }\n\n    var startAnimation by remember { mutableStateOf(false) }\n    LaunchedEffect(Unit) {\n        delay(200)\n        startAnimation = true\n    }\n\n    val contentAlpha by animateFloatAsState(\n        targetValue = if (startAnimation) 1f else 0f,\n        animationSpec = tween(durationMillis = 800, delayMillis = 200)\n    )\n\n    Box(modifier = Modifier.fillMaxSize()) {\n        AnimatedBackground(shapeTypes = listOf(ShapeType.Circle))\n        Column(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(32.dp)\n                .alpha(contentAlpha),\n            verticalArrangement = Arrangement.Center,\n            horizontalAlignment = Alignment.CenterHorizontally\n        ) {\n            AutoResizingText(\n                text = stringResource(R.string.wrapped_playlist_ready),\n                style = TextStyle(\n                    fontFamily = bbh_bartle,\n                    fontSize = 40.sp,\n                    color = Color.White,\n                    textAlign = TextAlign.Center,\n                    lineHeight = 48.sp\n                )\n            )\n            Spacer(modifier = Modifier.height(32.dp))\n            Image(\n                painter = painterResource(id = playlistImageRes),\n                contentDescription = stringResource(R.string.album_cover_desc),\n                modifier = Modifier\n                    .size(256.dp)\n                    .clip(RoundedCornerShape(3.dp))\n            )\n            Spacer(modifier = Modifier.height(24.dp))\n            Text(\n                text = stringResource(R.string.wrapped_playlist_title, WrappedConstants.YEAR),\n                style = TextStyle(\n                    fontSize = 22.sp,\n                    fontWeight = FontWeight.Bold,\n                    color = Color.White\n                )\n            )\n            Spacer(modifier = Modifier.height(48.dp))\n            Button(\n                onClick = {\n                    if (playlistCreationState == PlaylistCreationState.Idle) {\n                        manager.createPlaylist(playlistImageName)\n                    }\n                },\n                shape = CircleShape,\n                colors = ButtonDefaults.buttonColors(containerColor = Color.White),\n                modifier = Modifier.height(50.dp)\n            ) {\n                when (playlistCreationState) {\n                    is PlaylistCreationState.Idle -> Text(\n                        text = stringResource(R.string.wrapped_create_playlist),\n                        style = TextStyle(color = Color.Black, fontWeight = FontWeight.Bold)\n                    )\n                    is PlaylistCreationState.Creating -> CircularProgressIndicator(\n                        modifier = Modifier.size(24.dp),\n                        color = Color.Black,\n                        strokeWidth = 2.dp\n                    )\n                    is PlaylistCreationState.Success -> Text(\n                        text = stringResource(R.string.wrapped_playlist_saved),\n                        style = TextStyle(color = Color.Black, fontWeight = FontWeight.Bold)\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/pages/WrappedIntro.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.wrapped.pages\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.LinearEasing\nimport androidx.compose.animation.core.RepeatMode\nimport androidx.compose.animation.core.animateFloat\nimport androidx.compose.animation.core.infiniteRepeatable\nimport androidx.compose.animation.core.rememberInfiniteTransition\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.slideInVertically\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.drawWithContent\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.metrolist.music.R\nimport com.metrolist.music.ui.theme.bbhBartle\nimport kotlinx.coroutines.delay\n\nprivate const val FADE_IN_DURATION = 1000\nprivate const val SLIDE_IN_DURATION = 1000\nprivate const val INITIAL_DELAY = 200\nprivate const val ICON_DELAY = 200\nprivate const val TITLE_DELAY = 400\nprivate const val SUBTITLE_DELAY = 600\nprivate const val BUTTON_DELAY = 1000\nprivate val BOTTOM_PADDING = 64.dp\n\n@Composable\nfun AutoResizingText(\n    text: String,\n    modifier: Modifier = Modifier,\n    style: TextStyle\n) {\n    var scaledTextStyle by remember { mutableStateOf(style) }\n    var readyToDraw by remember { mutableStateOf(false) }\n\n    Text(\n        text = text,\n        style = scaledTextStyle,\n        maxLines = 1,\n        softWrap = false,\n        modifier = modifier.drawWithContent {\n            if (readyToDraw) {\n                drawContent()\n            }\n        },\n        onTextLayout = { textLayoutResult ->\n            if (textLayoutResult.didOverflowWidth) {\n                scaledTextStyle =\n                    scaledTextStyle.copy(fontSize = scaledTextStyle.fontSize * 0.9)\n            } else {\n                readyToDraw = true\n            }\n        }\n    )\n}\n\n@Composable\nfun WrappedIntro(onNext: () -> Unit) {\n    var visible by remember { mutableStateOf(false) }\n    LaunchedEffect(Unit) {\n        delay(INITIAL_DELAY.toLong())\n        visible = true\n    }\n\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .background(Color.Black)\n    ) {\n        val infiniteTransition = rememberInfiniteTransition(label = \"WrappedIntro bg\")\n        val scale by infiniteTransition.animateFloat(\n            initialValue = 1f,\n            targetValue = 1.1f,\n            animationSpec = infiniteRepeatable(\n                animation = tween(durationMillis = 3000, easing = LinearEasing),\n                repeatMode = RepeatMode.Reverse\n            ),\n            label = \"intro scale\"\n        )\n        val rotation by infiniteTransition.animateFloat(\n            initialValue = -95f,\n            targetValue = -85f,\n            animationSpec = infiniteRepeatable(\n                animation = tween(durationMillis = 5000, easing = LinearEasing),\n                repeatMode = RepeatMode.Reverse\n            ),\n            label = \"intro rotation\"\n        )\n\n        // Background \"2025\" text\n        Box(\n            modifier = Modifier\n                .align(Alignment.CenterStart)\n                .graphicsLayer {\n                    scaleX = scale\n                    scaleY = scale\n                    rotationZ = rotation\n                }\n        ) {\n            BoxWithConstraints {\n                AutoResizingText(\n                    text = stringResource(id = R.string.wrapped_year),\n                    style = TextStyle.Default.copy(\n                        fontFamily = bbhBartle,\n                        fontSize = 800.sp, // Increased size\n                        color = Color.White,\n                        drawStyle = Stroke(width = 2f)\n                    ),\n                    modifier = Modifier.width(this.maxHeight) // Use height for width due to rotation\n                )\n            }\n        }\n\n\n\n        // Main Content Column\n        Column(\n            modifier = Modifier.fillMaxSize(),\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.Center\n        ) {\n            // App Icon\n            AnimatedVisibility(\n                visible = visible,\n                enter = fadeIn(animationSpec = tween(FADE_IN_DURATION, delayMillis = ICON_DELAY)) + slideInVertically(animationSpec = tween(SLIDE_IN_DURATION, delayMillis = ICON_DELAY))\n            ) {\n                Icon(\n                    painter = painterResource(id = R.drawable.app_logo),\n                    contentDescription = stringResource(id = R.string.wrapped_logo_content_description),\n                    modifier = Modifier.size(100.dp)\n                )\n            }\n\n            Spacer(modifier = Modifier.height(16.dp))\n\n            // Metrolist Title with Layered Effect\n            AnimatedVisibility(\n                visible = visible,\n                enter = fadeIn(animationSpec = tween(FADE_IN_DURATION, delayMillis = TITLE_DELAY)) + slideInVertically(animationSpec = tween(SLIDE_IN_DURATION, delayMillis = TITLE_DELAY))\n            ) {\n                Box {\n                    val baseStyle = TextStyle(\n                        fontFamily = bbhBartle,\n                        textAlign = TextAlign.Center,\n                        letterSpacing = 2.sp,\n                        fontSize = 50.sp\n                    )\n                    AutoResizingText(text = stringResource(id = R.string.wrapped_intro_title), style = baseStyle.copy(color = Color.DarkGray), modifier = Modifier.offset(x = 2.dp, y = 2.dp))\n                    AutoResizingText(text = stringResource(id = R.string.wrapped_intro_title), style = baseStyle.copy(color = Color.Gray), modifier = Modifier.offset(x = 1.dp, y = 1.dp))\n                    AutoResizingText(text = stringResource(id = R.string.wrapped_intro_title), style = baseStyle.copy(color = Color.White))\n                }\n            }\n\n\n            Spacer(modifier = Modifier.height(8.dp))\n\n            // Subtitle\n            AnimatedVisibility(\n                visible = visible,\n                enter = fadeIn(animationSpec = tween(FADE_IN_DURATION, delayMillis = SUBTITLE_DELAY)) + slideInVertically(animationSpec = tween(SLIDE_IN_DURATION, delayMillis = SUBTITLE_DELAY))\n            ) {\n                Text(\n                    text = stringResource(id = R.string.wrapped_intro_subtitle),\n                    color = Color.White,\n                    fontSize = 16.sp,\n                    textAlign = TextAlign.Center\n                )\n            }\n        }\n\n        // \"Let's go!\" Button at the bottom\n        AnimatedVisibility(\n            visible = visible,\n            enter = fadeIn(animationSpec = tween(FADE_IN_DURATION, delayMillis = BUTTON_DELAY)) + slideInVertically(animationSpec = tween(SLIDE_IN_DURATION, delayMillis = BUTTON_DELAY)) { it },\n            modifier = Modifier\n                .align(Alignment.BottomCenter)\n                .padding(bottom = BOTTOM_PADDING)\n        ) {\n            Button(\n                onClick = onNext,\n                shape = RoundedCornerShape(50),\n                colors = ButtonDefaults.buttonColors(containerColor = Color.White)\n            ) {\n                Text(\n                    text = stringResource(id = R.string.wrapped_intro_button),\n                    color = Color.Black,\n                    fontWeight = FontWeight.Bold,\n                    modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/pages/WrappedMinutesScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.wrapped.pages\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.FastOutSlowInEasing\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.text.SpanStyle\nimport androidx.compose.ui.text.buildAnnotatedString\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.rememberTextMeasurer\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.withStyle\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.metrolist.music.ui.screens.wrapped.MessagePair\nimport com.metrolist.music.ui.screens.wrapped.components.AnimatedDecorativeElement\nimport com.metrolist.music.ui.theme.bbh_bartle\nimport kotlin.random.Random\n\n@Composable\nfun WrappedMinutesScreen(\n    messagePair: MessagePair?, totalMinutes: Long,\n    isVisible: Boolean\n) {\n    val animatedMinutes = remember { Animatable(0f) }\n    val textMeasurer = rememberTextMeasurer()\n\n    LaunchedEffect(isVisible, totalMinutes) {\n        if (isVisible && totalMinutes > 0) {\n            animatedMinutes.animateTo(targetValue = totalMinutes.toFloat(), animationSpec = tween(1500, easing = FastOutSlowInEasing))\n        }\n    }\n\n    Box(modifier = Modifier.fillMaxSize()) {\n        // More dynamic and overlapping decorative elements\n        Box(modifier = Modifier.align(Alignment.TopStart)) {\n            repeat(5) {\n                AnimatedDecorativeElement(\n                    Modifier.padding(start = (Random.nextInt(0, 150)).dp, top = (Random.nextInt(0, 150)).dp).size((Random.nextInt(20, 100)).dp),\n                    isVisible\n                )\n            }\n        }\n        Box(modifier = Modifier.align(Alignment.BottomEnd)) {\n            repeat(5) {\n                AnimatedDecorativeElement(\n                    Modifier.padding(end = (Random.nextInt(0, 150)).dp, bottom = (Random.nextInt(0, 150)).dp).size((Random.nextInt(20, 100)).dp),\n                    isVisible\n                )\n            }\n        }\n        Box(modifier = Modifier.align(Alignment.TopEnd)) {\n            repeat(3) {\n                AnimatedDecorativeElement(\n                    Modifier.padding(end = (Random.nextInt(0, 100)).dp, top = (Random.nextInt(0, 100)).dp).size((Random.nextInt(20, 80)).dp),\n                    isVisible\n                )\n            }\n        }\n        Box(modifier = Modifier.align(Alignment.BottomStart)) {\n            repeat(3) {\n                AnimatedDecorativeElement(\n                    Modifier.padding(start = (Random.nextInt(0, 100)).dp, bottom = (Random.nextInt(0, 100)).dp).size((Random.nextInt(20, 80)).dp),\n                    isVisible\n                )\n            }\n        }\n\n        Column(\n            modifier = Modifier.fillMaxSize().padding(vertical = 32.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.Center\n        ) {\n            FormattedText(\n                text = messagePair?.tease ?: \"\", modifier = Modifier.padding(horizontal = 24.dp),\n                style = MaterialTheme.typography.headlineSmall.copy(color = Color.White, textAlign = TextAlign.Center)\n            )\n            Spacer(Modifier.height(32.dp))\n            BoxWithConstraints(Modifier.fillMaxWidth().padding(horizontal = 16.dp)) {\n                val density = LocalDensity.current\n                val baseStyle = MaterialTheme.typography.displayLarge.copy(\n                    color = Color.White, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center,\n                    fontFamily = bbh_bartle, drawStyle = Stroke(with(density) { 2.dp.toPx() })\n                )\n                val textStyle = remember(totalMinutes, maxWidth) {\n                    val finalText = totalMinutes.toString()\n                    var style = baseStyle.copy(fontSize = 96.sp)\n                    var textWidth = textMeasurer.measure(finalText, style).size.width\n                    while (textWidth > constraints.maxWidth) {\n                        style = style.copy(fontSize = style.fontSize * 0.95f)\n                        textWidth = textMeasurer.measure(finalText, style).size.width\n                    }\n                    style.copy(lineHeight = style.fontSize * 1.08f)\n                }\n                Text(\n                    text = animatedMinutes.value.toInt().toString(),\n                    style = textStyle,\n                    maxLines = 1,\n                    modifier = Modifier.fillMaxWidth(),\n                    textAlign = TextAlign.Center\n                )\n            }\n            Spacer(Modifier.height(16.dp))\n            FormattedText(\n                text = messagePair?.reveal ?: \"\", modifier = Modifier.padding(horizontal = 24.dp),\n                style = MaterialTheme.typography.bodyLarge.copy(color = Color.White.copy(alpha = 0.8f), textAlign = TextAlign.Center)\n            )\n        }\n    }\n}\n\n@Composable\nfun FormattedText(text: String, modifier: Modifier = Modifier, style: androidx.compose.ui.text.TextStyle) {\n    val annotatedString = buildAnnotatedString {\n        val parts = text.split(\"(?=\\\\*\\\\*)|(?<=\\\\*\\\\*)\".toRegex())\n        var isBold = false\n        for (part in parts) {\n            if (part == \"**\") isBold = !isBold\n            else withStyle(SpanStyle(fontWeight = if (isBold) FontWeight.Bold else FontWeight.Normal)) { append(part) }\n        }\n    }\n    Text(annotatedString, modifier, style = style)\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/pages/WrappedMinutesTease.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.wrapped.pages\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.scaleIn\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.metrolist.music.ui.screens.wrapped.LocalWrappedManager\nimport com.metrolist.music.ui.screens.wrapped.MessagePair\nimport com.metrolist.music.ui.theme.bbh_bartle\nimport kotlinx.coroutines.delay\n\n@Composable\nfun WrappedMinutesTease(\n    messagePair: MessagePair?,\n    onNavigateForward: () -> Unit,\n    isDataReady: Boolean\n) {\n    val manager = LocalWrappedManager.current\n    LaunchedEffect(Unit) {\n        delay(3500)\n        onNavigateForward()\n    }\n    Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {\n        AnimatedVisibility(\n            visible = messagePair != null && isDataReady,\n            enter = fadeIn(tween(1000)) + scaleIn(initialScale = 0.9f, animationSpec = tween(1000))\n        ) {\n            Text(\n                text = messagePair?.tease ?: \"\", modifier = Modifier.padding(horizontal = 24.dp),\n                color = Color.White, fontSize = 30.sp, lineHeight = 34.sp, textAlign = TextAlign.Center,\n                fontFamily = try { bbh_bartle } catch (e: Exception) { FontFamily.Default }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/pages/WrappedTop5ArtistsScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.wrapped.pages\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.slideInVertically\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport coil3.compose.AsyncImage\nimport com.metrolist.music.R\nimport com.metrolist.music.db.entities.Artist\nimport com.metrolist.music.ui.screens.wrapped.components.AnimatedBackground\nimport com.metrolist.music.ui.screens.wrapped.components.ShapeType\nimport com.metrolist.music.ui.theme.bbh_bartle\nimport kotlinx.coroutines.delay\n\n@Composable\nfun WrappedTop5ArtistsScreen(topArtists: List<Artist>, isVisible: Boolean) {\n    var visible by remember { mutableStateOf(false) }\n\n    LaunchedEffect(isVisible) {\n        if (isVisible) {\n            delay(200)\n            visible = true\n        }\n    }\n\n    Box(modifier = Modifier.fillMaxSize()) {\n        AnimatedBackground(elementCount = 15, shapeTypes = listOf(ShapeType.Line))\n        Column(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(32.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.Center\n        ) {\n            AnimatedVisibility(\n                visible = visible,\n                enter = fadeIn(animationSpec = tween(1000, delayMillis = 200)) + slideInVertically(animationSpec = tween(1000, delayMillis = 200))\n            ) {\n                Text(\n                    text = stringResource(id = R.string.wrapped_top_5_artists_title),\n                    fontSize = 48.sp,\n                    color = Color.White,\n                    textAlign = TextAlign.Center\n                )\n            }\n\n            Spacer(modifier = Modifier.height(32.dp))\n\n            Column {\n                topArtists.forEachIndexed { index, artist ->\n                    AnimatedVisibility(\n                        visible = visible,\n                        enter = fadeIn(animationSpec = tween(600, delayMillis = 400 + (index * 200))) + slideInVertically(animationSpec = tween(600, delayMillis = 400 + (index * 200)))\n                    ) {\n                        Row(\n                            modifier = Modifier\n                                .padding(vertical = 8.dp),\n                            verticalAlignment = Alignment.CenterVertically\n                        ) {\n                            Text(\n                                text = \"${index + 1}\",\n                                fontFamily = bbh_bartle,\n                                fontSize = 36.sp,\n                                color = Color.White.copy(alpha = 0.8f),\n                                modifier = Modifier.width(40.dp)\n                            )\n                            Spacer(modifier = Modifier.width(16.dp))\n                            AsyncImage(\n                                model = artist.artist.thumbnailUrl,\n                                contentDescription = \"Artist image\",\n                                modifier = Modifier\n                                    .size(64.dp)\n                                    .clip(CircleShape),\n                                contentScale = ContentScale.Crop\n                            )\n                            Spacer(modifier = Modifier.width(16.dp))\n                            Column {\n                                Text(\n                                    text = artist.artist.name,\n                                    color = Color.White,\n                                    fontWeight = FontWeight.Bold,\n                                    fontSize = 16.sp\n                                )\n                                Text(\n                                    text = stringResource(id = R.string.wrapped_artist_listening_time, (artist.timeListened ?: 0) / 60000),\n                                    color = Color.White.copy(alpha = 0.7f),\n                                    fontSize = 14.sp\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/pages/WrappedTop5SongsScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.wrapped.pages\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.slideInVertically\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport coil3.compose.AsyncImage\nimport com.metrolist.music.R\nimport com.metrolist.music.db.entities.SongWithStats\nimport com.metrolist.music.ui.screens.wrapped.components.AnimatedBackground\nimport com.metrolist.music.ui.screens.wrapped.components.ShapeType\nimport com.metrolist.music.ui.theme.bbh_bartle\nimport kotlinx.coroutines.delay\n\n@Composable\nfun WrappedTop5SongsScreen(topSongs: List<SongWithStats>, isVisible: Boolean) {\n    var visible by remember { mutableStateOf(false) }\n\n    LaunchedEffect(isVisible) {\n        if (isVisible) {\n            delay(200)\n            visible = true\n        }\n    }\n\n    Box(modifier = Modifier.fillMaxSize()) {\n        AnimatedBackground(elementCount = 25, shapeTypes = listOf(ShapeType.Rect))\n        Column(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(32.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.Center\n        ) {\n            AnimatedVisibility(\n                visible = visible,\n                enter = fadeIn(animationSpec = tween(1000, delayMillis = 200)) + slideInVertically(animationSpec = tween(1000, delayMillis = 200))\n            ) {\n                Text(\n                    text = stringResource(id = R.string.wrapped_top_5_songs_title),\n                    fontSize = 48.sp,\n                    color = Color.White,\n                    textAlign = TextAlign.Center\n                )\n            }\n\n            Spacer(modifier = Modifier.height(32.dp))\n\n            Column {\n                topSongs.forEachIndexed { index, song ->\n                    AnimatedVisibility(\n                        visible = visible,\n                        enter = fadeIn(animationSpec = tween(600, delayMillis = 400 + (index * 200))) + slideInVertically(animationSpec = tween(600, delayMillis = 400 + (index * 200)))\n                    ) {\n                        Row(\n                            modifier = Modifier\n                                .padding(vertical = 8.dp),\n                            verticalAlignment = Alignment.CenterVertically\n                        ) {\n                            Text(\n                                text = \"${index + 1}\",\n                                fontFamily = bbh_bartle,\n                                fontSize = 36.sp,\n                                color = Color.White.copy(alpha = 0.8f),\n                                modifier = Modifier.width(40.dp)\n                            )\n                            Spacer(modifier = Modifier.width(16.dp))\n                            AsyncImage(\n                                model = song.thumbnailUrl,\n                                contentDescription = \"Album art\",\n                                modifier = Modifier\n                                    .size(64.dp)\n                                    .clip(RoundedCornerShape(3.dp)),\n                                contentScale = ContentScale.Crop\n                            )\n                            Spacer(modifier = Modifier.width(16.dp))\n                            Column {\n                                Text(\n                                    text = song.title,\n                                    color = Color.White,\n                                    fontWeight = FontWeight.Bold,\n                                    fontSize = 16.sp\n                                )\n                                Text(\n                                    text = song.artists.joinToString(\", \") { it.name }.ifBlank { song.artistName.orEmpty() },\n                                    color = Color.White.copy(alpha = 0.7f),\n                                    fontSize = 14.sp\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/pages/WrappedTopArtistScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.wrapped.pages\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.slideInVertically\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport coil3.compose.AsyncImage\nimport coil3.request.ImageRequest\nimport com.metrolist.music.R\nimport com.metrolist.music.db.entities.Artist\n\n@Composable\nfun WrappedTopArtistScreen(topArtist: Artist?, isVisible: Boolean) {\n    var visible by remember { mutableStateOf(false) }\n    LaunchedEffect(isVisible) {\n        if (isVisible) {\n            visible = true\n        }\n    }\n\n    Column(\n        modifier = Modifier\n            .fillMaxSize()\n            .padding(32.dp),\n        horizontalAlignment = Alignment.CenterHorizontally,\n        verticalArrangement = Arrangement.Center\n    ) {\n        AnimatedVisibility(\n            visible = visible,\n            enter = fadeIn(animationSpec = tween(1000, delayMillis = 200)) + slideInVertically(animationSpec = tween(1000, delayMillis = 200))\n        ) {\n            Text(\n                text = stringResource(id = R.string.wrapped_top_artist_title),\n                style = MaterialTheme.typography.headlineSmall,\n                color = Color.White,\n                textAlign = TextAlign.Center\n            )\n        }\n\n        Spacer(modifier = Modifier.height(32.dp))\n\n        AnimatedVisibility(\n            visible = visible,\n            enter = fadeIn(animationSpec = tween(1000, delayMillis = 400)) + slideInVertically(animationSpec = tween(1000, delayMillis = 400))\n        ) {\n            AsyncImage(\n                model = ImageRequest.Builder(LocalContext.current)\n                    .data(topArtist?.artist?.thumbnailUrl)\n                    .build(),\n                contentDescription = stringResource(id = R.string.wrapped_top_artist_image_content_description),\n                modifier = Modifier\n                    .size(200.dp)\n                    .clip(CircleShape),\n                contentScale = ContentScale.Crop\n            )\n        }\n\n        Spacer(modifier = Modifier.height(16.dp))\n\n        AnimatedVisibility(\n            visible = visible,\n            enter = fadeIn(animationSpec = tween(1000, delayMillis = 600)) + slideInVertically(animationSpec = tween(1000, delayMillis = 600))\n        ) {\n            Text(\n                text = topArtist?.artist?.name ?: stringResource(id = R.string.wrapped_no_data),\n                style = MaterialTheme.typography.headlineMedium,\n                color = Color.White,\n                fontWeight = FontWeight.Bold,\n                textAlign = TextAlign.Center\n            )\n        }\n\n        AnimatedVisibility(\n            visible = visible,\n            enter = fadeIn(animationSpec = tween(1000, delayMillis = 800)) + slideInVertically(animationSpec = tween(1000, delayMillis = 800))\n        ) {\n            Text(\n                text = stringResource(id = R.string.wrapped_top_artist_listening_time, topArtist?.timeListened?.div(60000) ?: 0),\n                style = MaterialTheme.typography.bodyLarge,\n                color = Color.White.copy(alpha = 0.8f),\n                textAlign = TextAlign.Center\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/pages/WrappedTopSongScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.wrapped.pages\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.slideInVertically\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.layout.ContentScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport coil3.compose.AsyncImage\nimport coil3.request.ImageRequest\nimport com.metrolist.music.R\nimport com.metrolist.music.db.entities.SongWithStats\nimport com.metrolist.music.ui.screens.wrapped.components.AnimatedDecorativeElement\nimport kotlin.random.Random\n\n@Composable\nfun WrappedTopSongScreen(topSong: SongWithStats?, isVisible: Boolean) {\n    var visible by remember { mutableStateOf(false) }\n    LaunchedEffect(isVisible) {\n        if (isVisible) {\n            visible = true\n        }\n    }\n\n    Box(modifier = Modifier.fillMaxSize()) {\n        Box(modifier = Modifier.align(Alignment.TopStart)) {\n            repeat(3) {\n                AnimatedDecorativeElement(\n                    Modifier.padding(start = (Random.nextInt(0, 100)).dp, top = (Random.nextInt(0, 100)).dp).size((Random.nextInt(20, 80)).dp),\n                    isVisible\n                )\n            }\n        }\n        Box(modifier = Modifier.align(Alignment.BottomEnd)) {\n            repeat(4) {\n                AnimatedDecorativeElement(\n                    Modifier.padding(end = (Random.nextInt(0, 120)).dp, bottom = (Random.nextInt(0, 120)).dp).size((Random.nextInt(20, 90)).dp),\n                    isVisible\n                )\n            }\n        }\n\n        Column(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(32.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.Center\n        ) {\n            AnimatedVisibility(\n                visible = visible,\n                enter = fadeIn(animationSpec = tween(1000, delayMillis = 200)) + slideInVertically(animationSpec = tween(1000, delayMillis = 200))\n            ) {\n                Text(\n                    text = stringResource(id = R.string.wrapped_top_song_title),\n                    style = MaterialTheme.typography.headlineSmall,\n                    color = Color.White,\n                    textAlign = TextAlign.Center\n                )\n            }\n\n            Spacer(modifier = Modifier.height(32.dp))\n\n            AnimatedVisibility(\n                visible = visible,\n                enter = fadeIn(animationSpec = tween(1000, delayMillis = 400)) + slideInVertically(animationSpec = tween(1000, delayMillis = 400))\n            ) {\n                AsyncImage(\n                    model = ImageRequest.Builder(LocalContext.current)\n                        .data(topSong?.thumbnailUrl)\n                        .build(),\n                contentDescription = stringResource(id = R.string.wrapped_top_song_album_art_content_description),\n                    modifier = Modifier\n                        .size(200.dp)\n                        .clip(RoundedCornerShape(3.dp)),\n                    contentScale = ContentScale.Crop\n                )\n            }\n\n            Spacer(modifier = Modifier.height(16.dp))\n\n            AnimatedVisibility(\n                visible = visible,\n                enter = fadeIn(animationSpec = tween(1000, delayMillis = 600)) + slideInVertically(animationSpec = tween(1000, delayMillis = 600))\n            ) {\n                Text(\n                text = topSong?.title ?: stringResource(id = R.string.wrapped_no_data),\n                    style = MaterialTheme.typography.headlineMedium,\n                    color = Color.White,\n                    fontWeight = FontWeight.Bold,\n                    textAlign = TextAlign.Center\n                )\n            }\n\n            // Artists are not available in SongWithStats, so this part is removed.\n            // A possible improvement would be to fetch artist data separately.\n\n            Spacer(modifier = Modifier.height(8.dp))\n\n            AnimatedVisibility(\n                visible = visible,\n                enter = fadeIn(animationSpec = tween(1000, delayMillis = 1000)) + slideInVertically(animationSpec = tween(1000, delayMillis = 1000))\n            ) {\n                Text(\n                text = stringResource(id = R.string.wrapped_top_song_listening_time, topSong?.timeListened?.div(60000) ?: 0),\n                    style = MaterialTheme.typography.bodyLarge,\n                    color = Color.White.copy(alpha = 0.8f),\n                    textAlign = TextAlign.Center\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/pages/WrappedTotalArtistsScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.wrapped.pages\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.FastOutSlowInEasing\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.rememberTextMeasurer\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.metrolist.music.R\nimport com.metrolist.music.ui.screens.wrapped.components.AnimatedDecorativeElement\nimport com.metrolist.music.ui.theme.bbh_bartle\nimport kotlin.random.Random\n\n@Composable\nfun WrappedTotalArtistsScreen(\n    uniqueArtistCount: Int,\n    isVisible: Boolean\n) {\n    val animatedArtists = remember { Animatable(0f) }\n    val textMeasurer = rememberTextMeasurer()\n\n    LaunchedEffect(isVisible, uniqueArtistCount) {\n        if (isVisible && uniqueArtistCount > 0) {\n            animatedArtists.animateTo(targetValue = uniqueArtistCount.toFloat(), animationSpec = tween(1500, easing = FastOutSlowInEasing))\n        }\n    }\n\n    Box(modifier = Modifier.fillMaxSize()) {\n        Box(modifier = Modifier.align(Alignment.TopStart)) {\n            repeat(5) {\n                AnimatedDecorativeElement(\n                    Modifier.padding(start = (Random.nextInt(0, 150)).dp, top = (Random.nextInt(0, 150)).dp).size((Random.nextInt(20, 100)).dp),\n                    isVisible\n                )\n            }\n        }\n        Box(modifier = Modifier.align(Alignment.BottomEnd)) {\n            repeat(5) {\n                AnimatedDecorativeElement(\n                    Modifier.padding(end = (Random.nextInt(0, 150)).dp, bottom = (Random.nextInt(0, 150)).dp).size((Random.nextInt(20, 100)).dp),\n                    isVisible\n                )\n            }\n        }\n\n        Column(\n            modifier = Modifier.fillMaxSize().padding(vertical = 32.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.Center\n        ) {\n            Text(\n                text = stringResource(id = R.string.wrapped_total_artists_title),\n                modifier = Modifier.padding(horizontal = 24.dp),\n                style = MaterialTheme.typography.headlineSmall.copy(color = Color.White, textAlign = TextAlign.Center)\n            )\n            Spacer(Modifier.height(32.dp))\n            BoxWithConstraints(Modifier.fillMaxWidth().padding(horizontal = 16.dp)) {\n                val density = LocalDensity.current\n                val baseStyle = MaterialTheme.typography.displayLarge.copy(\n                    color = Color.White, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center,\n                    fontFamily = bbh_bartle, drawStyle = Stroke(with(density) { 2.dp.toPx() })\n                )\n                val textStyle = remember(uniqueArtistCount, maxWidth) {\n                    val finalText = uniqueArtistCount.toString()\n                    var style = baseStyle.copy(fontSize = 96.sp)\n                    var textWidth = textMeasurer.measure(finalText, style).size.width\n                    while (textWidth > constraints.maxWidth) {\n                        style = style.copy(fontSize = style.fontSize * 0.95f)\n                        textWidth = textMeasurer.measure(finalText, style).size.width\n                    }\n                    style.copy(lineHeight = style.fontSize * 1.08f)\n                }\n                Text(\n                    text = animatedArtists.value.toInt().toString(),\n                    style = textStyle,\n                    maxLines = 1,\n                    modifier = Modifier.fillMaxWidth(),\n                    textAlign = TextAlign.Center\n                )\n            }\n            Spacer(Modifier.height(16.dp))\n            Text(\n                text = stringResource(id = R.string.wrapped_total_artists_subtitle),\n                modifier = Modifier.padding(horizontal = 24.dp),\n                style = MaterialTheme.typography.bodyLarge.copy(color = Color.White.copy(alpha = 0.8f), textAlign = TextAlign.Center)\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/screens/wrapped/pages/WrappedTotalSongsScreen.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.screens.wrapped.pages\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.FastOutSlowInEasing\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxWithConstraints\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.rememberTextMeasurer\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.metrolist.music.R\nimport com.metrolist.music.ui.screens.wrapped.components.AnimatedBackground\nimport com.metrolist.music.ui.screens.wrapped.components.ShapeType\nimport com.metrolist.music.ui.theme.bbh_bartle\n\n@Composable\nfun WrappedTotalSongsScreen(\n    uniqueSongCount: Int,\n    isVisible: Boolean\n) {\n    val animatedSongs = remember { Animatable(0f) }\n    val textMeasurer = rememberTextMeasurer()\n\n    LaunchedEffect(isVisible, uniqueSongCount) {\n        if (isVisible && uniqueSongCount > 0) {\n            animatedSongs.animateTo(targetValue = uniqueSongCount.toFloat(), animationSpec = tween(1500, easing = FastOutSlowInEasing))\n        }\n    }\n\n    Box(modifier = Modifier.fillMaxSize()) {\n        AnimatedBackground(shapeTypes = listOf(ShapeType.Line))\n        Column(\n            modifier = Modifier.fillMaxSize().padding(vertical = 32.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.Center\n        ) {\n            Text(\n                text = stringResource(id = R.string.wrapped_total_songs_title),\n                modifier = Modifier.padding(horizontal = 24.dp),\n                style = MaterialTheme.typography.headlineSmall.copy(color = Color.White, textAlign = TextAlign.Center)\n            )\n            Spacer(Modifier.height(32.dp))\n            BoxWithConstraints(Modifier.fillMaxWidth().padding(horizontal = 16.dp)) {\n                val density = LocalDensity.current\n                val baseStyle = MaterialTheme.typography.displayLarge.copy(\n                    color = Color.White, fontWeight = FontWeight.Bold, textAlign = TextAlign.Center,\n                    fontFamily = bbh_bartle, drawStyle = Stroke(with(density) { 2.dp.toPx() })\n                )\n                val textStyle = remember(uniqueSongCount, maxWidth) {\n                    val finalText = uniqueSongCount.toString()\n                    var style = baseStyle.copy(fontSize = 96.sp)\n                    var textWidth = textMeasurer.measure(finalText, style).size.width\n                    while (textWidth > constraints.maxWidth) {\n                        style = style.copy(fontSize = style.fontSize * 0.95f)\n                        textWidth = textMeasurer.measure(finalText, style).size.width\n                    }\n                    style.copy(lineHeight = style.fontSize * 1.08f)\n                }\n                Text(\n                    text = animatedSongs.value.toInt().toString(),\n                    style = textStyle,\n                    maxLines = 1,\n                    modifier = Modifier.fillMaxWidth(),\n                    textAlign = TextAlign.Center\n                )\n            }\n            Spacer(Modifier.height(16.dp))\n            Text(\n                text = stringResource(id = R.string.wrapped_total_songs_subtitle),\n                modifier = Modifier.padding(horizontal = 24.dp),\n                style = MaterialTheme.typography.bodyLarge.copy(color = Color.White.copy(alpha = 0.8f), textAlign = TextAlign.Center)\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/theme/Font.kt",
    "content": "package com.metrolist.music.ui.theme\n\nimport androidx.compose.ui.text.font.Font\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontWeight\nimport com.metrolist.music.R\n\nval bbhBartle = FontFamily(\n    Font(R.font.bbh_bartle_regular, FontWeight.Normal)\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/theme/PlayerColorExtractor.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.theme\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.palette.graphics.Palette\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\n/**\n * Player color extraction system for generating gradients from album artwork\n * \n * This system analyzes album artwork and extracts vibrant, dominant colors\n * to create visually appealing gradients for the music player interface.\n */\nobject PlayerColorExtractor {\n\n    /**\n     * Extracts colors from a palette and creates a gradient\n     * \n     * @param palette The color palette extracted from album artwork\n     * @param fallbackColor Fallback color to use if extraction fails\n     * @return List of colors for gradient (primary, darker variant, black)\n     */\n    suspend fun extractGradientColors(\n        palette: Palette,\n        fallbackColor: Int\n    ): List<Color> = withContext(Dispatchers.Default) {\n        \n        // Extract all available colors with priority for dominant colors\n        val colorCandidates = listOfNotNull(\n            palette.dominantSwatch, // High priority for dominant color\n            palette.vibrantSwatch,\n            palette.darkVibrantSwatch,\n            palette.lightVibrantSwatch,\n            palette.mutedSwatch,\n            palette.darkMutedSwatch,\n            palette.lightMutedSwatch\n        )\n\n        // Select best color based on weight (dominance + vibrancy)\n        val bestSwatch = colorCandidates.maxByOrNull { calculateColorWeight(it) }\n        val fallbackDominant = palette.dominantSwatch?.rgb?.let { Color(it) }\n            ?: Color(palette.getDominantColor(fallbackColor))\n\n        val primaryColor = if (bestSwatch != null) {\n            val bestColor = Color(bestSwatch.rgb)\n            // Ensure the color is suitable for use\n            if (isColorVibrant(bestColor)) {\n                enhanceColorVividness(bestColor, 1.3f)\n            } else {\n                // If not vibrant, use dominant color with slight enhancement\n                enhanceColorVividness(fallbackDominant, 1.1f)\n            }\n        } else {\n            enhanceColorVividness(fallbackDominant, 1.1f)\n        }\n        \n        // Create sophisticated gradient with 3 color points\n        listOf(\n            primaryColor, // Start: primary vibrant color\n            primaryColor.copy(\n                red = (primaryColor.red * 0.6f).coerceAtLeast(0f),\n                green = (primaryColor.green * 0.6f).coerceAtLeast(0f),\n                blue = (primaryColor.blue * 0.6f).coerceAtLeast(0f)\n            ), // Middle: darker version of primary color\n            Color.Black // End: black\n        )\n    }\n\n    /**\n     * Determines if a color is vibrant enough for use in player UI\n     * \n     * @param color The color to analyze\n     * @return true if the color has sufficient saturation and brightness\n     */\n    private fun isColorVibrant(color: Color): Boolean {\n        val argb = color.toArgb()\n        val hsv = FloatArray(3)\n        android.graphics.Color.colorToHSV(argb, hsv)\n        val saturation = hsv[1] // HSV[1] is saturation\n        val brightness = hsv[2] // HSV[2] is brightness\n        \n        // Color is vibrant if it has sufficient saturation and appropriate brightness\n        // Avoid colors that are too dark or too bright\n        return saturation > 0.25f && brightness > 0.2f && brightness < 0.9f\n    }\n    \n    /**\n     * Enhances color vividness by adjusting saturation and brightness\n     * \n     * @param color The color to enhance\n     * @param saturationFactor Factor to multiply saturation by (default 1.4)\n     * @return Enhanced color with improved vividness\n     */\n    private fun enhanceColorVividness(color: Color, saturationFactor: Float = 1.4f): Color {\n        val argb = color.toArgb()\n        val hsv = FloatArray(3)\n        android.graphics.Color.colorToHSV(argb, hsv)\n        \n        // Increase saturation for more vivid colors\n        hsv[1] = (hsv[1] * saturationFactor).coerceAtMost(1.0f)\n        // Adjust brightness for better visibility\n        hsv[2] = (hsv[2] * 0.9f).coerceIn(0.4f, 0.85f)\n        \n        return Color(android.graphics.Color.HSVToColor(hsv))\n    }\n\n    /**\n     * Calculates weight for color selection based on dominance and vibrancy\n     * \n     * @param swatch The palette swatch to analyze\n     * @return Weight value for color selection priority\n     */\n    private fun calculateColorWeight(swatch: Palette.Swatch?): Float {\n        if (swatch == null) return 0f\n        val population = swatch.population.toFloat()\n        val color = Color(swatch.rgb)\n        val argb = color.toArgb()\n        val hsv = FloatArray(3)\n        android.graphics.Color.colorToHSV(argb, hsv)\n        val saturation = hsv[1]\n        val brightness = hsv[2]\n        \n        // Give higher priority to dominance (population) while considering vibrancy\n        val populationWeight = population * 2f // Double dominance weight\n        val vibrancyBonus = if (saturation > 0.3f && brightness > 0.3f) 1.5f else 1f\n        \n        return populationWeight * vibrancyBonus * (saturation + brightness) / 2f\n    }\n\n    /**\n     * Configuration constants for color extraction\n     */\n    object Config {\n        const val MAX_COLOR_COUNT = 32\n        const val BITMAP_AREA = 8000\n        const val IMAGE_SIZE = 200\n        \n        // Color enhancement factors\n        const val VIBRANT_SATURATION_THRESHOLD = 0.25f\n        const val VIBRANT_BRIGHTNESS_MIN = 0.2f\n        const val VIBRANT_BRIGHTNESS_MAX = 0.9f\n        \n        const val POPULATION_WEIGHT_MULTIPLIER = 2f\n        const val VIBRANCY_THRESHOLD_SATURATION = 0.3f\n        const val VIBRANCY_THRESHOLD_BRIGHTNESS = 0.3f\n        const val VIBRANCY_BONUS = 1.5f\n        \n        const val DEFAULT_SATURATION_FACTOR = 1.4f\n        const val VIBRANT_SATURATION_FACTOR = 1.3f\n        const val FALLBACK_SATURATION_FACTOR = 1.1f\n        \n        const val BRIGHTNESS_MULTIPLIER = 0.9f\n        const val BRIGHTNESS_MIN = 0.4f\n        const val BRIGHTNESS_MAX = 0.85f\n        \n        const val DARKER_VARIANT_FACTOR = 0.6f\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/theme/PlayerSliderColors.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.theme\n\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.SliderColors\nimport androidx.compose.material3.SliderDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.graphics.Color\nimport com.metrolist.music.constants.PlayerBackgroundStyle\n\n/**\n * Player slider color configuration for consistent styling across all slider types\n * \n * This object provides standardized color schemes for Default, Squiggly, and Slim sliders\n * used in the music player interface, ensuring visual consistency and proper contrast.\n */\nobject PlayerSliderColors {\n\n    /**\n     * Standard slider colors for all slider types\n     * \n     * @param activeColor Color for active track, ticks, and thumb\n     * @param playerBackground The player background style\n     * @param useDarkTheme Whether dark theme is being used\n     * @return SliderColors configuration\n     */\n    @Composable\n    fun getSliderColors(\n        activeColor: Color,\n        playerBackground: PlayerBackgroundStyle,\n        useDarkTheme: Boolean\n    ): SliderColors {\n        val inactiveTrackColor = when (playerBackground) {\n            PlayerBackgroundStyle.DEFAULT -> {\n                if (useDarkTheme) {\n                    MaterialTheme.colorScheme.outline.copy(alpha = 0.4f)\n                } else {\n                    MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f)\n                }\n            }\n            PlayerBackgroundStyle.BLUR, PlayerBackgroundStyle.GRADIENT -> {\n                Color.White.copy(alpha = 0.4f)\n            }\n        }\n        \n        return SliderDefaults.colors(\n            activeTrackColor = activeColor,\n            activeTickColor = activeColor,\n            thumbColor = activeColor,\n            inactiveTrackColor = inactiveTrackColor,\n            disabledActiveTrackColor = activeColor,\n            disabledInactiveTrackColor = inactiveTrackColor,\n            disabledThumbColor = activeColor\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/theme/Theme.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.theme\n\nimport android.graphics.Bitmap\nimport android.os.Build\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.material3.ColorScheme\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.dynamicDarkColorScheme\nimport androidx.compose.material3.dynamicLightColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.Saver\nimport androidx.compose.runtime.saveable.SaverScope\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.luminance\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.palette.graphics.Palette\nimport com.materialkolor.PaletteStyle\nimport com.materialkolor.dynamiccolor.ColorSpec\nimport com.materialkolor.rememberDynamicColorScheme\nimport com.materialkolor.score.Score\n\nval DefaultThemeColor = Color(0xFFED5564)\n\n@Composable\nfun MetrolistTheme(\n    darkTheme: Boolean = isSystemInDarkTheme(),\n    pureBlack: Boolean = false,\n    themeColor: Color = DefaultThemeColor,\n    content: @Composable () -> Unit,\n) {\n    val context = LocalContext.current\n    // Determine if system dynamic colors should be used (Android S+ and default theme color)\n    val useSystemDynamicColor = (themeColor == DefaultThemeColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)\n\n    // Select the appropriate color scheme generation method\n    val baseColorScheme = if (useSystemDynamicColor) {\n        // Use standard Material 3 dynamic color functions for system wallpaper colors\n        if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)\n    } else {\n        // Use materialKolor only when a specific seed color is provided\n        rememberDynamicColorScheme(\n            seedColor = themeColor, // themeColor is guaranteed non-default here\n            isDark = darkTheme,\n            specVersion = ColorSpec.SpecVersion.SPEC_2025,\n            style = PaletteStyle.TonalSpot // Keep existing style\n        )\n    }\n\n    // Apply pureBlack modification if needed, similar to original logic\n    val colorScheme = remember(baseColorScheme, pureBlack, darkTheme) {\n        if (darkTheme && pureBlack) {\n            baseColorScheme.pureBlack(true)\n        } else {\n            baseColorScheme\n        }\n    }\n\n    // Use standard MaterialTheme instead of MaterialExpressiveTheme\n    MaterialTheme(\n        colorScheme = colorScheme,\n        typography = AppTypography, // Use the defined AppTypography\n        content = content\n    )\n}\n\nfun Bitmap.extractThemeColor(): Color {\n    val colorsToPopulation = Palette.from(this)\n        .maximumColorCount(8)\n        .generate()\n        .swatches\n        .associate { it.rgb to it.population }\n    val rankedColors = Score.score(colorsToPopulation)\n    return Color(rankedColors.first())\n}\n\nfun Bitmap.extractGradientColors(): List<Color> {\n    val extractedColors = Palette.from(this)\n        .maximumColorCount(64)\n        .generate()\n        .swatches\n        .associate { it.rgb to it.population }\n\n    val orderedColors = Score.score(extractedColors, 2, 0xff4285f4.toInt(), true)\n        .sortedByDescending { Color(it).luminance() }\n\n    return if (orderedColors.size >= 2)\n        listOf(Color(orderedColors[0]), Color(orderedColors[1]))\n    else\n        listOf(Color(0xFF595959), Color(0xFF0D0D0D))\n}\n\nfun ColorScheme.pureBlack(apply: Boolean) =\n    if (apply) copy(\n        surface = Color.Black,\n        background = Color.Black\n    ) else this\n\nval ColorSaver = object : Saver<Color, Int> {\n    override fun restore(value: Int): Color = Color(value)\n    override fun SaverScope.save(value: Color): Int = value.toArgb()\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/theme/Type.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.theme\n\nimport androidx.compose.material3.Typography\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.sp\n\n// TODO: Define or import actual M3 Expressive font families if needed.\n// For now, using default FontFamily as a placeholder.\n\n// Define M3 Expressive Typography based on Material Design guidelines\n// https://m3.material.io/styles/typography/type-scale-tokens\n// Note: M3 Expressive might introduce subtle changes or new roles.\n// Referencing standard M3 roles for now, adjust if Expressive spec differs significantly.\nval AppTypography = Typography(\n    displayLarge = TextStyle(\n        fontFamily = FontFamily.Default,\n        fontWeight = FontWeight.Normal,\n        fontSize = 57.sp,\n        lineHeight = 64.sp,\n        letterSpacing = (-0.25).sp\n    ),\n    displayMedium = TextStyle(\n        fontFamily = FontFamily.Default,\n        fontWeight = FontWeight.Normal,\n        fontSize = 45.sp,\n        lineHeight = 52.sp,\n        letterSpacing = 0.sp\n    ),\n    displaySmall = TextStyle(\n        fontFamily = FontFamily.Default,\n        fontWeight = FontWeight.Normal,\n        fontSize = 36.sp,\n        lineHeight = 44.sp,\n        letterSpacing = 0.sp\n    ),\n    headlineLarge = TextStyle(\n        fontFamily = FontFamily.Default,\n        fontWeight = FontWeight.Normal,\n        fontSize = 32.sp,\n        lineHeight = 40.sp,\n        letterSpacing = 0.sp\n    ),\n    headlineMedium = TextStyle(\n        fontFamily = FontFamily.Default,\n        fontWeight = FontWeight.Normal,\n        fontSize = 28.sp,\n        lineHeight = 36.sp,\n        letterSpacing = 0.sp\n    ),\n    headlineSmall = TextStyle(\n        fontFamily = FontFamily.Default,\n        fontWeight = FontWeight.Normal,\n        fontSize = 24.sp,\n        lineHeight = 32.sp,\n        letterSpacing = 0.sp\n    ),\n    titleLarge = TextStyle(\n        fontFamily = FontFamily.Default,\n        fontWeight = FontWeight.Normal, // M3 uses Normal, M2 used Medium\n        fontSize = 22.sp,\n        lineHeight = 28.sp,\n        letterSpacing = 0.sp\n    ),\n    titleMedium = TextStyle(\n        fontFamily = FontFamily.Default,\n        fontWeight = FontWeight.Medium,\n        fontSize = 16.sp,\n        lineHeight = 24.sp,\n        letterSpacing = 0.15.sp\n    ),\n    titleSmall = TextStyle(\n        fontFamily = FontFamily.Default,\n        fontWeight = FontWeight.Medium,\n        fontSize = 14.sp,\n        lineHeight = 20.sp,\n        letterSpacing = 0.1.sp\n    ),\n    bodyLarge = TextStyle(\n        fontFamily = FontFamily.Default,\n        fontWeight = FontWeight.Normal,\n        fontSize = 16.sp,\n        lineHeight = 24.sp,\n        letterSpacing = 0.5.sp // M3 uses 0.5, M2 used 0.15\n    ),\n    bodyMedium = TextStyle(\n        fontFamily = FontFamily.Default,\n        fontWeight = FontWeight.Normal,\n        fontSize = 14.sp,\n        lineHeight = 20.sp,\n        letterSpacing = 0.25.sp\n    ),\n    bodySmall = TextStyle(\n        fontFamily = FontFamily.Default,\n        fontWeight = FontWeight.Normal,\n        fontSize = 12.sp,\n        lineHeight = 16.sp,\n        letterSpacing = 0.4.sp\n    ),\n    labelLarge = TextStyle(\n        fontFamily = FontFamily.Default,\n        fontWeight = FontWeight.Medium,\n        fontSize = 14.sp,\n        lineHeight = 20.sp,\n        letterSpacing = 0.1.sp\n    ),\n    labelMedium = TextStyle(\n        fontFamily = FontFamily.Default,\n        fontWeight = FontWeight.Medium,\n        fontSize = 12.sp,\n        lineHeight = 16.sp,\n        letterSpacing = 0.5.sp\n    ),\n    labelSmall = TextStyle(\n        fontFamily = FontFamily.Default,\n        fontWeight = FontWeight.Medium,\n        fontSize = 11.sp,\n        lineHeight = 16.sp,\n        letterSpacing = 0.5.sp\n    )\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/theme/bbh_bartle.kt",
    "content": "package com.metrolist.music.ui.theme\n\nimport androidx.compose.ui.text.font.Font\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontWeight\nimport com.metrolist.music.R\n\nval bbh_bartle = FontFamily(\n    Font(R.font.bbh_bartle_regular, FontWeight.Normal)\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/utils/AppBar.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.utils\n\nimport androidx.compose.animation.core.AnimationSpec\nimport androidx.compose.animation.core.DecayAnimationSpec\nimport androidx.compose.animation.core.Spring\nimport androidx.compose.animation.core.animate\nimport androidx.compose.animation.core.spring\nimport androidx.compose.animation.rememberSplineBasedDecay\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.TopAppBarScrollBehavior\nimport androidx.compose.material3.TopAppBarState\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.input.nestedscroll.NestedScrollConnection\nimport androidx.compose.ui.input.nestedscroll.NestedScrollSource\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun appBarScrollBehavior(\n    state: TopAppBarState = rememberTopAppBarState(),\n    canScroll: () -> Boolean = { true },\n    snapAnimationSpec: AnimationSpec<Float>? = spring(stiffness = Spring.StiffnessMediumLow),\n    flingAnimationSpec: DecayAnimationSpec<Float>? = rememberSplineBasedDecay(),\n): TopAppBarScrollBehavior =\n    AppBarScrollBehavior(\n        state = state,\n        snapAnimationSpec = snapAnimationSpec,\n        flingAnimationSpec = flingAnimationSpec,\n        canScroll = canScroll,\n    )\n\n@ExperimentalMaterial3Api\nclass AppBarScrollBehavior(\n    override val state: TopAppBarState,\n    override val snapAnimationSpec: AnimationSpec<Float>?,\n    override val flingAnimationSpec: DecayAnimationSpec<Float>?,\n    val canScroll: () -> Boolean = { true },\n) : TopAppBarScrollBehavior {\n    override val isPinned: Boolean = true\n    override var nestedScrollConnection =\n        object : NestedScrollConnection {\n            override fun onPostScroll(\n                consumed: Offset,\n                available: Offset,\n                source: NestedScrollSource,\n            ): Offset {\n                if (!canScroll()) return Offset.Zero\n                state.contentOffset += consumed.y\n                if (state.heightOffset == 0f || state.heightOffset == state.heightOffsetLimit) {\n                    if (consumed.y == 0f && available.y > 0f) {\n                        // Reset the total content offset to zero when scrolling all the way down.\n                        // This will eliminate some float precision inaccuracies.\n                        state.contentOffset = 0f\n                    }\n                }\n                state.heightOffset += consumed.y\n                return Offset.Zero\n            }\n        }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\nsuspend fun TopAppBarState.resetHeightOffset() {\n    if (heightOffset != 0f) {\n        animate(\n            initialValue = heightOffset,\n            targetValue = 0f,\n        ) { value, _ ->\n            heightOffset = value\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/utils/FadingEdge.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.utils\n\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.drawWithContent\nimport androidx.compose.ui.graphics.BlendMode\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.unit.Dp\n\nfun Modifier.fadingEdge(\n    left: Dp? = null,\n    top: Dp? = null,\n    right: Dp? = null,\n    bottom: Dp? = null,\n) = graphicsLayer(alpha = 0.99f)\n    .drawWithContent {\n        drawContent()\n        if (top != null) {\n            drawRect(\n                brush =\n                Brush.verticalGradient(\n                    colors =\n                    listOf(\n                        Color.Transparent,\n                        Color.Black,\n                    ),\n                    startY = 0f,\n                    endY = top.toPx(),\n                ),\n                blendMode = BlendMode.DstIn,\n            )\n        }\n        if (bottom != null) {\n            drawRect(\n                brush =\n                Brush.verticalGradient(\n                    colors =\n                    listOf(\n                        Color.Black,\n                        Color.Transparent,\n                    ),\n                    startY = size.height - bottom.toPx(),\n                    endY = size.height,\n                ),\n                blendMode = BlendMode.DstIn,\n            )\n        }\n        if (left != null) {\n            drawRect(\n                brush =\n                Brush.horizontalGradient(\n                    colors =\n                    listOf(\n                        Color.Black,\n                        Color.Transparent,\n                    ),\n                    startX = 0f,\n                    endX = left.toPx(),\n                ),\n                blendMode = BlendMode.DstIn,\n            )\n        }\n        if (right != null) {\n            drawRect(\n                brush =\n                Brush.horizontalGradient(\n                    colors =\n                    listOf(\n                        Color.Transparent,\n                        Color.Black,\n                    ),\n                    startX = size.width - right.toPx(),\n                    endX = size.width,\n                ),\n                blendMode = BlendMode.DstIn,\n            )\n        }\n    }\n\nfun Modifier.fadingEdge(\n    horizontal: Dp? = null,\n    vertical: Dp? = null,\n) = fadingEdge(\n    left = horizontal,\n    right = horizontal,\n    top = vertical,\n    bottom = vertical,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/utils/ItemWrapper.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.utils\n\nimport androidx.compose.runtime.mutableStateOf\n\nclass ItemWrapper<T>(\n    val item: T,\n) {\n    private val _isSelected = mutableStateOf(true)\n\n    var isSelected: Boolean\n        get() = _isSelected.value\n        set(value) {\n            _isSelected.value = value\n        }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/utils/KeyUtils.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.utils\n\nimport java.util.concurrent.atomic.AtomicLong\n\n/**\n * Utility object for generating unique keys in LazyColumn/LazyRow to prevent duplicate key errors\n */\nobject KeyUtils {\n    private val counter = AtomicLong(0)\n    \n    /**\n     * Generates a unique key by combining a base identifier with a unique counter\n     * This prevents duplicate keys in LazyColumn/LazyRow implementations\n     */\n    fun generateUniqueKey(baseId: String, prefix: String = \"\"): String {\n        val uniqueId = counter.incrementAndGet()\n        return if (prefix.isNotEmpty()) {\n            \"${prefix}_${baseId}_$uniqueId\"\n        } else {\n            \"${baseId}_$uniqueId\"\n        }\n    }\n    \n    /**\n     * Generates a unique key for items in a list with their index\n     * Useful for preventing duplicate keys when items might have the same ID\n     */\n    fun generateIndexedKey(baseId: String, index: Int, prefix: String = \"\"): String {\n        val uniqueId = counter.incrementAndGet()\n        return if (prefix.isNotEmpty()) {\n            \"${prefix}_${baseId}_${index}_$uniqueId\"\n        } else {\n            \"${baseId}_${index}_$uniqueId\"\n        }\n    }\n    \n    /**\n     * Generates a timestamp-based unique key for dynamic content\n     * Useful for content that changes frequently\n     */\n    fun generateTimestampKey(baseId: String, prefix: String = \"\"): String {\n        val timestamp = System.currentTimeMillis()\n        val uniqueId = counter.incrementAndGet()\n        return if (prefix.isNotEmpty()) {\n            \"${prefix}_${baseId}_${timestamp}_$uniqueId\"\n        } else {\n            \"${baseId}_${timestamp}_$uniqueId\"\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/utils/LazyGridSnapLayoutInfoProvider.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.utils\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.gestures.Orientation\nimport androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider\nimport androidx.compose.foundation.lazy.grid.LazyGridItemInfo\nimport androidx.compose.foundation.lazy.grid.LazyGridLayoutInfo\nimport androidx.compose.foundation.lazy.grid.LazyGridState\n\n@ExperimentalFoundationApi\nfun SnapLayoutInfoProvider(\n    lazyGridState: LazyGridState,\n    positionInLayout: (layoutSize: Float, itemSize: Float) -> Float = { layoutSize, itemSize ->\n        (layoutSize / 2f - itemSize / 2f)\n    },\n): SnapLayoutInfoProvider = object : SnapLayoutInfoProvider {\n    private val layoutInfo: LazyGridLayoutInfo\n        get() = lazyGridState.layoutInfo\n\n    override fun calculateApproachOffset(velocity: Float, decayOffset: Float): Float = 0f\n\n    override fun calculateSnapOffset(velocity: Float): Float {\n        val bounds = calculateSnappingOffsetBounds()\n        return when {\n            velocity < 0 -> bounds.start\n            velocity > 0 -> bounds.endInclusive\n            else -> 0f\n        }\n    }\n\n    fun calculateSnappingOffsetBounds(): ClosedFloatingPointRange<Float> {\n        var lowerBoundOffset = Float.NEGATIVE_INFINITY\n        var upperBoundOffset = Float.POSITIVE_INFINITY\n\n        layoutInfo.visibleItemsInfo.forEach { item ->\n            val offset = calculateDistanceToDesiredSnapPosition(layoutInfo, item, positionInLayout)\n\n            if (offset <= 0 && offset > lowerBoundOffset) {\n                lowerBoundOffset = offset\n            }\n\n            if (offset >= 0 && offset < upperBoundOffset) {\n                upperBoundOffset = offset\n            }\n        }\n\n        return lowerBoundOffset.rangeTo(upperBoundOffset)\n    }\n}\n\nfun calculateDistanceToDesiredSnapPosition(\n    layoutInfo: LazyGridLayoutInfo,\n    item: LazyGridItemInfo,\n    positionInLayout: (layoutSize: Float, itemSize: Float) -> Float,\n): Float {\n    val containerSize =\n        layoutInfo.singleAxisViewportSize - layoutInfo.beforeContentPadding - layoutInfo.afterContentPadding\n\n    val desiredDistance = positionInLayout(containerSize.toFloat(), item.size.width.toFloat())\n    val itemCurrentPosition = item.offset.x.toFloat()\n\n    return itemCurrentPosition - desiredDistance\n}\n\nprivate val LazyGridLayoutInfo.singleAxisViewportSize: Int\n    get() = if (orientation == Orientation.Vertical) viewportSize.height else viewportSize.width\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/utils/NavControllerUtils.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.utils\n\nimport androidx.navigation.NavController\nimport com.metrolist.music.ui.screens.Screens\n\nfun NavController.backToMain() {\n    val mainRoutes = Screens.MainScreens.map { it.route }\n\n    while (previousBackStackEntry != null &&\n        currentBackStackEntry?.destination?.route !in mainRoutes\n    ) {\n        popBackStack()\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/utils/ScrollUtils.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.utils\n\nimport androidx.compose.foundation.ScrollState\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.foundation.lazy.grid.LazyGridState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\n\n@Composable\nfun LazyListState.isScrollingUp(): Boolean {\n    var previousIndex by remember(this) { androidx.compose.runtime.mutableIntStateOf(firstVisibleItemIndex) }\n    var previousScrollOffset by remember(this) { androidx.compose.runtime.mutableIntStateOf(firstVisibleItemScrollOffset) }\n    return remember(this) {\n        derivedStateOf {\n            if (previousIndex != firstVisibleItemIndex) {\n                previousIndex > firstVisibleItemIndex\n            } else {\n                previousScrollOffset >= firstVisibleItemScrollOffset\n            }.also {\n                previousIndex = firstVisibleItemIndex\n                previousScrollOffset = firstVisibleItemScrollOffset\n            }\n        }\n    }.value\n}\n\n@Composable\nfun LazyGridState.isScrollingUp(): Boolean {\n    var previousIndex by remember(this) { androidx.compose.runtime.mutableIntStateOf(firstVisibleItemIndex) }\n    var previousScrollOffset by remember(this) { androidx.compose.runtime.mutableIntStateOf(firstVisibleItemScrollOffset) }\n    return remember(this) {\n        derivedStateOf {\n            if (previousIndex != firstVisibleItemIndex) {\n                previousIndex > firstVisibleItemIndex\n            } else {\n                previousScrollOffset >= firstVisibleItemScrollOffset\n            }.also {\n                previousIndex = firstVisibleItemIndex\n                previousScrollOffset = firstVisibleItemScrollOffset\n            }\n        }\n    }.value\n}\n\n@Composable\nfun ScrollState.isScrollingUp(): Boolean {\n    var previousScrollOffset by remember(this) { mutableIntStateOf(value) }\n    return remember(this) {\n        derivedStateOf {\n            (previousScrollOffset >= value).also {\n                previousScrollOffset = value\n            }\n        }\n    }.value\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/utils/ShapeUtils.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.utils\n\nimport androidx.compose.foundation.shape.CornerBasedShape\nimport androidx.compose.foundation.shape.CornerSize\nimport androidx.compose.ui.unit.dp\n\nfun CornerBasedShape.top(): CornerBasedShape =\n    copy(bottomStart = CornerSize(0.dp), bottomEnd = CornerSize(0.dp))\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/utils/ShowMediaInfo.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.utils\n\nimport android.content.ClipData\nimport android.content.ClipboardManager\nimport android.content.Context\nimport android.text.format.Formatter\nimport android.widget.Toast\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.MediaInfo\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.LocalPlayerConnection\nimport com.metrolist.music.R\nimport com.metrolist.music.db.entities.FormatEntity\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.ui.component.Material3SettingsGroup\nimport com.metrolist.music.ui.component.Material3SettingsItem\nimport com.metrolist.music.ui.component.shimmer.ShimmerHost\nimport com.metrolist.music.ui.component.shimmer.TextPlaceholder\n\n@Composable\nfun ShowMediaInfo(videoId: String) {\n    if (videoId.isBlank() || videoId.isEmpty()) return\n\n    val windowInsets = WindowInsets.systemBars\n\n    var info by remember {\n        mutableStateOf<MediaInfo?>(null)\n    }\n\n    val database = LocalDatabase.current\n    var song by remember { mutableStateOf<Song?>(null) }\n\n    var currentFormat by remember { mutableStateOf<FormatEntity?>(null) }\n\n    val playerConnection = LocalPlayerConnection.current\n    val context = LocalContext.current\n\n    LaunchedEffect(Unit, videoId) {\n        info = YouTube.getMediaInfo(videoId).getOrNull()\n    }\n\n    LaunchedEffect(Unit, videoId) {\n        database.song(videoId).collect {\n            song = it\n        }\n    }\n\n    LaunchedEffect(Unit, videoId) {\n        database.format(videoId).collect {\n            currentFormat = it\n        }\n    }\n\n    LazyColumn(\n        state = rememberLazyListState(),\n        modifier = Modifier\n            .padding(\n                windowInsets\n                    .asPaddingValues()\n            )\n            .fillMaxSize()\n            .background(MaterialTheme.colorScheme.background)\n    ) {\n        if (info != null && song != null) {\n            item(contentType = \"MediaDetails\") {\n                Column {\n                    val baseList = listOf(\n                        stringResource(R.string.song_title) to song?.title,\n                        stringResource(R.string.song_artists) to song?.artists?.joinToString { it.name },\n                        stringResource(R.string.media_id) to song?.id\n                    )\n\n                    val baseIconsList = listOf(\n                        R.drawable.music_note,\n                        R.drawable.person,\n                        R.drawable.media3_icon_bookmark_filled,\n                    )\n\n                    val iconsList = listOf(\n                        R.drawable.media3_icon_feed,\n                        R.drawable.media3_icon_thumb_up_unfilled,\n                        R.drawable.media3_icon_thumb_down_unfilled,\n                        R.drawable.key,\n                        R.drawable.info,\n                        R.drawable.radio,\n                        R.drawable.gradient,\n                        R.drawable.contrast,\n                        R.drawable.volume_up,\n                        R.drawable.volume_mute,\n                        R.drawable.content_copy\n                    )\n\n                    val extendedList = if (currentFormat != null) {\n                        listOf(\n                            stringResource(R.string.views) to info?.viewCount?.let(::numberFormatter).orEmpty(),\n                            stringResource(R.string.likes) to info?.like?.let(::numberFormatter).orEmpty(),\n                            stringResource(R.string.dislikes) to info?.dislike?.let(::numberFormatter).orEmpty(),\n                            \"Itag\" to currentFormat?.itag?.toString(),\n                            stringResource(R.string.mime_type) to currentFormat?.mimeType,\n                            stringResource(R.string.codecs) to currentFormat?.codecs,\n                            stringResource(R.string.bitrate) to currentFormat?.bitrate?.let { \"${it / 1000} Kbps\" },\n                            stringResource(R.string.sample_rate) to currentFormat?.sampleRate?.let { \"$it Hz\" },\n                            stringResource(R.string.loudness) to currentFormat?.loudnessDb?.let { \"$it dB\" },\n                            stringResource(R.string.volume) to if (playerConnection != null) \"${(playerConnection.player.volume * 100).toInt()}%\" else null,\n                            stringResource(R.string.file_size) to\n                                    currentFormat?.contentLength?.let {\n                                        Formatter.formatShortFileSize(\n                                            context,\n                                            it\n                                        )\n                                    },\n                        )\n                    } else {\n                        emptyList()\n                    }\n\n                    val cardsBaseList = mutableListOf<Material3SettingsItem>()\n                    val cardsExtendedList = mutableListOf<Material3SettingsItem>()\n                    val cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager\n\n                    baseList.forEachIndexed { index, (label, text) ->\n                        val displayText = text ?: stringResource(R.string.unknown)\n                        cardsBaseList += Material3SettingsItem(\n                            title = { Text(label) },\n                            description = { Text(displayText) },\n                            icon = painterResource(baseIconsList[index]),\n                            onClick = {\n                                cm.setPrimaryClip(ClipData.newPlainText(\"text\", displayText))\n                                Toast.makeText(context, R.string.copied, Toast.LENGTH_SHORT).show()\n                            },\n                        )\n                    }\n\n                    extendedList.forEachIndexed { index, (label, text) ->\n                        val displayText = text ?: stringResource(R.string.unknown)\n                        cardsExtendedList += Material3SettingsItem(\n                            title = { Text(label) },\n                            description = { Text(displayText) },\n                            icon = painterResource(iconsList[index]),\n                            onClick = {\n                                cm.setPrimaryClip(ClipData.newPlainText(\"text\", displayText))\n                                Toast.makeText(context, R.string.copied, Toast.LENGTH_SHORT).show()\n                            },\n                        )\n                    }\n\n                    Material3SettingsGroup(\n                        title = stringResource(R.string.general),\n                        items = cardsBaseList\n                    )\n\n                    Spacer(Modifier.height(8.dp))\n\n                    Material3SettingsGroup(\n                        title = stringResource(R.string.information),\n                        items = cardsExtendedList\n                    )\n\n                    Spacer(Modifier.height(8.dp))\n\n                    val descriptionText = info?.description ?: stringResource(R.string.unknown)\n\n                    Material3SettingsGroup(\n                        title = stringResource(R.string.description),\n                        items = listOf(\n                            Material3SettingsItem(\n                                title = { Text(stringResource(R.string.description)) },\n                                description = { Text(descriptionText) },\n                                onClick = {\n                                    cm.setPrimaryClip(ClipData.newPlainText(\"text\", descriptionText))\n                                    Toast.makeText(context, R.string.copied, Toast.LENGTH_SHORT).show()\n                                }\n                            )\n                        )\n                    )\n                }\n            }\n        } else {\n            item(contentType = \"MediaInfoLoader\") {\n                ShimmerHost {\n                    Row(\n                        horizontalArrangement = Arrangement.Center,\n                        verticalAlignment = Alignment.CenterVertically,\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(all = 16.dp)\n                    ) {\n                        TextPlaceholder()\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/utils/ShowOffsetDialog.kt",
    "content": "package com.metrolist.music.ui.utils\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Slider\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextField\nimport androidx.compose.material3.TextFieldDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport com.metrolist.music.LocalDatabase\nimport com.metrolist.music.R\nimport com.metrolist.music.db.entities.SongEntity\nimport kotlinx.coroutines.FlowPreview\n\n@OptIn(FlowPreview::class)\n@Composable\nfun ShowOffsetDialog(songProvider: () -> SongEntity?) {\n    val database = LocalDatabase.current\n    val song = songProvider()\n    var lyricsOffset by rememberSaveable { mutableIntStateOf(song?.lyricsOffset ?: 0) }\n    var textFieldValue by rememberSaveable { mutableStateOf(lyricsOffset.toString()) }\n\n    LaunchedEffect(song?.id) {\n        song?.let {\n            lyricsOffset = it.lyricsOffset\n            textFieldValue = lyricsOffset.toString()\n        }\n    }\n\n    LaunchedEffect(lyricsOffset) {\n        songProvider()?.let { song ->\n            database.query {\n                upsert(\n                    song.copy(\n                        lyricsOffset = lyricsOffset\n                    )\n                )\n            }\n        }\n    }\n\n    Column(\n        horizontalAlignment = Alignment.CenterHorizontally,\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(horizontal = 24.dp, vertical = 20.dp)\n    ) {\n        Icon(\n            painter = painterResource(R.drawable.fast_forward),\n            contentDescription = null,\n            modifier = Modifier.size(40.dp),\n            tint = MaterialTheme.colorScheme.primary\n        )\n\n        Spacer(modifier = Modifier.height(12.dp))\n\n        Text(\n            text = stringResource(R.string.lyrics_offset),\n            style = MaterialTheme.typography.headlineSmall,\n            fontWeight = FontWeight.Bold\n        )\n\n        Spacer(modifier = Modifier.height(16.dp))\n\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n            horizontalArrangement = Arrangement.Center,\n            modifier = Modifier.fillMaxWidth()\n        ) {\n            TextField(\n                value = textFieldValue,\n                onValueChange = { newText ->\n                    val sanitized = newText.filter {\n                        it.isDigit() || (it == '-' && newText.indexOf('-') == 0)\n                    }\n\n                    val limited = if (sanitized.startsWith('-')) {\n                        sanitized.take(6)\n                    } else {\n                        sanitized.take(5)\n                    }\n\n                    textFieldValue = limited\n\n                    when {\n                        limited.isEmpty() -> {\n                            lyricsOffset = 0\n                            textFieldValue = \"0\"\n                        }\n\n                        limited == \"-\" -> {\n                        }\n\n                        else -> {\n                            limited.toIntOrNull()?.let { parsedValue ->\n                                val clampedValue = parsedValue.coerceIn(-9999, 9999)\n                                lyricsOffset = clampedValue\n\n                                if (parsedValue != clampedValue) {\n                                    textFieldValue = clampedValue.toString()\n                                }\n\n                                if (clampedValue == 0 && limited.startsWith('-')) {\n                                    textFieldValue = \"0\"\n                                }\n                            }\n                        }\n                    }\n                },\n                singleLine = true,\n                textStyle = MaterialTheme.typography.displaySmall.copy(\n                    textAlign = TextAlign.Center,\n                    fontWeight = FontWeight.Bold\n                ),\n                modifier = Modifier.widthIn(min = 120.dp, max = 160.dp),\n                colors = TextFieldDefaults.colors(\n                    focusedContainerColor = Color.Transparent,\n                    unfocusedContainerColor = Color.Transparent,\n                    cursorColor = MaterialTheme.colorScheme.primary,\n                    focusedIndicatorColor = MaterialTheme.colorScheme.primary,\n                    unfocusedIndicatorColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),\n                    disabledIndicatorColor = Color.Transparent,\n                    errorIndicatorColor = MaterialTheme.colorScheme.error\n                ),\n                keyboardOptions = KeyboardOptions(\n                    keyboardType = KeyboardType.Number,\n                    imeAction = ImeAction.Done\n                )\n            )\n\n            Spacer(modifier = Modifier.width(8.dp))\n\n            Text(\n                text = \"ms\",\n                style = MaterialTheme.typography.titleLarge,\n                color = MaterialTheme.colorScheme.onSurfaceVariant,\n                fontWeight = FontWeight.Medium\n            )\n\n            if (lyricsOffset != 0) {\n                Spacer(Modifier.width(8.dp))\n\n                IconButton(\n                    onClick = {\n                        lyricsOffset = 0\n                        textFieldValue = \"0\"\n                    }\n                ) {\n                    Icon(\n                        painter = painterResource(R.drawable.replay),\n                        tint = MaterialTheme.colorScheme.primary,\n                        contentDescription = \"Reset\"\n                    )\n                }\n            }\n        }\n\n        Spacer(modifier = Modifier.height(16.dp))\n\n        Row(\n            verticalAlignment = Alignment.CenterVertically,\n            modifier = Modifier.fillMaxWidth()\n        ) {\n            IconButton(\n                onClick = {\n                    lyricsOffset = (lyricsOffset - 50).coerceIn(-3000, 3000)\n                    textFieldValue = lyricsOffset.toString()\n                }\n            ) {\n                Icon(\n                    painter = painterResource(R.drawable.remove),\n                    contentDescription = \"Decrease\"\n                )\n            }\n\n            Slider(\n                value = lyricsOffset.toFloat(),\n                onValueChange = { newValue ->\n                    val rounded = (newValue / 100).toInt() * 100\n                    lyricsOffset = rounded\n                    textFieldValue = rounded.toString()\n                },\n                valueRange = -3000f..3000f,\n                steps = 59,\n                modifier = Modifier.weight(1f)\n            )\n\n            IconButton(\n                onClick = {\n                    lyricsOffset = (lyricsOffset + 50).coerceIn(-3000, 3000)\n                    textFieldValue = lyricsOffset.toString()\n                }\n            ) {\n                Icon(\n                    painter = painterResource(R.drawable.add),\n                    contentDescription = \"Increase\"\n                )\n            }\n        }\n\n        Spacer(modifier = Modifier.height(8.dp))\n\n        Row(\n            horizontalArrangement = Arrangement.SpaceBetween,\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 48.dp)\n        ) {\n            Text(\n                text = \"-3000ms\",\n                style = MaterialTheme.typography.labelLarge,\n                color = MaterialTheme.colorScheme.onSurfaceVariant\n            )\n            Text(\n                text = \"+3000ms\",\n                style = MaterialTheme.typography.labelLarge,\n                color = MaterialTheme.colorScheme.onSurfaceVariant\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/utils/StringUtils.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.utils\n\nimport java.text.DecimalFormat\nimport kotlin.math.absoluteValue\n\nfun formatFileSize(sizeBytes: Long): String {\n    val prefix = if (sizeBytes < 0) \"-\" else \"\"\n    var result: Long = sizeBytes.absoluteValue\n    var suffix = \"B\"\n    if (result > 900) {\n        suffix = \"KB\"\n        result /= 1024\n    }\n    if (result > 900) {\n        suffix = \"MB\"\n        result /= 1024\n    }\n    if (result > 900) {\n        suffix = \"GB\"\n        result /= 1024\n    }\n    if (result > 900) {\n        suffix = \"TB\"\n        result /= 1024\n    }\n    if (result > 900) {\n        suffix = \"PB\"\n        result /= 1024\n    }\n    return \"$prefix$result $suffix\"\n}\n\nfun numberFormatter(n: Int) =\n    DecimalFormat(\"#,###\")\n        .format(n)\n        .replace(\",\", \".\")\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/ui/utils/YouTubeUtils.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.ui.utils\n\nfun String.resize(\n    width: Int? = null,\n    height: Int? = null,\n): String {\n    if (width == null && height == null) return this\n    \"https://lh3\\\\.googleusercontent\\\\.com/.*=w(\\\\d+)-h(\\\\d+).*\".toRegex()\n        .matchEntire(this)?.groupValues?.let { group ->\n        val (W, H) = group.drop(1).map { it.toInt() }\n        var w = width\n        var h = height\n        if (w != null && h == null) h = (w / W) * H\n        if (w == null && h != null) w = (h / H) * W\n        return \"${split(\"=w\")[0]}=w$w-h$h-p-l90-rj\"\n    }\n    if (this matches \"https://yt3\\\\.ggpht\\\\.com/.*=s(\\\\d+)\".toRegex()) {\n        return \"$this-s${width ?: height}\"\n    }\n    return this\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/CoilBitmapLoader.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.utils\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.net.Uri\nimport androidx.core.graphics.createBitmap\nimport androidx.media3.common.util.BitmapLoader\nimport coil3.imageLoader\nimport coil3.request.ErrorResult\nimport coil3.request.ImageRequest\nimport coil3.request.SuccessResult\nimport coil3.request.allowHardware\nimport coil3.toBitmap\nimport com.google.common.util.concurrent.ListenableFuture\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.guava.future\nimport timber.log.Timber\n\nclass CoilBitmapLoader(\n    private val context: Context,\n    private val scope: CoroutineScope,\n) : BitmapLoader {\n    \n    override fun supportsMimeType(mimeType: String): Boolean = mimeType.startsWith(\"image/\")\n\n    private fun createFallbackBitmap(): Bitmap =\n        createBitmap(64, 64)\n\n    private fun Bitmap.copyIfNeeded(): Bitmap {\n        return if (isRecycled) {\n            createFallbackBitmap()\n        } else {\n            try {\n                copy(Bitmap.Config.ARGB_8888, false) ?: createFallbackBitmap()\n            } catch (e: Exception) {\n                createFallbackBitmap()\n            }\n        }\n    }\n\n    override fun decodeBitmap(data: ByteArray): ListenableFuture<Bitmap> =\n        scope.future(Dispatchers.IO) {\n            try {\n                val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)\n                bitmap?.copyIfNeeded() ?: createFallbackBitmap()\n            } catch (e: Exception) {\n                Timber.tag(\"CoilBitmapLoader\").w(e, \"Failed to decode bitmap data\")\n                createFallbackBitmap()\n            }\n        }\n\n    override fun loadBitmap(uri: Uri): ListenableFuture<Bitmap> =\n        scope.future(Dispatchers.IO) {\n            val request = ImageRequest.Builder(context)\n                .data(uri)\n                .allowHardware(false)\n                .build()\n\n            val result = context.imageLoader.execute(request)\n\n            when (result) {\n                is ErrorResult -> {\n                    createFallbackBitmap()\n                }\n                is SuccessResult -> {\n                    try {\n                        val bitmap = result.image.toBitmap()\n                        bitmap.copyIfNeeded()\n                    } catch (e: Exception) {\n                        Timber.tag(\"CoilBitmapLoader\").w(e, \"Failed to convert image to bitmap\")\n                        createFallbackBitmap()\n                    }\n                }\n            }\n        }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/ComposeDebugUtils.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.utils\n\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.mutableLongStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.composed\nimport androidx.compose.ui.draw.drawWithCache\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.drawscope.Fill\nimport androidx.compose.ui.graphics.drawscope.Stroke\nimport androidx.compose.ui.graphics.lerp\nimport androidx.compose.ui.platform.debugInspectorInfo\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.delay\nimport kotlin.math.min\n\n/**\n * A [Modifier] that draws a border around elements that are recomposing. The border increases in\n * size and interpolates from red to green as more recompositions occur before a timeout.\n */\n@Stable\nfun Modifier.recomposeHighlighter(): Modifier = this.then(recomposeModifier)\n\n// Use a single instance + @Stable to ensure that recompositions can enable skipping optimizations\n// Modifier.composed will still remember unique data per call site.\nprivate val recomposeModifier =\n    Modifier.composed(inspectorInfo = debugInspectorInfo { name = \"recomposeHighlighter\" }) {\n        // The total number of compositions that have occurred. We're not using a State<> here be\n        // able to read/write the value without invalidating (which would cause infinite\n        // recomposition).\n        val totalCompositions = remember { arrayOf(0L) }\n        totalCompositions[0]++\n\n        // The value of totalCompositions at the last timeout.\n        val totalCompositionsAtLastTimeout = remember { mutableLongStateOf(0L) }\n\n        // Start the timeout, and reset everytime there's a recomposition. (Using totalCompositions\n        // as the key is really just to cause the timer to restart every composition).\n        LaunchedEffect(totalCompositions[0]) {\n            delay(3000)\n            totalCompositionsAtLastTimeout.longValue = totalCompositions[0]\n        }\n\n        Modifier.drawWithCache {\n            onDrawWithContent {\n                // Draw actual content.\n                drawContent()\n\n                // Below is to draw the highlight, if necessary. A lot of the logic is copied from\n                // Modifier.border\n                val numCompositionsSinceTimeout =\n                    totalCompositions[0] - totalCompositionsAtLastTimeout.longValue\n\n                val hasValidBorderParams = size.minDimension > 0f\n                if (!hasValidBorderParams || numCompositionsSinceTimeout <= 0) {\n                    return@onDrawWithContent\n                }\n\n                val (color, strokeWidthPx) =\n                    when (numCompositionsSinceTimeout) {\n                        // We need at least one composition to draw, so draw the smallest border\n                        // color in blue.\n                        1L -> Color.Blue to 1f\n                        // 2 compositions is _probably_ okay.\n                        2L -> Color.Green to 2.dp.toPx()\n                        // 3 or more compositions before timeout may indicate an issue. lerp the\n                        // color from yellow to red, and continually increase the border size.\n                        else -> {\n                            lerp(\n                                Color.Yellow.copy(alpha = 0.8f),\n                                Color.Red.copy(alpha = 0.5f),\n                                min(1f, (numCompositionsSinceTimeout - 1).toFloat() / 100f),\n                            ) to numCompositionsSinceTimeout.toInt().dp.toPx()\n                        }\n                    }\n\n                val halfStroke = strokeWidthPx / 2\n                val topLeft = Offset(halfStroke, halfStroke)\n                val borderSize = Size(size.width - strokeWidthPx, size.height - strokeWidthPx)\n\n                val fillArea = (strokeWidthPx * 2) > size.minDimension\n                val rectTopLeft = if (fillArea) Offset.Zero else topLeft\n                val size = if (fillArea) size else borderSize\n                val style = if (fillArea) Fill else Stroke(strokeWidthPx)\n\n                drawRect(\n                    brush = SolidColor(color),\n                    topLeft = rectTopLeft,\n                    size = size,\n                    style = style,\n                )\n            }\n        }\n    }\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/ComposeToImage.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.utils\n\nimport android.content.ContentValues\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.Canvas\nimport android.graphics.LinearGradient\nimport android.graphics.Paint\nimport android.graphics.Path\nimport android.graphics.PorterDuff\nimport android.graphics.PorterDuffColorFilter\nimport android.graphics.RectF\nimport android.graphics.Shader\nimport android.graphics.Typeface\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Environment\nimport android.provider.MediaStore\nimport android.text.Layout\nimport android.text.StaticLayout\nimport android.text.TextPaint\nimport androidx.core.content.ContextCompat\nimport androidx.core.content.FileProvider\nimport androidx.core.graphics.createBitmap\nimport androidx.core.graphics.drawable.toBitmap\nimport androidx.core.graphics.withClip\nimport androidx.core.graphics.withTranslation\nimport androidx.palette.graphics.Palette\nimport coil3.ImageLoader\nimport coil3.request.ImageRequest\nimport coil3.request.allowHardware\nimport coil3.toBitmap\nimport com.metrolist.music.R\nimport com.metrolist.music.ui.component.LyricsBackgroundStyle\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport java.io.File\nimport java.io.FileOutputStream\nimport kotlin.math.abs\nimport kotlin.math.roundToInt\n\nobject ComposeToImage {\n    suspend fun createLyricsImage(\n        context: Context,\n        coverArtUrl: String?,\n        songTitle: String,\n        artistName: String,\n        lyrics: String,\n        width: Int,\n        height: Int,\n        backgroundColor: Int? = null,\n        backgroundStyle: LyricsBackgroundStyle = LyricsBackgroundStyle.SOLID,\n        textColor: Int? = null,\n        secondaryTextColor: Int? = null,\n        lyricsAlignment: Layout.Alignment = Layout.Alignment.ALIGN_CENTER,\n    ): Bitmap =\n        withContext(Dispatchers.Default) {\n            // Use fixed high resolution as requested (2160x2160)\n            // This ensures consistent high-quality output regardless of the device screen\n            val imageWidth = 2160\n            val imageHeight = 2160\n\n            val bitmap = createBitmap(imageWidth, imageHeight)\n            val canvas = Canvas(bitmap)\n\n            val defaultBackgroundColor = 0xFF121212.toInt()\n            val defaultTextColor = 0xFFFFFFFF.toInt()\n            val defaultSecondaryTextColor = 0xB3FFFFFF.toInt()\n\n            val bgColor = backgroundColor ?: defaultBackgroundColor\n            val mainTextColor = textColor ?: defaultTextColor\n            val secondaryTxtColor = secondaryTextColor ?: defaultSecondaryTextColor\n\n            // Pre-load cover art if needed for Blur/Gradient or just for the header\n            var coverArtBitmap: Bitmap? = null\n            if (coverArtUrl != null) {\n                try {\n                    val imageLoader = ImageLoader(context)\n                    val request =\n                        ImageRequest\n                            .Builder(context)\n                            .data(coverArtUrl)\n                            .size(1024)\n                            .allowHardware(false)\n                            .build()\n                    val result = imageLoader.execute(request)\n                    coverArtBitmap = result.image?.toBitmap()\n                } catch (_: Exception) {\n                }\n            }\n\n            // Draw Background\n            val backgroundRect = RectF(0f, 0f, imageWidth.toFloat(), imageHeight.toFloat())\n            val backgroundPaint =\n                Paint().apply {\n                    isAntiAlias = true\n                }\n\n            when (backgroundStyle) {\n                LyricsBackgroundStyle.SOLID -> {\n                    backgroundPaint.color = bgColor\n                    canvas.drawRect(backgroundRect, backgroundPaint)\n                }\n\n                LyricsBackgroundStyle.BLUR -> {\n                    // Draw black base\n                    backgroundPaint.color = 0xFF000000.toInt()\n                    canvas.drawRect(backgroundRect, backgroundPaint)\n\n                    if (coverArtBitmap != null) {\n                        try {\n                            // Create a scaled down version for blurring (performance)\n                            val scaledBitmap = Bitmap.createScaledBitmap(coverArtBitmap, imageWidth / 10, imageHeight / 10, true)\n                            val blurredBitmap = fastBlur(scaledBitmap, 1f, 20) // Radius 20 on small image is large blur\n\n                            if (blurredBitmap != null) {\n                                val blurRect = RectF(0f, 0f, imageWidth.toFloat(), imageHeight.toFloat())\n                                canvas.drawBitmap(blurredBitmap, null, blurRect, null)\n\n                                // Dark overlay for readability\n                                val overlayPaint =\n                                    Paint().apply {\n                                        color = 0x4D000000 // 30% black overlay\n                                    }\n                                canvas.drawRect(blurRect, overlayPaint)\n                            }\n                        } catch (e: Exception) {\n                            // Fallback to solid\n                            backgroundPaint.color = bgColor\n                            canvas.drawRect(backgroundRect, backgroundPaint)\n                        }\n                    } else {\n                        backgroundPaint.color = bgColor\n                        canvas.drawRect(backgroundRect, backgroundPaint)\n                    }\n                }\n\n                LyricsBackgroundStyle.GRADIENT -> {\n                    if (coverArtBitmap != null) {\n                        val palette = Palette.from(coverArtBitmap).generate()\n                        val vibrant = palette.getVibrantColor(bgColor)\n                        val darkVibrant = palette.getDarkVibrantColor(bgColor)\n\n                        val gradient =\n                            LinearGradient(\n                                0f,\n                                0f,\n                                imageWidth.toFloat(),\n                                imageHeight.toFloat(),\n                                intArrayOf(vibrant, darkVibrant),\n                                null,\n                                Shader.TileMode.CLAMP,\n                            )\n                        backgroundPaint.shader = gradient\n                        canvas.drawRect(backgroundRect, backgroundPaint)\n                    } else {\n                        backgroundPaint.color = bgColor\n                        canvas.drawRect(backgroundRect, backgroundPaint)\n                    }\n                }\n            }\n\n            // Base scale on width relative to the reference design (340dp)\n            // 2160 / 340 ≈ 6.35\n            val scale = imageWidth / 340f\n\n            val cornerRadius = 20f * scale\n\n            // Draw inner border\n            val borderPaint =\n                Paint().apply {\n                    color = mainTextColor\n                    alpha = (255 * 0.09).toInt()\n                    style = Paint.Style.STROKE\n                    strokeWidth = 1f * scale\n                    isAntiAlias = true\n                }\n            canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, borderPaint)\n\n            val padding = 28f * scale\n\n            // --- Header Section ---\n            val coverArtSize = 64f * scale\n            val headerBottomPadding = 12f * scale\n\n            val coverCornerRadius = 3f * scale\n            coverArtBitmap?.let {\n                val rect = RectF(padding, padding, padding + coverArtSize, padding + coverArtSize)\n                val path =\n                    Path().apply {\n                        addRoundRect(rect, coverCornerRadius, coverCornerRadius, Path.Direction.CW)\n                    }\n\n                // Draw border for cover art\n                val coverBorderPaint =\n                    Paint().apply {\n                        color = mainTextColor\n                        alpha = (255 * 0.16).toInt()\n                        style = Paint.Style.STROKE\n                        strokeWidth = 1f * scale\n                        isAntiAlias = true\n                    }\n\n                canvas.save()\n                canvas.clipPath(path)\n                canvas.drawBitmap(it, null, rect, null)\n                canvas.restore()\n                canvas.drawRoundRect(rect, coverCornerRadius, coverCornerRadius, coverBorderPaint)\n            }\n\n            val textStartX = padding + coverArtSize + (16f * scale)\n            val textMaxWidth = imageWidth - textStartX - padding\n\n            val titlePaint =\n                TextPaint().apply {\n                    color = mainTextColor\n                    textSize = 20f * scale\n                    typeface = Typeface.DEFAULT_BOLD\n                    isAntiAlias = true\n                }\n\n            val artistPaint =\n                TextPaint().apply {\n                    color = secondaryTxtColor\n                    textSize = 16f * scale\n                    typeface = Typeface.DEFAULT\n                    isAntiAlias = true\n                }\n\n            val titleLayout =\n                StaticLayout.Builder\n                    .obtain(songTitle, 0, songTitle.length, titlePaint, textMaxWidth.toInt())\n                    .setAlignment(Layout.Alignment.ALIGN_NORMAL)\n                    .setMaxLines(1)\n                    .setEllipsize(android.text.TextUtils.TruncateAt.END)\n                    .build()\n\n            val artistLayout =\n                StaticLayout.Builder\n                    .obtain(artistName, 0, artistName.length, artistPaint, textMaxWidth.toInt())\n                    .setAlignment(Layout.Alignment.ALIGN_NORMAL)\n                    .setMaxLines(1)\n                    .setEllipsize(android.text.TextUtils.TruncateAt.END)\n                    .build()\n\n            // Vertically align text block with cover art\n            val headerTextHeight = titleLayout.height + artistLayout.height + (2f * scale) // +2dp padding between title and artist\n            val headerCenterY = padding + coverArtSize / 2f\n            val titleY = headerCenterY - headerTextHeight / 2f\n\n            canvas.save()\n            canvas.translate(textStartX, titleY)\n            titleLayout.draw(canvas)\n            canvas.translate(0f, titleLayout.height.toFloat() + (2f * scale))\n            artistLayout.draw(canvas)\n            canvas.restore()\n\n            // --- Footer Section ---\n            val logoBoxSize = 22f * scale\n            val logoIconSize = 16f * scale\n            val footerY = imageHeight - padding - logoBoxSize\n\n            // Draw Logo Background Box\n            val logoBgPaint =\n                Paint().apply {\n                    color = secondaryTxtColor\n                    isAntiAlias = true\n                }\n            val logoBoxRect = RectF(padding, footerY, padding + logoBoxSize, footerY + logoBoxSize)\n            // Since it's a circle in preview: .clip(RoundedCornerShape(50)) which is usually circle for square box\n            canvas.drawOval(logoBoxRect, logoBgPaint)\n\n            // Draw Logo Icon\n            val rawLogo = ContextCompat.getDrawable(context, R.drawable.small_icon)?.toBitmap()\n            rawLogo?.let {\n                val logoPaint =\n                    Paint().apply {\n                        // If background is gradient/blur, tint might be tricky.\n                        // Using bgColor for tint is safe for Solid, but for Gradient/Blur\n                        // we might want a color that contrasts with secondaryTxtColor.\n                        // Let's use the 'bgColor' passed in which is likely the dominant color or selected color.\n                        // Or for simplicity, use a generic dark/light depending on theme.\n                        colorFilter = PorterDuffColorFilter(bgColor, PorterDuff.Mode.SRC_IN)\n                        isAntiAlias = true\n                    }\n\n                // Center logo in box\n                val logoOffset = (logoBoxSize - logoIconSize) / 2f\n                val logoRect =\n                    RectF(\n                        padding + logoOffset,\n                        footerY + logoOffset,\n                        padding + logoBoxSize - logoOffset,\n                        footerY + logoBoxSize - logoOffset,\n                    )\n                canvas.drawBitmap(it, null, logoRect, logoPaint)\n            }\n\n            // Draw App Name\n            val appName = context.getString(R.string.app_name)\n            val appNamePaint =\n                TextPaint().apply {\n                    color = secondaryTxtColor\n                    textSize = 14f * scale\n                    typeface = Typeface.DEFAULT_BOLD\n                    isAntiAlias = true\n                }\n\n            val appNameX = padding + logoBoxSize + (8f * scale)\n            // Center text vertically relative to logo box\n            val appNameY = footerY + logoBoxSize / 2f - (appNamePaint.descent() + appNamePaint.ascent()) / 2f\n            canvas.drawText(appName, appNameX, appNameY, appNamePaint)\n\n            // --- Lyrics Section ---\n            // Calculate available space\n            val lyricsTop = padding + coverArtSize + headerBottomPadding\n            val lyricsBottom = footerY - (12f * scale) // Add some padding above footer\n            val lyricsHeight = lyricsBottom - lyricsTop\n            val lyricsWidth = imageWidth - (padding * 2)\n\n            val lyricsPaint =\n                TextPaint().apply {\n                    color = mainTextColor\n                    typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)\n                    isAntiAlias = true\n                    letterSpacing = 0.005f\n                }\n\n            // Adaptive font size calculation\n            // Start with a large size (e.g. 50sp equivalent) and scale down until it fits\n            var lyricsTextSize = 50f * scale\n            val minLyricsSize = 13f * scale\n            var lyricsLayout: StaticLayout\n\n            while (lyricsTextSize > minLyricsSize) {\n                lyricsPaint.textSize = lyricsTextSize\n                lyricsLayout =\n                    StaticLayout.Builder\n                        .obtain(lyrics, 0, lyrics.length, lyricsPaint, lyricsWidth.toInt())\n                        .setAlignment(lyricsAlignment)\n                        .setLineSpacing(0f, 1.2f)\n                        .setIncludePad(false)\n                        .build()\n\n                if (lyricsLayout.height <= lyricsHeight) {\n                    break\n                }\n\n                lyricsTextSize -= 1f * scale // Decrease by ~1sp equivalent steps\n            }\n\n            // One final rebuild with the determined size\n            lyricsPaint.textSize = lyricsTextSize\n            lyricsLayout =\n                StaticLayout.Builder\n                    .obtain(lyrics, 0, lyrics.length, lyricsPaint, lyricsWidth.toInt())\n                    .setAlignment(lyricsAlignment)\n                    .setLineSpacing(0f, 1.2f)\n                    .setIncludePad(false)\n                    .build()\n\n            // Center vertically in the available space\n            val lyricsContentHeight = lyricsLayout.height\n            val lyricsY =\n                if (lyricsContentHeight < lyricsHeight) {\n                    lyricsTop + (lyricsHeight - lyricsContentHeight) / 2f\n                } else {\n                    lyricsTop\n                }\n\n            canvas.save()\n            canvas.translate(padding, lyricsY)\n            lyricsLayout.draw(canvas)\n            canvas.restore()\n\n            return@withContext bitmap\n        }\n\n    // Stack Blur v1.0 from http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html\n    // Java Author: Mario Klingemann <mario at quasimondo.com>\n    // http://incubator.quasimondo.com\n    //\n    // created Feburary 29, 2004\n    // Android port : Yahel Bouaziz <yahel at kayenko.com>\n    // http://www.kayenko.com\n    // ported to Kotlin and adapted\n    private fun fastBlur(\n        sentBitmap: Bitmap,\n        scale: Float,\n        radius: Int,\n    ): Bitmap? {\n        val width = (sentBitmap.width * scale).roundToInt()\n        val height = (sentBitmap.height * scale).roundToInt()\n\n        if (width <= 0 || height <= 0) return null\n\n        val bitmap = Bitmap.createScaledBitmap(sentBitmap, width, height, false)\n        val w = bitmap.width\n        val h = bitmap.height\n        val pix = IntArray(w * h)\n        bitmap.getPixels(pix, 0, w, 0, 0, w, h)\n        val wm = w - 1\n        val hm = h - 1\n        val wh = w * h\n        val div = radius + radius + 1\n        val r = IntArray(wh)\n        val g = IntArray(wh)\n        val b = IntArray(wh)\n        var rsum: Int\n        var gsum: Int\n        var bsum: Int\n        var x: Int\n        var y: Int\n        var i: Int\n        var p: Int\n        var yp: Int\n        var yi: Int\n        var yw: Int\n        val vmin = IntArray(Math.max(w, h))\n        var divsum = div + 1 shr 1\n        divsum *= divsum\n        val dv = IntArray(256 * divsum)\n        i = 0\n        while (i < 256 * divsum) {\n            dv[i] = i / divsum\n            i++\n        }\n        yw = 0\n        yi = 0\n        val stack = Array(div) { IntArray(3) }\n        var stackpointer: Int\n        var stackstart: Int\n        var sir: IntArray\n        var rbs: Int\n        var r1 = radius + 1\n        var routsum: Int\n        var goutsum: Int\n        var boutsum: Int\n        var rinsum: Int\n        var ginsum: Int\n        var binsum: Int\n        y = 0\n        while (y < h) {\n            bsum = 0\n            gsum = 0\n            rsum = 0\n            boutsum = 0\n            goutsum = 0\n            routsum = 0\n            binsum = 0\n            ginsum = 0\n            rinsum = 0\n            i = -radius\n            while (i <= radius) {\n                p = pix[yi + Math.min(wm, Math.max(i, 0))]\n                sir = stack[i + radius]\n                sir[0] = p and 0xff0000 shr 16\n                sir[1] = p and 0x00ff00 shr 8\n                sir[2] = p and 0x0000ff\n                rbs = r1 - Math.abs(i)\n                rsum += sir[0] * rbs\n                gsum += sir[1] * rbs\n                bsum += sir[2] * rbs\n                if (i > 0) {\n                    rinsum += sir[0]\n                    ginsum += sir[1]\n                    binsum += sir[2]\n                } else {\n                    routsum += sir[0]\n                    goutsum += sir[1]\n                    boutsum += sir[2]\n                }\n                i++\n            }\n            stackpointer = radius\n            x = 0\n            while (x < w) {\n                r[yi] = dv[rsum]\n                g[yi] = dv[gsum]\n                b[yi] = dv[bsum]\n                rsum -= routsum\n                gsum -= goutsum\n                bsum -= boutsum\n                stackstart = stackpointer - radius + div\n                sir = stack[stackstart % div]\n                routsum -= sir[0]\n                goutsum -= sir[1]\n                boutsum -= sir[2]\n                if (y == 0) {\n                    vmin[x] = Math.min(x + radius + 1, wm)\n                }\n                p = pix[yw + vmin[x]]\n                sir[0] = p and 0xff0000 shr 16\n                sir[1] = p and 0x00ff00 shr 8\n                sir[2] = p and 0x0000ff\n                rinsum += sir[0]\n                ginsum += sir[1]\n                binsum += sir[2]\n                rsum += rinsum\n                gsum += ginsum\n                bsum += binsum\n                stackpointer = (stackpointer + 1) % div\n                sir = stack[stackpointer % div]\n                routsum += sir[0]\n                goutsum += sir[1]\n                boutsum += sir[2]\n                rinsum -= sir[0]\n                ginsum -= sir[1]\n                binsum -= sir[2]\n                yi++\n                x++\n            }\n            yw += w\n            y++\n        }\n        x = 0\n        while (x < w) {\n            bsum = 0\n            gsum = 0\n            rsum = 0\n            boutsum = 0\n            goutsum = 0\n            routsum = 0\n            binsum = 0\n            ginsum = 0\n            rinsum = 0\n            yp = -radius * w\n            i = -radius\n            while (i <= radius) {\n                yi = Math.max(0, yp) + x\n                sir = stack[i + radius]\n                sir[0] = r[yi]\n                sir[1] = g[yi]\n                sir[2] = b[yi]\n                rbs = r1 - Math.abs(i)\n                rsum += sir[0] * rbs\n                gsum += sir[1] * rbs\n                bsum += sir[2] * rbs\n                if (i > 0) {\n                    rinsum += sir[0]\n                    ginsum += sir[1]\n                    binsum += sir[2]\n                } else {\n                    routsum += sir[0]\n                    goutsum += sir[1]\n                    boutsum += sir[2]\n                }\n                if (i < hm) {\n                    yp += w\n                }\n                i++\n            }\n            yi = x\n            stackpointer = radius\n            y = 0\n            while (y < h) {\n                pix[yi] = -0x1000000 or (dv[rsum] shl 16) or (dv[gsum] shl 8) or dv[bsum]\n                rsum -= routsum\n                gsum -= goutsum\n                bsum -= boutsum\n                stackstart = stackpointer - radius + div\n                sir = stack[stackstart % div]\n                routsum -= sir[0]\n                goutsum -= sir[1]\n                boutsum -= sir[2]\n                if (x == 0) {\n                    vmin[y] = Math.min(y + r1, hm) * w\n                }\n                p = x + vmin[y]\n                sir[0] = r[p]\n                sir[1] = g[p]\n                sir[2] = b[p]\n                rinsum += sir[0]\n                ginsum += sir[1]\n                binsum += sir[2]\n                rsum += rinsum\n                gsum += ginsum\n                bsum += binsum\n                stackpointer = (stackpointer + 1) % div\n                sir = stack[stackpointer % div]\n                routsum += sir[0]\n                goutsum += sir[1]\n                boutsum += sir[2]\n                rinsum -= sir[0]\n                ginsum -= sir[1]\n                binsum -= sir[2]\n                yi += w\n                y++\n            }\n            x++\n        }\n        bitmap.setPixels(pix, 0, w, 0, 0, w, h)\n        return bitmap\n    }\n\n    fun saveBitmapAsFile(\n        context: Context,\n        bitmap: Bitmap,\n        fileName: String,\n    ): Uri =\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n            val contentValues =\n                ContentValues().apply {\n                    put(MediaStore.MediaColumns.DISPLAY_NAME, \"$fileName.png\")\n                    put(MediaStore.MediaColumns.MIME_TYPE, \"image/png\")\n                    put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + \"/Metrolist\")\n                }\n            val uri =\n                context.contentResolver.insert(\n                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,\n                    contentValues,\n                ) ?: throw IllegalStateException(\"Failed to create new MediaStore record\")\n\n            context.contentResolver.openOutputStream(uri)?.use { outputStream ->\n                bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)\n            }\n            uri\n        } else {\n            val cachePath = File(context.cacheDir, \"images\")\n            cachePath.mkdirs()\n            val imageFile = File(cachePath, \"$fileName.png\")\n            FileOutputStream(imageFile).use { outputStream ->\n                bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)\n            }\n            FileProvider.getUriForFile(\n                context,\n                \"${context.packageName}.FileProvider\",\n                imageFile,\n            )\n        }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/CrashHandler.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.utils\n\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Build\nimport com.metrolist.music.BuildConfig\nimport com.metrolist.music.ui.screens.CrashActivity\nimport timber.log.Timber\nimport java.io.PrintWriter\nimport java.io.StringWriter\nimport kotlin.system.exitProcess\n\nclass CrashHandler private constructor(\n    private val applicationContext: Context\n) : Thread.UncaughtExceptionHandler {\n\n    private val defaultHandler: Thread.UncaughtExceptionHandler? =\n        Thread.getDefaultUncaughtExceptionHandler()\n\n    override fun uncaughtException(thread: Thread, throwable: Throwable) {\n        try {\n            val crashLog = buildCrashLog(throwable)\n            Timber.e(throwable, \"App crashed\")\n            \n            // Launch crash activity\n            val intent = Intent(applicationContext, CrashActivity::class.java).apply {\n                putExtra(EXTRA_CRASH_LOG, crashLog)\n                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)\n            }\n            applicationContext.startActivity(intent)\n            \n            // Kill the current process\n            android.os.Process.killProcess(android.os.Process.myPid())\n            exitProcess(1)\n        } catch (e: Exception) {\n            // If we fail to handle the crash, fall back to default handler\n            Timber.e(e, \"Error handling crash\")\n            defaultHandler?.uncaughtException(thread, throwable)\n        }\n    }\n\n    private fun buildCrashLog(throwable: Throwable): String {\n        val stackTrace = StringWriter().apply {\n            throwable.printStackTrace(PrintWriter(this))\n        }.toString()\n\n        return buildString {\n            appendLine(\"Metrolist Crash Report\")\n            appendLine(\"=\".repeat(50))\n            appendLine()\n            appendLine(\"Manufacturer: ${Build.MANUFACTURER}\")\n            appendLine(\"Device: ${Build.MODEL}\")\n            appendLine(\"Android version: ${Build.VERSION.RELEASE} (SDK ${Build.VERSION.SDK_INT})\")\n            appendLine(\"App version: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})\")\n            appendLine()\n            appendLine(\"=\".repeat(50))\n            appendLine(\"Stacktrace:\")\n            appendLine(\"=\".repeat(50))\n            appendLine()\n            append(stackTrace)\n        }\n    }\n\n    companion object {\n        const val EXTRA_CRASH_LOG = \"crash_log\"\n\n        fun install(context: Context) {\n            val handler = CrashHandler(context.applicationContext)\n            Thread.setDefaultUncaughtExceptionHandler(handler)\n            Timber.d(\"CrashHandler installed\")\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/DataStore.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.utils\n\nimport android.content.Context\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.datastore.core.DataStore\nimport androidx.datastore.preferences.core.Preferences\nimport androidx.datastore.preferences.core.edit\nimport androidx.datastore.preferences.preferencesDataStore\nimport com.metrolist.music.extensions.toEnum\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.runBlocking\nimport kotlin.properties.ReadOnlyProperty\n\nval Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = \"settings\")\n\noperator fun <T> DataStore<Preferences>.get(key: Preferences.Key<T>): T? =\n    runBlocking(Dispatchers.IO) {\n        data.first()[key]\n    }\n\nfun <T> DataStore<Preferences>.get(\n    key: Preferences.Key<T>,\n    defaultValue: T,\n): T =\n    runBlocking(Dispatchers.IO) {\n        data.first()[key] ?: defaultValue\n    }\n\nfun <T> preference(\n    context: Context,\n    key: Preferences.Key<T>,\n    defaultValue: T,\n) = ReadOnlyProperty<Any?, T> { _, _ -> context.dataStore[key] ?: defaultValue }\n\ninline fun <reified T : Enum<T>> enumPreference(\n    context: Context,\n    key: Preferences.Key<String>,\n    defaultValue: T,\n) = ReadOnlyProperty<Any?, T> { _, _ -> context.dataStore[key].toEnum(defaultValue) }\n\n@Composable\nfun <T> rememberPreference(\n    key: Preferences.Key<T>,\n    defaultValue: T,\n): MutableState<T> {\n    val context = LocalContext.current\n    val coroutineScope = rememberCoroutineScope()\n\n    val state =\n        remember {\n            context.dataStore.data\n                .map { it[key] ?: defaultValue }\n                .distinctUntilChanged()\n        }.collectAsState(context.dataStore[key] ?: defaultValue)\n\n    return remember {\n        object : MutableState<T> {\n            override var value: T\n                get() = state.value\n                set(value) {\n                    coroutineScope.launch {\n                        context.dataStore.edit {\n                            it[key] = value\n                        }\n                    }\n                }\n\n            override fun component1() = value\n\n            override fun component2(): (T) -> Unit = { value = it }\n        }\n    }\n}\n\n@Composable\ninline fun <reified T : Enum<T>> rememberEnumPreference(\n    key: Preferences.Key<String>,\n    defaultValue: T,\n): MutableState<T> {\n    val context = LocalContext.current\n    val coroutineScope = rememberCoroutineScope()\n\n    val initialValue = context.dataStore[key].toEnum(defaultValue = defaultValue)\n    val state =\n        remember {\n            context.dataStore.data\n                .map { it[key].toEnum(defaultValue = defaultValue) }\n                .distinctUntilChanged()\n        }.collectAsState(initialValue)\n\n    return remember {\n        object : MutableState<T> {\n            override var value: T\n                get() = state.value\n                set(value) {\n                    coroutineScope.launch {\n                        context.dataStore.edit {\n                            it[key] = value.name\n                        }\n                    }\n                }\n\n            override fun component1() = value\n\n            override fun component2(): (T) -> Unit = { value = it }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/DiscordRPC.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.utils\n\nimport android.content.Context\nimport com.metrolist.music.R\nimport com.metrolist.music.db.entities.Song\nimport com.my.kizzy.rpc.KizzyRPC\nimport com.my.kizzy.rpc.RpcImage\n\nclass DiscordRPC(\n    val context: Context,\n    token: String,\n) : KizzyRPC(\n    token = token,\n    os = \"Android\",\n    browser = \"Discord Android\",\n    device = android.os.Build.DEVICE,\n    userAgent = SuperProperties.userAgent,\n    superPropertiesBase64 = SuperProperties.superPropertiesBase64\n) {\n    suspend fun updateSong(\n        song: Song,\n        currentPlaybackTimeMillis: Long,\n        playbackSpeed: Float = 1.0f,\n        useDetails: Boolean = false,\n        status: String = \"online\",\n        button1Text: String = \"\",\n        button1Visible: Boolean = true,\n        button2Text: String = \"\",\n        button2Visible: Boolean = true,\n        activityType: String = \"listening\",\n        activityName: String = \"\",\n    ) = runCatching {\n        val currentTime = System.currentTimeMillis()\n\n        val adjustedPlaybackTime = (currentPlaybackTimeMillis / playbackSpeed).toLong()\n        val calculatedStartTime = currentTime - adjustedPlaybackTime\n\n        val songTitleWithRate = if (playbackSpeed != 1.0f) {\n            \"${song.song.title} [${String.format(\"%.2fx\", playbackSpeed)}]\"\n        } else {\n            song.song.title\n        }\n\n        val remainingDuration = song.song.duration * 1000L - currentPlaybackTimeMillis\n        val adjustedRemainingDuration = (remainingDuration / playbackSpeed).toLong()\n\n        val buttonsList = mutableListOf<Pair<String, String>>()\n        if (button1Visible) {\n            val resolvedText = resolveVariables(\n                button1Text.ifEmpty { \"Listen on YouTube Music\" },\n                song\n            )\n            buttonsList.add(resolvedText to \"https://music.youtube.com/watch?v=${song.song.id}\")\n        }\n        if (button2Visible) {\n            val resolvedText = resolveVariables(\n                button2Text.ifEmpty { \"Visit Metrolist\" },\n                song\n            )\n            buttonsList.add(resolvedText to \"https://github.com/MetrolistGroup/Metrolist\")\n        }\n\n        val type = when (activityType) {\n            \"playing\" -> Type.PLAYING\n            \"watching\" -> Type.WATCHING\n            \"competing\" -> Type.COMPETING\n            else -> Type.LISTENING\n        }\n\n        val name = activityName.ifEmpty {\n            context.getString(R.string.app_name).removeSuffix(\" Debug\")\n        }\n\n        setActivity(\n            name = name,\n            details = songTitleWithRate,\n            state = song.artists.joinToString { it.name },\n            detailsUrl = \"https://music.youtube.com/watch?v=${song.song.id}\",\n            largeImage = song.song.thumbnailUrl?.let { RpcImage.ExternalImage(it) },\n            smallImage = song.artists.firstOrNull()?.thumbnailUrl?.let { RpcImage.ExternalImage(it) },\n            largeText = song.album?.title,\n            smallText = song.artists.firstOrNull()?.name,\n            buttons = if (buttonsList.isNotEmpty()) buttonsList else null,\n            type = type,\n            statusDisplayType = if (useDetails) StatusDisplayType.DETAILS else StatusDisplayType.STATE,\n            since = currentTime,\n            startTime = calculatedStartTime,\n            endTime = currentTime + adjustedRemainingDuration,\n            applicationId = APPLICATION_ID,\n            status = status\n        )\n    }\n\n    override suspend fun close() {\n        super.close()\n    }\n\n    companion object {\n        private const val APPLICATION_ID = \"1411019391843172514\"\n\n        /**\n         * Resolves template variables in text.\n         * Supported: {song_name}, {artist_name}, {album_name}\n         */\n        fun resolveVariables(text: String, song: Song): String {\n            return text\n                .replace(\"{song_name}\", song.song.title)\n                .replace(\"{artist_name}\", song.artists.joinToString { it.name })\n                .replace(\"{album_name}\", song.album?.title ?: \"\")\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/IconUtils.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.utils\n\nimport android.content.ComponentName\nimport android.content.Context\nimport android.content.pm.PackageManager\n\nobject IconUtils {\n    fun setIcon(context: Context, enabled: Boolean) {\n        val pm = context.packageManager\n        val dynamic = ComponentName(context, \"com.metrolist.music.MainActivityAlias\")\n        val static = ComponentName(context, \"com.metrolist.music.MainActivityStatic\")\n\n        pm.setComponentEnabledSetting(\n            dynamic,\n            if (enabled) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else PackageManager.COMPONENT_ENABLED_STATE_DISABLED,\n            PackageManager.DONT_KILL_APP\n        )\n        pm.setComponentEnabledSetting(\n            static,\n            if (enabled) PackageManager.COMPONENT_ENABLED_STATE_DISABLED else PackageManager.COMPONENT_ENABLED_STATE_ENABLED,\n            PackageManager.DONT_KILL_APP\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/NetworkConnectivityObserver.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.utils\n\nimport android.content.Context\nimport android.net.ConnectivityManager\nimport android.net.Network\nimport android.net.NetworkCapabilities\nimport android.net.NetworkRequest\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.flow.receiveAsFlow\n\n/**\n * Simple NetworkConnectivityObserver based on OuterTune's implementation\n * Provides network connectivity monitoring for auto-play functionality\n */\nclass NetworkConnectivityObserver(context: Context) {\n    private val connectivityManager =\n        context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager\n\n    private val _networkStatus = Channel<Boolean>(Channel.CONFLATED)\n    val networkStatus = _networkStatus.receiveAsFlow()\n\n    private val networkCallback = object : ConnectivityManager.NetworkCallback() {\n        override fun onAvailable(network: Network) {\n            _networkStatus.trySend(true)\n        }\n\n        override fun onLost(network: Network) {\n            _networkStatus.trySend(false)\n        }\n    }\n\n    init {\n        val request = NetworkRequest.Builder()\n            .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)\n            .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)\n            .build()\n        \n        try {\n            connectivityManager.registerNetworkCallback(request, networkCallback)\n        } catch (e: Exception) {\n            // Fallback: assume connected if registration fails\n            _networkStatus.trySend(true)\n        }\n        \n        // Send initial state\n        val isInitiallyConnected = isCurrentlyConnected()\n        _networkStatus.trySend(isInitiallyConnected)\n    }\n\n    fun unregister() {\n        connectivityManager.unregisterNetworkCallback(networkCallback)\n    }\n    \n    /**\n     * Check current connectivity state synchronously\n     */\n    fun isCurrentlyConnected(): Boolean {\n        return try {\n            val activeNetwork = connectivityManager.activeNetwork\n            val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork)\n            \n            // Check if we have internet capability\n            val hasInternet = networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true\n            \n            // For API 23+, also check if connection is validated\n            val isValidated =\n                networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) == true\n\n            hasInternet && isValidated\n        } catch (e: Exception) {\n            false\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/NetworkUtils.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.utils\n\nimport android.content.Context\nimport android.net.ConnectivityManager\nimport android.net.NetworkCapabilities\nimport androidx.core.content.getSystemService\n\nfun isInternetAvailable(context: Context): Boolean {\n    val connectivityManager = context.getSystemService<ConnectivityManager>() ?: return false\n    val activeNetwork = connectivityManager.activeNetwork ?: return false\n    val networkCapabilities =\n        connectivityManager.getNetworkCapabilities(activeNetwork) ?: return false\n\n    return when {\n        networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true\n        networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true\n        networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true\n        else -> false\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/PlaylistExporter.kt",
    "content": "package com.metrolist.music.utils\n\nimport android.content.ContentValues\nimport android.content.Context\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Environment\nimport android.provider.MediaStore\nimport androidx.core.content.FileProvider\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.music.db.entities.PlaylistSong\nimport java.io.File\nimport java.io.FileWriter\nimport java.io.IOException\n\nobject PlaylistExporter {\n    fun exportPlaylistAsCSV(\n        context: Context,\n        playlistName: String,\n        songs: List<PlaylistSong>,\n    ): Result<File> =\n        try {\n            val csvContent =\n                buildString {\n                    // Add CSV header\n                    append(\"Title,Artist,Album,YouTube Video ID\\n\")\n\n                    // Add each song as a CSV row\n                    songs.forEach { playlistSong ->\n                        val song = playlistSong.song.song\n                        val artists = playlistSong.song.artists\n                        val album = playlistSong.song.album\n                        append(\"\\\"${song.title.replace(\"\\\"\", \"\\\"\\\"\")}\\\"\")\n                        append(\",\")\n                        append(\"\\\"${artists.joinToString(\"; \") { it.name.replace(\"\\\"\", \"\\\"\\\"\") }}\\\"\")\n                        append(\",\")\n                        append(\"\\\"${album?.title?.replace(\"\\\"\", \"\\\"\\\"\") ?: \"\"}\\\"\")\n                        append(\",\")\n                        append(\"${song.id}\")\n                        append(\"\\n\")\n                    }\n                }\n\n            // Save to file\n            val file = createExportFile(context, \"$playlistName.csv\")\n            FileWriter(file).use { it.write(csvContent) }\n\n            Result.success(file)\n        } catch (e: IOException) {\n            Result.failure(e)\n        }\n\n    fun exportPlaylistAsM3U(\n        context: Context,\n        playlistName: String,\n        songs: List<PlaylistSong>,\n    ): Result<File> =\n        try {\n            val m3uContent =\n                buildString {\n                    // Add M3U header\n                    append(\"#EXTM3U\\n\")\n\n                    // Add each song as M3U entry\n                    songs.forEach { playlistSong ->\n                        val song = playlistSong.song.song\n                        val artists = playlistSong.song.artists\n                        append(\"#EXTINF:${song.duration},\")\n                        append(\"${artists.joinToString(\";\") { it.name }} - ${song.title}\")\n                        append(\"\\n\")\n                        append(\"https://youtube.com/watch?v=${song.id}\\n\")\n                    }\n                }\n\n            // Save to file\n            val file = createExportFile(context, \"$playlistName.m3u\")\n            FileWriter(file).use { it.write(m3uContent) }\n\n            Result.success(file)\n        } catch (e: IOException) {\n            Result.failure(e)\n        }\n}\n\nfun exportYouTubePlaylistAsCSV(\n    context: Context,\n    playlistName: String,\n    songs: List<SongItem>,\n): Result<File> =\n    try {\n        val csvContent =\n            buildString {\n                // Add CSV header\n                append(\"Title,Artist,Album,YouTube Video ID\\n\")\n\n                // Add each song as a CSV row\n                songs.forEach { songItem ->\n                    append(\"\\\"${songItem.title.replace(\"\\\"\", \"\\\"\\\"\")}\\\"\")\n                    append(\",\")\n                    append(\"\\\"${songItem.artists.joinToString(\"; \") { it.name.replace(\"\\\"\", \"\\\"\\\"\") }}\\\"\")\n                    append(\",\")\n                    append(\"\\\"${songItem.album?.name?.replace(\"\\\"\", \"\\\"\\\"\") ?: \"\"}\\\"\")\n                    append(\",\")\n                    append(\"${songItem.id}\")\n                    append(\"\\n\")\n                }\n            }\n\n        // Save to file\n        val file = createExportFile(context, \"$playlistName.csv\")\n        FileWriter(file).use { it.write(csvContent) }\n\n        Result.success(file)\n    } catch (e: IOException) {\n        Result.failure(e)\n    }\n\nfun exportYouTubePlaylistAsM3U(\n    context: Context,\n    playlistName: String,\n    songs: List<SongItem>,\n): Result<File> =\n    try {\n        val m3uContent =\n            buildString {\n                // Add M3U header\n                append(\"#EXTM3U\\n\")\n\n                // Add each song as M3U entry\n                songs.forEach { songItem ->\n                    append(\"#EXTINF:${songItem.duration},\")\n                    append(\"${songItem.artists.joinToString(\" - \") { it.name }} - ${songItem.title}\")\n                    append(\"\\n\")\n                    // For M3U, we would typically include a URL, but since we don't have direct URLs,\n                    // we'll use a placeholder that indicates this is a YouTube Music track\n                    append(\"#YTM:${songItem.id}\\n\")\n                }\n            }\n\n        // Save to file\n        val file = createExportFile(context, \"$playlistName.m3u\")\n        FileWriter(file).use { it.write(m3uContent) }\n\n        Result.success(file)\n    } catch (e: IOException) {\n        Result.failure(e)\n    }\n\nprivate fun createExportFile(\n    context: Context,\n    filename: String,\n): File {\n    // Create directory if it doesn't exist\n    val exportDir = File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), \"MetrolistExports\")\n    if (!exportDir.exists()) {\n        exportDir.mkdirs()\n    }\n\n    // Create file with unique name (add timestamp if file exists)\n    val baseFilename = filename.substringBeforeLast('.')\n    val extension = filename.substringAfterLast('.', \"\")\n    var exportFile = File(exportDir, filename)\n    var counter = 1\n\n    while (exportFile.exists()) {\n        val newFilename =\n            if (extension.isNotEmpty()) {\n                \"${baseFilename}_$counter.$extension\"\n            } else {\n                \"${baseFilename}_$counter\"\n            }\n        exportFile = File(exportDir, newFilename)\n        counter++\n    }\n\n    exportFile.createNewFile()\n    return exportFile\n}\n\nprivate fun getFileUri(\n    context: Context,\n    file: File,\n): Uri =\n    FileProvider.getUriForFile(\n        context,\n        \"${context.packageName}.FileProvider\",\n        file,\n    )\n\nfun getExportFileUri(\n    context: Context,\n    file: File,\n): Uri = getFileUri(context, file)\n\n/**\n * Copy a generated export file into the public Documents/MetrolistExports folder using MediaStore (scoped storage).\n * Returns the Uri to the public copy on success.\n */\nfun saveToPublicDocuments(\n    context: Context,\n    source: File,\n    mimeType: String,\n    subdirectory: String = \"MetrolistExports\",\n): Result<Uri> {\n    return try {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {\n            val resolver = context.contentResolver\n            val relativePath = Environment.DIRECTORY_DOCUMENTS + \"/\" + subdirectory\n\n            val values =\n                ContentValues().apply {\n                    put(MediaStore.MediaColumns.DISPLAY_NAME, source.name)\n                    put(MediaStore.MediaColumns.MIME_TYPE, mimeType)\n                    put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)\n                    put(MediaStore.MediaColumns.IS_PENDING, 1)\n                }\n\n            // Use the primary external volume for generic files\n            val collection = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)\n            val destUri =\n                resolver.insert(collection, values)\n                    ?: return Result.failure(IOException(\"Failed to create destination in MediaStore\"))\n\n            resolver.openOutputStream(destUri)?.use { out ->\n                source.inputStream().use { input ->\n                    input.copyTo(out)\n                }\n            } ?: return Result.failure(IOException(\"Failed to open output stream for MediaStore uri\"))\n\n            // Mark as not pending so it becomes visible\n            val complete = ContentValues().apply { put(MediaStore.MediaColumns.IS_PENDING, 0) }\n            resolver.update(destUri, complete, null, null)\n\n            Result.success(destUri)\n        } else {\n            // Best-effort fallback: keep the file in app-scoped Documents and return a sharable uri\n            // Older Android versions would require WRITE_EXTERNAL_STORAGE for true public Documents\n            Result.success(getExportFileUri(context, source))\n        }\n    } catch (e: Exception) {\n        Result.failure(e)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/PodcastRefreshTrigger.kt",
    "content": "package com.metrolist.music.utils\n\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.asSharedFlow\n\n/**\n * Simple singleton to trigger podcast library refresh from anywhere.\n * Used when subscribing/unsubscribing from channels.\n */\nobject PodcastRefreshTrigger {\n    private val _refreshFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)\n    val refreshFlow = _refreshFlow.asSharedFlow()\n\n    fun triggerRefresh() {\n        _refreshFlow.tryEmit(Unit)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/ScrobbleManager.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.utils\n\nimport com.metrolist.lastfm.LastFM\nimport com.metrolist.music.models.MediaMetadata\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport kotlin.math.min\n\nclass ScrobbleManager(\n    private val scope: CoroutineScope,\n    var minSongDuration: Int = 30,\n    var scrobbleDelayPercent: Float = 0.5f,\n    var scrobbleDelaySeconds: Int = 50\n) {\n    private var scrobbleJob: Job? = null\n    private var scrobbleRemainingMillis: Long = 0L\n    private var scrobbleTimerStartedAt: Long = 0L\n    private var songStartedAt: Long = 0L\n    private var songStarted = false\n    var useNowPlaying = true\n\n    fun destroy() {\n        scrobbleJob?.cancel()\n        scrobbleRemainingMillis = 0L\n        scrobbleTimerStartedAt = 0L\n        songStartedAt = 0L\n        songStarted = false\n    }\n\n    fun onSongStart(metadata: MediaMetadata?, duration: Long? = null) {\n        if (metadata == null) return\n        songStartedAt = System.currentTimeMillis() / 1000\n        songStarted = true\n        startScrobbleTimer(metadata, duration)\n        if (useNowPlaying) {\n            updateNowPlaying(metadata)\n        }\n    }\n\n    fun onSongResume(metadata: MediaMetadata) {\n        resumeScrobbleTimer(metadata)\n    }\n\n    fun onSongPause() {\n        pauseScrobbleTimer()\n    }\n\n    fun onSongStop() {\n        stopScrobbleTimer()\n        songStarted = false\n    }\n\n    private fun startScrobbleTimer(metadata: MediaMetadata, duration: Long? = null) {\n        scrobbleJob?.cancel()\n        val duration = duration?.toInt()?.div(1000) ?: metadata.duration\n\n        if (duration <= minSongDuration) return\n\n        val threshold = duration * 1000L * scrobbleDelayPercent\n        scrobbleRemainingMillis = min(threshold.toLong(), scrobbleDelaySeconds * 1000L)\n\n        if (scrobbleRemainingMillis <= 0) {\n            scrobbleSong(metadata)\n            return\n        }\n        scrobbleTimerStartedAt = System.currentTimeMillis()\n        scrobbleJob = scope.launch {\n            delay(scrobbleRemainingMillis)\n            scrobbleSong(metadata)\n            scrobbleJob = null\n        }\n    }\n\n    private fun pauseScrobbleTimer() {\n        scrobbleJob?.cancel()\n        if (scrobbleTimerStartedAt != 0L) {\n            val elapsed = System.currentTimeMillis() - scrobbleTimerStartedAt\n            scrobbleRemainingMillis -= elapsed\n            if (scrobbleRemainingMillis < 0) scrobbleRemainingMillis = 0\n            scrobbleTimerStartedAt = 0L\n        } else {\n        }\n    }\n\n    private fun resumeScrobbleTimer(metadata: MediaMetadata) {\n        if (scrobbleRemainingMillis <= 0) return\n        scrobbleJob?.cancel()\n        scrobbleTimerStartedAt = System.currentTimeMillis()\n        scrobbleJob = scope.launch {\n            delay(scrobbleRemainingMillis)\n            scrobbleSong(metadata)\n            scrobbleJob = null\n        }\n    }\n\n    private fun stopScrobbleTimer() {\n        scrobbleJob?.cancel()\n        scrobbleJob = null\n        scrobbleRemainingMillis = 0\n    }\n\n    private fun scrobbleSong(metadata: MediaMetadata) {\n        scope.launch {\n            LastFM.scrobble(\n                artist = metadata.artists.joinToString { it.name },\n                track = metadata.title,\n                duration = metadata.duration,\n                timestamp = songStartedAt,\n                album = metadata.album?.title,\n            )\n        }\n    }\n\n    private fun updateNowPlaying(metadata: MediaMetadata) {\n        scope.launch {\n            LastFM.updateNowPlaying(\n                artist = metadata.artists.joinToString { it.name },\n                track = metadata.title,\n                album = metadata.album?.title,\n                duration = metadata.duration\n            )\n        }\n    }\n\n    fun onPlayerStateChanged(isPlaying: Boolean, metadata: MediaMetadata?, duration: Long? = null) {\n        if (metadata == null) return\n        if (isPlaying) {\n            if (!songStarted) {\n                onSongStart(metadata, duration)\n            } else {\n                onSongResume(metadata)\n            }\n        } else {\n            onSongPause()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/StringUtils.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.utils\n\nimport java.math.BigInteger\nimport java.security.MessageDigest\n\nfun makeTimeString(duration: Long?): String {\n    if (duration == null || duration < 0) return \"\"\n    var sec = duration / 1000\n    val day = sec / 86400\n    sec %= 86400\n    val hour = sec / 3600\n    sec %= 3600\n    val minute = sec / 60\n    sec %= 60\n    return when {\n        day > 0 -> \"%d:%02d:%02d:%02d\".format(day, hour, minute, sec)\n        hour > 0 -> \"%d:%02d:%02d\".format(hour, minute, sec)\n        else -> \"%d:%02d\".format(minute, sec)\n    }\n}\n\nfun md5(str: String): String {\n    val md = MessageDigest.getInstance(\"MD5\")\n    return BigInteger(1, md.digest(str.toByteArray())).toString(16).padStart(32, '0')\n}\n\nfun joinByBullet(vararg str: String?) =\n    str\n        .filterNot {\n            it.isNullOrEmpty()\n        }.joinToString(separator = \" • \")\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/SuperProperties.kt",
    "content": "package com.metrolist.music.utils\n\nimport android.os.Build\nimport android.util.Base64\nimport org.json.JSONObject\nimport java.util.Locale\nimport java.util.UUID\n\nobject SuperProperties {\n    // Constants from research for Discord Android 314.13\n    private const val CLIENT_VERSION = \"314.13 - Stable\"\n    private const val CLIENT_BUILD_NUMBER = 314013\n    private const val RELEASE_CHANNEL = \"googleRelease\"\n    \n    // Lazy loaded properties to avoid re-generating UUIDs\n    val superProperties: JSONObject by lazy {\n        JSONObject().apply {\n            put(\"os\", \"Android\")\n            put(\"browser\", \"Discord Android\")\n            put(\"device\", Build.DEVICE)\n            put(\"system_locale\", Locale.getDefault().toString())\n            put(\"client_version\", CLIENT_VERSION)\n            put(\"release_channel\", RELEASE_CHANNEL)\n            put(\"device_vendor_id\", UUID.randomUUID().toString())\n            put(\"client_uuid\", UUID.randomUUID().toString())\n            put(\"client_launch_id\", UUID.randomUUID().toString())\n            put(\"os_version\", Build.VERSION.RELEASE)\n            put(\"os_sdk_version\", Build.VERSION.SDK_INT.toString())\n            put(\"client_build_number\", CLIENT_BUILD_NUMBER)\n            put(\"client_event_source\", JSONObject.NULL)\n            put(\"design_id\", 0)\n        }\n    }\n\n    val superPropertiesBase64: String by lazy {\n        val jsonString = superProperties.toString()\n        Base64.encodeToString(jsonString.toByteArray(), Base64.NO_WRAP)\n    }\n\n    val userAgent: String by lazy {\n        \"Discord-Android/$CLIENT_BUILD_NUMBER;RNA\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/SyncUtils.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * OuterTune Project Copyright (C) 2025\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.utils\n\nimport android.content.Context\nimport androidx.datastore.preferences.core.edit\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.ArtistItem\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.PodcastItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.utils.completed\nimport com.metrolist.innertube.utils.parseCookieString\nimport com.metrolist.lastfm.LastFM\nimport com.metrolist.music.constants.InnerTubeCookieKey\nimport com.metrolist.music.constants.LastFMUseSendLikes\nimport com.metrolist.music.constants.LastFullSyncKey\nimport com.metrolist.music.constants.SYNC_COOLDOWN\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.db.entities.ArtistEntity\nimport com.metrolist.music.db.entities.PlaylistEntity\nimport com.metrolist.music.db.entities.PlaylistSongMap\nimport com.metrolist.music.db.entities.PodcastEntity\nimport com.metrolist.music.db.entities.SetVideoIdEntity\nimport com.metrolist.music.db.entities.SongEntity\nimport com.metrolist.music.extensions.collectLatest\nimport com.metrolist.music.extensions.isInternetConnected\nimport com.metrolist.music.extensions.isSyncEnabled\nimport com.metrolist.music.models.toMediaMetadata\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.CancellationException\nimport kotlinx.coroutines.CoroutineExceptionHandler\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.firstOrNull\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport timber.log.Timber\nimport java.time.LocalDateTime\nimport java.time.ZoneOffset\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\nsealed class SyncOperation {\n    data object FullSync : SyncOperation()\n    data object LikedSongs : SyncOperation()\n    data object LibrarySongs : SyncOperation()\n    data object UploadedSongs : SyncOperation()\n    data object LikedAlbums : SyncOperation()\n    data object UploadedAlbums : SyncOperation()\n    data object ArtistsSubscriptions : SyncOperation()\n    data object PodcastSubscriptions : SyncOperation()\n    data object EpisodesForLater : SyncOperation()\n    data object SavedPlaylists : SyncOperation()\n    data object AutoSyncPlaylists : SyncOperation()\n    data class SinglePlaylist(val browseId: String, val playlistId: String) : SyncOperation()\n    data class LikeSong(val song: SongEntity) : SyncOperation()\n    data class SubscribeChannel(val channelId: String, val subscribe: Boolean) : SyncOperation()\n    data class SavePodcast(val podcastId: String, val save: Boolean) : SyncOperation()\n    data class SaveEpisode(val episodeId: String, val save: Boolean, val setVideoId: String?) : SyncOperation()\n    data object CleanupDuplicates : SyncOperation()\n    data object ClearAllSynced : SyncOperation()\n    data object ClearPodcastData : SyncOperation()\n}\n\nsealed class SyncStatus {\n    data object Idle : SyncStatus()\n    data object Syncing : SyncStatus()\n    data class Error(val message: String) : SyncStatus()\n    data object Completed : SyncStatus()\n}\n\ndata class SyncState(\n    val overallStatus: SyncStatus = SyncStatus.Idle,\n    val likedSongs: SyncStatus = SyncStatus.Idle,\n    val librarySongs: SyncStatus = SyncStatus.Idle,\n    val uploadedSongs: SyncStatus = SyncStatus.Idle,\n    val likedAlbums: SyncStatus = SyncStatus.Idle,\n    val uploadedAlbums: SyncStatus = SyncStatus.Idle,\n    val artists: SyncStatus = SyncStatus.Idle,\n    val playlists: SyncStatus = SyncStatus.Idle,\n    val currentOperation: String = \"\"\n)\n\n@Singleton\nclass SyncUtils @Inject constructor(\n    @ApplicationContext private val context: Context,\n    private val database: MusicDatabase,\n) {\n    private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->\n        if (throwable !is CancellationException) {\n            Timber.e(throwable, \"Sync coroutine exception\")\n        }\n    }\n\n    private val syncJob = SupervisorJob()\n    private val syncScope = CoroutineScope(Dispatchers.IO + syncJob + exceptionHandler)\n\n    private val syncChannel = Channel<SyncOperation>(Channel.BUFFERED)\n    private var processingJob: Job? = null\n\n    private val _syncState = MutableStateFlow(SyncState())\n    val syncState: StateFlow<SyncState> = _syncState.asStateFlow()\n\n    private var lastfmSendLikes = false\n\n    companion object {\n        private const val MAX_RETRIES = 3\n        private const val INITIAL_RETRY_DELAY_MS = 1000L\n        private const val DB_OPERATION_DELAY_MS = 50L\n    }\n\n    init {\n        context.dataStore.data\n            .map { it[LastFMUseSendLikes] ?: false }\n            .distinctUntilChanged()\n            .collectLatest(syncScope) {\n                lastfmSendLikes = it\n            }\n\n        startProcessingQueue()\n    }\n\n    private fun startProcessingQueue() {\n        processingJob = syncScope.launch {\n            for (operation in syncChannel) {\n                try {\n                    processOperation(operation)\n                } catch (e: CancellationException) {\n                    throw e\n                } catch (e: Exception) {\n                    Timber.e(e, \"Error processing sync operation: $operation\")\n                }\n            }\n        }\n    }\n\n    private suspend fun processOperation(operation: SyncOperation) {\n        when (operation) {\n            is SyncOperation.FullSync -> executeFullSync()\n            is SyncOperation.LikedSongs -> executeSyncLikedSongs()\n            is SyncOperation.LibrarySongs -> executeSyncLibrarySongs()\n            is SyncOperation.UploadedSongs -> executeSyncUploadedSongs()\n            is SyncOperation.LikedAlbums -> executeSyncLikedAlbums()\n            is SyncOperation.UploadedAlbums -> executeSyncUploadedAlbums()\n            is SyncOperation.ArtistsSubscriptions -> executeSyncArtistsSubscriptions()\n            is SyncOperation.PodcastSubscriptions -> executeSyncPodcastSubscriptions()\n            is SyncOperation.EpisodesForLater -> executeSyncEpisodesForLater()\n            is SyncOperation.SavedPlaylists -> executeSyncSavedPlaylists()\n            is SyncOperation.AutoSyncPlaylists -> executeSyncAutoSyncPlaylists()\n            is SyncOperation.SinglePlaylist -> executeSyncPlaylist(operation.browseId, operation.playlistId)\n            is SyncOperation.LikeSong -> executeLikeSong(operation.song)\n            is SyncOperation.SubscribeChannel -> executeSubscribeChannel(operation.channelId, operation.subscribe)\n            is SyncOperation.SavePodcast -> executeSavePodcast(operation.podcastId, operation.save)\n            is SyncOperation.SaveEpisode -> executeSaveEpisode(operation.episodeId, operation.save, operation.setVideoId)\n            is SyncOperation.CleanupDuplicates -> executeCleanupDuplicatePlaylists()\n            is SyncOperation.ClearAllSynced -> executeClearAllSyncedContent()\n            is SyncOperation.ClearPodcastData -> executeClearPodcastData()\n        }\n    }\n\n    private suspend fun isLoggedIn(): Boolean {\n        return try {\n            val cookie = context.dataStore.data\n                .map { it[InnerTubeCookieKey] }\n                .first()\n            cookie?.let { \"SAPISID\" in parseCookieString(it) } ?: false\n        } catch (e: Exception) {\n            Timber.e(e, \"Error checking login status\")\n            false\n        }\n    }\n\n    private suspend fun <T> withRetry(\n        maxRetries: Int = MAX_RETRIES,\n        initialDelay: Long = INITIAL_RETRY_DELAY_MS,\n        block: suspend () -> T\n    ): Result<T> {\n        var currentDelay = initialDelay\n        repeat(maxRetries) { attempt ->\n            try {\n                return Result.success(block())\n            } catch (e: CancellationException) {\n                throw e\n            } catch (e: Exception) {\n                Timber.w(e, \"Attempt ${attempt + 1}/$maxRetries failed\")\n                if (attempt == maxRetries - 1) {\n                    return Result.failure(e)\n                }\n                delay(currentDelay)\n                currentDelay *= 2\n            }\n        }\n        return Result.failure(Exception(\"Max retries exceeded\"))\n    }\n\n    private fun updateState(update: SyncState.() -> SyncState) {\n        _syncState.value = _syncState.value.update()\n    }\n\n    // Public API methods - Queue operations\n\n    fun performFullSync() {\n        syncScope.launch {\n            syncChannel.send(SyncOperation.FullSync)\n        }\n    }\n\n    suspend fun performFullSyncSuspend() {\n        if (!isLoggedIn()) {\n            Timber.w(\"Skipping full sync - user not logged in\")\n            return\n        }\n        executeFullSync()\n    }\n\n    fun tryAutoSync() {\n        syncScope.launch {\n            if (!isLoggedIn()) {\n                Timber.d(\"Skipping auto sync - user not logged in\")\n                return@launch\n            }\n\n            if (!context.isSyncEnabled() || !context.isInternetConnected()) {\n                return@launch\n            }\n\n            val lastSync = context.dataStore.get(LastFullSyncKey, 0L)\n            val currentTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)\n            if (lastSync > 0 && (currentTime - lastSync) < SYNC_COOLDOWN) {\n                return@launch\n            }\n\n            syncChannel.send(SyncOperation.FullSync)\n\n            context.dataStore.edit { settings ->\n                settings[LastFullSyncKey] = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)\n            }\n        }\n    }\n\n    fun runAllSyncs() {\n        performFullSync()\n    }\n\n    fun likeSong(s: SongEntity) {\n        syncScope.launch {\n            syncChannel.send(SyncOperation.LikeSong(s))\n        }\n    }\n\n    fun subscribeChannel(channelId: String, subscribe: Boolean) {\n        syncScope.launch {\n            syncChannel.send(SyncOperation.SubscribeChannel(channelId, subscribe))\n        }\n    }\n\n    fun savePodcast(podcastId: String, save: Boolean) {\n        Timber.d(\"[PODCAST_TOGGLE] SyncUtils.savePodcast called: podcastId=$podcastId, save=$save\")\n        syncScope.launch {\n            Timber.d(\"[PODCAST_TOGGLE] Sending SavePodcast operation to channel\")\n            syncChannel.send(SyncOperation.SavePodcast(podcastId, save))\n        }\n    }\n\n    fun saveEpisode(episodeId: String, save: Boolean, setVideoId: String? = null) {\n        syncScope.launch {\n            syncChannel.send(SyncOperation.SaveEpisode(episodeId, save, setVideoId))\n        }\n    }\n\n    fun syncLikedSongs() {\n        syncScope.launch {\n            syncChannel.send(SyncOperation.LikedSongs)\n        }\n    }\n\n    fun syncLibrarySongs() {\n        syncScope.launch {\n            syncChannel.send(SyncOperation.LibrarySongs)\n        }\n    }\n\n    fun syncUploadedSongs() {\n        syncScope.launch {\n            syncChannel.send(SyncOperation.UploadedSongs)\n        }\n    }\n\n    fun syncLikedAlbums() {\n        syncScope.launch {\n            syncChannel.send(SyncOperation.LikedAlbums)\n        }\n    }\n\n    fun syncUploadedAlbums() {\n        syncScope.launch {\n            syncChannel.send(SyncOperation.UploadedAlbums)\n        }\n    }\n\n    fun syncArtistsSubscriptions() {\n        syncScope.launch {\n            syncChannel.send(SyncOperation.ArtistsSubscriptions)\n        }\n    }\n\n    fun syncSavedPlaylists() {\n        syncScope.launch {\n            syncChannel.send(SyncOperation.SavedPlaylists)\n        }\n    }\n\n    fun syncAutoSyncPlaylists() {\n        syncScope.launch {\n            syncChannel.send(SyncOperation.AutoSyncPlaylists)\n        }\n    }\n\n    fun syncAllAlbums() {\n        syncScope.launch {\n            syncChannel.send(SyncOperation.LikedAlbums)\n            syncChannel.send(SyncOperation.UploadedAlbums)\n        }\n    }\n\n    fun syncAllArtists() {\n        syncScope.launch {\n            syncChannel.send(SyncOperation.ArtistsSubscriptions)\n        }\n    }\n\n    fun syncPodcastSubscriptions() {\n        syncScope.launch {\n            syncChannel.send(SyncOperation.PodcastSubscriptions)\n        }\n    }\n\n    fun syncEpisodesForLater() {\n        syncScope.launch {\n            syncChannel.send(SyncOperation.EpisodesForLater)\n        }\n    }\n\n    fun cleanupDuplicatePlaylists() {\n        syncScope.launch {\n            syncChannel.send(SyncOperation.CleanupDuplicates)\n        }\n    }\n\n    fun clearAllSyncedContent() {\n        syncScope.launch {\n            syncChannel.send(SyncOperation.ClearAllSynced)\n        }\n    }\n\n    fun clearPodcastData() {\n        syncScope.launch {\n            syncChannel.send(SyncOperation.ClearPodcastData)\n        }\n    }\n\n    // Suspend versions for direct calls\n\n    suspend fun syncLikedSongsSuspend() = executeSyncLikedSongs()\n    suspend fun syncLibrarySongsSuspend() = executeSyncLibrarySongs()\n    suspend fun syncUploadedSongsSuspend() = executeSyncUploadedSongs()\n    suspend fun syncLikedAlbumsSuspend() = executeSyncLikedAlbums()\n    suspend fun syncUploadedAlbumsSuspend() = executeSyncUploadedAlbums()\n    suspend fun syncArtistsSubscriptionsSuspend() = executeSyncArtistsSubscriptions()\n    suspend fun syncPodcastSubscriptionsSuspend() = executeSyncPodcastSubscriptions()\n    suspend fun syncEpisodesForLaterSuspend() = executeSyncEpisodesForLater()\n    suspend fun syncSavedPlaylistsSuspend() = executeSyncSavedPlaylists()\n    suspend fun syncAutoSyncPlaylistsSuspend() = executeSyncAutoSyncPlaylists()\n    suspend fun cleanupDuplicatePlaylistsSuspend() = executeCleanupDuplicatePlaylists()\n    suspend fun clearAllSyncedContentSuspend() = executeClearAllSyncedContent()\n\n    suspend fun clearAllLibraryData() = withContext(Dispatchers.IO) {\n        Timber.d(\"[LOGOUT_CLEAR] Starting complete library data cleanup\")\n        try {\n            // Clear podcast data first\n            Timber.d(\"[LOGOUT_CLEAR] Clearing podcast data\")\n            executeClearPodcastData()\n\n            // Clear history\n            Timber.d(\"[LOGOUT_CLEAR] Clearing listen history and search history\")\n            database.clearListenHistory()\n            database.clearSearchHistory()\n\n            // Get all user tables from the database (auto-detect)\n            val allTables = getAllUserTables()\n            Timber.d(\"[LOGOUT_CLEAR] Found ${allTables.size} tables: $allTables\")\n\n            // Tables to skip (system tables and tables we handle specially)\n            val skipTables = setOf(\n                \"android_metadata\",\n                \"room_master_table\",\n                \"sqlite_sequence\",\n                \"search_history\",  // Already cleared above\n                \"listen_history\"   // Already cleared above\n            )\n\n            // Tables with foreign key references - delete these first (mapping tables)\n            val mappingTables = listOf(\n                \"playlist_song_map\",\n                \"song_album_map\",\n                \"song_artist_map\",\n                \"album_artist_map\",\n                \"related_song_map\"\n            )\n\n            // Delete mapping tables first\n            Timber.d(\"[LOGOUT_CLEAR] Deleting mapping tables\")\n            for (table in mappingTables) {\n                if (table in allTables) {\n                    safeDeleteTable(table)\n                }\n            }\n\n            // Delete all other tables except song (handled specially to keep downloads)\n            Timber.d(\"[LOGOUT_CLEAR] Deleting remaining tables\")\n            for (table in allTables) {\n                if (table in skipTables || table in mappingTables || table == \"song\") {\n                    continue\n                }\n                safeDeleteTable(table)\n            }\n\n            // Finally, delete songs but keep downloaded ones\n            if (\"song\" in allTables) {\n                Timber.d(\"[LOGOUT_CLEAR] Deleting songs (keeping downloaded)\")\n                safeRawQuery(\"DELETE FROM song WHERE dateDownload IS NULL\")\n            }\n\n            Timber.d(\"[LOGOUT_CLEAR] All library data cleared successfully\")\n        } catch (e: Exception) {\n            Timber.e(e, \"[LOGOUT_CLEAR] Error clearing library data\")\n            throw e\n        }\n    }\n\n    private fun getAllUserTables(): List<String> {\n        val tables = mutableListOf<String>()\n        try {\n            database.openHelper.writableDatabase.query(\n                \"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'\"\n            ).use { cursor ->\n                while (cursor.moveToNext()) {\n                    tables.add(cursor.getString(0))\n                }\n            }\n        } catch (e: Exception) {\n            Timber.e(e, \"[LOGOUT_CLEAR] Error getting table list\")\n        }\n        return tables\n    }\n\n    private fun safeDeleteTable(tableName: String) {\n        try {\n            database.raw(androidx.sqlite.db.SimpleSQLiteQuery(\"DELETE FROM $tableName\"))\n            Timber.d(\"[LOGOUT_CLEAR] Cleared table: $tableName\")\n        } catch (e: Exception) {\n            Timber.w(\"[LOGOUT_CLEAR] Table $tableName error: ${e.message}\")\n        }\n    }\n\n    private fun safeRawQuery(query: String) {\n        try {\n            database.raw(androidx.sqlite.db.SimpleSQLiteQuery(query))\n            Timber.d(\"[LOGOUT_CLEAR] Executed: $query\")\n        } catch (e: Exception) {\n            Timber.w(\"[LOGOUT_CLEAR] Query failed: $query - ${e.message}\")\n        }\n    }\n\n    suspend fun syncAllAlbumsSuspend() {\n        executeSyncLikedAlbums()\n        executeSyncUploadedAlbums()\n    }\n\n    suspend fun syncAllArtistsSuspend() {\n        executeSyncArtistsSubscriptions()\n    }\n\n    // Private execution methods\n\n    private suspend fun executeFullSync() = withContext(Dispatchers.IO) {\n        if (!isLoggedIn()) {\n            Timber.w(\"Skipping full sync - user not logged in\")\n            return@withContext\n        }\n\n        updateState { copy(overallStatus = SyncStatus.Syncing, currentOperation = \"Starting full sync\") }\n\n        try {\n            // Sync in sequence to avoid overwhelming the API and database\n            executeSyncLikedSongs()\n            delay(DB_OPERATION_DELAY_MS)\n\n            executeSyncLibrarySongs()\n            delay(DB_OPERATION_DELAY_MS)\n\n            executeSyncUploadedSongs()\n            delay(DB_OPERATION_DELAY_MS)\n\n            executeSyncLikedAlbums()\n            delay(DB_OPERATION_DELAY_MS)\n\n            executeSyncUploadedAlbums()\n            delay(DB_OPERATION_DELAY_MS)\n\n            executeSyncArtistsSubscriptions()\n            delay(DB_OPERATION_DELAY_MS)\n\n            executeSyncPodcastSubscriptions()\n            delay(DB_OPERATION_DELAY_MS)\n\n            executeSyncEpisodesForLater()\n            delay(DB_OPERATION_DELAY_MS)\n\n            executeSyncSavedPlaylists()\n            delay(DB_OPERATION_DELAY_MS)\n\n            executeSyncAutoSyncPlaylists()\n\n            updateState { copy(overallStatus = SyncStatus.Completed, currentOperation = \"\") }\n            Timber.d(\"Full sync completed successfully\")\n        } catch (e: CancellationException) {\n            throw e\n        } catch (e: Exception) {\n            Timber.e(e, \"Error during full sync\")\n            updateState { copy(overallStatus = SyncStatus.Error(e.message ?: \"Unknown error\"), currentOperation = \"\") }\n        }\n    }\n\n    private suspend fun executeLikeSong(s: SongEntity) = withContext(Dispatchers.IO) {\n        if (!isLoggedIn()) {\n            Timber.w(\"Skipping likeSong - user not logged in\")\n            return@withContext\n        }\n\n        withRetry {\n            YouTube.likeVideo(s.id, s.liked)\n        }.onFailure { e ->\n            Timber.e(e, \"Failed to like song on YouTube: ${s.id}\")\n        }\n\n        if (lastfmSendLikes) {\n            try {\n                val dbSong = database.song(s.id).firstOrNull()\n                LastFM.setLoveStatus(\n                    artist = dbSong?.artists?.joinToString { a -> a.name } ?: \"\",\n                    track = s.title,\n                    love = s.liked\n                )\n            } catch (e: Exception) {\n                Timber.e(e, \"Failed to update LastFM love status\")\n            }\n        }\n    }\n\n    private suspend fun executeSubscribeChannel(channelId: String, subscribe: Boolean) = withContext(Dispatchers.IO) {\n        Timber.d(\"[CHANNEL_TOGGLE] executeSubscribeChannel called: channelId=$channelId, subscribe=$subscribe\")\n        if (!isLoggedIn()) {\n            Timber.d(\"[CHANNEL_TOGGLE] Skipping subscribeChannel - user not logged in\")\n            return@withContext\n        }\n\n        Timber.d(\"[CHANNEL_TOGGLE] User is logged in, calling YouTube.subscribeChannel\")\n        withRetry {\n            YouTube.subscribeChannel(channelId, subscribe)\n        }.onSuccess {\n            Timber.d(\"[CHANNEL_TOGGLE] Successfully subscribed/unsubscribed channel: $channelId\")\n            PodcastRefreshTrigger.triggerRefresh()\n        }.onFailure { e ->\n            Timber.e(e, \"[CHANNEL_TOGGLE] Failed to subscribe/unsubscribe channel: $channelId\")\n        }\n    }\n\n    private suspend fun executeSavePodcast(podcastId: String, save: Boolean) = withContext(Dispatchers.IO) {\n        Timber.d(\"[PODCAST_TOGGLE] executeSavePodcast called: podcastId=$podcastId, save=$save\")\n        if (!isLoggedIn()) {\n            Timber.d(\"[PODCAST_TOGGLE] Skipping savePodcast - user not logged in\")\n            return@withContext\n        }\n\n        Timber.d(\"[PODCAST_TOGGLE] User is logged in, calling YouTube.savePodcast\")\n        withRetry {\n            YouTube.savePodcast(podcastId, save)\n        }.onSuccess {\n            Timber.d(\"[PODCAST_TOGGLE] Successfully saved/unsaved podcast: $podcastId\")\n        }.onFailure { e ->\n            Timber.e(e, \"[PODCAST_TOGGLE] Failed to save/unsave podcast: $podcastId\")\n        }\n    }\n\n    private suspend fun executeSaveEpisode(episodeId: String, save: Boolean, setVideoId: String?) = withContext(Dispatchers.IO) {\n        if (!isLoggedIn()) {\n            Timber.d(\"Skipping saveEpisode - user not logged in\")\n            return@withContext\n        }\n\n        if (save) {\n            withRetry {\n                YouTube.addEpisodeToSavedEpisodes(episodeId)\n            }.onFailure { e ->\n                Timber.e(e, \"Failed to save episode: $episodeId\")\n            }\n        } else {\n            if (setVideoId != null) {\n                withRetry {\n                    YouTube.removeEpisodeFromSavedEpisodes(episodeId, setVideoId)\n                }.onFailure { e ->\n                    Timber.e(e, \"Failed to remove episode: $episodeId\")\n                }\n            }\n        }\n    }\n\n    private suspend fun executeSyncLikedSongs() = withContext(Dispatchers.IO) {\n        if (!isLoggedIn()) {\n            Timber.w(\"Skipping syncLikedSongs - user not logged in\")\n            return@withContext\n        }\n\n        updateState { copy(likedSongs = SyncStatus.Syncing, currentOperation = \"Syncing liked songs\") }\n\n        withRetry {\n            YouTube.playlist(\"LM\").completed()\n        }.onSuccess { result ->\n            result.onSuccess { page ->\n                try {\n                    val remoteSongs = page.songs\n                    val remoteIds = remoteSongs.map { it.id }.toSet()\n                    val localSongs = database.likedSongsByNameAsc().first()\n\n                    // Remove likes from songs not in remote\n                    localSongs.filterNot { it.id in remoteIds }.forEach { song ->\n                        try {\n                            database.update(song.song.localToggleLike())\n                            delay(DB_OPERATION_DELAY_MS)\n                        } catch (e: Exception) {\n                            Timber.e(e, \"Failed to update song: ${song.id}\")\n                        }\n                    }\n\n                    // Add/update songs from remote\n                    val now = LocalDateTime.now()\n                    remoteSongs.forEachIndexed { index, song ->\n                        try {\n                            val dbSong = database.song(song.id).firstOrNull()\n                            val timestamp = now.minusSeconds(index.toLong())\n                            val isVideoSong = song.isVideoSong\n\n                            database.transaction {\n                                if (dbSong == null) {\n                                    insert(song.toMediaMetadata()) {\n                                        it.copy(liked = true, likedDate = timestamp, isVideo = isVideoSong)\n                                    }\n                                } else if (!dbSong.song.liked || dbSong.song.likedDate != timestamp || dbSong.song.isVideo != isVideoSong) {\n                                    update(dbSong.song.copy(liked = true, likedDate = timestamp, isVideo = isVideoSong))\n                                }\n                            }\n                            delay(DB_OPERATION_DELAY_MS)\n                        } catch (e: Exception) {\n                            Timber.e(e, \"Failed to process song: ${song.id}\")\n                        }\n                    }\n\n                    updateState { copy(likedSongs = SyncStatus.Completed) }\n                    Timber.d(\"Synced ${remoteSongs.size} liked songs\")\n                } catch (e: Exception) {\n                    Timber.e(e, \"Error processing liked songs\")\n                    updateState { copy(likedSongs = SyncStatus.Error(e.message ?: \"Unknown error\")) }\n                }\n            }.onFailure { e ->\n                Timber.e(e, \"Failed to fetch liked songs from YouTube\")\n                updateState { copy(likedSongs = SyncStatus.Error(e.message ?: \"Unknown error\")) }\n            }\n        }.onFailure { e ->\n            Timber.e(e, \"Failed to sync liked songs after retries\")\n            updateState { copy(likedSongs = SyncStatus.Error(e.message ?: \"Unknown error\")) }\n        }\n    }\n\n    private suspend fun executeSyncLibrarySongs() = withContext(Dispatchers.IO) {\n        if (!isLoggedIn()) {\n            Timber.w(\"Skipping syncLibrarySongs - user not logged in\")\n            return@withContext\n        }\n\n        updateState { copy(librarySongs = SyncStatus.Syncing, currentOperation = \"Syncing library songs\") }\n\n        withRetry {\n            YouTube.library(\"FEmusic_liked_videos\").completed()\n        }.onSuccess { result ->\n            result.onSuccess { page ->\n                try {\n                    val remoteSongs = page.items.filterIsInstance<SongItem>().reversed()\n                    val remoteIds = remoteSongs.map { it.id }.toSet()\n                    val localSongs = database.songsByNameAsc().first()\n\n                    localSongs.filterNot { it.id in remoteIds }.forEach { song ->\n                        try {\n                            database.update(song.song.toggleLibrary())\n                            delay(DB_OPERATION_DELAY_MS)\n                        } catch (e: Exception) {\n                            Timber.e(e, \"Failed to update song: ${song.id}\")\n                        }\n                    }\n\n                    remoteSongs.forEach { song ->\n                        try {\n                            val dbSong = database.song(song.id).firstOrNull()\n                            database.transaction {\n                                if (dbSong == null) {\n                                    insert(song.toMediaMetadata()) { it.toggleLibrary() }\n                                } else if (dbSong.song.inLibrary == null) {\n                                    update(dbSong.song.toggleLibrary())\n                                }\n                            }\n                            delay(DB_OPERATION_DELAY_MS)\n                        } catch (e: Exception) {\n                            Timber.e(e, \"Failed to process song: ${song.id}\")\n                        }\n                    }\n\n                    updateState { copy(librarySongs = SyncStatus.Completed) }\n                    Timber.d(\"Synced ${remoteSongs.size} library songs\")\n                } catch (e: Exception) {\n                    Timber.e(e, \"Error processing library songs\")\n                    updateState { copy(librarySongs = SyncStatus.Error(e.message ?: \"Unknown error\")) }\n                }\n            }.onFailure { e ->\n                Timber.e(e, \"Failed to fetch library songs from YouTube\")\n                updateState { copy(librarySongs = SyncStatus.Error(e.message ?: \"Unknown error\")) }\n            }\n        }.onFailure { e ->\n            Timber.e(e, \"Failed to sync library songs after retries\")\n            updateState { copy(librarySongs = SyncStatus.Error(e.message ?: \"Unknown error\")) }\n        }\n    }\n\n    private suspend fun executeSyncUploadedSongs() = withContext(Dispatchers.IO) {\n        if (!isLoggedIn()) {\n            Timber.w(\"Skipping syncUploadedSongs - user not logged in\")\n            return@withContext\n        }\n\n        updateState { copy(uploadedSongs = SyncStatus.Syncing, currentOperation = \"Syncing uploaded songs\") }\n\n        withRetry {\n            // Uploaded songs are in Tab 1 (\"Uploads\"), not Tab 0 (\"Library\")\n            YouTube.library(\"FEmusic_library_privately_owned_tracks\", tabIndex = 1).completed()\n        }.onSuccess { result ->\n            result.onSuccess { page ->\n                try {\n                    val remoteSongs = page.items.filterIsInstance<SongItem>().reversed()\n                    val remoteIds = remoteSongs.map { it.id }.toSet()\n                    val localSongs = database.uploadedSongsByNameAsc().first()\n\n                    // Remove uploaded flag from songs no longer in remote\n                    localSongs.filterNot { it.id in remoteIds }.forEach { song ->\n                        try {\n                            database.update(song.song.toggleUploaded())\n                            delay(DB_OPERATION_DELAY_MS)\n                        } catch (e: Exception) {\n                            Timber.e(e, \"Failed to update song: ${song.id}\")\n                        }\n                    }\n\n                    // Sync remote songs to local database\n                    remoteSongs.forEach { song ->\n                        try {\n                            val dbSong = database.song(song.id).firstOrNull()\n                            database.transaction {\n                                if (dbSong == null) {\n                                    insert(song.toMediaMetadata()) { it.toggleUploaded() }\n                                } else if (!dbSong.song.isUploaded) {\n                                    update(dbSong.song.copy(isUploaded = true, uploadEntityId = song.uploadEntityId))\n                                } else if (dbSong.song.uploadEntityId != song.uploadEntityId && song.uploadEntityId != null) {\n                                    // Update uploadEntityId if it differs from remote\n                                    update(dbSong.song.copy(uploadEntityId = song.uploadEntityId))\n                                }\n                            }\n                            delay(DB_OPERATION_DELAY_MS)\n                        } catch (e: Exception) {\n                            Timber.e(e, \"Failed to process song: ${song.id}\")\n                        }\n                    }\n\n                    updateState { copy(uploadedSongs = SyncStatus.Completed) }\n                    Timber.d(\"Synced ${remoteSongs.size} uploaded songs\")\n                } catch (e: Exception) {\n                    Timber.e(e, \"Error processing uploaded songs\")\n                    updateState { copy(uploadedSongs = SyncStatus.Error(e.message ?: \"Unknown error\")) }\n                }\n            }.onFailure { e ->\n                Timber.e(e, \"Failed to fetch uploaded songs from YouTube\")\n                updateState { copy(uploadedSongs = SyncStatus.Error(e.message ?: \"Unknown error\")) }\n            }\n        }.onFailure { e ->\n            Timber.e(e, \"Failed to sync uploaded songs after retries\")\n            updateState { copy(uploadedSongs = SyncStatus.Error(e.message ?: \"Unknown error\")) }\n        }\n    }\n\n    private suspend fun executeSyncLikedAlbums() = withContext(Dispatchers.IO) {\n        if (!isLoggedIn()) {\n            Timber.w(\"Skipping syncLikedAlbums - user not logged in\")\n            return@withContext\n        }\n\n        updateState { copy(likedAlbums = SyncStatus.Syncing, currentOperation = \"Syncing liked albums\") }\n\n        withRetry {\n            YouTube.library(\"FEmusic_liked_albums\").completed()\n        }.onSuccess { result ->\n            result.onSuccess { page ->\n                try {\n                    val remoteAlbums = page.items.filterIsInstance<AlbumItem>().reversed()\n                    val remoteIds = remoteAlbums.map { it.id }.toSet()\n                    val localAlbums = database.albumsLikedByNameAsc().first()\n\n                    localAlbums.filterNot { it.id in remoteIds }.forEach { album ->\n                        try {\n                            database.update(album.album.localToggleLike())\n                            delay(DB_OPERATION_DELAY_MS)\n                        } catch (e: Exception) {\n                            Timber.e(e, \"Failed to update album: ${album.id}\")\n                        }\n                    }\n\n                    remoteAlbums.forEach { album ->\n                        try {\n                            val dbAlbum = database.album(album.id).firstOrNull()\n                            YouTube.album(album.browseId).onSuccess { albumPage ->\n                                if (dbAlbum == null) {\n                                    database.insert(albumPage)\n                                    database.album(album.id).firstOrNull()?.let { newDbAlbum ->\n                                        database.update(newDbAlbum.album.localToggleLike())\n                                    }\n                                } else if (dbAlbum.album.bookmarkedAt == null) {\n                                    database.update(dbAlbum.album.localToggleLike())\n                                }\n                            }\n                            delay(DB_OPERATION_DELAY_MS)\n                        } catch (e: Exception) {\n                            Timber.e(e, \"Failed to process album: ${album.id}\")\n                        }\n                    }\n\n                    updateState { copy(likedAlbums = SyncStatus.Completed) }\n                    Timber.d(\"Synced ${remoteAlbums.size} liked albums\")\n                } catch (e: Exception) {\n                    Timber.e(e, \"Error processing liked albums\")\n                    updateState { copy(likedAlbums = SyncStatus.Error(e.message ?: \"Unknown error\")) }\n                }\n            }.onFailure { e ->\n                Timber.e(e, \"Failed to fetch liked albums from YouTube\")\n                updateState { copy(likedAlbums = SyncStatus.Error(e.message ?: \"Unknown error\")) }\n            }\n        }.onFailure { e ->\n            Timber.e(e, \"Failed to sync liked albums after retries\")\n            updateState { copy(likedAlbums = SyncStatus.Error(e.message ?: \"Unknown error\")) }\n        }\n    }\n\n    private suspend fun executeSyncUploadedAlbums() = withContext(Dispatchers.IO) {\n        if (!isLoggedIn()) {\n            Timber.w(\"Skipping syncUploadedAlbums - user not logged in\")\n            return@withContext\n        }\n\n        updateState { copy(uploadedAlbums = SyncStatus.Syncing, currentOperation = \"Syncing uploaded albums\") }\n\n        withRetry {\n            YouTube.library(\"FEmusic_library_privately_owned_releases\").completed()\n        }.onSuccess { result ->\n            result.onSuccess { page ->\n                try {\n                    val remoteAlbums = page.items.filterIsInstance<AlbumItem>().reversed()\n                    val remoteIds = remoteAlbums.map { it.id }.toSet()\n                    val localAlbums = database.albumsUploadedByNameAsc().first()\n\n                    localAlbums.filterNot { it.id in remoteIds }.forEach { album ->\n                        try {\n                            database.update(album.album.toggleUploaded())\n                            delay(DB_OPERATION_DELAY_MS)\n                        } catch (e: Exception) {\n                            Timber.e(e, \"Failed to update album: ${album.id}\")\n                        }\n                    }\n\n                    remoteAlbums.forEach { album ->\n                        try {\n                            val dbAlbum = database.album(album.id).firstOrNull()\n                            YouTube.album(album.browseId).onSuccess { albumPage ->\n                                if (dbAlbum == null) {\n                                    database.insert(albumPage)\n                                    database.album(album.id).firstOrNull()?.let { newDbAlbum ->\n                                        database.update(newDbAlbum.album.toggleUploaded())\n                                    }\n                                } else if (!dbAlbum.album.isUploaded) {\n                                    database.update(dbAlbum.album.toggleUploaded())\n                                }\n                            }.onFailure { reportException(it) }\n                            delay(DB_OPERATION_DELAY_MS)\n                        } catch (e: Exception) {\n                            Timber.e(e, \"Failed to process album: ${album.id}\")\n                        }\n                    }\n\n                    updateState { copy(uploadedAlbums = SyncStatus.Completed) }\n                    Timber.d(\"Synced ${remoteAlbums.size} uploaded albums\")\n                } catch (e: Exception) {\n                    Timber.e(e, \"Error processing uploaded albums\")\n                    updateState { copy(uploadedAlbums = SyncStatus.Error(e.message ?: \"Unknown error\")) }\n                }\n            }.onFailure { e ->\n                Timber.e(e, \"Failed to fetch uploaded albums from YouTube\")\n                updateState { copy(uploadedAlbums = SyncStatus.Error(e.message ?: \"Unknown error\")) }\n            }\n        }.onFailure { e ->\n            Timber.e(e, \"Failed to sync uploaded albums after retries\")\n            updateState { copy(uploadedAlbums = SyncStatus.Error(e.message ?: \"Unknown error\")) }\n        }\n    }\n\n    private suspend fun executeSyncArtistsSubscriptions() = withContext(Dispatchers.IO) {\n        if (!isLoggedIn()) {\n            Timber.w(\"Skipping syncArtistsSubscriptions - user not logged in\")\n            return@withContext\n        }\n\n        updateState { copy(artists = SyncStatus.Syncing, currentOperation = \"Syncing artist subscriptions\") }\n\n        withRetry {\n            YouTube.library(\"FEmusic_library_corpus_artists\").completed()\n        }.onSuccess { result ->\n            result.onSuccess { page ->\n                try {\n                    val remoteArtists = page.items.filterIsInstance<ArtistItem>()\n                    val remoteIds = remoteArtists.map { it.id }.toSet()\n                    val localArtists = database.artistsBookmarkedByNameAsc().first()\n\n                    localArtists.filterNot { it.id in remoteIds }.forEach { artist ->\n                        try {\n                            database.update(artist.artist.localToggleLike())\n                            delay(DB_OPERATION_DELAY_MS)\n                        } catch (e: Exception) {\n                            Timber.e(e, \"Failed to update artist: ${artist.id}\")\n                        }\n                    }\n\n                    remoteArtists.forEach { artist ->\n                        try {\n                            val dbArtist = database.artist(artist.id).firstOrNull()\n                            val channelId = artist.channelId ?: if (artist.id.startsWith(\"UC\")) {\n                                try {\n                                    YouTube.getChannelId(artist.id).takeIf { it.isNotEmpty() }\n                                } catch (e: Exception) {\n                                    null\n                                }\n                            } else null\n\n                            database.transaction {\n                                if (dbArtist == null) {\n                                    insert(\n                                        ArtistEntity(\n                                            id = artist.id,\n                                            name = artist.title,\n                                            thumbnailUrl = artist.thumbnail,\n                                            channelId = channelId,\n                                            bookmarkedAt = LocalDateTime.now()\n                                        )\n                                    )\n                                } else {\n                                    val existing = dbArtist.artist\n                                    val needsChannelIdUpdate = existing.channelId == null && channelId != null\n                                    if (existing.bookmarkedAt == null || needsChannelIdUpdate ||\n                                        existing.name != artist.title || existing.thumbnailUrl != artist.thumbnail) {\n                                        update(\n                                            existing.copy(\n                                                name = artist.title,\n                                                thumbnailUrl = artist.thumbnail,\n                                                channelId = channelId ?: existing.channelId,\n                                                bookmarkedAt = existing.bookmarkedAt ?: LocalDateTime.now(),\n                                                lastUpdateTime = LocalDateTime.now()\n                                            )\n                                        )\n                                    }\n                                }\n                            }\n                            delay(DB_OPERATION_DELAY_MS)\n                        } catch (e: Exception) {\n                            Timber.e(e, \"Failed to process artist: ${artist.id}\")\n                        }\n                    }\n\n                    updateState { copy(artists = SyncStatus.Completed) }\n                    Timber.d(\"Synced ${remoteArtists.size} artist subscriptions\")\n                } catch (e: Exception) {\n                    Timber.e(e, \"Error processing artist subscriptions\")\n                    updateState { copy(artists = SyncStatus.Error(e.message ?: \"Unknown error\")) }\n                }\n            }.onFailure { e ->\n                Timber.e(e, \"Failed to fetch artist subscriptions from YouTube\")\n                updateState { copy(artists = SyncStatus.Error(e.message ?: \"Unknown error\")) }\n            }\n        }.onFailure { e ->\n            Timber.e(e, \"Failed to sync artist subscriptions after retries\")\n            updateState { copy(artists = SyncStatus.Error(e.message ?: \"Unknown error\")) }\n        }\n    }\n\n    private suspend fun executeSyncPodcastSubscriptions() = withContext(Dispatchers.IO) {\n        Timber.d(\"[PODCAST_SYNC] executeSyncPodcastSubscriptions() started\")\n        if (!isLoggedIn()) {\n            Timber.w(\"[PODCAST_SYNC] Skipping syncPodcastSubscriptions - user not logged in\")\n            return@withContext\n        }\n        Timber.d(\"[PODCAST_SYNC] User is logged in, proceeding with sync\")\n\n        updateState { copy(currentOperation = \"Syncing podcast subscriptions\") }\n\n        // Sync saved podcast shows (most common - saved via likePlaylist)\n        withRetry {\n            Timber.d(\"[PODCAST_SYNC] Calling YouTube.savedPodcastShows()\")\n            YouTube.savedPodcastShows()\n        }.onSuccess { result ->\n            Timber.d(\"[PODCAST_SYNC] savedPodcastShows succeeded, result isSuccess=${result.isSuccess}\")\n            result.onSuccess { remotePodcasts ->\n                try {\n                    Timber.d(\"[PODCAST_SYNC] Fetched ${remotePodcasts.size} saved podcast shows\")\n\n                    remotePodcasts.forEachIndexed { index, podcast ->\n                        Timber.d(\"[PODCAST_SYNC] Remote podcast $index: id=${podcast.id}, title=${podcast.title}, author=${podcast.author?.name}\")\n                    }\n\n                    // Server-first: YouTube Music is the source of truth\n                    // Add/update podcasts from remote\n                    remotePodcasts.forEach { podcast ->\n                        try {\n                            val dbPodcast = database.podcast(podcast.id).firstOrNull()\n                            Timber.d(\"[PODCAST_SYNC] Processing remote podcast ${podcast.id}: exists in db=${dbPodcast != null}, isSubscribed=${dbPodcast?.bookmarkedAt != null}\")\n\n                            database.transaction {\n                                if (dbPodcast == null) {\n                                    // Only add truly new podcasts from server\n                                    Timber.d(\"[PODCAST_SYNC] Inserting new podcast: ${podcast.id}\")\n                                    insert(\n                                        PodcastEntity(\n                                            id = podcast.id,\n                                            title = podcast.title,\n                                            author = podcast.author?.name,\n                                            thumbnailUrl = podcast.thumbnail,\n                                            channelId = podcast.channelId ?: podcast.author?.id,\n                                            bookmarkedAt = LocalDateTime.now(),\n                                        )\n                                    )\n                                } else if (dbPodcast.bookmarkedAt != null) {\n                                    // Update metadata for already-saved podcasts, but don't re-bookmark\n                                    // ones that user has removed locally (respect local state)\n                                    Timber.d(\"[PODCAST_SYNC] Updating metadata for saved podcast: ${podcast.id}\")\n                                    update(\n                                        dbPodcast.copy(\n                                            title = podcast.title,\n                                            author = podcast.author?.name,\n                                            thumbnailUrl = podcast.thumbnail,\n                                            channelId = podcast.channelId ?: podcast.author?.id ?: dbPodcast.channelId,\n                                            lastUpdateTime = LocalDateTime.now(),\n                                        )\n                                    )\n                                } else {\n                                    // Podcast exists locally but is unbookmarked - user removed it\n                                    // Don't re-add; the server removal is likely still pending\n                                    Timber.d(\"[PODCAST_SYNC] Skipping unbookmarked podcast: ${podcast.id}\")\n                                }\n                            }\n                            delay(DB_OPERATION_DELAY_MS)\n                        } catch (e: Exception) {\n                            Timber.e(e, \"[PODCAST_SYNC] Failed to process podcast: ${podcast.id}\")\n                        }\n                    }\n\n                    Timber.d(\"[PODCAST_SYNC] Synced ${remotePodcasts.size} saved podcast shows successfully\")\n                } catch (e: Exception) {\n                    Timber.e(e, \"[PODCAST_SYNC] Error processing saved podcast shows\")\n                }\n            }.onFailure { e ->\n                Timber.e(e, \"[PODCAST_SYNC] Failed to fetch saved podcast shows from YouTube\")\n            }\n        }.onFailure { e ->\n            Timber.e(e, \"[PODCAST_SYNC] Failed to sync saved podcast shows after retries\")\n        }\n\n        // Also sync subscribed podcast channels (subscribed via subscribeChannel API)\n        withRetry {\n            Timber.d(\"[PODCAST_SYNC] Calling YouTube.libraryPodcastChannels()\")\n            YouTube.libraryPodcastChannels()\n        }.onSuccess { result ->\n            Timber.d(\"[PODCAST_SYNC] libraryPodcastChannels succeeded, result isSuccess=${result.isSuccess}\")\n            result.onSuccess { page ->\n                try {\n                    val remotePodcasts = page.items.filterIsInstance<PodcastItem>()\n                    Timber.d(\"[PODCAST_SYNC] Fetched ${remotePodcasts.size} subscribed podcast channels\")\n\n                    // Add/update podcasts from remote channels\n                    remotePodcasts.forEach { podcast ->\n                        try {\n                            val dbPodcast = database.podcast(podcast.id).firstOrNull()\n                            Timber.d(\"[PODCAST_SYNC] Processing subscribed channel ${podcast.id}: exists in db=${dbPodcast != null}\")\n\n                            database.transaction {\n                                if (dbPodcast == null) {\n                                    // Only add truly new podcasts from server\n                                    Timber.d(\"[PODCAST_SYNC] Inserting new subscribed channel: ${podcast.id}\")\n                                    insert(\n                                        PodcastEntity(\n                                            id = podcast.id,\n                                            title = podcast.title,\n                                            author = podcast.author?.name,\n                                            thumbnailUrl = podcast.thumbnail,\n                                            channelId = podcast.channelId ?: podcast.author?.id,\n                                            bookmarkedAt = LocalDateTime.now(),\n                                        )\n                                    )\n                                } else if (dbPodcast.bookmarkedAt != null) {\n                                    // Update metadata for already-saved podcasts\n                                    Timber.d(\"[PODCAST_SYNC] Updating metadata for subscribed channel: ${podcast.id}\")\n                                    update(\n                                        dbPodcast.copy(\n                                            title = podcast.title,\n                                            author = podcast.author?.name,\n                                            thumbnailUrl = podcast.thumbnail,\n                                            channelId = podcast.channelId ?: podcast.author?.id ?: dbPodcast.channelId,\n                                            lastUpdateTime = LocalDateTime.now(),\n                                        )\n                                    )\n                                } else {\n                                    // Podcast exists locally but is unbookmarked - don't re-add\n                                    Timber.d(\"[PODCAST_SYNC] Skipping unbookmarked channel: ${podcast.id}\")\n                                }\n                            }\n                            delay(DB_OPERATION_DELAY_MS)\n                        } catch (e: Exception) {\n                            Timber.e(e, \"[PODCAST_SYNC] Failed to process subscribed channel: ${podcast.id}\")\n                        }\n                    }\n\n                    Timber.d(\"[PODCAST_SYNC] Synced ${remotePodcasts.size} subscribed podcast channels successfully\")\n                } catch (e: Exception) {\n                    Timber.e(e, \"[PODCAST_SYNC] Error processing subscribed podcast channels\")\n                }\n            }.onFailure { e ->\n                Timber.e(e, \"[PODCAST_SYNC] Failed to fetch subscribed podcast channels from YouTube\")\n            }\n        }.onFailure { e ->\n            Timber.e(e, \"[PODCAST_SYNC] Failed to sync subscribed podcast channels after retries\")\n        }\n\n        // Cleanup: Remove local podcasts that are no longer subscribed on YouTube Music\n        try {\n            val allRemoteIds = mutableSetOf<String>()\n\n            // Collect all remote podcast IDs\n            YouTube.savedPodcastShows().onSuccess { podcasts ->\n                allRemoteIds.addAll(podcasts.map { it.id })\n            }\n            YouTube.libraryPodcastChannels().onSuccess { page ->\n                allRemoteIds.addAll(page.items.filterIsInstance<PodcastItem>().map { it.id })\n            }\n\n            if (allRemoteIds.isNotEmpty()) {\n                val localPodcasts = database.subscribedPodcasts().first()\n                val localOnlyPodcasts = localPodcasts.filterNot { it.id in allRemoteIds }\n                Timber.d(\"[PODCAST_SYNC] Cleanup: removing ${localOnlyPodcasts.size} podcasts not on YTM\")\n\n                localOnlyPodcasts.forEach { podcast ->\n                    try {\n                        // Remove subscription (set bookmarkedAt to null)\n                        database.transaction {\n                            update(podcast.copy(bookmarkedAt = null))\n                        }\n                        Timber.d(\"[PODCAST_SYNC] Unsubscribed from local podcast: ${podcast.id}\")\n                    } catch (e: Exception) {\n                        Timber.e(e, \"[PODCAST_SYNC] Failed to cleanup podcast: ${podcast.id}\")\n                    }\n                }\n            }\n        } catch (e: Exception) {\n            Timber.e(e, \"[PODCAST_SYNC] Error during cleanup\")\n        }\n    }\n\n    private suspend fun executeSyncEpisodesForLater() = withContext(Dispatchers.IO) {\n        Timber.d(\"[EPISODES_SYNC] executeSyncEpisodesForLater() started\")\n        if (!isLoggedIn()) {\n            Timber.w(\"[EPISODES_SYNC] Skipping syncEpisodesForLater - user not logged in\")\n            return@withContext\n        }\n        Timber.d(\"[EPISODES_SYNC] User is logged in, proceeding with sync\")\n\n        updateState { copy(currentOperation = \"Syncing episodes for later\") }\n\n        withRetry {\n            Timber.d(\"[EPISODES_SYNC] Calling YouTube.episodesForLater() (VLSE playlist)\")\n            YouTube.episodesForLater()\n        }.onSuccess { result ->\n            result.onSuccess { remoteEpisodes ->\n                try {\n                    Timber.d(\"[EPISODES_SYNC] Fetched ${remoteEpisodes.size} episodes from VLSE playlist\")\n                    val remoteIds = remoteEpisodes.map { it.id }.toSet()\n\n                    // Get local episodes that are saved (for cleanup later)\n                    val localSavedEpisodes = database.podcastEpisodesByCreateDateAsc().first()\n                        .filter { it.song.inLibrary != null }\n                    Timber.d(\"[EPISODES_SYNC] Local saved episodes: ${localSavedEpisodes.size}\")\n\n                    // Server-first: YouTube Music is the source of truth\n                    // Sync remote episodes to local database\n                    remoteEpisodes.forEach { episode ->\n                        try {\n                            val dbSong = database.song(episode.id).firstOrNull()\n                            Timber.d(\"[EPISODES_SYNC] Processing remote episode ${episode.id}: exists in db=${dbSong != null}\")\n\n                            database.transaction {\n                                if (dbSong == null) {\n                                    Timber.d(\"[EPISODES_SYNC] Inserting new episode: ${episode.id}\")\n                                    val mediaMetadata = episode.toMediaMetadata()\n                                    insert(mediaMetadata.toSongEntity().copy(\n                                        inLibrary = LocalDateTime.now(),\n                                        isEpisode = true\n                                    ))\n                                    // Insert artists\n                                    mediaMetadata.artists.forEach { artist ->\n                                        artist.id?.let { artistId ->\n                                            insert(\n                                                ArtistEntity(\n                                                    id = artistId,\n                                                    name = artist.name,\n                                                )\n                                            )\n                                        }\n                                    }\n                                } else if (!dbSong.song.isEpisode || dbSong.song.inLibrary == null) {\n                                    Timber.d(\"[EPISODES_SYNC] Updating existing song to episode in library: ${episode.id}\")\n                                    update(\n                                        dbSong.song.copy(\n                                            isEpisode = true,\n                                            inLibrary = dbSong.song.inLibrary ?: LocalDateTime.now(),\n                                            libraryAddToken = episode.libraryAddToken ?: dbSong.song.libraryAddToken,\n                                            libraryRemoveToken = episode.libraryRemoveToken ?: dbSong.song.libraryRemoveToken,\n                                        )\n                                    )\n                                } else {\n                                    // Update tokens if we got new ones\n                                    if (episode.libraryAddToken != null || episode.libraryRemoveToken != null) {\n                                        update(\n                                            dbSong.song.copy(\n                                                libraryAddToken = episode.libraryAddToken ?: dbSong.song.libraryAddToken,\n                                                libraryRemoveToken = episode.libraryRemoveToken ?: dbSong.song.libraryRemoveToken,\n                                            )\n                                        )\n                                    }\n                                    Timber.d(\"[EPISODES_SYNC] Episode already in library: ${episode.id}\")\n                                }\n                                // Store setVideoId for removal capability\n                                episode.setVideoId?.let { svid ->\n                                    Timber.d(\"[EPISODES_SYNC] Storing setVideoId for ${episode.id}: $svid\")\n                                    insert(SetVideoIdEntity(videoId = episode.id, setVideoId = svid))\n                                }\n                            }\n                            delay(DB_OPERATION_DELAY_MS)\n                        } catch (e: Exception) {\n                            Timber.e(e, \"[EPISODES_SYNC] Failed to process episode: ${episode.id}\")\n                        }\n                    }\n\n                    // Cleanup: Remove local episodes that are no longer in Episodes for Later\n                    val localToRemove = localSavedEpisodes.filterNot { it.id in remoteIds }\n                    Timber.d(\"[EPISODES_SYNC] Cleanup: removing ${localToRemove.size} episodes not in VLSE\")\n                    localToRemove.forEach { song ->\n                        try {\n                            database.transaction {\n                                update(song.song.copy(inLibrary = null))\n                            }\n                            Timber.d(\"[EPISODES_SYNC] Removed episode from library: ${song.id}\")\n                        } catch (e: Exception) {\n                            Timber.e(e, \"[EPISODES_SYNC] Failed to cleanup episode: ${song.id}\")\n                        }\n                    }\n\n                    Timber.d(\"[EPISODES_SYNC] Synced ${remoteEpisodes.size} episodes successfully\")\n                } catch (e: Exception) {\n                    Timber.e(e, \"[EPISODES_SYNC] Error processing episodes\")\n                }\n            }.onFailure { e ->\n                Timber.e(e, \"[EPISODES_SYNC] Failed to fetch episodes from YouTube\")\n            }\n        }.onFailure { e ->\n            Timber.e(e, \"[EPISODES_SYNC] Failed to sync episodes after retries\")\n        }\n    }\n\n    private suspend fun executeSyncSavedPlaylists() = withContext(Dispatchers.IO) {\n        if (!isLoggedIn()) {\n            Timber.w(\"Skipping syncSavedPlaylists - user not logged in\")\n            return@withContext\n        }\n\n        updateState { copy(playlists = SyncStatus.Syncing, currentOperation = \"Syncing saved playlists\") }\n\n        withRetry {\n            YouTube.library(\"FEmusic_liked_playlists\").completed()\n        }.onSuccess { result ->\n            result.onSuccess { page ->\n                try {\n                    val remotePlaylists = page.items.filterIsInstance<PlaylistItem>()\n                        .filterNot { it.id == \"LM\" || it.id == \"SE\" }\n                        .reversed()\n                    val remoteIds = remotePlaylists.map { it.id }.toSet()\n\n                    val localPlaylists = database.playlistsByNameAsc().first()\n                    localPlaylists.filterNot { it.playlist.browseId in remoteIds }\n                        .filterNot { it.playlist.browseId == null }\n                        .forEach { playlist ->\n                            try {\n                                database.update(playlist.playlist.localToggleLike())\n                                delay(DB_OPERATION_DELAY_MS)\n                            } catch (e: Exception) {\n                                Timber.e(e, \"Failed to update playlist: ${playlist.id}\")\n                            }\n                        }\n\n                    for (playlist in remotePlaylists) {\n                        try {\n                            var playlistEntity = localPlaylists.find { it.playlist.browseId == playlist.id }?.playlist\n\n                            if (playlistEntity == null) {\n                                playlistEntity = PlaylistEntity(\n                                    name = playlist.title,\n                                    browseId = playlist.id,\n                                    thumbnailUrl = playlist.thumbnail,\n                                    isEditable = playlist.isEditable,\n                                    bookmarkedAt = LocalDateTime.now(),\n                                    remoteSongCount = playlist.songCountText?.let {\n                                        Regex(\"\"\"\\d+\"\"\").find(it)?.value?.toIntOrNull()\n                                    },\n                                    playEndpointParams = playlist.playEndpoint?.params,\n                                    shuffleEndpointParams = playlist.shuffleEndpoint?.params,\n                                    radioEndpointParams = playlist.radioEndpoint?.params\n                                )\n                                database.insert(playlistEntity)\n                                Timber.d(\"syncSavedPlaylists: Created new playlist ${playlist.title} (${playlist.id})\")\n                            } else {\n                                database.update(playlistEntity, playlist)\n                                Timber.d(\"syncSavedPlaylists: Updated existing playlist ${playlist.title} (${playlist.id})\")\n                            }\n\n                            executeSyncPlaylist(playlist.id, playlistEntity.id)\n                            delay(DB_OPERATION_DELAY_MS)\n                        } catch (e: Exception) {\n                            Timber.e(e, \"Failed to sync playlist ${playlist.title}\")\n                        }\n                    }\n\n                    updateState { copy(playlists = SyncStatus.Completed) }\n                    Timber.d(\"Synced ${remotePlaylists.size} saved playlists\")\n                } catch (e: Exception) {\n                    Timber.e(e, \"Error processing saved playlists\")\n                    updateState { copy(playlists = SyncStatus.Error(e.message ?: \"Unknown error\")) }\n                }\n            }.onFailure { e ->\n                Timber.e(e, \"syncSavedPlaylists: Failed to fetch playlists from YouTube\")\n                updateState { copy(playlists = SyncStatus.Error(e.message ?: \"Unknown error\")) }\n            }\n        }.onFailure { e ->\n            Timber.e(e, \"Failed to sync saved playlists after retries\")\n            updateState { copy(playlists = SyncStatus.Error(e.message ?: \"Unknown error\")) }\n        }\n    }\n\n    private suspend fun executeSyncAutoSyncPlaylists() = withContext(Dispatchers.IO) {\n        if (!isLoggedIn()) {\n            Timber.w(\"Skipping syncAutoSyncPlaylists - user not logged in\")\n            return@withContext\n        }\n\n        try {\n            val autoSyncPlaylists = database.playlistsByNameAsc().first()\n                .filter { it.playlist.isAutoSync && it.playlist.browseId != null }\n\n            Timber.d(\"syncAutoSyncPlaylists: Found ${autoSyncPlaylists.size} playlists to sync\")\n\n            autoSyncPlaylists.forEach { playlist ->\n                try {\n                    executeSyncPlaylist(playlist.playlist.browseId!!, playlist.playlist.id)\n                    delay(DB_OPERATION_DELAY_MS)\n                } catch (e: Exception) {\n                    Timber.e(e, \"Failed to sync playlist ${playlist.playlist.name}\")\n                }\n            }\n        } catch (e: Exception) {\n            Timber.e(e, \"Error syncing auto-sync playlists\")\n        }\n    }\n\n    private suspend fun executeSyncPlaylist(browseId: String, playlistId: String) = withContext(Dispatchers.IO) {\n        Timber.d(\"syncPlaylist: Starting sync for browseId=$browseId, playlistId=$playlistId\")\n\n        withRetry {\n            YouTube.playlist(browseId).completed()\n        }.onSuccess { result ->\n            result.onSuccess { page ->\n                try {\n                    val songs = page.songs.map(SongItem::toMediaMetadata)\n                    Timber.d(\"syncPlaylist: Fetched ${songs.size} songs from remote\")\n\n                    if (songs.isEmpty()) {\n                        Timber.w(\"syncPlaylist: Remote playlist is empty, skipping sync\")\n                        return@onSuccess\n                    }\n\n                    val remoteIds = songs.map { it.id }\n                    val localIds = database.playlistSongs(playlistId).first()\n                        .sortedBy { it.map.position }\n                        .map { it.song.id }\n\n                    if (remoteIds == localIds) {\n                        Timber.d(\"syncPlaylist: Local and remote are in sync, no changes needed\")\n                        return@onSuccess\n                    }\n\n                    Timber.d(\"syncPlaylist: Updating local playlist (remote: ${remoteIds.size}, local: ${localIds.size})\")\n\n                    database.withTransaction {\n                        database.clearPlaylist(playlistId)\n                        songs.forEachIndexed { idx, song ->\n                            if (database.song(song.id).firstOrNull() == null) {\n                                database.insert(song)\n                            }\n                            database.insert(\n                                PlaylistSongMap(\n                                    songId = song.id,\n                                    playlistId = playlistId,\n                                    position = idx,\n                                    setVideoId = song.setVideoId\n                                )\n                            )\n                        }\n                    }\n                    Timber.d(\"syncPlaylist: Successfully synced playlist\")\n                } catch (e: Exception) {\n                    Timber.e(e, \"Error processing playlist sync\")\n                }\n            }.onFailure { e ->\n                Timber.e(e, \"syncPlaylist: Failed to fetch playlist from YouTube\")\n            }\n        }.onFailure { e ->\n            Timber.e(e, \"syncPlaylist: Failed after retries\")\n        }\n    }\n\n    private suspend fun executeCleanupDuplicatePlaylists() = withContext(Dispatchers.IO) {\n        try {\n            val allPlaylists = database.playlistsByNameAsc().first()\n            val browseIdGroups = allPlaylists\n                .filter { it.playlist.browseId != null }\n                .groupBy { it.playlist.browseId }\n\n            for ((browseId, playlists) in browseIdGroups) {\n                if (playlists.size > 1) {\n                    Timber.w(\"Found ${playlists.size} duplicate playlists for browseId: $browseId\")\n                    val toKeep = playlists.maxByOrNull { it.songCount } ?: playlists.first()\n\n                    playlists.filter { it.id != toKeep.id }.forEach { duplicate ->\n                        try {\n                            Timber.d(\"Removing duplicate playlist: ${duplicate.playlist.name} (${duplicate.id})\")\n                            database.clearPlaylist(duplicate.id)\n                            database.delete(duplicate.playlist)\n                            delay(DB_OPERATION_DELAY_MS)\n                        } catch (e: Exception) {\n                            Timber.e(e, \"Failed to remove duplicate playlist: ${duplicate.id}\")\n                        }\n                    }\n                }\n            }\n        } catch (e: Exception) {\n            Timber.e(e, \"Error cleaning up duplicate playlists\")\n        }\n    }\n\n    private suspend fun executeClearAllSyncedContent() = withContext(Dispatchers.IO) {\n        Timber.d(\"clearAllSyncedContent: Starting cleanup\")\n\n        updateState { copy(overallStatus = SyncStatus.Syncing, currentOperation = \"Clearing synced content\") }\n\n        try {\n            database.withTransaction {\n                // Clear liked songs\n                val likedSongs = database.likedSongsByNameAsc().first()\n                likedSongs.forEach {\n                    database.update(it.song.copy(liked = false, likedDate = null))\n                }\n\n                // Clear library songs\n                val librarySongs = database.songsByNameAsc().first()\n                librarySongs.forEach {\n                    if (it.song.inLibrary != null) {\n                        database.update(it.song.copy(inLibrary = null))\n                    }\n                }\n\n                // Clear liked albums\n                val likedAlbums = database.albumsLikedByNameAsc().first()\n                likedAlbums.forEach {\n                    database.update(it.album.copy(bookmarkedAt = null))\n                }\n\n                // Clear subscribed artists\n                val subscribedArtists = database.artistsBookmarkedByNameAsc().first()\n                subscribedArtists.forEach {\n                    database.update(it.artist.copy(bookmarkedAt = null))\n                }\n\n                // Delete synced playlists\n                val savedPlaylists = database.playlistsByNameAsc().first()\n                savedPlaylists.forEach {\n                    if (it.playlist.browseId != null) {\n                        database.clearPlaylist(it.playlist.id)\n                        database.delete(it.playlist)\n                    }\n                }\n\n                // Clear uploaded songs\n                val uploadedSongs = database.uploadedSongsByNameAsc().first()\n                uploadedSongs.forEach {\n                    database.update(it.song.copy(isUploaded = false, uploadEntityId = null))\n                }\n\n                // Clear uploaded albums\n                val uploadedAlbums = database.albumsUploadedByCreateDateAsc().first()\n                uploadedAlbums.forEach {\n                    database.update(it.album.copy(isUploaded = false))\n                }\n            }\n\n            // Reset sync timestamp\n            context.dataStore.edit { settings ->\n                settings[LastFullSyncKey] = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)\n            }\n\n            updateState { copy(overallStatus = SyncStatus.Completed, currentOperation = \"\") }\n            Timber.d(\"clearAllSyncedContent: Cleanup completed successfully\")\n        } catch (e: Exception) {\n            Timber.e(e, \"clearAllSyncedContent: Error during cleanup\")\n            updateState { copy(overallStatus = SyncStatus.Error(e.message ?: \"Unknown error\"), currentOperation = \"\") }\n        }\n    }\n\n    private suspend fun executeClearPodcastData() = withContext(Dispatchers.IO) {\n        Timber.d(\"[PODCAST_CLEAR] Starting podcast data cleanup\")\n\n        updateState { copy(overallStatus = SyncStatus.Syncing, currentOperation = \"Clearing podcast data\") }\n\n        try {\n            database.withTransaction {\n                // Clear subscribed podcasts\n                val subscribedPodcasts = database.subscribedPodcasts().first()\n                Timber.d(\"[PODCAST_CLEAR] Clearing ${subscribedPodcasts.size} subscribed podcasts\")\n                subscribedPodcasts.forEach { podcast ->\n                    database.update(podcast.copy(bookmarkedAt = null))\n                }\n\n                // Clear episode library status (inLibrary) for episodes\n                val savedEpisodes = database.podcastEpisodesByCreateDateAsc().first()\n                    .filter { it.song.inLibrary != null }\n                Timber.d(\"[PODCAST_CLEAR] Clearing ${savedEpisodes.size} saved episodes\")\n                savedEpisodes.forEach { song ->\n                    database.update(song.song.copy(inLibrary = null))\n                }\n            }\n\n            updateState { copy(overallStatus = SyncStatus.Completed, currentOperation = \"\") }\n            Timber.d(\"[PODCAST_CLEAR] Podcast data cleared successfully\")\n        } catch (e: Exception) {\n            Timber.e(e, \"[PODCAST_CLEAR] Error during cleanup\")\n            updateState { copy(overallStatus = SyncStatus.Error(e.message ?: \"Unknown error\"), currentOperation = \"\") }\n        }\n    }\n\n    fun cancelAllSyncs() {\n        processingJob?.cancel()\n        startProcessingQueue()\n        updateState { SyncState() }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/Updater.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.utils\n\nimport com.metrolist.music.BuildConfig\nimport io.ktor.client.HttpClient\nimport io.ktor.client.request.get\nimport io.ktor.client.statement.bodyAsText\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport org.json.JSONArray\nimport org.json.JSONObject\n\ndata class ReleaseInfo(\n    val tagName: String,\n    val versionName: String,\n    val description: String,\n    val releaseDate: String,\n    val assets: List<ReleaseAsset>\n)\n\ndata class ReleaseAsset(\n    val name: String,\n    val downloadUrl: String,\n    val size: Long,\n    val architecture: String,\n    val variant: String // \"foss\" or \"gms\"\n)\n\nobject Updater {\n    private val client = HttpClient()\n    var lastCheckTime = -1L\n        private set\n    \n    private var cachedReleaseInfo: ReleaseInfo? = null\n    private var cachedAllReleases: List<ReleaseInfo> = emptyList()\n    \n    private const val CHECK_INTERVAL_MILLIS = 2 * 60 * 60 * 1000L // 2 hours\n    private const val GITHUB_API_BASE = \"https://api.github.com/repos/MetrolistGroup/Metrolist\"\n\n    /**\n     * Compares two version strings.\n     * Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal\n     */\n    fun compareVersions(v1: String, v2: String): Int {\n        val v1Parts = v1.removePrefix(\"v\").split(\".\").map { it.toIntOrNull() ?: 0 }\n        val v2Parts = v2.removePrefix(\"v\").split(\".\").map { it.toIntOrNull() ?: 0 }\n        val maxLength = maxOf(v1Parts.size, v2Parts.size)\n        \n        for (i in 0 until maxLength) {\n            val part1 = v1Parts.getOrNull(i) ?: 0\n            val part2 = v2Parts.getOrNull(i) ?: 0\n            when {\n                part1 > part2 -> return 1\n                part1 < part2 -> return -1\n            }\n        }\n        return 0\n    }\n\n    /**\n     * Checks if the latest version is newer than the current version.\n     * Returns true if an update is available (latestVersion > currentVersion)\n     */\n    fun isUpdateAvailable(currentVersion: String, latestVersion: String): Boolean {\n        return compareVersions(latestVersion, currentVersion) > 0\n    }\n\n    /**\n     * Get the current app's architecture and variant\n     */\n    private fun getCurrentAppVariant(): Pair<String, String> {\n        val architecture = BuildConfig.ARCHITECTURE\n        val variant = if (BuildConfig.CAST_AVAILABLE) \"gms\" else \"foss\"\n        return architecture to variant\n    }\n\n    /**\n     * Parse release assets from GitHub API response\n     */\n    private fun parseAssets(assetsArray: JSONArray): List<ReleaseAsset> {\n        val assets = mutableListOf<ReleaseAsset>()\n        \n        for (i in 0 until assetsArray.length()) {\n            val asset = assetsArray.getJSONObject(i)\n            val name = asset.getString(\"name\")\n            \n            // Skip non-APK files\n            if (!name.endsWith(\".apk\")) continue\n            \n            val downloadUrl = asset.getString(\"browser_download_url\")\n            val size = asset.getLong(\"size\")\n            \n            // Parse architecture and variant from filename\n            val (arch, variant) = when {\n                name == \"Metrolist.apk\" -> \"universal\" to \"foss\"\n                name == \"Metrolist-with-Google-Cast.apk\" -> \"universal\" to \"gms\"\n                name.startsWith(\"app-\") && name.endsWith(\"-release.apk\") -> {\n                    val arch = name.removePrefix(\"app-\").removeSuffix(\"-release.apk\")\n                    arch to \"foss\"\n                }\n                name.startsWith(\"app-\") && name.endsWith(\"-with-Google-Cast.apk\") -> {\n                    val arch = name.removePrefix(\"app-\").removeSuffix(\"-with-Google-Cast.apk\")\n                    arch to \"gms\"\n                }\n                else -> null to null\n            }\n            \n            if (arch != null && variant != null) {\n                assets.add(ReleaseAsset(name, downloadUrl, size, arch, variant))\n            }\n        }\n        \n        return assets\n    }\n\n    /**\n     * Fetch latest release from GitHub API\n     */\n    suspend fun getLatestRelease(forceRefresh: Boolean = false): Result<ReleaseInfo> =\n        withContext(Dispatchers.IO) {\n            runCatching {\n                // Return cached if available and not forcing refresh\n                if (cachedReleaseInfo != null && !forceRefresh) {\n                    return@runCatching cachedReleaseInfo!!\n                }\n                \n                val response = client.get(\"$GITHUB_API_BASE/releases/latest\")\n                    .bodyAsText()\n                val json = JSONObject(response)\n                \n                val releaseInfo = ReleaseInfo(\n                    tagName = json.getString(\"tag_name\"),\n                    versionName = json.getString(\"name\"),\n                    description = json.getString(\"body\"),\n                    releaseDate = json.getString(\"published_at\"),\n                    assets = parseAssets(json.getJSONArray(\"assets\"))\n                )\n                \n                cachedReleaseInfo = releaseInfo\n                lastCheckTime = System.currentTimeMillis()\n                releaseInfo\n            }\n        }\n\n    /**\n     * Fetch all releases from GitHub API (paginated)\n     */\n    suspend fun getAllReleases(forceRefresh: Boolean = false): Result<List<ReleaseInfo>> =\n        withContext(Dispatchers.IO) {\n            runCatching {\n                if (cachedAllReleases.isNotEmpty() && !forceRefresh) {\n                    return@runCatching cachedAllReleases\n                }\n                \n                val releases = mutableListOf<ReleaseInfo>()\n                var page = 1\n                var hasMore = true\n                \n                while (hasMore && page <= 10) { // Limit to 10 pages\n                    val response = client.get(\"$GITHUB_API_BASE/releases?page=$page&per_page=30\")\n                        .bodyAsText()\n                    val json = JSONArray(response)\n                    \n                    if (json.length() == 0) {\n                        hasMore = false\n                        break\n                    }\n                    \n                    for (i in 0 until json.length()) {\n                        val releaseObj = json.getJSONObject(i)\n                        releases.add(ReleaseInfo(\n                            tagName = releaseObj.getString(\"tag_name\"),\n                            versionName = releaseObj.getString(\"name\"),\n                            description = releaseObj.getString(\"body\"),\n                            releaseDate = releaseObj.getString(\"published_at\"),\n                            assets = parseAssets(releaseObj.getJSONArray(\"assets\"))\n                        ))\n                    }\n                    \n                    page++\n                }\n                \n                cachedAllReleases = releases\n                releases\n            }\n        }\n\n    /**\n     * Get the download URL for the correct app variant\n     */\n    fun getDownloadUrlForCurrentVariant(releaseInfo: ReleaseInfo): String? {\n        val (currentArch, currentVariant) = getCurrentAppVariant()\n        \n        return releaseInfo.assets\n            .find { it.architecture == currentArch && it.variant == currentVariant }\n            ?.downloadUrl\n    }\n\n    /**\n     * Get all available download URLs for a release\n     */\n    fun getAllDownloadUrls(releaseInfo: ReleaseInfo): Map<String, String> {\n        return releaseInfo.assets.associate { \"${it.architecture}-${it.variant}\" to it.downloadUrl }\n    }\n\n    /**\n     * Check if update is needed (respects 2-hour cache)\n     */\n    suspend fun checkForUpdate(forceRefresh: Boolean = false): Result<Pair<ReleaseInfo?, Boolean>> =\n        withContext(Dispatchers.IO) {\n            runCatching {\n                // Check if we should fetch (2 hour interval)\n                val shouldFetch = forceRefresh || \n                    (System.currentTimeMillis() - lastCheckTime) > CHECK_INTERVAL_MILLIS\n                \n                if (!shouldFetch && cachedReleaseInfo != null) {\n                    val hasUpdate = isUpdateAvailable(\n                        BuildConfig.VERSION_NAME,\n                        cachedReleaseInfo!!.versionName\n                    )\n                    return@runCatching cachedReleaseInfo!! to hasUpdate\n                }\n                \n                val result = getLatestRelease(forceRefresh = true)\n                if (result.isSuccess) {\n                    val releaseInfo = result.getOrThrow()\n                    val hasUpdate = isUpdateAvailable(\n                        BuildConfig.VERSION_NAME,\n                        releaseInfo.versionName\n                    )\n                    releaseInfo to hasUpdate\n                } else {\n                    throw result.exceptionOrNull() ?: Exception(\"Unknown error\")\n                }\n            }\n        }\n\n    /**\n     * Get the download URL for the correct app variant\n     * Returns null if no matching asset is found\n     */\n    fun getLatestDownloadUrl(): String? {\n        return cachedReleaseInfo?.let { getDownloadUrlForCurrentVariant(it) }\n    }\n    \n    /**\n     * Get the latest release info (cached)\n     */\n    fun getCachedLatestRelease(): ReleaseInfo? = cachedReleaseInfo\n}"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/Utils.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.utils\n\nimport android.content.Context\nimport android.content.res.Configuration\nimport java.util.Locale\n\nfun reportException(throwable: Throwable) {\n    throwable.printStackTrace()\n}\n\n@Suppress(\"DEPRECATION\")\nfun setAppLocale(context: Context, locale: Locale) {\n    val config = Configuration(context.resources.configuration)\n    config.setLocale(locale)\n    context.resources.updateConfiguration(config, context.resources.displayMetrics)\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/YTPlayerUtils.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.utils\n\nimport android.net.ConnectivityManager\nimport android.net.Uri\nimport android.util.Log\nimport androidx.media3.common.PlaybackException\nimport com.metrolist.innertube.NewPipeExtractor\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.YouTubeClient\nimport com.metrolist.innertube.models.YouTubeClient.Companion.ANDROID_CREATOR\nimport com.metrolist.innertube.models.YouTubeClient.Companion.ANDROID_VR_1_43_32\nimport com.metrolist.innertube.models.YouTubeClient.Companion.ANDROID_VR_1_61_48\nimport com.metrolist.innertube.models.YouTubeClient.Companion.ANDROID_VR_NO_AUTH\nimport com.metrolist.innertube.models.YouTubeClient.Companion.IOS\nimport com.metrolist.innertube.models.YouTubeClient.Companion.IPADOS\nimport com.metrolist.innertube.models.YouTubeClient.Companion.MOBILE\nimport com.metrolist.innertube.models.YouTubeClient.Companion.TVHTML5\nimport com.metrolist.innertube.models.YouTubeClient.Companion.TVHTML5_SIMPLY_EMBEDDED_PLAYER\nimport com.metrolist.innertube.models.YouTubeClient.Companion.WEB\nimport com.metrolist.innertube.models.YouTubeClient.Companion.WEB_CREATOR\nimport com.metrolist.innertube.models.YouTubeClient.Companion.WEB_REMIX\nimport com.metrolist.innertube.models.response.PlayerResponse\nimport com.metrolist.music.constants.AudioQuality\nimport com.metrolist.music.utils.cipher.CipherDeobfuscator\nimport com.metrolist.music.utils.YTPlayerUtils.MAIN_CLIENT\nimport com.metrolist.music.utils.YTPlayerUtils.STREAM_FALLBACK_CLIENTS\nimport com.metrolist.music.utils.YTPlayerUtils.validateStatus\nimport com.metrolist.music.utils.potoken.PoTokenGenerator\nimport com.metrolist.music.utils.potoken.PoTokenResult\nimport com.metrolist.music.utils.sabr.EjsNTransformSolver\nimport okhttp3.OkHttpClient\nimport timber.log.Timber\n\nobject YTPlayerUtils {\n    private const val logTag = \"YTPlayerUtils\"\n    private const val TAG = \"YTPlayerUtils\"\n\n    private val httpClient = OkHttpClient.Builder()\n        .proxy(YouTube.proxy)\n        .build()\n\n    private val poTokenGenerator = PoTokenGenerator()\n\n    private val MAIN_CLIENT: YouTubeClient = WEB_REMIX\n\n    private val STREAM_FALLBACK_CLIENTS: Array<YouTubeClient> = arrayOf(\n        TVHTML5_SIMPLY_EMBEDDED_PLAYER,  // Try embedded player first for age-restricted content\n        TVHTML5,\n        ANDROID_VR_1_43_32,\n        ANDROID_VR_1_61_48,\n        ANDROID_CREATOR,\n        IPADOS,\n        ANDROID_VR_NO_AUTH,\n        MOBILE,\n        IOS,\n        WEB,\n        WEB_CREATOR\n    )\n    data class PlaybackData(\n        val audioConfig: PlayerResponse.PlayerConfig.AudioConfig?,\n        val videoDetails: PlayerResponse.VideoDetails?,\n        val playbackTracking: PlayerResponse.PlaybackTracking?,\n        val format: PlayerResponse.StreamingData.Format,\n        val streamUrl: String,\n        val streamExpiresInSeconds: Int,\n    )\n    /**\n     * Custom player response intended to use for playback.\n     * Metadata like audioConfig and videoDetails are from [MAIN_CLIENT].\n     * Format & stream can be from [MAIN_CLIENT] or [STREAM_FALLBACK_CLIENTS].\n     */\n    suspend fun playerResponseForPlayback(\n        videoId: String,\n        playlistId: String? = null,\n        audioQuality: AudioQuality,\n        connectivityManager: ConnectivityManager,\n    ): Result<PlaybackData> = runCatching {\n        Timber.tag(TAG).d(\"=== PLAYER RESPONSE FOR PLAYBACK ===\")\n        Timber.tag(TAG).d(\"videoId: $videoId\")\n        Timber.tag(TAG).d(\"playlistId: $playlistId\")\n        Timber.tag(TAG).d(\"audioQuality: $audioQuality\")\n\n        // Check if this is an uploaded/privately owned track\n        val isUploadedTrack = playlistId == \"MLPT\" || playlistId?.contains(\"MLPT\") == true\n        Timber.tag(TAG).d(\"Content type detection (preliminary):\")\n        Timber.tag(TAG).d(\"  isUploadedTrack (from playlistId): $isUploadedTrack\")\n\n        val isLoggedIn = YouTube.cookie != null\n        Timber.tag(TAG).d(\"Authentication status: ${if (isLoggedIn) \"LOGGED_IN\" else \"ANONYMOUS\"}\")\n\n        // Get signature timestamp (same as before for normal content)\n        val signatureTimestamp = getSignatureTimestampOrNull(videoId)\n        Timber.tag(logTag).d(\"Signature timestamp: ${signatureTimestamp.timestamp}\")\n\n        // Generate PoToken\n        var poToken: PoTokenResult? = null\n        val sessionId = if (isLoggedIn) YouTube.dataSyncId else YouTube.visitorData\n        if (MAIN_CLIENT.useWebPoTokens && sessionId != null) {\n            Timber.tag(logTag).d(\"Generating PoToken for WEB_REMIX with sessionId\")\n            try {\n                poToken = poTokenGenerator.getWebClientPoToken(videoId, sessionId)\n                if (poToken != null) {\n                    Timber.tag(logTag).d(\"PoToken generated successfully\")\n                }\n            } catch (e: Exception) {\n                Timber.tag(logTag).e(e, \"PoToken generation failed: ${e.message}\")\n            }\n        }\n\n        // Try WEB_REMIX with signature timestamp and poToken (same as before)\n        Timber.tag(logTag).d(\"Attempting to get player response using MAIN_CLIENT: ${MAIN_CLIENT.clientName}\")\n        var mainPlayerResponse = YouTube.player(videoId, playlistId, MAIN_CLIENT, signatureTimestamp.timestamp, poToken?.playerRequestPoToken).getOrThrow()\n\n        // Debug uploaded track response\n        if (isUploadedTrack || playlistId?.contains(\"MLPT\") == true) {\n            println(\"[PLAYBACK_DEBUG] Main player response status: ${mainPlayerResponse.playabilityStatus.status}\")\n            println(\"[PLAYBACK_DEBUG] Playability reason: ${mainPlayerResponse.playabilityStatus.reason}\")\n            println(\"[PLAYBACK_DEBUG] Video details: title=${mainPlayerResponse.videoDetails?.title}, videoId=${mainPlayerResponse.videoDetails?.videoId}\")\n            println(\"[PLAYBACK_DEBUG] Streaming data null? ${mainPlayerResponse.streamingData == null}\")\n            println(\"[PLAYBACK_DEBUG] Adaptive formats count: ${mainPlayerResponse.streamingData?.adaptiveFormats?.size ?: 0}\")\n        }\n\n        var usedAgeRestrictedClient: YouTubeClient? = null\n        val wasOriginallyAgeRestricted: Boolean\n\n        // Check if WEB_REMIX response indicates age-restricted\n        val mainStatus = mainPlayerResponse.playabilityStatus.status\n        val isAgeRestrictedFromResponse = mainStatus in listOf(\"AGE_CHECK_REQUIRED\", \"AGE_VERIFICATION_REQUIRED\", \"LOGIN_REQUIRED\", \"CONTENT_CHECK_REQUIRED\")\n        wasOriginallyAgeRestricted = isAgeRestrictedFromResponse\n\n        if (isAgeRestrictedFromResponse && isLoggedIn) {\n            // Age-restricted: use WEB_CREATOR directly (no NewPipe needed from here)\n            Timber.tag(logTag).d(\"Age-restricted detected, using WEB_CREATOR\")\n            Timber.tag(TAG).i(\"Age-restricted: using WEB_CREATOR for videoId=$videoId\")\n            val creatorResponse = YouTube.player(videoId, playlistId, WEB_CREATOR, null, null).getOrNull()\n            if (creatorResponse?.playabilityStatus?.status == \"OK\") {\n                Timber.tag(logTag).d(\"WEB_CREATOR works for age-restricted content\")\n                mainPlayerResponse = creatorResponse\n                usedAgeRestrictedClient = WEB_CREATOR\n            }\n        }\n\n        // If we still don't have a valid response, throw\n\n        val audioConfig = mainPlayerResponse.playerConfig?.audioConfig\n        val videoDetails = mainPlayerResponse.videoDetails\n        val playbackTracking = mainPlayerResponse.playbackTracking\n        var format: PlayerResponse.StreamingData.Format? = null\n        var streamUrl: String? = null\n        var streamExpiresInSeconds: Int? = null\n        var streamPlayerResponse: PlayerResponse? = null\n        val retryMainPlayerResponse: PlayerResponse? = if (usedAgeRestrictedClient != null) mainPlayerResponse else null\n\n        // Check current status\n        val currentStatus = mainPlayerResponse.playabilityStatus.status\n        val isAgeRestricted = currentStatus in listOf(\"AGE_CHECK_REQUIRED\", \"AGE_VERIFICATION_REQUIRED\", \"LOGIN_REQUIRED\", \"CONTENT_CHECK_REQUIRED\")\n\n        if (isAgeRestricted) {\n            Timber.tag(logTag).d(\"Content is still age-restricted (status: $currentStatus), will try fallback clients\")\n            Timber.tag(TAG)\n                .i(\"Age-restricted content detected: videoId=$videoId, status=$currentStatus\")\n        }\n\n        // Check if this is a privately owned track (uploaded song)\n        val isPrivateTrack = mainPlayerResponse.videoDetails?.musicVideoType == \"MUSIC_VIDEO_TYPE_PRIVATELY_OWNED_TRACK\"\n\n        // For private tracks: use TVHTML5 (index 1) with PoToken + n-transform\n        // For age-restricted: skip main client, start with fallbacks\n        // For normal content: standard order\n        val startIndex = when {\n            isPrivateTrack -> 1  // TVHTML5\n            isAgeRestricted -> 0\n            else -> -1\n        }\n\n        for (clientIndex in (startIndex until STREAM_FALLBACK_CLIENTS.size)) {\n            // reset for each client\n            format = null\n            streamUrl = null\n            streamExpiresInSeconds = null\n\n            // decide which client to use for streams and load its player response\n            val client: YouTubeClient\n            if (clientIndex == -1) {\n                // try with streams from main client first (use retry response if available)\n                client = MAIN_CLIENT\n                streamPlayerResponse = retryMainPlayerResponse ?: mainPlayerResponse\n                Timber.tag(logTag).d(\"Trying stream from MAIN_CLIENT: ${client.clientName}\")\n            } else {\n                // after main client use fallback clients\n                client = STREAM_FALLBACK_CLIENTS[clientIndex]\n                Timber.tag(logTag).d(\"Trying fallback client ${clientIndex + 1}/${STREAM_FALLBACK_CLIENTS.size}: ${client.clientName}\")\n\n                if (client.loginRequired && !isLoggedIn && YouTube.cookie == null) {\n                    // skip client if it requires login but user is not logged in\n                    Timber.tag(logTag).d(\"Skipping client ${client.clientName} - requires login but user is not logged in\")\n                    continue\n                }\n\n                Timber.tag(logTag).d(\"Fetching player response for fallback client: ${client.clientName}\")\n                // Only pass poToken for clients that support it\n                val clientPoToken = if (client.useWebPoTokens) poToken?.playerRequestPoToken else null\n                // Skip signature timestamp for age-restricted (faster), use it for normal content\n                val clientSigTimestamp = if (wasOriginallyAgeRestricted) null else signatureTimestamp.timestamp\n                streamPlayerResponse =\n                    YouTube.player(videoId, playlistId, client, clientSigTimestamp, clientPoToken).getOrNull()\n            }\n\n            // process current client response\n            if (streamPlayerResponse?.playabilityStatus?.status == \"OK\") {\n                Timber.tag(logTag).d(\"Player response status OK for client: ${if (clientIndex == -1) MAIN_CLIENT.clientName else STREAM_FALLBACK_CLIENTS[clientIndex].clientName}\")\n\n                // Skip NewPipe for age-restricted content (NewPipe doesn't use our auth)\n                val responseToUse = if (wasOriginallyAgeRestricted) {\n                    Timber.tag(logTag).d(\"Skipping NewPipe for age-restricted content\")\n                    streamPlayerResponse\n                } else {\n                    // Try to get streams using newPipePlayer method\n                    val newPipeResponse = YouTube.newPipePlayer(videoId, streamPlayerResponse)\n                    newPipeResponse ?: streamPlayerResponse\n                }\n\n                format =\n                    findFormat(\n                        responseToUse,\n                        audioQuality,\n                        connectivityManager,\n                    )\n\n                if (format == null) {\n                    Timber.tag(logTag).d(\"No suitable format found for client: ${if (clientIndex == -1) MAIN_CLIENT.clientName else STREAM_FALLBACK_CLIENTS[clientIndex].clientName}\")\n                    continue\n                }\n\n                Timber.tag(logTag).d(\"Format found: ${format.mimeType}, bitrate: ${format.bitrate}\")\n\n                streamUrl = findUrlOrNull(format, videoId, responseToUse, skipNewPipe = wasOriginallyAgeRestricted)\n                if (streamUrl == null) {\n                    Timber.tag(logTag).d(\"Stream URL not found for format\")\n                    continue\n                }\n\n                // Apply n-transform for throttle parameter handling\n                val currentClient = if (clientIndex == -1) {\n                    usedAgeRestrictedClient ?: MAIN_CLIENT\n                } else {\n                    STREAM_FALLBACK_CLIENTS[clientIndex]\n                }\n\n                // Check if this is a privately owned track\n                val isPrivatelyOwnedTrack = streamPlayerResponse.videoDetails?.musicVideoType == \"MUSIC_VIDEO_TYPE_PRIVATELY_OWNED_TRACK\"\n                val musicVideoType = streamPlayerResponse.videoDetails?.musicVideoType\n\n                Timber.tag(TAG).d(\"=== N-TRANSFORM DECISION ===\")\n                Timber.tag(TAG).d(\"Content type analysis:\")\n                Timber.tag(TAG).d(\"  musicVideoType: $musicVideoType\")\n                Timber.tag(TAG).d(\"  isPrivatelyOwnedTrack: $isPrivatelyOwnedTrack\")\n                Timber.tag(TAG).d(\"  isUploadedTrack (from playlistId): $isUploadedTrack\")\n                Timber.tag(TAG).d(\"  wasOriginallyAgeRestricted: $wasOriginallyAgeRestricted\")\n                Timber.tag(TAG).d(\"Client analysis:\")\n                Timber.tag(TAG).d(\"  currentClient: ${currentClient.clientName}\")\n                Timber.tag(TAG).d(\"  useWebPoTokens: ${currentClient.useWebPoTokens}\")\n\n                // Apply n-transform and PoToken for web clients OR for private tracks (including TVHTML5)\n                val needsNTransform = currentClient.useWebPoTokens ||\n                    currentClient.clientName in listOf(\"WEB\", \"WEB_REMIX\", \"WEB_CREATOR\", \"TVHTML5\") ||\n                    isPrivatelyOwnedTrack\n\n                Timber.tag(TAG).d(\"N-transform decision:\")\n                Timber.tag(TAG).d(\"  needsNTransform: $needsNTransform\")\n                Timber.tag(TAG).d(\"  Reason: useWebPoTokens=${currentClient.useWebPoTokens}, \" +\n                    \"clientInList=${currentClient.clientName in listOf(\"WEB\", \"WEB_REMIX\", \"WEB_CREATOR\", \"TVHTML5\")}, \" +\n                    \"isPrivatelyOwnedTrack=$isPrivatelyOwnedTrack\")\n\n                if (needsNTransform) {\n                    try {\n                        Timber.tag(TAG).d(\"Applying n-transform to stream URL...\")\n                        Timber.tag(TAG).d(\"  Original URL length: ${streamUrl.length}\")\n                        Timber.tag(TAG).d(\"  Original URL preview: ${streamUrl.take(100)}...\")\n\n                        val originalUrl = streamUrl\n                        // Use CipherDeobfuscator for n-transform (fixed implementation)\n                        streamUrl = CipherDeobfuscator.transformNParamInUrl(streamUrl)\n\n                        Timber.tag(TAG).d(\"  Transformed URL length: ${streamUrl.length}\")\n                        Timber.tag(TAG).d(\"  URL changed: ${originalUrl != streamUrl}\")\n\n                        // Append pot= parameter with streaming data poToken\n                        val needsPoToken = (currentClient.useWebPoTokens || isPrivatelyOwnedTrack) && poToken?.streamingDataPoToken != null\n                        Timber.tag(TAG).d(\"PoToken decision:\")\n                        Timber.tag(TAG).d(\"  needsPoToken: $needsPoToken\")\n                        Timber.tag(TAG).d(\"  hasStreamingDataPoToken: ${poToken?.streamingDataPoToken != null}\")\n\n                        if (needsPoToken) {\n                            Timber.tag(TAG).d(\"Appending pot= parameter to stream URL\")\n                            val separator = if (\"?\" in streamUrl) \"&\" else \"?\"\n                            streamUrl = \"${streamUrl}${separator}pot=${Uri.encode(poToken!!.streamingDataPoToken)}\"\n                            Timber.tag(TAG).d(\"  Final URL length (with pot): ${streamUrl.length}\")\n                        }\n                    } catch (e: Exception) {\n                        Timber.tag(TAG).e(e, \"N-transform or pot append failed: ${e.message}\")\n                        Timber.tag(TAG).e(\"Stack trace: ${e.stackTraceToString().take(500)}\")\n                        // Continue with original URL\n                    }\n                } else {\n                    Timber.tag(TAG).d(\"Skipping n-transform (not required for this client/content)\")\n                }\n\n                streamExpiresInSeconds = streamPlayerResponse.streamingData?.expiresInSeconds\n                if (streamExpiresInSeconds == null) {\n                    Timber.tag(logTag).d(\"Stream expiration time not found\")\n                    continue\n                }\n\n                Timber.tag(logTag).d(\"Stream expires in: $streamExpiresInSeconds seconds\")\n\n                // Check if this is a privately owned track (uploaded song)\n                val isPrivatelyOwned = streamPlayerResponse.videoDetails?.musicVideoType == \"MUSIC_VIDEO_TYPE_PRIVATELY_OWNED_TRACK\"\n\n                if (clientIndex == STREAM_FALLBACK_CLIENTS.size - 1 || isPrivatelyOwned) {\n                    /** skip [validateStatus] for last client or private tracks */\n                    if (isPrivatelyOwned) {\n                        Timber.tag(logTag).d(\"Skipping validation for privately owned track: ${currentClient.clientName}\")\n                        println(\"[PLAYBACK_DEBUG] Using stream without validation for PRIVATELY_OWNED_TRACK\")\n                    } else {\n                        Timber.tag(logTag).d(\"Using last fallback client without validation: ${STREAM_FALLBACK_CLIENTS[clientIndex].clientName}\")\n                    }\n                    Timber.tag(TAG)\n                        .i(\"Playback: client=${currentClient.clientName}, videoId=$videoId, private=$isPrivatelyOwned\")\n                    break\n                }\n\n                if (validateStatus(streamUrl)) {\n                    // working stream found\n                    Timber.tag(logTag).d(\"Stream validated successfully with client: ${currentClient.clientName}\")\n                    // Log for release builds\n                    Timber.tag(TAG).i(\"Playback: client=${currentClient.clientName}, videoId=$videoId\")\n                    break\n                } else {\n                    Timber.tag(logTag).d(\"Stream validation failed for client: ${currentClient.clientName}\")\n                }\n            } else {\n                Timber.tag(logTag).d(\"Player response status not OK: ${streamPlayerResponse?.playabilityStatus?.status}, reason: ${streamPlayerResponse?.playabilityStatus?.reason}\")\n            }\n        }\n\n        if (streamPlayerResponse == null) {\n            Timber.tag(logTag).e(\"Bad stream player response - all clients failed\")\n            if (isUploadedTrack) {\n                println(\"[PLAYBACK_DEBUG] FAILURE: All clients failed for uploaded track videoId=$videoId\")\n            }\n            throw Exception(\"Bad stream player response\")\n        }\n\n        if (streamPlayerResponse.playabilityStatus.status != \"OK\") {\n            val errorReason = streamPlayerResponse.playabilityStatus.reason\n            Timber.tag(logTag).e(\"Playability status not OK: $errorReason\")\n            if (isUploadedTrack) {\n                println(\"[PLAYBACK_DEBUG] FAILURE: Playability not OK for uploaded track - status=${streamPlayerResponse.playabilityStatus.status}, reason=$errorReason\")\n            }\n            throw PlaybackException(\n                errorReason,\n                null,\n                PlaybackException.ERROR_CODE_REMOTE_ERROR\n            )\n        }\n\n        if (streamExpiresInSeconds == null) {\n            Timber.tag(logTag).e(\"Missing stream expire time\")\n            throw Exception(\"Missing stream expire time\")\n        }\n\n        if (format == null) {\n            Timber.tag(logTag).e(\"Could not find format\")\n            throw Exception(\"Could not find format\")\n        }\n\n        if (streamUrl == null) {\n            Timber.tag(logTag).e(\"Could not find stream url\")\n            throw Exception(\"Could not find stream url\")\n        }\n\n        Timber.tag(logTag).d(\"Successfully obtained playback data with format: ${format.mimeType}, bitrate: ${format.bitrate}\")\n        if (isUploadedTrack) {\n            println(\"[PLAYBACK_DEBUG] SUCCESS: Got playback data for uploaded track - format=${format.mimeType}, streamUrl=${streamUrl.take(100)}...\")\n        }\n        PlaybackData(\n            audioConfig,\n            videoDetails,\n            playbackTracking,\n            format,\n            streamUrl,\n            streamExpiresInSeconds,\n        )\n    }.onFailure { e ->\n        println(\"[PLAYBACK_DEBUG] EXCEPTION during playback for videoId=$videoId: ${e::class.simpleName}: ${e.message}\")\n        e.printStackTrace()\n    }\n    /**\n     * Simple player response intended to use for metadata only.\n     * Stream URLs of this response might not work so don't use them.\n     */\n    suspend fun playerResponseForMetadata(\n        videoId: String,\n        playlistId: String? = null,\n    ): Result<PlayerResponse> {\n        Timber.tag(logTag).d(\"Fetching metadata-only player response for videoId: $videoId using MAIN_CLIENT: ${MAIN_CLIENT.clientName}\")\n        return YouTube.player(videoId, playlistId, client = WEB_REMIX) // ANDROID_VR does not work with history\n            .onSuccess { Timber.tag(logTag).d(\"Successfully fetched metadata\") }\n            .onFailure { Timber.tag(logTag).e(it, \"Failed to fetch metadata\") }\n    }\n\n    private fun findFormat(\n        playerResponse: PlayerResponse,\n        audioQuality: AudioQuality,\n        connectivityManager: ConnectivityManager,\n    ): PlayerResponse.StreamingData.Format? {\n        Timber.tag(logTag).d(\"Finding format with audioQuality: $audioQuality, network metered: ${connectivityManager.isActiveNetworkMetered}\")\n\n        val format = playerResponse.streamingData?.adaptiveFormats\n            ?.filter { it.isAudio && it.isOriginal }\n            ?.maxByOrNull {\n                it.bitrate * when (audioQuality) {\n                    AudioQuality.AUTO -> if (connectivityManager.isActiveNetworkMetered) -1 else 1\n                    AudioQuality.HIGH -> 1\n                    AudioQuality.LOW -> -1\n                } + (if (it.mimeType.startsWith(\"audio/webm\")) 10240 else 0) // prefer opus stream\n            }\n\n        if (format != null) {\n            Timber.tag(logTag).d(\"Selected format: ${format.mimeType}, bitrate: ${format.bitrate}\")\n        } else {\n            Timber.tag(logTag).d(\"No suitable audio format found\")\n        }\n\n        return format\n    }\n    /**\n     * Checks if the stream url returns a successful status.\n     * If this returns true the url is likely to work.\n     * If this returns false the url might cause an error during playback.\n     */\n    private fun validateStatus(url: String): Boolean {\n        Timber.tag(logTag).d(\"Validating stream URL status\")\n        try {\n            val requestBuilder = okhttp3.Request.Builder()\n                .head()\n                .url(url)\n\n            // Add authentication cookie for privately owned tracks\n            YouTube.cookie?.let { cookie ->\n                requestBuilder.addHeader(\"Cookie\", cookie)\n                println(\"[PLAYBACK_DEBUG] Added cookie to validation request\")\n            }\n\n            val response = httpClient.newCall(requestBuilder.build()).execute()\n            val isSuccessful = response.isSuccessful\n            Timber.tag(logTag).d(\"Stream URL validation result: ${if (isSuccessful) \"Success\" else \"Failed\"} (${response.code})\")\n            return isSuccessful\n        } catch (e: Exception) {\n            Timber.tag(logTag).e(e, \"Stream URL validation failed with exception\")\n            reportException(e)\n        }\n        return false\n    }\n    data class SignatureTimestampResult(\n        val timestamp: Int?,\n        val isAgeRestricted: Boolean\n    )\n\n    private fun getSignatureTimestampOrNull(videoId: String): SignatureTimestampResult {\n        Timber.tag(logTag).d(\"Getting signature timestamp for videoId: $videoId\")\n        val result = NewPipeExtractor.getSignatureTimestamp(videoId)\n        return result.fold(\n            onSuccess = { timestamp ->\n                Timber.tag(logTag).d(\"Signature timestamp obtained: $timestamp\")\n                SignatureTimestampResult(timestamp, isAgeRestricted = false)\n            },\n            onFailure = { error ->\n                val isAgeRestricted = error.message?.contains(\"age-restricted\", ignoreCase = true) == true ||\n                    error.cause?.message?.contains(\"age-restricted\", ignoreCase = true) == true\n                if (isAgeRestricted) {\n                    Timber.tag(logTag).d(\"Age-restricted content detected from NewPipe\")\n                    Timber.tag(TAG).i(\"Age-restricted detected early via NewPipe: videoId=$videoId\")\n                } else {\n                    Timber.tag(logTag).e(error, \"Failed to get signature timestamp\")\n                    reportException(error)\n                }\n                SignatureTimestampResult(null, isAgeRestricted)\n            }\n        )\n    }\n\n    private suspend fun findUrlOrNull(\n        format: PlayerResponse.StreamingData.Format,\n        videoId: String,\n        playerResponse: PlayerResponse,\n        skipNewPipe: Boolean = false\n    ): String? {\n        Timber.tag(logTag).d(\"Finding stream URL for format: ${format.mimeType}, videoId: $videoId, skipNewPipe: $skipNewPipe\")\n\n        // First check if format already has a URL\n        if (!format.url.isNullOrEmpty()) {\n            Timber.tag(logTag).d(\"Using URL from format directly\")\n            return format.url\n        }\n\n        // Try custom cipher deobfuscation for signatureCipher formats\n        val signatureCipher = format.signatureCipher ?: format.cipher\n        if (!signatureCipher.isNullOrEmpty()) {\n            Timber.tag(logTag).d(\"Format has signatureCipher, using custom deobfuscation\")\n            val customDeobfuscatedUrl = CipherDeobfuscator.deobfuscateStreamUrl(signatureCipher, videoId)\n            if (customDeobfuscatedUrl != null) {\n                Timber.tag(logTag).d(\"Stream URL obtained via custom cipher deobfuscation\")\n                return customDeobfuscatedUrl\n            }\n            Timber.tag(logTag).d(\"Custom cipher deobfuscation failed\")\n        }\n\n        // Skip NewPipe for age-restricted content\n        if (skipNewPipe) {\n            Timber.tag(logTag).d(\"Skipping NewPipe methods for age-restricted content\")\n            return null\n        }\n\n        // Try to get URL using NewPipeExtractor signature deobfuscation\n        val deobfuscatedUrl = NewPipeExtractor.getStreamUrl(format, videoId)\n        if (deobfuscatedUrl != null) {\n            Timber.tag(logTag).d(\"Stream URL obtained via NewPipe deobfuscation\")\n            return deobfuscatedUrl\n        }\n\n        // Fallback: try to get URL from StreamInfo\n        Timber.tag(logTag).d(\"Trying StreamInfo fallback for URL\")\n        val streamUrls = YouTube.getNewPipeStreamUrls(videoId)\n        if (streamUrls.isNotEmpty()) {\n            val streamUrl = streamUrls.find { it.first == format.itag }?.second\n            if (streamUrl != null) {\n                Timber.tag(logTag).d(\"Stream URL obtained from StreamInfo\")\n                return streamUrl\n            }\n\n            // If exact itag not found, try to find any audio stream\n            val audioStream = streamUrls.find { urlPair ->\n                playerResponse.streamingData?.adaptiveFormats?.any {\n                    it.itag == urlPair.first && it.isAudio\n                } == true\n            }?.second\n\n            if (audioStream != null) {\n                Timber.tag(logTag).d(\"Audio stream URL obtained from StreamInfo (different itag)\")\n                return audioStream\n            }\n        }\n\n        Timber.tag(logTag).e(\"Failed to get stream URL\")\n        return null\n    }\n\n    fun forceRefreshForVideo(videoId: String) {\n        Timber.tag(logTag).d(\"Force refreshing for videoId: $videoId\")\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/cipher/CipherDeobfuscator.kt",
    "content": "package com.metrolist.music.utils.cipher\n\nimport android.content.Context\nimport android.net.Uri\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport timber.log.Timber\n\n/**\n * Main cipher deobfuscation orchestrator for YouTube stream URLs.\n *\n * Handles both signature deobfuscation (for signatureCipher streams) and\n * n-parameter transformation (for throttle avoidance / 403 fix).\n */\nobject CipherDeobfuscator {\n    private const val TAG = \"Metrolist_CipherDeobfusc\"\n\n    lateinit var appContext: Context\n        private set\n\n    fun initialize(context: Context) {\n        Timber.tag(TAG).d(\"CipherDeobfuscator initializing...\")\n        appContext = context.applicationContext\n        Timber.tag(TAG).d(\"CipherDeobfuscator initialized\")\n    }\n\n    private var cipherWebView: CipherWebView? = null\n    private var currentPlayerHash: String? = null\n\n    /**\n     * Deobfuscate a signatureCipher stream URL.\n     *\n     * The signatureCipher is a query string containing:\n     * - s: The obfuscated signature\n     * - sp: The signature parameter name (usually \"sig\" or \"signature\")\n     * - url: The base stream URL\n     *\n     * Returns the full URL with deobfuscated signature, or null if failed.\n     */\n    suspend fun deobfuscateStreamUrl(signatureCipher: String, videoId: String): String? {\n        Timber.tag(TAG).d(\"=== DEOBFUSCATE STREAM URL ===\")\n        Timber.tag(TAG).d(\"videoId: $videoId\")\n        Timber.tag(TAG).d(\"signatureCipher length: ${signatureCipher.length}\")\n        Timber.tag(TAG).d(\"signatureCipher preview: ${signatureCipher.take(100)}...\")\n\n        return try {\n            deobfuscateInternal(signatureCipher, videoId, isRetry = false)\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Cipher deobfuscation failed, retrying with fresh JS: ${e.message}\")\n            Timber.tag(TAG).d(\"Invalidating cache and retrying...\")\n            try {\n                PlayerJsFetcher.invalidateCache()\n                closeWebView()\n                deobfuscateInternal(signatureCipher, videoId, isRetry = true)\n            } catch (retryE: Exception) {\n                Timber.tag(TAG).e(retryE, \"Cipher deobfuscation retry also failed: ${retryE.message}\")\n                null\n            }\n        }\n    }\n\n    private suspend fun deobfuscateInternal(signatureCipher: String, videoId: String, isRetry: Boolean): String? {\n        Timber.tag(TAG).d(\"deobfuscateInternal: videoId=$videoId, isRetry=$isRetry\")\n\n        // Parse the signatureCipher query string\n        val params = parseQueryParams(signatureCipher)\n        val obfuscatedSig = params[\"s\"]\n        val sigParam = params[\"sp\"] ?: \"signature\"\n        val baseUrl = params[\"url\"]\n\n        Timber.tag(TAG).d(\"Parsed signatureCipher params:\")\n        Timber.tag(TAG).d(\"  s (obfuscated sig): ${obfuscatedSig?.take(30)}... (length=${obfuscatedSig?.length})\")\n        Timber.tag(TAG).d(\"  sp (sig param name): $sigParam\")\n        Timber.tag(TAG).d(\"  url: ${baseUrl?.take(80)}...\")\n\n        if (obfuscatedSig == null || baseUrl == null) {\n            Timber.tag(TAG).e(\"Could not parse signatureCipher params: s=${obfuscatedSig != null}, url=${baseUrl != null}\")\n            return null\n        }\n\n        val webView = getOrCreateWebView(forceRefresh = isRetry)\n        if (webView == null) {\n            Timber.tag(TAG).e(\"Failed to get/create CipherWebView\")\n            return null\n        }\n\n        Timber.tag(TAG).d(\"Calling webView.deobfuscateSignature()...\")\n        val deobfuscatedSig = webView.deobfuscateSignature(obfuscatedSig)\n        Timber.tag(TAG).d(\"Deobfuscated signature: ${deobfuscatedSig.take(30)}... (length=${deobfuscatedSig.length})\")\n\n        // Build the URL with deobfuscated signature\n        val separator = if (\"?\" in baseUrl) \"&\" else \"?\"\n        val finalUrl = \"$baseUrl${separator}${sigParam}=${Uri.encode(deobfuscatedSig)}\"\n\n        Timber.tag(TAG).d(\"=== CIPHER DEOBFUSCATION SUCCESS ===\")\n        Timber.tag(TAG).d(\"videoId: $videoId\")\n        Timber.tag(TAG).d(\"Final URL length: ${finalUrl.length}\")\n        Timber.tag(TAG).d(\"Final URL preview: ${finalUrl.take(100)}...\")\n\n        return finalUrl\n    }\n\n    /**\n     * Transform the 'n' parameter in a streaming URL to avoid throttling/403.\n     *\n     * Uses the runtime-discovered n-function from the player JS WebView.\n     * Returns the URL with the transformed 'n' value, or the original URL if transform fails.\n     *\n     * IMPORTANT: This must be called for WEB_REMIX, WEB, WEB_CREATOR, TVHTML5 clients\n     * and for privately owned tracks (uploaded songs).\n     */\n    suspend fun transformNParamInUrl(url: String): String {\n        Timber.tag(TAG).d(\"=== N-TRANSFORM URL ===\")\n        Timber.tag(TAG).d(\"Input URL length: ${url.length}\")\n        Timber.tag(TAG).d(\"Input URL preview: ${url.take(100)}...\")\n\n        return try {\n            transformNInternal(url)\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"N-transform failed, returning original URL: ${e.message}\")\n            url\n        }\n    }\n\n    private suspend fun transformNInternal(url: String): String {\n        // Extract the 'n' parameter value from the URL\n        val nMatch = Regex(\"[?&]n=([^&]+)\").find(url)\n        if (nMatch == null) {\n            Timber.tag(TAG).d(\"No 'n' parameter found in URL, skipping transform\")\n            return url\n        }\n\n        val nValueEncoded = nMatch.groupValues[1]\n        val nValue = Uri.decode(nValueEncoded)\n        Timber.tag(TAG).d(\"N-param found:\")\n        Timber.tag(TAG).d(\"  encoded: $nValueEncoded\")\n        Timber.tag(TAG).d(\"  decoded: $nValue\")\n\n        val webView = getOrCreateWebView(forceRefresh = false)\n        if (webView == null) {\n            Timber.tag(TAG).e(\"Failed to get CipherWebView for n-transform\")\n            return url\n        }\n\n        Timber.tag(TAG).d(\"CipherWebView state:\")\n        Timber.tag(TAG).d(\"  nFunctionAvailable: ${webView.nFunctionAvailable}\")\n        Timber.tag(TAG).d(\"  discoveredNFuncName: ${webView.discoveredNFuncName}\")\n        Timber.tag(TAG).d(\"  usingHardcodedMode: ${webView.usingHardcodedMode}\")\n\n        if (!webView.nFunctionAvailable) {\n            Timber.tag(TAG).e(\"N-transform function was not discovered at init time\")\n            return url\n        }\n\n        Timber.tag(TAG).d(\"Calling webView.transformN()...\")\n        val transformedN = webView.transformN(nValue)\n\n        Timber.tag(TAG).d(\"=== N-TRANSFORM SUCCESS ===\")\n        Timber.tag(TAG).d(\"N-param: $nValue -> $transformedN\")\n\n        // Replace n= parameter in URL\n        val transformedUrl = url.replaceFirst(\n            Regex(\"([?&])n=[^&]+\"),\n            \"$1n=${Uri.encode(transformedN)}\"\n        )\n\n        Timber.tag(TAG).d(\"Transformed URL length: ${transformedUrl.length}\")\n        return transformedUrl\n    }\n\n    private suspend fun getOrCreateWebView(forceRefresh: Boolean): CipherWebView? {\n        Timber.tag(TAG).d(\"getOrCreateWebView: forceRefresh=$forceRefresh, existing=${cipherWebView != null}\")\n\n        if (!forceRefresh && cipherWebView != null) {\n            Timber.tag(TAG).d(\"Reusing existing CipherWebView (hash=$currentPlayerHash)\")\n            return cipherWebView\n        }\n\n        // Close existing WebView if any\n        if (cipherWebView != null) {\n            Timber.tag(TAG).d(\"Closing existing CipherWebView...\")\n            closeWebView()\n        }\n\n        // Fetch player JS\n        Timber.tag(TAG).d(\"Fetching player JS...\")\n        val result = PlayerJsFetcher.getPlayerJs(forceRefresh = forceRefresh)\n        if (result == null) {\n            Timber.tag(TAG).e(\"Failed to get player JS\")\n            return null\n        }\n        val (playerJs, hash) = result\n        Timber.tag(TAG).d(\"Got player JS: hash=$hash, length=${playerJs.length}\")\n\n        // Run full analysis for logging - pass the known hash from PlayerJsFetcher\n        Timber.tag(TAG).d(\"Analyzing player JS for cipher functions (knownHash=$hash)...\")\n        val analysis = FunctionNameExtractor.analyzePlayerJs(playerJs, knownHash = hash)\n\n        if (analysis.sigInfo == null) {\n            Timber.tag(TAG).e(\"Could not extract signature function info from player JS\")\n            return null\n        }\n\n        if (analysis.nFuncInfo == null) {\n            Timber.tag(TAG).w(\"Could not extract n-function info from player JS (will try brute-force)\")\n        }\n\n        Timber.tag(TAG).d(\"Creating CipherWebView...\")\n        Timber.tag(TAG).d(\"  sig: ${analysis.sigInfo.name} (constantArg=${analysis.sigInfo.constantArg}, hardcoded=${analysis.sigInfo.isHardcoded})\")\n        Timber.tag(TAG).d(\"  nFunc: ${analysis.nFuncInfo?.name}[${analysis.nFuncInfo?.arrayIndex}] (hardcoded=${analysis.nFuncInfo?.isHardcoded})\")\n\n        // Create WebView\n        val webView = CipherWebView.create(\n            context = appContext,\n            playerJs = playerJs,\n            sigInfo = analysis.sigInfo,\n            nFuncInfo = analysis.nFuncInfo,\n        )\n\n        Timber.tag(TAG).d(\"CipherWebView created successfully\")\n        Timber.tag(TAG).d(\"  nFunctionAvailable: ${webView.nFunctionAvailable}\")\n        Timber.tag(TAG).d(\"  sigFunctionAvailable: ${webView.sigFunctionAvailable}\")\n        Timber.tag(TAG).d(\"  discoveredNFuncName: ${webView.discoveredNFuncName}\")\n\n        cipherWebView = webView\n        currentPlayerHash = hash\n        return webView\n    }\n\n    private suspend fun closeWebView() {\n        Timber.tag(TAG).d(\"closeWebView: existing=${cipherWebView != null}\")\n        withContext(Dispatchers.Main) {\n            cipherWebView?.close()\n        }\n        cipherWebView = null\n        currentPlayerHash = null\n        Timber.tag(TAG).d(\"CipherWebView closed and cleared\")\n    }\n\n    private fun parseQueryParams(query: String): Map<String, String> {\n        val result = mutableMapOf<String, String>()\n        for (pair in query.split(\"&\")) {\n            val idx = pair.indexOf('=')\n            if (idx > 0) {\n                val key = Uri.decode(pair.substring(0, idx))\n                val value = Uri.decode(pair.substring(idx + 1))\n                result[key] = value\n            }\n        }\n        Timber.tag(TAG).v(\"parseQueryParams: ${result.keys.joinToString()}\")\n        return result\n    }\n\n    /**\n     * Debug method: Get current state information\n     */\n    fun getDebugInfo(): Map<String, Any?> {\n        return mapOf(\n            \"hasWebView\" to (cipherWebView != null),\n            \"playerHash\" to currentPlayerHash,\n            \"nFunctionAvailable\" to cipherWebView?.nFunctionAvailable,\n            \"sigFunctionAvailable\" to cipherWebView?.sigFunctionAvailable,\n            \"discoveredNFuncName\" to cipherWebView?.discoveredNFuncName,\n            \"usingHardcodedMode\" to cipherWebView?.usingHardcodedMode,\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/cipher/CipherWebView.kt",
    "content": "package com.metrolist.music.utils.cipher\n\nimport android.content.Context\nimport android.webkit.ConsoleMessage\nimport android.webkit.JavascriptInterface\nimport android.webkit.WebChromeClient\nimport android.webkit.WebView\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.suspendCancellableCoroutine\nimport kotlinx.coroutines.withContext\nimport timber.log.Timber\nimport java.io.File\nimport kotlin.coroutines.Continuation\nimport kotlin.coroutines.resume\nimport kotlin.coroutines.resumeWithException\n\n/**\n * WebView-based cipher executor for YouTube stream URL deobfuscation\n *\n * Executes signature decipher and n-transform functions extracted from player.js.\n * Supports both regex-extracted functions and hardcoded fallback for Q-array obfuscated players.\n */\nclass CipherWebView private constructor(\n    context: Context,\n    private val playerJs: String,\n    private val sigInfo: FunctionNameExtractor.SigFunctionInfo?,\n    private val nFuncInfo: FunctionNameExtractor.NFunctionInfo?,\n    private val initContinuation: Continuation<CipherWebView>,\n) {\n    private val webView = WebView(context)\n    private var sigContinuation: Continuation<String>? = null\n    private var nContinuation: Continuation<String>? = null\n\n    @Volatile\n    var nFunctionAvailable: Boolean = false\n        private set\n\n    @Volatile\n    var sigFunctionAvailable: Boolean = false\n        private set\n\n    @Volatile\n    var discoveredNFuncName: String? = null\n        private set\n\n    @Volatile\n    var usingHardcodedMode: Boolean = false\n        private set\n\n    init {\n        Timber.tag(TAG).d(\"Initializing CipherWebView...\")\n        Timber.tag(TAG).d(\"  sigInfo: name=${sigInfo?.name}, constantArg=${sigInfo?.constantArg}, hardcoded=${sigInfo?.isHardcoded}\")\n        Timber.tag(TAG).d(\"  nFuncInfo: name=${nFuncInfo?.name}, arrayIdx=${nFuncInfo?.arrayIndex}, hardcoded=${nFuncInfo?.isHardcoded}\")\n\n        val settings = webView.settings\n        @Suppress(\"SetJavaScriptEnabled\")\n        settings.javaScriptEnabled = true\n        settings.allowFileAccess = true\n        @Suppress(\"DEPRECATION\")\n        settings.allowFileAccessFromFileURLs = true\n        settings.blockNetworkLoads = true\n\n        webView.addJavascriptInterface(this, JS_INTERFACE)\n\n        webView.webChromeClient = object : WebChromeClient() {\n            override fun onConsoleMessage(m: ConsoleMessage): Boolean {\n                val msg = m.message()\n                val src = \"${m.sourceId()}:${m.lineNumber()}\"\n\n                // Log all console messages for debugging\n                when (m.messageLevel()) {\n                    ConsoleMessage.MessageLevel.ERROR -> {\n                        if (!msg.contains(\"is not defined\")) {\n                            Timber.tag(TAG).e(\"JS ERROR: $msg at $src\")\n                        }\n                    }\n                    ConsoleMessage.MessageLevel.WARNING -> {\n                        Timber.tag(TAG).w(\"JS WARN: $msg at $src\")\n                    }\n                    else -> {\n                        Timber.tag(TAG).v(\"JS LOG: $msg\")\n                    }\n                }\n                return super.onConsoleMessage(m)\n            }\n        }\n\n        Timber.tag(TAG).d(\"WebView settings configured\")\n    }\n\n    private fun loadPlayerJsFromFile() {\n        val sigFuncName = sigInfo?.name\n        val nFuncName = nFuncInfo?.name\n        val nArrayIdx = nFuncInfo?.arrayIndex\n        val isHardcoded = sigInfo?.isHardcoded == true || nFuncInfo?.isHardcoded == true\n\n        Timber.tag(TAG).d(\"=== LOADING PLAYER.JS INTO WEBVIEW ===\")\n        Timber.tag(TAG).d(\"Player.js size: ${playerJs.length} chars\")\n        Timber.tag(TAG).d(\"Export mode: ${if (isHardcoded) \"HARDCODED\" else \"EXTRACTED\"}\")\n        Timber.tag(TAG).d(\"Sig function: $sigFuncName (constantArg=${sigInfo?.constantArg})\")\n        Timber.tag(TAG).d(\"N function: $nFuncName (arrayIdx=$nArrayIdx)\")\n\n        usingHardcodedMode = isHardcoded\n\n        val exports = buildList {\n            if (sigFuncName != null) {\n                val sigConstArgs = sigInfo?.constantArgs\n                val preprocessFunc = sigInfo?.preprocessFunc\n                val preprocessArgs = sigInfo?.preprocessArgs\n\n                if (!sigConstArgs.isNullOrEmpty() && preprocessFunc != null && !preprocessArgs.isNullOrEmpty()) {\n                    // Full wrapper: JI(48, 1918, f1(1, 6528, sig))\n                    val mainArgsStr = sigConstArgs.joinToString(\", \")\n                    val prepArgsStr = preprocessArgs.joinToString(\", \")\n                    Timber.tag(TAG).d(\"Sig function needs full wrapper:\")\n                    Timber.tag(TAG).d(\"  $sigFuncName($mainArgsStr, $preprocessFunc($prepArgsStr, sig))\")\n                    add(\"window._cipherSigFunc = function(sig) { return $sigFuncName($mainArgsStr, $preprocessFunc($prepArgsStr, sig)); };\")\n                } else if (!sigConstArgs.isNullOrEmpty()) {\n                    // Wrapper with constant args only (no preprocessing)\n                    val argsStr = sigConstArgs.joinToString(\", \")\n                    Timber.tag(TAG).d(\"Sig function needs wrapper with constant args: $argsStr\")\n                    add(\"window._cipherSigFunc = function(sig) { return $sigFuncName($argsStr, sig); };\")\n                } else if (isHardcoded) {\n                    // For hardcoded mode without full args, we'll inject the function export after player.js loads\n                    Timber.tag(TAG).d(\"Will export sig function $sigFuncName in hardcoded mode (legacy)\")\n                    add(\"window._cipherSigFunc = typeof $sigFuncName !== 'undefined' ? $sigFuncName : null;\")\n                } else {\n                    add(\"window._cipherSigFunc = typeof $sigFuncName !== 'undefined' ? $sigFuncName : null;\")\n                }\n            }\n            if (nFuncName != null) {\n                val nConstArgs = nFuncInfo?.constantArgs\n                if (!nConstArgs.isNullOrEmpty()) {\n                    // Generate wrapper function for n-functions that require constant args\n                    // e.g. GU(6, 6010, n) -> window._nTransformFunc = function(n) { return GU(6, 6010, n); };\n                    val argsStr = nConstArgs.joinToString(\", \")\n                    Timber.tag(TAG).d(\"N-function needs wrapper with constant args: $argsStr\")\n                    add(\"window._nTransformFunc = function(n) { return $nFuncName($argsStr, n); };\")\n                } else {\n                    val nExpr = if (nArrayIdx != null) {\n                        \"$nFuncName[$nArrayIdx]\"\n                    } else {\n                        nFuncName\n                    }\n                    add(\"window._nTransformFunc = typeof $nFuncName !== 'undefined' ? $nExpr : null;\")\n                }\n            }\n        }\n\n        Timber.tag(TAG).d(\"Export statements: ${exports.size}\")\n        exports.forEachIndexed { idx, stmt ->\n            Timber.tag(TAG).v(\"  Export[$idx]: ${stmt.take(80)}...\")\n        }\n\n        val modifiedJs = if (exports.isNotEmpty()) {\n            val exportCode = \"; \" + exports.joinToString(\" \")\n            val modified = playerJs.replace(\"})(_yt_player);\", \"$exportCode })(_yt_player);\")\n            if (modified == playerJs) {\n                Timber.tag(TAG).w(\"Export injection point '})(_yt_player);' not found, appending exports\")\n                playerJs + \"\\n\" + exportCode\n            } else {\n                Timber.tag(TAG).d(\"Exports injected into IIFE closure\")\n                modified\n            }\n        } else {\n            Timber.tag(TAG).w(\"No exports to inject\")\n            playerJs\n        }\n\n        val cacheDir = File(webView.context.cacheDir, \"cipher\")\n        cacheDir.mkdirs()\n        val playerJsFile = File(cacheDir, \"player.js\")\n        playerJsFile.writeText(modifiedJs)\n        Timber.tag(TAG).d(\"Player.js written to cache: ${playerJsFile.absolutePath} (${modifiedJs.length} chars)\")\n\n        // Build HTML with comprehensive discovery and validation\n        val html = buildDiscoveryHtml()\n        Timber.tag(TAG).d(\"Discovery HTML built (${html.length} chars)\")\n\n        webView.loadDataWithBaseURL(\n            \"file://${cacheDir.absolutePath}/\",\n            html, \"text/html\", \"utf-8\", null\n        )\n        Timber.tag(TAG).d(\"WebView loading started...\")\n    }\n\n    /**\n     * Build HTML with JS discovery logic\n     *\n     * Key changes from original:\n     * 1. Removed outdated `_w8_` pattern check\n     * 2. Accept any valid alphanumeric transform result\n     * 3. More comprehensive logging to bridge\n     */\n    private fun buildDiscoveryHtml(): String = \"\"\"<!DOCTYPE html>\n<html><head><script>\n// ============================================================\n// SIGNATURE DEOBFUSCATION\n// ============================================================\nfunction deobfuscateSig(funcName, constantArg, obfuscatedSig) {\n    CipherBridge.logDebug(\"deobfuscateSig called: funcName=\" + funcName + \", constantArg=\" + constantArg + \", sigLen=\" + obfuscatedSig.length);\n\n    try {\n        var func = window._cipherSigFunc;\n        CipherBridge.logDebug(\"window._cipherSigFunc type: \" + typeof func + \", length: \" + (func ? func.length : \"N/A\"));\n\n        if (typeof func !== 'function') {\n            CipherBridge.onSigError(\"Sig func not found on window (type: \" + typeof func + \")\");\n            return;\n        }\n\n        var result;\n        // Check if this is a wrapper function (takes 1 arg: sig) vs direct function (takes 2+ args)\n        if (func.length === 1) {\n            // Wrapper function: window._cipherSigFunc = function(sig) { return JI(48, 1918, f1(1, 6528, sig)); }\n            CipherBridge.logDebug(\"Calling wrapped sig func with just sig (func.length=1)\");\n            result = func(obfuscatedSig);\n        } else if (constantArg !== null && constantArg !== undefined) {\n            // Direct function with constantArg\n            CipherBridge.logDebug(\"Calling sig func with constantArg: \" + constantArg);\n            result = func(constantArg, obfuscatedSig);\n        } else {\n            CipherBridge.logDebug(\"Calling sig func without constantArg\");\n            result = func(obfuscatedSig);\n        }\n\n        if (result === undefined || result === null) {\n            CipherBridge.onSigError(\"Function returned null/undefined\");\n            return;\n        }\n\n        CipherBridge.logDebug(\"Sig result type: \" + typeof result + \", length: \" + String(result).length);\n        CipherBridge.onSigResult(String(result));\n    } catch (error) {\n        CipherBridge.onSigError(error + \"\\n\" + (error.stack || \"\"));\n    }\n}\n\n// ============================================================\n// N-PARAMETER TRANSFORM\n// ============================================================\nfunction transformN(nValue) {\n    CipherBridge.logDebug(\"transformN called: nValue=\" + nValue);\n\n    try {\n        var func = window._nTransformFunc;\n        CipherBridge.logDebug(\"window._nTransformFunc type: \" + typeof func);\n\n        if (typeof func !== 'function') {\n            CipherBridge.onNError(\"N-transform func not available (type: \" + typeof func + \")\");\n            return;\n        }\n\n        var result = func(nValue);\n        CipherBridge.logDebug(\"N-transform raw result: \" + (result ? String(result).substring(0, 50) : \"null/undefined\"));\n\n        if (result === undefined || result === null) {\n            CipherBridge.onNError(\"N-transform returned null/undefined\");\n            return;\n        }\n\n        var resultStr = String(result);\n        CipherBridge.logDebug(\"N-transform result: length=\" + resultStr.length + \", value=\" + resultStr.substring(0, 30));\n        CipherBridge.onNResult(resultStr);\n    } catch (error) {\n        CipherBridge.onNError(error + \"\\n\" + (error.stack || \"\"));\n    }\n}\n\n// ============================================================\n// FUNCTION DISCOVERY AND INITIALIZATION\n// ============================================================\nfunction discoverAndInit() {\n    CipherBridge.logDebug(\"========== DISCOVERY AND INIT ==========\");\n\n    var nFuncName = \"\";\n    var sigFuncName = \"\";\n    var info = \"\";\n\n    // Check if signature function was exported\n    if (typeof window._cipherSigFunc === 'function') {\n        sigFuncName = \"exported_sig_func\";\n        CipherBridge.logDebug(\"Signature function found on window._cipherSigFunc\");\n    } else {\n        CipherBridge.logDebug(\"WARNING: window._cipherSigFunc not available (type=\" + typeof window._cipherSigFunc + \")\");\n    }\n\n    // Check if N-transform function was exported\n    if (typeof window._nTransformFunc === 'function') {\n        CipherBridge.logDebug(\"Testing exported window._nTransformFunc...\");\n        try {\n            var testInput = \"KdrqFlzJXl9EcCwlmEy\";\n            var testResult = window._nTransformFunc(testInput);\n\n            CipherBridge.logDebug(\"N-func test input: \" + testInput);\n            CipherBridge.logDebug(\"N-func test result: \" + (testResult ? String(testResult).substring(0, 50) : \"null\"));\n\n            if (typeof testResult === 'string' && testResult !== testInput && testResult.length >= 5) {\n                // FIXED: Accept any valid alphanumeric result, not just _w8_ pattern\n                if (/^[a-zA-Z0-9_-]+$/.test(testResult)) {\n                    nFuncName = \"exported_n_func\";\n                    info = \"export_valid,test=\" + testResult.substring(0, 20);\n                    CipherBridge.logDebug(\"N-function VALID: \" + testResult);\n                } else {\n                    info = \"export_bad_chars:\" + testResult.substring(0, 20);\n                    CipherBridge.logDebug(\"N-function has invalid characters\");\n                    window._nTransformFunc = null;\n                }\n            } else {\n                info = \"export_bad_result:type=\" + typeof testResult + \",eq=\" + (testResult === testInput);\n                CipherBridge.logDebug(\"N-function test failed: \" + info);\n                window._nTransformFunc = null;\n            }\n        } catch(e) {\n            info = \"export_threw:\" + e;\n            CipherBridge.logDebug(\"N-function threw exception: \" + e);\n            window._nTransformFunc = null;\n        }\n    } else {\n        CipherBridge.logDebug(\"window._nTransformFunc not exported, trying brute force discovery...\");\n    }\n\n    // Brute force discovery if export failed\n    if (!nFuncName) {\n        try {\n            var testInput = \"T2Xw3pWQ_Wk0xbOg\";\n            var keys = Object.getOwnPropertyNames(window);\n            var tested = 0;\n            var candidates = [];\n            var skipped = 0;\n\n            CipherBridge.logDebug(\"Brute force: scanning \" + keys.length + \" window properties\");\n\n            for (var i = 0; i < keys.length; i++) {\n                try {\n                    var key = keys[i];\n                    // Skip known non-candidates\n                    if (key.startsWith(\"webkit\") || key.startsWith(\"on\") ||\n                        key === \"CipherBridge\" || key === \"_cipherSigFunc\" ||\n                        key === \"_nTransformFunc\" || key === \"window\" || key === \"self\") {\n                        skipped++;\n                        continue;\n                    }\n\n                    var fn = window[key];\n                    if (typeof fn !== 'function') continue;\n\n                    // N-transform functions typically take 1 argument\n                    if (fn.length !== 1) continue;\n\n                    tested++;\n                    var result = fn(testInput);\n\n                    if (typeof result === 'string' && result !== testInput && result.length >= 5) {\n                        // FIXED: Accept any valid alphanumeric transform\n                        if (/^[a-zA-Z0-9_-]+$/.test(result)) {\n                            candidates.push({\n                                name: key,\n                                result: result.substring(0, 30),\n                                len: result.length\n                            });\n\n                            // Accept the first valid candidate\n                            if (!nFuncName) {\n                                window._nTransformFunc = fn;\n                                nFuncName = key;\n                                CipherBridge.logDebug(\"N-function discovered: \" + key + \" -> \" + result.substring(0, 30));\n                            }\n                        }\n                    }\n                } catch(e) {\n                    // Expected - many window properties throw when called\n                }\n            }\n\n            info = \"brute_force:tested=\" + tested + \"/skipped=\" + skipped + \"/total=\" + keys.length;\n            if (candidates.length > 0) {\n                info += \",candidates=\" + candidates.length;\n                CipherBridge.logDebug(\"Candidates found: \" + JSON.stringify(candidates.slice(0, 5)));\n            }\n        } catch(e) {\n            info = \"brute_force_error:\" + e;\n            CipherBridge.logDebug(\"Brute force failed: \" + e);\n        }\n    }\n\n    CipherBridge.logDebug(\"Discovery complete:\");\n    CipherBridge.logDebug(\"  sigFuncName=\" + sigFuncName);\n    CipherBridge.logDebug(\"  nFuncName=\" + nFuncName);\n    CipherBridge.logDebug(\"  info=\" + info);\n\n    CipherBridge.onDiscoveryDone(sigFuncName, nFuncName, info);\n    CipherBridge.onPlayerJsLoaded();\n}\n</script>\n<script src=\"player.js\"\n    onload=\"discoverAndInit()\"\n    onerror=\"CipherBridge.onPlayerJsError('Failed to load player.js from file')\">\n</script>\n</head><body></body></html>\"\"\"\n\n    // ==================== JAVASCRIPT INTERFACE ====================\n\n    @JavascriptInterface\n    fun logDebug(message: String) {\n        Timber.tag(TAG).d(\"JS: $message\")\n    }\n\n    @JavascriptInterface\n    fun onDiscoveryDone(sigFuncName: String, nFuncName: String, info: String) {\n        Timber.tag(TAG).d(\"=== DISCOVERY COMPLETE ===\")\n        Timber.tag(TAG).d(\"Sig function: ${sigFuncName.ifEmpty { \"NOT FOUND\" }}\")\n        Timber.tag(TAG).d(\"N function: ${nFuncName.ifEmpty { \"NOT FOUND\" }}\")\n        Timber.tag(TAG).d(\"Info: $info\")\n\n        sigFunctionAvailable = sigFuncName.isNotEmpty()\n        if (nFuncName.isNotEmpty()) {\n            discoveredNFuncName = nFuncName\n            nFunctionAvailable = true\n            Timber.tag(TAG).d(\"N-function AVAILABLE: $nFuncName\")\n        } else {\n            Timber.tag(TAG).e(\"N-function NOT AVAILABLE\")\n            nFunctionAvailable = false\n        }\n    }\n\n    @JavascriptInterface\n    fun onNDiscoveryDone(funcName: String, info: String) {\n        // Legacy interface - redirects to new combined discovery\n        Timber.tag(TAG).d(\"Legacy onNDiscoveryDone: funcName=$funcName, info=$info\")\n        if (funcName.isNotEmpty()) {\n            discoveredNFuncName = funcName\n            nFunctionAvailable = true\n        }\n    }\n\n    @JavascriptInterface\n    fun onPlayerJsLoaded() {\n        Timber.tag(TAG).d(\"=== PLAYER.JS LOAD COMPLETE ===\")\n        Timber.tag(TAG).d(\"sigFunctionAvailable=$sigFunctionAvailable\")\n        Timber.tag(TAG).d(\"nFunctionAvailable=$nFunctionAvailable\")\n        Timber.tag(TAG).d(\"discoveredNFuncName=$discoveredNFuncName\")\n        Timber.tag(TAG).d(\"usingHardcodedMode=$usingHardcodedMode\")\n\n        initContinuation.resume(this)\n    }\n\n    @JavascriptInterface\n    fun onPlayerJsError(error: String) {\n        Timber.tag(TAG).e(\"=== PLAYER.JS LOAD FAILED ===\")\n        Timber.tag(TAG).e(\"Error: $error\")\n        initContinuation.resumeWithException(CipherException(\"Player JS load failed: $error\"))\n    }\n\n    // ==================== SIGNATURE DEOBFUSCATION ====================\n\n    suspend fun deobfuscateSignature(obfuscatedSig: String): String {\n        Timber.tag(TAG).d(\"========== DEOBFUSCATE SIGNATURE ==========\")\n        Timber.tag(TAG).d(\"Input sig length: ${obfuscatedSig.length}\")\n        Timber.tag(TAG).d(\"Input sig preview: ${obfuscatedSig.take(50)}...\")\n        Timber.tag(TAG).d(\"sigInfo: name=${sigInfo?.name}, constantArg=${sigInfo?.constantArg}\")\n\n        if (sigInfo == null) {\n            Timber.tag(TAG).e(\"Signature function info not available\")\n            throw CipherException(\"Signature function info not available\")\n        }\n\n        return withContext(Dispatchers.Main) {\n            suspendCancellableCoroutine { cont ->\n                sigContinuation = cont\n                val constArgJs = if (sigInfo.constantArg != null) \"${sigInfo.constantArg}\" else \"null\"\n                val jsCall = \"deobfuscateSig('${sigInfo.name}', $constArgJs, '${escapeJsString(obfuscatedSig)}')\"\n                Timber.tag(TAG).d(\"Evaluating JS: ${jsCall.take(100)}...\")\n                webView.evaluateJavascript(jsCall, null)\n            }\n        }\n    }\n\n    @JavascriptInterface\n    fun onSigResult(result: String) {\n        Timber.tag(TAG).d(\"========== SIGNATURE RESULT ==========\")\n        Timber.tag(TAG).d(\"Result length: ${result.length}\")\n        Timber.tag(TAG).d(\"Result preview: ${result.take(50)}...\")\n        sigContinuation?.resume(result)\n        sigContinuation = null\n    }\n\n    @JavascriptInterface\n    fun onSigError(error: String) {\n        Timber.tag(TAG).e(\"========== SIGNATURE ERROR ==========\")\n        Timber.tag(TAG).e(\"Error: $error\")\n        sigContinuation?.resumeWithException(CipherException(\"Sig deobfuscation failed: $error\"))\n        sigContinuation = null\n    }\n\n    // ==================== N-TRANSFORM ====================\n\n    suspend fun transformN(nValue: String): String {\n        Timber.tag(TAG).d(\"========== N-TRANSFORM ==========\")\n        Timber.tag(TAG).d(\"Input n value: $nValue\")\n        Timber.tag(TAG).d(\"nFunctionAvailable: $nFunctionAvailable\")\n        Timber.tag(TAG).d(\"discoveredNFuncName: $discoveredNFuncName\")\n\n        if (!nFunctionAvailable) {\n            Timber.tag(TAG).e(\"N-transform function not discovered\")\n            throw CipherException(\"N-transform function not discovered\")\n        }\n\n        return withContext(Dispatchers.Main) {\n            suspendCancellableCoroutine { cont ->\n                nContinuation = cont\n                val jsCall = \"transformN('${escapeJsString(nValue)}')\"\n                Timber.tag(TAG).d(\"Evaluating JS: $jsCall\")\n                webView.evaluateJavascript(jsCall, null)\n            }\n        }\n    }\n\n    @JavascriptInterface\n    fun onNResult(result: String) {\n        Timber.tag(TAG).d(\"========== N-TRANSFORM RESULT ==========\")\n        Timber.tag(TAG).d(\"Result: $result\")\n        Timber.tag(TAG).d(\"Result length: ${result.length}\")\n        nContinuation?.resume(result)\n        nContinuation = null\n    }\n\n    @JavascriptInterface\n    fun onNError(error: String) {\n        Timber.tag(TAG).e(\"========== N-TRANSFORM ERROR ==========\")\n        Timber.tag(TAG).e(\"Error: $error\")\n        nContinuation?.resumeWithException(CipherException(\"N-transform failed: $error\"))\n        nContinuation = null\n    }\n\n    // ==================== CLEANUP ====================\n\n    fun close() {\n        Timber.tag(TAG).d(\"Closing CipherWebView...\")\n        webView.clearHistory()\n        webView.clearCache(true)\n        webView.loadUrl(\"about:blank\")\n        webView.onPause()\n        webView.removeAllViews()\n        webView.destroy()\n        Timber.tag(TAG).d(\"CipherWebView closed\")\n    }\n\n    // ==================== UTILITIES ====================\n\n    private fun escapeJsString(s: String): String {\n        return s.replace(\"\\\\\", \"\\\\\\\\\")\n            .replace(\"'\", \"\\\\'\")\n            .replace(\"\\\"\", \"\\\\\\\"\")\n            .replace(\"\\n\", \"\\\\n\")\n            .replace(\"\\r\", \"\\\\r\")\n            .replace(\"\\t\", \"\\\\t\")\n    }\n\n    companion object {\n        private const val TAG = \"Metrolist_CipherWebView\"\n        private const val JS_INTERFACE = \"CipherBridge\"\n\n        suspend fun create(\n            context: Context,\n            playerJs: String,\n            sigInfo: FunctionNameExtractor.SigFunctionInfo?,\n            nFuncInfo: FunctionNameExtractor.NFunctionInfo? = null,\n        ): CipherWebView {\n            Timber.tag(TAG).d(\"=== CREATING CIPHER WEBVIEW ===\")\n            Timber.tag(TAG).d(\"playerJs size: ${playerJs.length} chars\")\n            Timber.tag(TAG).d(\"sigInfo: $sigInfo\")\n            Timber.tag(TAG).d(\"nFuncInfo: $nFuncInfo\")\n\n            return withContext(Dispatchers.Main) {\n                suspendCancellableCoroutine { cont ->\n                    val wv = CipherWebView(context, playerJs, sigInfo, nFuncInfo, cont)\n                    wv.loadPlayerJsFromFile()\n                }\n            }\n        }\n    }\n}\n\nclass CipherException(message: String) : Exception(message)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/cipher/FunctionNameExtractor.kt",
    "content": "package com.metrolist.music.utils.cipher\n\nimport timber.log.Timber\nimport java.security.MessageDigest\n\n/**\n * Extracts cipher function names from YouTube's player.js\n *\n * Handles both legacy patterns and modern Q-array obfuscation (2025+).\n * Falls back to hardcoded configs for known player.js hashes when regex fails.\n */\nobject FunctionNameExtractor {\n    private const val TAG = \"Metrolist_CipherFnExtract\"\n\n    // ==================== DATA CLASSES ====================\n\n    data class SigFunctionInfo(\n        val name: String,\n        val constantArg: Int?, // The first numeric argument (e.g., 48 in JI(48, sig)) - legacy\n        val constantArgs: List<Int>? = null, // All constant args e.g., JI(48, 1918, ...) -> [48, 1918]\n        val preprocessFunc: String? = null, // Preprocessing function e.g., f1\n        val preprocessArgs: List<Int>? = null, // Preprocess args e.g., f1(1, 6528, sig) -> [1, 6528]\n        val isHardcoded: Boolean = false\n    )\n\n    data class NFunctionInfo(\n        val name: String,\n        val arrayIndex: Int?, // e.g. FUNC[0] -> index=0\n        val constantArgs: List<Int>? = null, // e.g. GU(6, 6010, n) -> [6, 6010]\n        val isHardcoded: Boolean = false\n    )\n\n    /**\n     * Hardcoded player.js configuration for when regex extraction fails\n     * Due to Q-array obfuscation, patterns like `.get(\"n\")` become `Q[T^6001]`\n     */\n    data class HardcodedPlayerConfig(\n        val sigFuncName: String,\n        val sigConstantArg: Int?, // Legacy single arg\n        val sigConstantArgs: List<Int>? = null, // e.g. JI(48, 1918, ...) -> [48, 1918]\n        val sigPreprocessFunc: String? = null, // e.g. f1\n        val sigPreprocessArgs: List<Int>? = null, // e.g. f1(1, 6528, sig) -> [1, 6528]\n        val nFuncName: String,\n        val nArrayIndex: Int?,\n        val nConstantArgs: List<Int>?, // e.g. GU(6, 6010, n) -> [6, 6010]\n        val signatureTimestamp: Int\n    )\n\n    // ==================== KNOWN PLAYER CONFIGS ====================\n\n    /**\n     * Known player.js configurations indexed by hash\n     *\n     * Player hash 74edf1a3 (March 2026):\n     * - Signature: JI(48, 1918, f1(1, 6528, sig)) -> reverse, swap(0, 57%), reverse\n     * - N-transform: GU(6, 6010, n) with 87-element self-referential array\n     */\n    private val KNOWN_PLAYER_CONFIGS = mapOf(\n        \"74edf1a3\" to HardcodedPlayerConfig(\n            sigFuncName = \"JI\",\n            sigConstantArg = 48, // Legacy\n            sigConstantArgs = listOf(48, 1918), // JI(48, 1918, processedSig)\n            sigPreprocessFunc = \"f1\", // sig must be preprocessed through f1()\n            sigPreprocessArgs = listOf(1, 6528), // f1(1, 6528, sig)\n            nFuncName = \"GU\",\n            nArrayIndex = null, // Direct function, not array access\n            nConstantArgs = listOf(6, 6010), // GU(6, 6010, n) - the function requires 3 args!\n            signatureTimestamp = 20522\n        )\n    )\n\n    // ==================== DETECTION PATTERNS ====================\n\n    // Detect Q-array obfuscation: var Q=\"...\".split(\"}\")\n    private val Q_ARRAY_PATTERN = Regex(\"\"\"var\\s+Q\\s*=\\s*\"[^\"]+\"\\s*\\.\\s*split\\s*\\(\\s*\"\\}\"\\s*\\)\"\"\")\n\n    // Extract player hash from common patterns\n    private val PLAYER_HASH_PATTERNS = listOf(\n        Regex(\"\"\"jsUrl['\":\\s]+[^\"']*?/player/([a-f0-9]{8})/\"\"\"),\n        Regex(\"\"\"player_ias\\.vflset/[^/]+/([a-f0-9]{8})/\"\"\"),\n        Regex(\"\"\"/s/player/([a-f0-9]{8})/\"\"\")\n    )\n\n    // Modern 2025+ signature deobfuscation function patterns\n    private val SIG_FUNCTION_PATTERNS = listOf(\n        // Pattern 1 (2025+): &&(VAR=FUNC(NUM,decodeURIComponent(VAR))\n        Regex(\"\"\"&&\\s*\\(\\s*[a-zA-Z0-9$]+\\s*=\\s*([a-zA-Z0-9$]+)\\s*\\(\\s*(\\d+)\\s*,\\s*decodeURIComponent\\s*\\(\\s*[a-zA-Z0-9$]+\\s*\\)\"\"\"),\n        // Classic patterns (pre-2025, kept as fallback)\n        Regex(\"\"\"\\b[cs]\\s*&&\\s*[adf]\\.set\\([^,]+\\s*,\\s*encodeURIComponent\\(([a-zA-Z0-9$]+)\\(\"\"\"),\n        Regex(\"\"\"\\b[a-zA-Z0-9]+\\s*&&\\s*[a-zA-Z0-9]+\\.set\\([^,]+\\s*,\\s*encodeURIComponent\\(([a-zA-Z0-9$]+)\\(\"\"\"),\n        Regex(\"\"\"\\bm=([a-zA-Z0-9${'$'}]{2,})\\(decodeURIComponent\\(h\\.s\\)\\)\"\"\"),\n        Regex(\"\"\"\\bc\\s*&&\\s*d\\.set\\([^,]+\\s*,\\s*(?:encodeURIComponent\\s*\\()([a-zA-Z0-9$]+)\\(\"\"\"),\n        Regex(\"\"\"\\bc\\s*&&\\s*[a-z]\\.set\\([^,]+\\s*,\\s*encodeURIComponent\\(([a-zA-Z0-9$]+)\\(\"\"\"),\n    )\n\n    // N-parameter (throttle) transform function patterns\n    private val N_FUNCTION_PATTERNS = listOf(\n        // Pattern 1: .get(\"n\"))&&(b=FUNC[IDX](VAR)\n        Regex(\"\"\"\\.get\\(\"n\"\\)\\)&&\\(b=([a-zA-Z0-9$]+)(?:\\[(\\d+)\\])?\\(([a-zA-Z0-9])\\)\"\"\"),\n        // Pattern 2: .get(\"n\"))&&(FUNC=VAR[IDX](FUNC) (2025+ variant)\n        Regex(\"\"\"\\.get\\(\"n\"\\)\\)\\s*&&\\s*\\(([a-zA-Z0-9$]+)\\s*=\\s*([a-zA-Z0-9$]+)(?:\\[(\\d+)\\])?\\(\\1\\)\"\"\"),\n        // Pattern 3: String.fromCharCode(110) variant (110 = 'n')\n        Regex(\"\"\"\\(\\s*([a-zA-Z0-9$]+)\\s*=\\s*String\\.fromCharCode\\(110\\)\"\"\"),\n        // Pattern 4: enhanced_except_ function pattern\n        Regex(\"\"\"([a-zA-Z0-9$]+)\\s*=\\s*function\\([a-zA-Z0-9]\\)\\s*\\{[^}]*?enhanced_except_\"\"\"),\n    )\n\n    // ==================== EXTRACTION FUNCTIONS ====================\n\n    /**\n     * Detect if player.js uses Q-array obfuscation\n     */\n    fun hasQArrayObfuscation(playerJs: String): Boolean {\n        val hasQArray = Q_ARRAY_PATTERN.containsMatchIn(playerJs)\n        Timber.tag(TAG).d(\"Q-array obfuscation check: hasQArray=$hasQArray\")\n\n        if (hasQArray) {\n            // Try to count Q array elements for additional info\n            val match = Q_ARRAY_PATTERN.find(playerJs)\n            if (match != null) {\n                val start = match.range.first\n                val qDefEnd = playerJs.indexOf(\";\", start)\n                if (qDefEnd > start) {\n                    val qDef = playerJs.substring(start, qDefEnd)\n                    val elementCount = qDef.count { it == '}' } + 1\n                    Timber.tag(TAG).d(\"Q-array detected with ~$elementCount elements\")\n                }\n            }\n        }\n        return hasQArray\n    }\n\n    /**\n     * Extract player.js hash from embedded URLs or compute from content\n     */\n    fun extractPlayerHash(playerJs: String): String? {\n        Timber.tag(TAG).d(\"Extracting player hash from playerJs (${playerJs.length} chars)\")\n\n        // Try to extract from embedded URLs first\n        for ((index, pattern) in PLAYER_HASH_PATTERNS.withIndex()) {\n            val match = pattern.find(playerJs)\n            if (match != null) {\n                val hash = match.groupValues[1]\n                Timber.tag(TAG).d(\"Player hash found via pattern $index: $hash\")\n                return hash\n            }\n        }\n\n        // Fallback: compute hash from first 10KB of content\n        val contentToHash = playerJs.take(10000)\n        val md = MessageDigest.getInstance(\"MD5\")\n        val digest = md.digest(contentToHash.toByteArray())\n        val computedHash = digest.take(4).joinToString(\"\") { \"%02x\".format(it) }\n        Timber.tag(TAG).d(\"Player hash computed from content: $computedHash\")\n        return computedHash\n    }\n\n    /**\n     * Get hardcoded config for a known player.js hash\n     */\n    fun getHardcodedConfig(playerHash: String): HardcodedPlayerConfig? {\n        val config = KNOWN_PLAYER_CONFIGS[playerHash]\n        if (config != null) {\n            Timber.tag(TAG).d(\"Found hardcoded config for hash $playerHash:\")\n            Timber.tag(TAG).d(\"  sigFunc=${config.sigFuncName}(${config.sigConstantArg}, ...)\")\n            Timber.tag(TAG).d(\"  nFunc=${config.nFuncName}[${config.nArrayIndex}]\")\n            Timber.tag(TAG).d(\"  signatureTimestamp=${config.signatureTimestamp}\")\n        } else {\n            Timber.tag(TAG).w(\"No hardcoded config for hash: $playerHash\")\n            Timber.tag(TAG).w(\"Known hashes: ${KNOWN_PLAYER_CONFIGS.keys.joinToString()}\")\n        }\n        return config\n    }\n\n    /**\n     * Extract signature function info from player.js\n     *\n     * Uses regex patterns first, falls back to hardcoded config if Q-array detected\n     * @param playerJs The player.js content\n     * @param knownHash Optional hash for hardcoded config lookup\n     */\n    fun extractSigFunctionInfo(playerJs: String, knownHash: String? = null): SigFunctionInfo? {\n        Timber.tag(TAG).d(\"========== EXTRACTING SIG FUNCTION ==========\")\n        Timber.tag(TAG).d(\"Player.js size: ${playerJs.length} chars\")\n\n        // Try regex patterns first\n        for ((index, pattern) in SIG_FUNCTION_PATTERNS.withIndex()) {\n            Timber.tag(TAG).v(\"Trying sig pattern $index: ${pattern.pattern.take(60)}...\")\n            val match = pattern.find(playerJs)\n            if (match != null) {\n                val name = match.groupValues[1]\n                val constArg = if (match.groupValues.size > 2) match.groupValues[2].toIntOrNull() else null\n                Timber.tag(TAG).d(\"SIG FUNCTION FOUND via pattern $index:\")\n                Timber.tag(TAG).d(\"  name=$name, constantArg=$constArg\")\n                Timber.tag(TAG).d(\"  match context: ...${playerJs.substring(maxOf(0, match.range.first - 20), minOf(playerJs.length, match.range.last + 20))}...\")\n                return SigFunctionInfo(name, constArg, isHardcoded = false)\n            }\n        }\n\n        Timber.tag(TAG).w(\"No sig pattern matched, checking for Q-array obfuscation...\")\n\n        // Check for Q-array obfuscation and use hardcoded fallback\n        if (hasQArrayObfuscation(playerJs)) {\n            // Use knownHash if provided, otherwise try to extract\n            val hashToUse = knownHash ?: extractPlayerHash(playerJs)\n            Timber.tag(TAG).d(\"Using hash for hardcoded lookup: $hashToUse (knownHash=$knownHash)\")\n            if (hashToUse != null) {\n                val config = getHardcodedConfig(hashToUse)\n                if (config != null) {\n                    Timber.tag(TAG).d(\"USING HARDCODED SIG FUNCTION: ${config.sigFuncName}(${config.sigConstantArgs}, ...)\")\n                    Timber.tag(TAG).d(\"Sig preprocess: ${config.sigPreprocessFunc}(${config.sigPreprocessArgs}, sig)\")\n                    return SigFunctionInfo(\n                        name = config.sigFuncName,\n                        constantArg = config.sigConstantArg,\n                        constantArgs = config.sigConstantArgs,\n                        preprocessFunc = config.sigPreprocessFunc,\n                        preprocessArgs = config.sigPreprocessArgs,\n                        isHardcoded = true\n                    )\n                }\n            }\n        }\n\n        Timber.tag(TAG).e(\"========== SIG FUNCTION EXTRACTION FAILED ==========\")\n        Timber.tag(TAG).e(\"Could not find signature deobfuscation function name\")\n        return null\n    }\n\n    /**\n     * Extract N-transform function info from player.js\n     *\n     * Uses regex patterns first, falls back to hardcoded config if Q-array detected\n     * @param playerJs The player.js content\n     * @param knownHash Optional hash for hardcoded config lookup\n     */\n    fun extractNFunctionInfo(playerJs: String, knownHash: String? = null): NFunctionInfo? {\n        Timber.tag(TAG).d(\"========== EXTRACTING N-FUNCTION ==========\")\n        Timber.tag(TAG).d(\"Player.js size: ${playerJs.length} chars\")\n\n        // Try regex patterns first\n        for ((index, pattern) in N_FUNCTION_PATTERNS.withIndex()) {\n            Timber.tag(TAG).v(\"Trying n-func pattern $index: ${pattern.pattern.take(60)}...\")\n            val match = pattern.find(playerJs)\n            if (match != null) {\n                when (index) {\n                    0 -> {\n                        val name = match.groupValues[1]\n                        val arrayIdx = match.groupValues[2].toIntOrNull()\n                        Timber.tag(TAG).d(\"N-FUNCTION FOUND via pattern $index:\")\n                        Timber.tag(TAG).d(\"  name=$name, arrayIndex=$arrayIdx\")\n                        return NFunctionInfo(name, arrayIdx, isHardcoded = false)\n                    }\n                    1 -> {\n                        val name = match.groupValues[2]\n                        val arrayIdx = match.groupValues[3].toIntOrNull()\n                        Timber.tag(TAG).d(\"N-FUNCTION FOUND via pattern $index:\")\n                        Timber.tag(TAG).d(\"  name=$name, arrayIndex=$arrayIdx\")\n                        return NFunctionInfo(name, arrayIdx, isHardcoded = false)\n                    }\n                    else -> {\n                        val name = match.groupValues[1]\n                        Timber.tag(TAG).d(\"N-FUNCTION FOUND via pattern $index:\")\n                        Timber.tag(TAG).d(\"  name=$name\")\n                        return NFunctionInfo(name, null, isHardcoded = false)\n                    }\n                }\n            }\n        }\n\n        Timber.tag(TAG).w(\"No n-func pattern matched, checking for Q-array obfuscation...\")\n\n        // Check for Q-array obfuscation and use hardcoded fallback\n        if (hasQArrayObfuscation(playerJs)) {\n            // Use knownHash if provided, otherwise try to extract\n            val hashToUse = knownHash ?: extractPlayerHash(playerJs)\n            Timber.tag(TAG).d(\"Using hash for hardcoded lookup: $hashToUse (knownHash=$knownHash)\")\n            if (hashToUse != null) {\n                val config = getHardcodedConfig(hashToUse)\n                if (config != null) {\n                    Timber.tag(TAG).d(\"USING HARDCODED N-FUNCTION: ${config.nFuncName}[${config.nArrayIndex}]\")\n                    Timber.tag(TAG).d(\"N-function constant args: ${config.nConstantArgs}\")\n                    return NFunctionInfo(config.nFuncName, config.nArrayIndex, config.nConstantArgs, isHardcoded = true)\n                }\n            }\n        }\n\n        Timber.tag(TAG).e(\"========== N-FUNCTION EXTRACTION FAILED ==========\")\n        Timber.tag(TAG).e(\"Could not find n-transform function name\")\n        return null\n    }\n\n    /**\n     * Extract signatureTimestamp from player.js\n     */\n    fun extractSignatureTimestamp(playerJs: String): Int? {\n        Timber.tag(TAG).d(\"Extracting signatureTimestamp...\")\n\n        val patterns = listOf(\n            Regex(\"\"\"signatureTimestamp['\":\\s]+(\\d+)\"\"\"),\n            Regex(\"\"\"sts['\":\\s]+(\\d+)\"\"\"),\n            Regex(\"\"\"\"signatureTimestamp\"\\s*:\\s*(\\d+)\"\"\")\n        )\n\n        for ((index, pattern) in patterns.withIndex()) {\n            val match = pattern.find(playerJs)\n            if (match != null) {\n                val sts = match.groupValues[1].toIntOrNull()\n                if (sts != null) {\n                    Timber.tag(TAG).d(\"signatureTimestamp found via pattern $index: $sts\")\n                    return sts\n                }\n            }\n        }\n\n        // Fallback to hardcoded config\n        val playerHash = extractPlayerHash(playerJs)\n        if (playerHash != null) {\n            val config = getHardcodedConfig(playerHash)\n            if (config != null) {\n                Timber.tag(TAG).d(\"Using hardcoded signatureTimestamp: ${config.signatureTimestamp}\")\n                return config.signatureTimestamp\n            }\n        }\n\n        Timber.tag(TAG).w(\"Could not extract signatureTimestamp\")\n        return null\n    }\n\n    /**\n     * Full analysis of player.js - extracts all cipher info\n     * @param playerJs The player.js content\n     * @param knownHash Optional hash from PlayerJsFetcher (preferred over computed)\n     */\n    fun analyzePlayerJs(playerJs: String, knownHash: String? = null): PlayerAnalysis {\n        Timber.tag(TAG).d(\"=== PLAYER.JS CIPHER ANALYSIS ===\")\n\n        // Use knownHash from PlayerJsFetcher if provided, otherwise extract/compute\n        val playerHash = if (knownHash != null) {\n            Timber.tag(TAG).d(\"Using known hash from PlayerJsFetcher: $knownHash\")\n            knownHash\n        } else {\n            extractPlayerHash(playerJs)\n        }\n\n        val hasQArray = hasQArrayObfuscation(playerJs)\n        val sigInfo = extractSigFunctionInfo(playerJs, playerHash)\n        val nFuncInfo = extractNFunctionInfo(playerJs, playerHash)\n        val signatureTimestamp = extractSignatureTimestamp(playerJs)\n\n        Timber.tag(TAG).d(\"=== ANALYSIS SUMMARY ===\")\n        Timber.tag(TAG).d(\"Player Hash:        ${playerHash ?: \"unknown\"}\")\n        Timber.tag(TAG).d(\"Q-Array Obfuscated: $hasQArray\")\n        Timber.tag(TAG).d(\"Sig Function:       ${sigInfo?.name ?: \"NOT FOUND\"} (hardcoded=${sigInfo?.isHardcoded})\")\n        Timber.tag(TAG).d(\"Sig Constant Arg:   ${sigInfo?.constantArg}\")\n        Timber.tag(TAG).d(\"N-Function:         ${nFuncInfo?.name ?: \"NOT FOUND\"} (hardcoded=${nFuncInfo?.isHardcoded})\")\n        Timber.tag(TAG).d(\"N-Array Index:      ${nFuncInfo?.arrayIndex}\")\n        Timber.tag(TAG).d(\"Signature TS:       $signatureTimestamp\")\n\n        return PlayerAnalysis(\n            playerHash = playerHash,\n            hasQArrayObfuscation = hasQArray,\n            sigInfo = sigInfo,\n            nFuncInfo = nFuncInfo,\n            signatureTimestamp = signatureTimestamp\n        )\n    }\n\n    data class PlayerAnalysis(\n        val playerHash: String?,\n        val hasQArrayObfuscation: Boolean,\n        val sigInfo: SigFunctionInfo?,\n        val nFuncInfo: NFunctionInfo?,\n        val signatureTimestamp: Int?\n    )\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/cipher/PlayerJsFetcher.kt",
    "content": "package com.metrolist.music.utils.cipher\n\nimport com.metrolist.innertube.YouTube\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport okhttp3.OkHttpClient\nimport okhttp3.Request\nimport timber.log.Timber\nimport java.io.File\n\n/**\n * Fetches and caches YouTube's player.js for cipher operations.\n *\n * The player.js contains the signature deobfuscation and n-transform functions\n * that are required to access stream URLs on web clients.\n */\nobject PlayerJsFetcher {\n    private const val TAG = \"Metrolist_CipherFetcher\"\n    private const val IFRAME_API_URL = \"https://www.youtube.com/iframe_api\"\n    private const val PLAYER_JS_URL_TEMPLATE = \"https://www.youtube.com/s/player/%s/player_ias.vflset/en_GB/base.js\"\n    private const val CACHE_TTL_MS = 6 * 60 * 60 * 1000L // 6 hours\n\n    private val httpClient = OkHttpClient.Builder()\n        .proxy(YouTube.proxy)\n        .build()\n\n    // Regex to extract player hash from iframe_api response\n    private val PLAYER_HASH_REGEX = Regex(\"\"\"\\\\?/s\\\\?/player\\\\?/([a-zA-Z0-9_-]+)\\\\?/\"\"\")\n\n    private fun getCacheDir(): File = File(CipherDeobfuscator.appContext.filesDir, \"cipher_cache\")\n\n    private fun getCacheFile(hash: String): File = File(getCacheDir(), \"player_$hash.js\")\n\n    private fun getHashFile(): File = File(getCacheDir(), \"current_hash.txt\")\n\n    /**\n     * Get player.js content and hash.\n     *\n     * Uses cached version if available and not expired, otherwise fetches fresh.\n     * Returns Pair(playerJs, hash) or null if failed.\n     */\n    suspend fun getPlayerJs(forceRefresh: Boolean = false): Pair<String, String>? = withContext(Dispatchers.IO) {\n        Timber.tag(TAG).d(\"=== GET PLAYER.JS ===\")\n        Timber.tag(TAG).d(\"forceRefresh: $forceRefresh\")\n\n        try {\n            val cacheDir = getCacheDir()\n            if (!cacheDir.exists()) {\n                Timber.tag(TAG).d(\"Creating cache directory: ${cacheDir.absolutePath}\")\n                cacheDir.mkdirs()\n            }\n\n            // Check cache first (unless forced refresh)\n            if (!forceRefresh) {\n                val cached = readFromCache()\n                if (cached != null) {\n                    Timber.tag(TAG).d(\"=== CACHE HIT ===\")\n                    Timber.tag(TAG).d(\"Using cached player JS (hash=${cached.second}, length=${cached.first.length})\")\n                    return@withContext cached\n                }\n                Timber.tag(TAG).d(\"Cache miss, will fetch fresh\")\n            }\n\n            // Fetch player hash from iframe_api\n            Timber.tag(TAG).d(\"Fetching player hash from iframe_api...\")\n            val hash = fetchPlayerHash()\n            if (hash == null) {\n                Timber.tag(TAG).e(\"Failed to extract player hash from iframe_api\")\n                return@withContext null\n            }\n            Timber.tag(TAG).d(\"Extracted player hash: $hash\")\n\n            // Download player JS\n            Timber.tag(TAG).d(\"Downloading player JS for hash: $hash...\")\n            val playerJs = downloadPlayerJs(hash)\n            if (playerJs == null) {\n                Timber.tag(TAG).e(\"Failed to download player JS for hash=$hash\")\n                return@withContext null\n            }\n\n            Timber.tag(TAG).d(\"=== PLAYER.JS DOWNLOADED ===\")\n            Timber.tag(TAG).d(\"hash: $hash\")\n            Timber.tag(TAG).d(\"length: ${playerJs.length} chars\")\n            Timber.tag(TAG).d(\"preview: ${playerJs.take(100)}...\")\n\n            // Cache the result\n            writeToCache(hash, playerJs)\n\n            Pair(playerJs, hash)\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"getPlayerJs exception: ${e.message}\")\n            null\n        }\n    }\n\n    /**\n     * Invalidate the player.js cache.\n     * Call this when cipher operations fail to force a fresh fetch.\n     */\n    fun invalidateCache() {\n        Timber.tag(TAG).d(\"Invalidating cache...\")\n        try {\n            val cacheDir = getCacheDir()\n            if (cacheDir.exists()) {\n                val files = cacheDir.listFiles()\n                Timber.tag(TAG).d(\"Deleting ${files?.size ?: 0} cache files\")\n                files?.forEach {\n                    Timber.tag(TAG).v(\"Deleting: ${it.name}\")\n                    it.delete()\n                }\n            }\n            Timber.tag(TAG).d(\"Cache invalidated successfully\")\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Failed to invalidate cache: ${e.message}\")\n        }\n    }\n\n    private fun readFromCache(): Pair<String, String>? {\n        Timber.tag(TAG).d(\"Checking cache...\")\n        try {\n            val hashFile = getHashFile()\n            if (!hashFile.exists()) {\n                Timber.tag(TAG).d(\"Hash file does not exist\")\n                return null\n            }\n\n            val hashData = hashFile.readText().split(\"\\n\")\n            if (hashData.size < 2) {\n                Timber.tag(TAG).d(\"Hash file malformed (expected 2 lines, got ${hashData.size})\")\n                return null\n            }\n\n            val hash = hashData[0]\n            val timestamp = hashData[1].toLongOrNull()\n            if (timestamp == null) {\n                Timber.tag(TAG).d(\"Could not parse timestamp from hash file\")\n                return null\n            }\n\n            val ageMs = System.currentTimeMillis() - timestamp\n            val ageHours = ageMs / (1000 * 60 * 60)\n            Timber.tag(TAG).d(\"Cache age: ${ageHours}h (TTL: ${CACHE_TTL_MS / (1000 * 60 * 60)}h)\")\n\n            // Check TTL\n            if (ageMs > CACHE_TTL_MS) {\n                Timber.tag(TAG).d(\"Cache expired (hash=$hash, age=${ageHours}h)\")\n                return null\n            }\n\n            val cacheFile = getCacheFile(hash)\n            if (!cacheFile.exists()) {\n                Timber.tag(TAG).d(\"Cache file does not exist for hash: $hash\")\n                return null\n            }\n\n            val playerJs = cacheFile.readText()\n            if (playerJs.isEmpty()) {\n                Timber.tag(TAG).d(\"Cache file is empty\")\n                return null\n            }\n\n            Timber.tag(TAG).d(\"Cache valid: hash=$hash, length=${playerJs.length}, age=${ageHours}h\")\n            return Pair(playerJs, hash)\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Error reading cache: ${e.message}\")\n            return null\n        }\n    }\n\n    private fun writeToCache(hash: String, playerJs: String) {\n        Timber.tag(TAG).d(\"Writing to cache: hash=$hash, length=${playerJs.length}\")\n        try {\n            val cacheDir = getCacheDir()\n\n            // Clean old cache files\n            val oldFiles = cacheDir.listFiles()?.filter { it.name.startsWith(\"player_\") }\n            Timber.tag(TAG).d(\"Cleaning ${oldFiles?.size ?: 0} old cache files\")\n            oldFiles?.forEach { it.delete() }\n\n            getCacheFile(hash).writeText(playerJs)\n            getHashFile().writeText(\"$hash\\n${System.currentTimeMillis()}\")\n\n            Timber.tag(TAG).d(\"Cache written successfully\")\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"Error writing cache: ${e.message}\")\n        }\n    }\n\n    private fun fetchPlayerHash(): String? {\n        Timber.tag(TAG).d(\"Fetching iframe_api from: $IFRAME_API_URL\")\n\n        val request = Request.Builder()\n            .url(IFRAME_API_URL)\n            .header(\"User-Agent\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\")\n            .build()\n\n        val response = httpClient.newCall(request).execute()\n        Timber.tag(TAG).d(\"iframe_api response: HTTP ${response.code}\")\n\n        if (!response.isSuccessful) {\n            Timber.tag(TAG).e(\"iframe_api HTTP ${response.code}\")\n            return null\n        }\n\n        val body = response.body?.string()\n        if (body == null) {\n            Timber.tag(TAG).e(\"iframe_api response body is null\")\n            return null\n        }\n\n        Timber.tag(TAG).d(\"iframe_api body length: ${body.length}\")\n        Timber.tag(TAG).v(\"iframe_api body preview: ${body.take(200)}...\")\n\n        val match = PLAYER_HASH_REGEX.find(body)\n        if (match == null) {\n            Timber.tag(TAG).e(\"Could not find player hash in iframe_api response\")\n            Timber.tag(TAG).d(\"Regex pattern: ${PLAYER_HASH_REGEX.pattern}\")\n            return null\n        }\n\n        val hash = match.groupValues[1]\n        Timber.tag(TAG).d(\"Found player hash: $hash\")\n        return hash\n    }\n\n    private fun downloadPlayerJs(hash: String): String? {\n        val url = PLAYER_JS_URL_TEMPLATE.format(hash)\n        Timber.tag(TAG).d(\"Downloading player.js from: $url\")\n\n        val request = Request.Builder()\n            .url(url)\n            .header(\"User-Agent\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\")\n            .build()\n\n        val response = httpClient.newCall(request).execute()\n        Timber.tag(TAG).d(\"player.js response: HTTP ${response.code}\")\n\n        if (!response.isSuccessful) {\n            Timber.tag(TAG).e(\"player.js download HTTP ${response.code}\")\n            return null\n        }\n\n        val body = response.body?.string()\n        if (body == null) {\n            Timber.tag(TAG).e(\"player.js response body is null\")\n            return null\n        }\n\n        Timber.tag(TAG).d(\"player.js downloaded: ${body.length} chars\")\n        return body\n    }\n\n    /**\n     * Debug method: Get cache information\n     */\n    fun getCacheInfo(): Map<String, Any?> {\n        return try {\n            val hashFile = getHashFile()\n            if (!hashFile.exists()) {\n                return mapOf(\"exists\" to false)\n            }\n\n            val hashData = hashFile.readText().split(\"\\n\")\n            val hash = hashData.getOrNull(0)\n            val timestamp = hashData.getOrNull(1)?.toLongOrNull()\n            val cacheFile = hash?.let { getCacheFile(it) }\n\n            mapOf(\n                \"exists\" to true,\n                \"hash\" to hash,\n                \"timestamp\" to timestamp,\n                \"ageMs\" to (timestamp?.let { System.currentTimeMillis() - it }),\n                \"fileExists\" to (cacheFile?.exists() == true),\n                \"fileSize\" to (cacheFile?.length()),\n            )\n        } catch (e: Exception) {\n            mapOf(\"error\" to e.message)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/potoken/JavaScriptUtil.kt",
    "content": "package com.metrolist.music.utils.potoken\n\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.json.JsonNull\nimport kotlinx.serialization.json.JsonObject\nimport kotlinx.serialization.json.JsonPrimitive\nimport kotlinx.serialization.json.jsonArray\nimport kotlinx.serialization.json.jsonPrimitive\nimport kotlinx.serialization.json.long\nimport okio.ByteString.Companion.decodeBase64\nimport okio.ByteString.Companion.toByteString\n\n/**\n * Parses the raw challenge data obtained from the Create endpoint and returns an object that can be\n * embedded in a JavaScript snippet.\n */\nfun parseChallengeData(rawChallengeData: String): String {\n    val scrambled = Json.parseToJsonElement(rawChallengeData).jsonArray\n\n    val challengeData = if (scrambled.size > 1 && scrambled[1].jsonPrimitive.isString) {\n        val descrambled = descramble(scrambled[1].jsonPrimitive.content)\n        Json.parseToJsonElement(descrambled).jsonArray\n    } else {\n        scrambled[0].jsonArray\n    }\n\n    val messageId = challengeData[0].jsonPrimitive.content\n    val interpreterHash = challengeData[3].jsonPrimitive.content\n    val program = challengeData[4].jsonPrimitive.content\n    val globalName = challengeData[5].jsonPrimitive.content\n    val clientExperimentsStateBlob = challengeData[7].jsonPrimitive.content\n\n    val privateDoNotAccessOrElseSafeScriptWrappedValue = challengeData[1]\n        .takeIf { it !is JsonNull }\n        ?.jsonArray\n        ?.find { it.jsonPrimitive.isString }\n    val privateDoNotAccessOrElseTrustedResourceUrlWrappedValue = challengeData[2]\n        .takeIf { it !is JsonNull }\n        ?.jsonArray\n        ?.find { it.jsonPrimitive.isString }\n\n    return Json.encodeToString(\n        JsonObject.serializer(), JsonObject(\n            mapOf(\n                \"messageId\" to JsonPrimitive(messageId),\n                \"interpreterJavascript\" to JsonObject(\n                    mapOf(\n                        \"privateDoNotAccessOrElseSafeScriptWrappedValue\" to (privateDoNotAccessOrElseSafeScriptWrappedValue\n                            ?: JsonNull),\n                        \"privateDoNotAccessOrElseTrustedResourceUrlWrappedValue\" to (privateDoNotAccessOrElseTrustedResourceUrlWrappedValue\n                            ?: JsonNull)\n                    )\n                ),\n                \"interpreterHash\" to JsonPrimitive(interpreterHash),\n                \"program\" to JsonPrimitive(program),\n                \"globalName\" to JsonPrimitive(globalName),\n                \"clientExperimentsStateBlob\" to JsonPrimitive(clientExperimentsStateBlob)\n            )\n        )\n    )\n}\n\n/**\n * Parses the raw integrity token data obtained from the GenerateIT endpoint to a JavaScript\n * `Uint8Array` that can be embedded directly in JavaScript code, and a [Long] representing the\n * duration of this token in seconds.\n */\nfun parseIntegrityTokenData(rawIntegrityTokenData: String): Pair<String, Long> {\n    val integrityTokenData = Json.parseToJsonElement(rawIntegrityTokenData).jsonArray\n    return base64ToU8(integrityTokenData[0].jsonPrimitive.content) to integrityTokenData[1].jsonPrimitive.long\n}\n\n/**\n * Converts a string (usually the identifier used as input to `obtainPoToken`) to a JavaScript\n * `Uint8Array` that can be embedded directly in JavaScript code.\n */\nfun stringToU8(identifier: String): String {\n    return newUint8Array(identifier.toByteArray())\n}\n\n/**\n * Takes a poToken encoded as a sequence of bytes represented as integers separated by commas\n * (e.g. \"97,98,99\" would be \"abc\"), which is the output of `Uint8Array::toString()` in JavaScript,\n * and converts it to the specific base64 representation for poTokens.\n */\nfun u8ToBase64(poToken: String): String {\n    return poToken.split(\",\")\n        .map { it.toUByte().toByte() }\n        .toByteArray()\n        .toByteString()\n        .base64()\n        .replace(\"+\", \"-\")\n        .replace(\"/\", \"_\")\n}\n\n/**\n * Takes the scrambled challenge, decodes it from base64, adds 97 to each byte.\n */\nprivate fun descramble(scrambledChallenge: String): String {\n    return base64ToByteString(scrambledChallenge)\n        .map { (it + 97).toByte() }\n        .toByteArray()\n        .decodeToString()\n}\n\n/**\n * Decodes a base64 string encoded in the specific base64 representation used by YouTube, and\n * returns a JavaScript `Uint8Array` that can be embedded directly in JavaScript code.\n */\nprivate fun base64ToU8(base64: String): String {\n    return newUint8Array(base64ToByteString(base64))\n}\n\nprivate fun newUint8Array(contents: ByteArray): String {\n    return \"new Uint8Array([\" + contents.joinToString(separator = \",\") { it.toUByte().toString() } + \"])\"\n}\n\n/**\n * Decodes a base64 string encoded in the specific base64 representation used by YouTube.\n */\nprivate fun base64ToByteString(base64: String): ByteArray {\n    val base64Mod = base64\n        .replace('-', '+')\n        .replace('_', '/')\n        .replace('.', '=')\n\n    return (base64Mod.decodeBase64() ?: throw PoTokenException(\"Cannot base64 decode\"))\n        .toByteArray()\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/potoken/PoTokenException.kt",
    "content": "package com.metrolist.music.utils.potoken\n\nclass PoTokenException(message: String) : Exception(message)\n\n// to be thrown if the WebView provided by the system is broken\nclass BadWebViewException(message: String) : Exception(message)\n\nfun buildExceptionForJsError(error: String): Exception {\n    return if (error.contains(\"SyntaxError\"))\n        BadWebViewException(error)\n    else\n        PoTokenException(error)\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/potoken/PoTokenGenerator.kt",
    "content": "package com.metrolist.music.utils.potoken\n\nimport android.webkit.CookieManager\nimport com.metrolist.music.utils.cipher.CipherDeobfuscator\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport kotlinx.coroutines.withContext\nimport timber.log.Timber\n\nclass PoTokenGenerator {\n    private val TAG = \"PoTokenGenerator\"\n\n    private val webViewSupported by lazy { runCatching { CookieManager.getInstance() }.isSuccess }\n    private var webViewBadImpl = false // whether the system has a bad WebView implementation\n\n    private val webPoTokenGenLock = Mutex()\n    private var webPoTokenSessionId: String? = null\n    private var webPoTokenStreamingPot: String? = null\n    private var webPoTokenGenerator: PoTokenWebView? = null\n\n    fun getWebClientPoToken(videoId: String, sessionId: String): PoTokenResult? {\n        Timber.tag(TAG).d(\"getWebClientPoToken called: videoId=$videoId, sessionId=$sessionId\")\n        Timber.tag(TAG).d(\"WebView state: supported=$webViewSupported, badImpl=$webViewBadImpl\")\n        if (!webViewSupported || webViewBadImpl) {\n            Timber.tag(TAG).d(\"WebView not available: supported=$webViewSupported, badImpl=$webViewBadImpl\")\n            return null\n        }\n\n        return try {\n            Timber.tag(TAG).d(\"Calling runBlocking to generate poToken...\")\n            runBlocking { getWebClientPoToken(videoId, sessionId, forceRecreate = false) }\n        } catch (e: Exception) {\n            Timber.tag(TAG).e(e, \"poToken generation exception: ${e.javaClass.simpleName}: ${e.message}\")\n            when (e) {\n                is BadWebViewException -> {\n                    Timber.tag(TAG).e(e, \"Could not obtain poToken because WebView is broken\")\n                    webViewBadImpl = true\n                    null\n                }\n                else -> throw e // includes PoTokenException\n            }\n        }\n    }\n\n    /**\n     * @param forceRecreate whether to force the recreation of [webPoTokenGenerator], to be used in\n     * case the current [webPoTokenGenerator] threw an error last time\n     * [PoTokenWebView.generatePoToken] was called\n     */\n    private suspend fun getWebClientPoToken(videoId: String, sessionId: String, forceRecreate: Boolean): PoTokenResult {\n        Timber.tag(TAG).d(\"Web poToken requested: videoId=$videoId, sessionId=$sessionId\")\n\n        val (poTokenGenerator, streamingPot, hasBeenRecreated) =\n            webPoTokenGenLock.withLock {\n                val shouldRecreate =\n                    forceRecreate || webPoTokenGenerator == null || webPoTokenGenerator!!.isExpired || webPoTokenSessionId != sessionId\n\n                if (shouldRecreate) {\n                    Timber.tag(TAG).d(\"Creating new PoTokenWebView (forceRecreate=$forceRecreate)\")\n                    webPoTokenSessionId = sessionId\n\n                    withContext(Dispatchers.Main) {\n                        webPoTokenGenerator?.close()\n                    }\n\n                    // create a new webPoTokenGenerator\n                    webPoTokenGenerator = PoTokenWebView.getNewPoTokenGenerator(CipherDeobfuscator.appContext)\n\n                    // The streaming poToken needs to be generated exactly once before generating\n                    // any other (player) tokens.\n                    webPoTokenStreamingPot = webPoTokenGenerator!!.generatePoToken(webPoTokenSessionId!!)\n                    Timber.tag(TAG).d(\"Streaming poToken generated for sessionId=${webPoTokenSessionId?.take(20)}...\")\n                }\n\n                Triple(webPoTokenGenerator!!, webPoTokenStreamingPot!!, shouldRecreate)\n            }\n\n        val playerPot = try {\n            poTokenGenerator.generatePoToken(videoId)\n        } catch (throwable: Throwable) {\n            if (hasBeenRecreated) {\n                // the poTokenGenerator has just been recreated (and possibly this is already the\n                // second time we try), so there is likely nothing we can do\n                throw throwable\n            } else {\n                // retry, this time recreating the [webPoTokenGenerator] from scratch;\n                // this might happen for example if the app goes in the background and the WebView\n                // content is lost\n                Timber.tag(TAG).e(throwable, \"Failed to obtain poToken, retrying\")\n                return getWebClientPoToken(videoId = videoId, sessionId = sessionId, forceRecreate = true)\n            }\n        }\n\n        Timber.tag(TAG).d(\"poToken generated successfully: player=${playerPot.take(20)}..., streaming=${streamingPot.take(20)}...\")\n\n        return PoTokenResult(playerPot, streamingPot)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/potoken/PoTokenResult.kt",
    "content": "package com.metrolist.music.utils.potoken\n\nclass PoTokenResult(\n    val playerRequestPoToken: String,\n    val streamingDataPoToken: String,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/potoken/PoTokenWebView.kt",
    "content": "package com.metrolist.music.utils.potoken\n\nimport android.content.Context\nimport android.webkit.ConsoleMessage\nimport android.webkit.JavascriptInterface\nimport android.webkit.WebChromeClient\nimport android.webkit.WebView\nimport androidx.annotation.MainThread\nimport androidx.collection.ArrayMap\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.music.BuildConfig\nimport kotlinx.coroutines.CoroutineExceptionHandler\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.MainScope\nimport kotlinx.coroutines.cancel\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.suspendCancellableCoroutine\nimport kotlinx.coroutines.withContext\nimport okhttp3.Headers.Companion.toHeaders\nimport okhttp3.OkHttpClient\nimport okhttp3.RequestBody.Companion.toRequestBody\nimport timber.log.Timber\nimport java.time.Instant\nimport java.time.temporal.ChronoUnit\nimport java.util.Collections\nimport kotlin.coroutines.Continuation\nimport kotlin.coroutines.resume\nimport kotlin.coroutines.resumeWithException\n\nclass PoTokenWebView private constructor(\n    context: Context,\n    // to be used exactly once only during initialization!\n    private val continuation: Continuation<PoTokenWebView>,\n) {\n    private val webView = WebView(context)\n    private val scope = MainScope()\n    private val poTokenContinuations =\n        Collections.synchronizedMap(ArrayMap<String, Continuation<String>>())\n    private val exceptionHandler = CoroutineExceptionHandler { _, t ->\n        onInitializationErrorCloseAndCancel(t)\n    }\n    private lateinit var expirationInstant: Instant\n\n    //region Initialization\n    init {\n        val webViewSettings = webView.settings\n        //noinspection SetJavaScriptEnabled we want to use JavaScript!\n        webViewSettings.javaScriptEnabled = true\n        webViewSettings.userAgentString = USER_AGENT\n        webViewSettings.blockNetworkLoads = true // the WebView does not need internet access\n\n        // so that we can run async functions and get back the result\n        webView.addJavascriptInterface(this, JS_INTERFACE)\n\n        webView.webChromeClient = object : WebChromeClient() {\n            override fun onConsoleMessage(m: ConsoleMessage): Boolean {\n                val msg = m.message()\n                // Log all console messages for debugging\n                when (m.messageLevel()) {\n                    ConsoleMessage.MessageLevel.ERROR -> Timber.tag(TAG).e(\"JS: $msg\")\n                    ConsoleMessage.MessageLevel.WARNING -> Timber.tag(TAG).w(\"JS: $msg\")\n                    else -> Timber.tag(TAG).d(\"JS: $msg\")\n                }\n\n                if (msg.contains(\"Uncaught\")) {\n                    val fmt = \"\\\"$msg\\\", source: ${m.sourceId()} (${m.lineNumber()})\"\n                    val exception = BadWebViewException(fmt)\n                    Timber.tag(TAG).e(\"This WebView implementation is broken: $fmt\")\n\n                    onInitializationErrorCloseAndCancel(exception)\n                    popAllPoTokenContinuations().forEach { (_, cont) -> cont.resumeWithException(exception) }\n                }\n                return super.onConsoleMessage(m)\n            }\n        }\n    }\n\n    /**\n     * Must be called right after instantiating [PoTokenWebView] to perform the actual\n     * initialization. This will asynchronously go through all the steps needed to load BotGuard,\n     * run it, and obtain an `integrityToken`.\n     */\n    private fun loadHtmlAndObtainBotguard() {\n        Timber.tag(TAG).d(\"loadHtmlAndObtainBotguard() called\")\n\n        scope.launch(exceptionHandler) {\n            val html = withContext(Dispatchers.IO) {\n                webView.context.assets.open(\"po_token.html\").bufferedReader().use { it.readText() }\n            }\n\n            // calls downloadAndRunBotguard() when the page has finished loading\n            val data = html.replaceFirst(\"</script>\", \"\\n$JS_INTERFACE.downloadAndRunBotguard()</script>\")\n            webView.loadDataWithBaseURL(\"https://www.youtube.com\", data, \"text/html\", \"utf-8\", null)\n        }\n    }\n\n    /**\n     * Called during initialization by the JavaScript snippet appended to the HTML page content in\n     * [loadHtmlAndObtainBotguard] after the WebView content has been loaded.\n     */\n    @JavascriptInterface\n    fun downloadAndRunBotguard() {\n        Timber.tag(TAG).d(\"downloadAndRunBotguard() called\")\n\n        makeBotguardServiceRequest(\n            \"https://www.youtube.com/api/jnn/v1/Create\",\n            \"[ \\\"$REQUEST_KEY\\\" ]\",\n        ) { responseBody ->\n            val parsedChallengeData = parseChallengeData(responseBody)\n            webView.evaluateJavascript(\n                \"\"\"try {\n                    data = $parsedChallengeData\n                    runBotGuard(data).then(function (result) {\n                        this.webPoSignalOutput = result.webPoSignalOutput\n                        $JS_INTERFACE.onRunBotguardResult(result.botguardResponse)\n                    }, function (error) {\n                        $JS_INTERFACE.onJsInitializationError(error + \"\\n\" + error.stack)\n                    })\n                } catch (error) {\n                    $JS_INTERFACE.onJsInitializationError(error + \"\\n\" + error.stack)\n                }\"\"\",\n                null\n            )\n        }\n    }\n\n    /**\n     * Called during initialization by the JavaScript snippets from either\n     * [downloadAndRunBotguard] or [onRunBotguardResult].\n     */\n    @JavascriptInterface\n    fun onJsInitializationError(error: String) {\n        if (BuildConfig.DEBUG) {\n            Timber.tag(TAG).e(\"Initialization error from JavaScript: $error\")\n        }\n        onInitializationErrorCloseAndCancel(buildExceptionForJsError(error))\n    }\n\n    /**\n     * Called during initialization by the JavaScript snippet from [downloadAndRunBotguard] after\n     * obtaining the BotGuard execution output [botguardResponse].\n     */\n    @JavascriptInterface\n    fun onRunBotguardResult(botguardResponse: String) {\n        Timber.tag(TAG).d(\"botguardResponse: $botguardResponse\")\n        makeBotguardServiceRequest(\n            \"https://www.youtube.com/api/jnn/v1/GenerateIT\",\n            \"[ \\\"$REQUEST_KEY\\\", \\\"$botguardResponse\\\" ]\",\n        ) { responseBody ->\n            Timber.tag(TAG).d(\"GenerateIT response: $responseBody\")\n            try {\n                val (integrityToken, expirationTimeInSeconds) = parseIntegrityTokenData(responseBody)\n                Timber.tag(TAG).d(\"Parsed integrityToken (${integrityToken.take(50)}...), expires in $expirationTimeInSeconds sec\")\n\n                // leave 10 minutes of margin just to be sure\n                expirationInstant = Instant.now().plusSeconds(expirationTimeInSeconds).minus(10, ChronoUnit.MINUTES)\n\n                // Store integrityToken and create the minter callback ONCE\n                // NOTE: createPoTokenMinter is now async, so we use .then()\n                Timber.tag(TAG).d(\"Evaluating createPoTokenMinter JavaScript...\")\n                webView.evaluateJavascript(\n                    \"\"\"try {\n                        console.log('[JS] Setting integrityToken and calling createPoTokenMinter...');\n                        this.integrityToken = $integrityToken\n                        console.log('[JS] integrityToken set, now calling createPoTokenMinter...');\n                        createPoTokenMinter(webPoSignalOutput, integrityToken).then(function() {\n                            console.log('[JS] createPoTokenMinter .then() resolved!');\n                            $JS_INTERFACE.onMinterCreated()\n                        }).catch(function(error) {\n                            console.log('[JS] createPoTokenMinter .catch() error: ' + error);\n                            $JS_INTERFACE.onJsInitializationError(error + \"\\n\" + (error.stack || ''))\n                        })\n                    } catch (error) {\n                        console.log('[JS] createPoTokenMinter SYNC error: ' + error);\n                        $JS_INTERFACE.onJsInitializationError(error + \"\\n\" + error.stack)\n                    }\"\"\",\n                    null\n                )\n            } catch (e: Exception) {\n                Timber.tag(TAG).e(e, \"Failed to parse integrity token data: ${e.message}\")\n                onInitializationErrorCloseAndCancel(PoTokenException(\"parseIntegrityTokenData failed: ${e.message}\"))\n            }\n        }\n    }\n    /**\n     * Called during initialization after the poToken minter has been created successfully.\n     */\n    @JavascriptInterface\n    fun onMinterCreated() {\n        Timber.tag(TAG).d(\"poToken minter created successfully, initialization complete\")\n        continuation.resume(this)\n    }\n    //endregion\n\n    //region Obtaining poTokens\n    suspend fun generatePoToken(identifier: String): String {\n        return withContext(Dispatchers.Main) {\n            suspendCancellableCoroutine { cont ->\n                Timber.tag(TAG).d(\"generatePoToken() called with identifier $identifier\")\n                addPoTokenEmitter(identifier, cont)\n                // NOTE: obtainPoToken is now async, so we use .then()\n                webView.evaluateJavascript(\n                    \"\"\"try {\n                        identifier = \"$identifier\"\n                        u8Identifier = ${stringToU8(identifier)}\n                        obtainPoToken(u8Identifier).then(function(poTokenU8) {\n                            poTokenU8String = poTokenU8.join(\",\")\n                            $JS_INTERFACE.onObtainPoTokenResult(identifier, poTokenU8String)\n                        }).catch(function(error) {\n                            $JS_INTERFACE.onObtainPoTokenError(identifier, error + \"\\n\" + (error.stack || ''))\n                        })\n                    } catch (error) {\n                        $JS_INTERFACE.onObtainPoTokenError(identifier, error + \"\\n\" + error.stack)\n                    }\"\"\",\n                    null\n                )\n            }\n        }\n    }\n\n    /**\n     * Called by the JavaScript snippet from [generatePoToken] when an error occurs in calling the\n     * JavaScript `obtainPoToken()` function.\n     */\n    @JavascriptInterface\n    fun onObtainPoTokenError(identifier: String, error: String) {\n        if (BuildConfig.DEBUG) {\n            Timber.tag(TAG).e(\"obtainPoToken error from JavaScript: $error\")\n        }\n        popPoTokenContinuation(identifier)?.resumeWithException(buildExceptionForJsError(error))\n    }\n\n    /**\n     * Called by the JavaScript snippet from [generatePoToken] with the original identifier and the\n     * result of the JavaScript `obtainPoToken()` function.\n     */\n    @JavascriptInterface\n    fun onObtainPoTokenResult(identifier: String, poTokenU8: String) {\n        Timber.tag(TAG).d(\"Generated poToken (before decoding): identifier=$identifier poTokenU8=$poTokenU8\")\n        val poToken = try {\n            u8ToBase64(poTokenU8)\n        } catch (t: Throwable) {\n            popPoTokenContinuation(identifier)?.resumeWithException(t)\n            return\n        }\n\n        Timber.tag(TAG).d(\"Generated poToken: identifier=$identifier poToken=$poToken\")\n        popPoTokenContinuation(identifier)?.resume(poToken)\n    }\n\n    val isExpired: Boolean\n        get() = Instant.now().isAfter(expirationInstant)\n    //endregion\n\n    //region Handling multiple emitters\n    private fun addPoTokenEmitter(identifier: String, continuation: Continuation<String>) {\n        poTokenContinuations[identifier] = continuation\n    }\n\n    private fun popPoTokenContinuation(identifier: String): Continuation<String>? {\n        return poTokenContinuations.remove(identifier)\n    }\n\n    private fun popAllPoTokenContinuations(): Map<String, Continuation<String>> {\n        val result = poTokenContinuations.toMap()\n        poTokenContinuations.clear()\n        return result\n    }\n    //endregion\n\n    //region Utils\n    private fun makeBotguardServiceRequest(\n        url: String,\n        data: String,\n        handleResponseBody: (String) -> Unit,\n    ) {\n        scope.launch(exceptionHandler) {\n            val requestBuilder = okhttp3.Request.Builder()\n                .post(data.toRequestBody())\n                .headers(mapOf(\n                    \"User-Agent\" to USER_AGENT,\n                    \"Accept\" to \"application/json\",\n                    \"Content-Type\" to \"application/json+protobuf\",\n                    \"x-goog-api-key\" to GOOGLE_API_KEY,\n                    \"x-user-agent\" to \"grpc-web-javascript/0.1\",\n                ).toHeaders())\n                .url(url)\n            val response = withContext(Dispatchers.IO) {\n                httpClient.newCall(requestBuilder.build()).execute()\n            }\n            val httpCode = response.code\n            if (httpCode != 200) {\n                onInitializationErrorCloseAndCancel(PoTokenException(\"Invalid response code: $httpCode\"))\n            } else {\n                val body = withContext(Dispatchers.IO) {\n                    response.body!!.string()\n                }\n                handleResponseBody(body)\n            }\n        }\n    }\n\n    private fun onInitializationErrorCloseAndCancel(error: Throwable) {\n        close()\n        continuation.resumeWithException(error)\n    }\n\n    @MainThread\n    fun close() {\n        scope.cancel()\n\n        webView.clearHistory()\n        webView.clearCache(true)\n\n        webView.loadUrl(\"about:blank\")\n\n        webView.onPause()\n        webView.removeAllViews()\n        webView.destroy()\n    }\n    //endregion\n\n    companion object {\n        private const val TAG = \"PoTokenWebView\"\n        private const val GOOGLE_API_KEY = \"AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw\"\n        private const val REQUEST_KEY = \"O43z0dpjhgX20SCx4KAo\"\n        private const val USER_AGENT = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) \" +\n                \"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.3\"\n        private const val JS_INTERFACE = \"PoTokenWebView\"\n\n        private val httpClient = OkHttpClient.Builder()\n            .proxy(YouTube.proxy)\n            .build()\n\n        suspend fun getNewPoTokenGenerator(context: Context): PoTokenWebView {\n            return withContext(Dispatchers.Main) {\n                suspendCancellableCoroutine { cont ->\n                    val potWv = PoTokenWebView(context, cont)\n                    potWv.loadHtmlAndObtainBotguard()\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/sabr/EjsNTransformSolver.kt",
    "content": "package com.metrolist.music.utils.sabr\n\nimport android.content.Context\nimport android.net.Uri\nimport android.webkit.ConsoleMessage\nimport android.webkit.JavascriptInterface\nimport android.webkit.WebChromeClient\nimport android.webkit.WebView\nimport com.metrolist.music.utils.cipher.CipherDeobfuscator\nimport com.metrolist.music.utils.cipher.PlayerJsFetcher\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.NonCancellable\nimport kotlinx.coroutines.suspendCancellableCoroutine\nimport kotlinx.coroutines.withContext\nimport timber.log.Timber\nimport java.io.File\nimport kotlin.coroutines.Continuation\nimport kotlin.coroutines.resume\nimport kotlin.coroutines.resumeWithException\n\n/**\n * Standalone EJS-based n-parameter transform solver for SABR URLs.\n *\n * Uses the same AST-based approach as yt-dlp's EJS solver (meriyah + astring +\n * yt.solver.core.js) to reliably extract and execute the n-transform function\n * from the YouTube player JS.\n */\nobject EjsNTransformSolver {\n    private const val TAG = \"Metrolist_EjsNSolver\"\n\n    private var solverWebView: SolverWebView? = null\n\n    /**\n     * Transform the 'n' parameter in a SABR streaming URL.\n     * Returns the URL with the transformed 'n' value, or the original URL if transform fails.\n     */\n    suspend fun transformNParamInUrl(url: String): String {\n        val nMatch = Regex(\"[?&]n=([^&]+)\").find(url)\n        if (nMatch == null) {\n            Timber.tag(TAG).d(\"No 'n' parameter in SABR URL\")\n            return url\n        }\n        val nValue = Uri.decode(nMatch.groupValues[1])\n        Timber.tag(TAG).d(\"SABR n-param: $nValue\")\n\n        return withContext(NonCancellable) {\n            val solver = getOrCreateSolver()\n            if (solver == null) {\n                return@withContext url\n            }\n\n            if (!solver.nFunctionAvailable) {\n                Timber.tag(TAG).e(\"EJS n-solver not available\")\n                return@withContext url\n            }\n\n            try {\n                val transformed = solver.transformN(nValue)\n                Timber.tag(TAG).d(\"SABR n-param transformed: $nValue -> $transformed\")\n                url.replaceFirst(\n                    Regex(\"([?&])n=[^&]+\"),\n                    \"$1n=${Uri.encode(transformed)}\"\n                )\n            } catch (e: Exception) {\n                Timber.tag(TAG).e(e, \"SABR n-transform failed: ${e.message}\")\n                url\n            }\n        }\n    }\n\n    private suspend fun getOrCreateSolver(): SolverWebView? {\n        solverWebView?.let { return it }\n\n        return withContext(NonCancellable) {\n            solverWebView?.let { return@withContext it }\n\n            val result = PlayerJsFetcher.getPlayerJs(forceRefresh = false)\n            if (result == null) {\n                Timber.tag(TAG).e(\"Failed to get player JS for EJS solver\")\n                return@withContext null\n            }\n            val (playerJs, hash) = result\n            Timber.tag(TAG).d(\"Creating EJS n-solver (player hash=$hash, ${playerJs.length} chars)\")\n\n            try {\n                val sv = SolverWebView.create(CipherDeobfuscator.appContext, playerJs)\n                solverWebView = sv\n                sv\n            } catch (e: Exception) {\n                Timber.tag(TAG).e(e, \"Failed to create EJS solver: ${e.message}\")\n                null\n            }\n        }\n    }\n\n    suspend fun close() {\n        withContext(Dispatchers.Main) {\n            solverWebView?.close()\n        }\n        solverWebView = null\n    }\n\n    class SolverWebView private constructor(\n        context: Context,\n        private val playerJs: String,\n        private val initContinuation: Continuation<SolverWebView>,\n    ) {\n        private val webView = WebView(context)\n        private var nContinuation: Continuation<String>? = null\n\n        @Volatile\n        var nFunctionAvailable: Boolean = false\n            private set\n\n        init {\n            val settings = webView.settings\n            @Suppress(\"SetJavaScriptEnabled\")\n            settings.javaScriptEnabled = true\n            settings.allowFileAccess = true\n            @Suppress(\"DEPRECATION\")\n            settings.allowFileAccessFromFileURLs = true\n            settings.blockNetworkLoads = true\n\n            webView.addJavascriptInterface(this, \"EjsBridge\")\n\n            webView.webChromeClient = object : WebChromeClient() {\n                override fun onConsoleMessage(m: ConsoleMessage): Boolean {\n                    val msg = m.message()\n                    if (msg.contains(\"Uncaught\")) {\n                        Timber.tag(TAG).e(\"EJS WebView error: $msg at ${m.sourceId()}:${m.lineNumber()}\")\n                    }\n                    return super.onConsoleMessage(m)\n                }\n            }\n        }\n\n        private fun loadSolverAndPlayer() {\n            val cacheDir = File(webView.context.cacheDir, \"ejs_solver\")\n            cacheDir.mkdirs()\n\n            val assetManager = webView.context.assets\n            for (file in listOf(\"meriyah.js\", \"astring.js\", \"yt.solver.core.js\")) {\n                assetManager.open(\"solver/$file\").use { input ->\n                    File(cacheDir, file).outputStream().use { output ->\n                        input.copyTo(output)\n                    }\n                }\n            }\n\n            File(cacheDir, \"player.js\").writeText(playerJs)\n            Timber.tag(TAG).d(\"EJS solver assets written (${playerJs.length} chars)\")\n\n            val html = \"\"\"<!DOCTYPE html>\n<html><head>\n<script src=\"meriyah.js\"></script>\n<script src=\"astring.js\"></script>\n<script src=\"yt.solver.core.js\"></script>\n<script>\nvar _nSolver = null;\n\nfunction initSolver() {\n    try {\n        EjsBridge.onLog('Reading player.js via XHR...');\n        var xhr = new XMLHttpRequest();\n        xhr.open('GET', 'player.js', true);\n        xhr.onload = function() {\n            try {\n                var playerCode = xhr.responseText;\n                if (!playerCode || playerCode.length < 1000) {\n                    EjsBridge.onSolverError('Player JS too small: ' + (playerCode ? playerCode.length : 0));\n                    return;\n                }\n                EjsBridge.onLog('Preprocessing with EJS solver (' + playerCode.length + ' chars)...');\n\n                var result = jsc({\n                    type: 'player',\n                    player: playerCode,\n                    requests: [],\n                    output_preprocessed: true\n                });\n\n                if (result.type === 'error') {\n                    EjsBridge.onSolverError('EJS preprocess: ' + result.error);\n                    return;\n                }\n\n                EjsBridge.onLog('Evaluating preprocessed code...');\n                var resultObj = {n: null, sig: null};\n                (new Function('_result', result.preprocessed_player))(resultObj);\n\n                _nSolver = resultObj.n;\n                EjsBridge.onSolverReady((typeof _nSolver === 'function').toString());\n            } catch(e) {\n                EjsBridge.onSolverError(e.toString() + '\\n' + (e.stack || ''));\n            }\n        };\n        xhr.onerror = function() {\n            EjsBridge.onSolverError('XHR failed to read player.js');\n        };\n        xhr.send();\n    } catch(e) {\n        EjsBridge.onSolverError(e.toString() + '\\n' + (e.stack || ''));\n    }\n}\n\nfunction transformN(nValue) {\n    try {\n        if (!_nSolver) {\n            EjsBridge.onNError('N solver not available');\n            return;\n        }\n        var result = _nSolver(nValue);\n        if (result === undefined || result === null) {\n            EjsBridge.onNError('N solver returned null/undefined');\n            return;\n        }\n        EjsBridge.onNResult(String(result));\n    } catch(e) {\n        EjsBridge.onNError(e.toString() + '\\n' + (e.stack || ''));\n    }\n}\n</script>\n</head><body onload=\"initSolver()\"></body></html>\"\"\"\n\n            webView.loadDataWithBaseURL(\n                \"file://${cacheDir.absolutePath}/\",\n                html, \"text/html\", \"utf-8\", null\n            )\n        }\n\n        @JavascriptInterface\n        fun onLog(message: String) {\n            Timber.tag(TAG).d(message)\n        }\n\n        @JavascriptInterface\n        fun onSolverReady(nAvailable: String) {\n            nFunctionAvailable = nAvailable == \"true\"\n            Timber.tag(TAG).d(\"EJS solver ready: n=$nFunctionAvailable\")\n            initContinuation.resume(this)\n        }\n\n        @JavascriptInterface\n        fun onSolverError(error: String) {\n            Timber.tag(TAG).e(\"EJS solver error: $error\")\n            initContinuation.resume(this)\n        }\n\n        suspend fun transformN(nValue: String): String {\n            if (!nFunctionAvailable) {\n                throw SabrException(\"EJS n-transform not available\")\n            }\n\n            return withContext(Dispatchers.Main) {\n                suspendCancellableCoroutine { cont ->\n                    nContinuation = cont\n                    webView.evaluateJavascript(\n                        \"transformN('${escapeJsString(nValue)}')\",\n                        null\n                    )\n                }\n            }\n        }\n\n        @JavascriptInterface\n        fun onNResult(result: String) {\n            Timber.tag(TAG).d(\"N-transform result: ${result.take(50)}\")\n            nContinuation?.resume(result)\n            nContinuation = null\n        }\n\n        @JavascriptInterface\n        fun onNError(error: String) {\n            Timber.tag(TAG).e(\"N-transform error: $error\")\n            nContinuation?.resumeWithException(SabrException(\"EJS n-transform failed: $error\"))\n            nContinuation = null\n        }\n\n        fun close() {\n            webView.clearHistory()\n            webView.clearCache(true)\n            webView.loadUrl(\"about:blank\")\n            webView.onPause()\n            webView.removeAllViews()\n            webView.destroy()\n            Timber.tag(TAG).d(\"EJS solver WebView closed\")\n        }\n\n        private fun escapeJsString(s: String): String {\n            return s.replace(\"\\\\\", \"\\\\\\\\\")\n                .replace(\"'\", \"\\\\'\")\n                .replace(\"\\n\", \"\\\\n\")\n                .replace(\"\\r\", \"\\\\r\")\n        }\n\n        companion object {\n            suspend fun create(context: Context, playerJs: String): SolverWebView {\n                return withContext(Dispatchers.Main) {\n                    suspendCancellableCoroutine { cont ->\n                        val sv = SolverWebView(context, playerJs, cont)\n                        sv.loadSolverAndPlayer()\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/utils/sabr/SabrException.kt",
    "content": "package com.metrolist.music.utils.sabr\n\nclass SabrException(message: String, cause: Throwable? = null) : Exception(message, cause)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/AccountSettingsViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport android.content.Context\nimport android.content.Intent\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.music.App\nimport com.metrolist.music.constants.AccountChannelHandleKey\nimport com.metrolist.music.constants.AccountEmailKey\nimport com.metrolist.music.constants.AccountNameKey\nimport com.metrolist.music.constants.DataSyncIdKey\nimport com.metrolist.music.constants.InnerTubeCookieKey\nimport com.metrolist.music.constants.VisitorDataKey\nimport com.metrolist.music.utils.SyncUtils\nimport com.metrolist.music.utils.dataStore\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport timber.log.Timber\nimport javax.inject.Inject\nimport androidx.datastore.preferences.core.edit\n\n@HiltViewModel\nclass AccountSettingsViewModel @Inject constructor(\n    private val syncUtils: SyncUtils,\n) : ViewModel() {\n\n    /**\n     * Logout user and clear all synced content to prevent data mixing between accounts\n     */\n    fun logoutAndClearSyncedContent(context: Context, onCookieChange: (String) -> Unit) {\n        viewModelScope.launch(Dispatchers.IO) {\n            // Clear all YouTube Music synced content first\n            syncUtils.clearAllSyncedContent()\n\n            // Then clear account preferences\n            App.forgetAccount(context)\n\n            // Clear cookie in UI\n            onCookieChange(\"\")\n        }\n    }\n\n    /**\n     * Clear all library data including songs, albums, artists, playlists, podcasts.\n     */\n    suspend fun clearAllLibraryData() {\n        Timber.d(\"[LOGOUT_CLEAR] ViewModel: clearAllLibraryData called\")\n        syncUtils.clearAllLibraryData()\n        Timber.d(\"[LOGOUT_CLEAR] ViewModel: clearAllLibraryData completed\")\n    }\n\n    /**\n     * Just logout without clearing library data\n     */\n    suspend fun logoutKeepData(context: Context, onCookieChange: (String) -> Unit) {\n        Timber.d(\"[LOGOUT_KEEP] ViewModel: logoutKeepData called\")\n        withContext(Dispatchers.IO) {\n            App.forgetAccount(context)\n        }\n        Timber.d(\"[LOGOUT_KEEP] ViewModel: Account forgotten, clearing cookie in UI\")\n        onCookieChange(\"\")\n    }\n\n    /**\n     * Save token credentials atomically to DataStore, then restart the app.\n     * This ensures all writes complete before the process is killed,\n     * preventing the race condition where Runtime.exit(0) kills the process\n     * before async DataStore coroutines finish writing.\n     */\n    fun saveTokenAndRestart(\n        context: Context,\n        cookie: String,\n        visitorData: String,\n        dataSyncId: String,\n        accountName: String,\n        accountEmail: String,\n        accountChannelHandle: String,\n    ) {\n        viewModelScope.launch(Dispatchers.IO) {\n            context.dataStore.edit { settings ->\n                settings[InnerTubeCookieKey] = cookie\n                settings[VisitorDataKey] = visitorData\n                settings[DataSyncIdKey] = dataSyncId\n                settings[AccountNameKey] = accountName\n                settings[AccountEmailKey] = accountEmail\n                settings[AccountChannelHandleKey] = accountChannelHandle\n            }\n            withContext(Dispatchers.Main) {\n                val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)\n                intent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)\n                context.startActivity(intent)\n                Runtime.getRuntime().exit(0)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/AccountViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport android.content.Context\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.ArtistItem\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.filterYoutubeShorts\nimport com.metrolist.innertube.utils.completed\nimport com.metrolist.music.constants.HideYoutubeShortsKey\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.db.entities.PodcastEntity\nimport com.metrolist.music.ui.utils.resize\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\nimport com.metrolist.music.utils.reportException\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\nenum class AccountContentType {\n    PLAYLISTS, ALBUMS, ARTISTS, PODCASTS\n}\n\n@HiltViewModel\nclass AccountViewModel @Inject constructor(\n    @ApplicationContext private val context: Context,\n    database: MusicDatabase,\n) : ViewModel() {\n    val playlists = MutableStateFlow<List<PlaylistItem>?>(null)\n    val albums = MutableStateFlow<List<AlbumItem>?>(null)\n    val artists = MutableStateFlow<List<ArtistItem>?>(null)\n    // SE \"Episodes for Later\" playlist shown in Podcasts tab\n    val sePlaylist = MutableStateFlow<PlaylistItem?>(null)\n    // RDPN \"New Episodes\" playlist (real thumbnail + count from YouTube)\n    val rdpnPlaylist = MutableStateFlow<PlaylistItem?>(null)\n    // Subscribed podcast shows (from local DB, synced from YT Music)\n    val podcastPlaylists = database.subscribedPodcasts()\n        .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n    // Podcast host channels from YT Music library\n    val podcastChannels = MutableStateFlow<List<ArtistItem>>(emptyList())\n\n    // Selected content type for chips\n    val selectedContentType = MutableStateFlow(AccountContentType.PLAYLISTS)\n\n    private suspend fun loadPlaylists() {\n        val hideYoutubeShorts = context.dataStore.get(HideYoutubeShortsKey, false)\n        YouTube.library(\"FEmusic_liked_playlists\").completed().onSuccess {\n            val all = it.items.filterIsInstance<PlaylistItem>()\n            // Extract SE playlist separately for Podcasts tab\n            sePlaylist.value = all.find { it.id == \"SE\" }\n            playlists.value = all\n                .filterNot { it.id == \"SE\" }\n                .filterYoutubeShorts(hideYoutubeShorts)\n        }.onFailure {\n            reportException(it)\n        }\n    }\n\n    init {\n        viewModelScope.launch {\n            loadPlaylists()\n            YouTube.library(\"FEmusic_liked_albums\").completed().onSuccess {\n                albums.value = it.items.filterIsInstance<AlbumItem>()\n            }.onFailure {\n                reportException(it)\n            }\n            YouTube.library(\"FEmusic_library_corpus_artists\").completed().onSuccess {\n                artists.value = it.items.filterIsInstance<ArtistItem>().map { artist ->\n                    artist.copy(\n                        thumbnail = artist.thumbnail?.resize(544, 544)\n                    )\n                }\n            }.onFailure {\n                reportException(it)\n            }\n        }\n        viewModelScope.launch {\n            YouTube.newEpisodesPlaylistInfo().onSuccess {\n                rdpnPlaylist.value = it\n            }.onFailure {\n                reportException(it)\n            }\n        }\n        viewModelScope.launch(Dispatchers.IO) {\n            YouTube.libraryPodcastChannels().onSuccess {\n                podcastChannels.value = it.items.filterIsInstance<ArtistItem>()\n            }.onFailure {\n                reportException(it)\n            }\n        }\n\n        // Listen for HideYoutubeShorts preference changes and reload playlists instantly\n        viewModelScope.launch(Dispatchers.IO) {\n            context.dataStore.data\n                .map { it[HideYoutubeShortsKey] ?: false }\n                .distinctUntilChanged()\n                .collect {\n                    if (playlists.value != null) {\n                        loadPlaylists()\n                    }\n                }\n        }\n    }\n\n    fun setSelectedContentType(contentType: AccountContentType) {\n        selectedContentType.value = contentType\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/AlbumViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.utils.reportException\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass AlbumViewModel\n@Inject\nconstructor(\n    database: MusicDatabase,\n    savedStateHandle: SavedStateHandle,\n) : ViewModel() {\n    val albumId = savedStateHandle.get<String>(\"albumId\")!!\n    val playlistId = MutableStateFlow(\"\")\n    val albumWithSongs =\n        database\n            .albumWithSongs(albumId)\n            .stateIn(viewModelScope, SharingStarted.Eagerly, null)\n    var otherVersions = MutableStateFlow<List<AlbumItem>>(emptyList())\n\n    init {\n        viewModelScope.launch {\n            val album = database.album(albumId).first()\n            YouTube\n                .album(albumId)\n                .onSuccess {\n                    playlistId.value = it.album.playlistId\n                    otherVersions.value = it.otherVersions\n                    database.transaction {\n                        if (album == null) {\n                            insert(it)\n                        } else {\n                            update(album.album, it, album.artists)\n                        }\n                    }\n                }.onFailure {\n                    reportException(it)\n                    if (it.message?.contains(\"NOT_FOUND\") == true) {\n                        database.query {\n                            album?.album?.let(::delete)\n                        }\n                    }\n                }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/ArtistAlbumsViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.music.db.MusicDatabase\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.stateIn\nimport javax.inject.Inject\n\n@HiltViewModel\nclass ArtistAlbumsViewModel @Inject constructor(\n    database: MusicDatabase,\n    savedStateHandle: SavedStateHandle,\n) : ViewModel() {\n    private val artistId = savedStateHandle.get<String>(\"artistId\")!!\n    val artist = database.artist(artistId)\n        .stateIn(viewModelScope, SharingStarted.Lazily, null)\n\n    val albums = database.artistAlbumsPreview(artistId)\n        .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/ArtistItemsViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport android.content.Context\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.BrowseEndpoint\nimport com.metrolist.innertube.models.filterExplicit\nimport com.metrolist.innertube.models.filterVideoSongs\nimport com.metrolist.music.constants.HideExplicitKey\nimport com.metrolist.music.constants.HideVideoSongsKey\nimport com.metrolist.music.models.ItemsPage\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\nimport com.metrolist.music.utils.reportException\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass ArtistItemsViewModel\n@Inject\nconstructor(\n    @ApplicationContext val context: Context,\n    savedStateHandle: SavedStateHandle,\n) : ViewModel() {\n    private val browseId = savedStateHandle.get<String>(\"browseId\")!!\n    private val params = savedStateHandle.get<String>(\"params\")\n\n    val title = MutableStateFlow(\"\")\n    val itemsPage = MutableStateFlow<ItemsPage?>(null)\n\n    init {\n        viewModelScope.launch {\n            YouTube\n                .artistItems(\n                    BrowseEndpoint(\n                        browseId = browseId,\n                        params = params,\n                    ),\n                ).onSuccess { artistItemsPage ->\n                    val hideExplicit = context.dataStore.get(HideExplicitKey, false)\n                    val hideVideoSongs = context.dataStore.get(HideVideoSongsKey, false)\n                    title.value = artistItemsPage.title\n                    itemsPage.value =\n                        ItemsPage(\n                            items = artistItemsPage.items\n                                .distinctBy { it.id }\n                                .filterExplicit(hideExplicit)\n                                .filterVideoSongs(hideVideoSongs),\n                            continuation = artistItemsPage.continuation,\n                        )\n                }.onFailure {\n                    reportException(it)\n                }\n        }\n    }\n\n    fun loadMore() {\n        viewModelScope.launch {\n            val oldItemsPage = itemsPage.value ?: return@launch\n            val continuation = oldItemsPage.continuation ?: return@launch\n            YouTube\n                .artistItemsContinuation(continuation)\n                .onSuccess { artistItemsContinuationPage ->\n                    val hideExplicit = context.dataStore.get(HideExplicitKey, false)\n                    val hideVideoSongs = context.dataStore.get(HideVideoSongsKey, false)\n                    itemsPage.update {\n                        ItemsPage(\n                            items =\n                            (oldItemsPage.items + artistItemsContinuationPage.items)\n                                .distinctBy { it.id }\n                                .filterExplicit(hideExplicit)\n                                .filterVideoSongs(hideVideoSongs),\n                            continuation = artistItemsContinuationPage.continuation,\n                        )\n                    }\n                }.onFailure {\n                    reportException(it)\n                }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/ArtistViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport android.content.Context\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.filterExplicit\nimport com.metrolist.innertube.models.filterVideoSongs\nimport com.metrolist.innertube.models.filterYoutubeShorts\nimport com.metrolist.innertube.pages.ArtistPage\nimport com.metrolist.music.constants.HideExplicitKey\nimport com.metrolist.music.constants.HideVideoSongsKey\nimport com.metrolist.music.constants.HideYoutubeShortsKey\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.db.entities.ArtistEntity\nimport com.metrolist.music.extensions.filterExplicit\nimport com.metrolist.music.extensions.filterExplicitAlbums\nimport com.metrolist.music.utils.SyncUtils\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\nimport com.metrolist.music.utils.reportException\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.launch\nimport timber.log.Timber\nimport javax.inject.Inject\nimport com.metrolist.music.extensions.filterVideoSongs as filterVideoSongsLocal\n\n@OptIn(ExperimentalCoroutinesApi::class)\n@HiltViewModel\nclass ArtistViewModel @Inject constructor(\n    @ApplicationContext private val context: Context,\n    private val database: MusicDatabase,\n    private val syncUtils: SyncUtils,\n    savedStateHandle: SavedStateHandle,\n) : ViewModel() {\n    val artistId = savedStateHandle.get<String>(\"artistId\")!!\n    private val isPodcastChannel = savedStateHandle.get<Boolean>(\"isPodcastChannel\") ?: false\n    var artistPage by mutableStateOf<ArtistPage?>(null)\n\n    // Track API subscription state separately\n    private val _apiSubscribed = MutableStateFlow<Boolean?>(null)\n\n    val libraryArtist = database.artist(artistId)\n        .stateIn(viewModelScope, SharingStarted.Lazily, null)\n\n    // Combine API state with local database state - local takes precedence when not logged in\n    val isChannelSubscribed = kotlinx.coroutines.flow.combine(\n        _apiSubscribed,\n        database.artist(artistId),\n    ) { apiState, localArtist ->\n        val locallyBookmarked = localArtist?.artist?.bookmarkedAt != null\n        locallyBookmarked || (apiState == true)\n    }.stateIn(viewModelScope, SharingStarted.Eagerly, false)\n    val librarySongs = context.dataStore.data\n        .map { (it[HideExplicitKey] ?: false) to (it[HideVideoSongsKey] ?: false) }\n        .distinctUntilChanged()\n        .flatMapLatest { (hideExplicit, hideVideoSongs) ->\n            database.artistSongsPreview(artistId).map { it.filterExplicit(hideExplicit).filterVideoSongsLocal(hideVideoSongs) }\n        }\n        .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n    val libraryAlbums = context.dataStore.data\n        .map { it[HideExplicitKey] ?: false }\n        .distinctUntilChanged()\n        .flatMapLatest { hideExplicit ->\n            database.artistAlbumsPreview(artistId).map { it.filterExplicitAlbums(hideExplicit) }\n        }\n        .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n\n    init {\n        // Load artist page and reload when hide explicit setting changes\n        viewModelScope.launch {\n            context.dataStore.data\n                .map {\n                    Triple(\n                        it[HideExplicitKey] ?: false,\n                        it[HideVideoSongsKey] ?: false,\n                        it[HideYoutubeShortsKey] ?: false\n                    )\n                }\n                .distinctUntilChanged()\n                .collect {\n                    fetchArtistsFromYTM()\n                }\n        }\n    }\n\n    fun fetchArtistsFromYTM() {\n        viewModelScope.launch {\n            val hideExplicit = context.dataStore.get(HideExplicitKey, false)\n            val hideVideoSongs = context.dataStore.get(HideVideoSongsKey, false)\n            val hideYoutubeShorts = context.dataStore.get(HideYoutubeShortsKey, false)\n            YouTube.artist(artistId)\n                .onSuccess { page ->\n                    val filteredSections = page.sections\n                        .map { section ->\n                            section.copy(items = section.items.filterExplicit(hideExplicit).filterVideoSongs(hideVideoSongs).filterYoutubeShorts(hideYoutubeShorts))\n                        }\n                        .filter { section -> section.items.isNotEmpty() }\n\n                    artistPage = page.copy(sections = filteredSections)\n                    // Store API subscription state\n                    _apiSubscribed.value = page.isSubscribed\n                }.onFailure {\n                    reportException(it)\n                }\n        }\n    }\n\n    fun toggleChannelSubscription() {\n        val channelId = artistPage?.artist?.channelId ?: artistId\n        val isCurrentlySubscribed = isChannelSubscribed.value\n        val shouldBeSubscribed = !isCurrentlySubscribed\n\n        Timber.d(\"[CHANNEL_TOGGLE] toggleChannelSubscription called: artistId=$artistId, channelId=$channelId, isCurrentlySubscribed=$isCurrentlySubscribed, shouldBeSubscribed=$shouldBeSubscribed\")\n\n        // Optimistically update API state for immediate UI feedback\n        _apiSubscribed.value = shouldBeSubscribed\n\n        viewModelScope.launch(Dispatchers.IO) {\n            Timber.d(\"[CHANNEL_TOGGLE] Inside coroutine, updating database...\")\n            // Update local database first (optimistic update)\n            // Call DAO methods directly - they're synchronous on IO dispatcher\n            val artist = libraryArtist.value?.artist\n            Timber.d(\"[CHANNEL_TOGGLE] libraryArtist.value?.artist = $artist\")\n            if (artist != null) {\n                val newBookmark = if (shouldBeSubscribed) {\n                    artist.bookmarkedAt ?: java.time.LocalDateTime.now()\n                } else {\n                    null\n                }\n                // Also set isPodcastChannel if subscribing from podcast context\n                val updatedArtist = artist.copy(\n                    bookmarkedAt = newBookmark,\n                    isPodcastChannel = if (shouldBeSubscribed && isPodcastChannel) true else artist.isPodcastChannel\n                )\n                Timber.d(\"[CHANNEL_TOGGLE] Updating existing artist: ${artist.id} -> bookmarkedAt=$newBookmark, isPodcastChannel=${updatedArtist.isPodcastChannel}\")\n                database.update(updatedArtist)\n            } else if (shouldBeSubscribed) {\n                Timber.d(\"[CHANNEL_TOGGLE] No existing artist, inserting new one\")\n                artistPage?.artist?.let {\n                    database.insert(\n                        ArtistEntity(\n                            id = artistId,\n                            name = it.title,\n                            channelId = it.channelId,\n                            thumbnailUrl = it.thumbnail,\n                            bookmarkedAt = java.time.LocalDateTime.now(),\n                            isPodcastChannel = isPodcastChannel,\n                        )\n                    )\n                    Timber.d(\"[CHANNEL_TOGGLE] Inserted new artist: $artistId, isPodcastChannel=$isPodcastChannel\")\n                } ?: Timber.d(\"[CHANNEL_TOGGLE] artistPage?.artist is null, cannot insert\")\n            } else {\n                Timber.d(\"[CHANNEL_TOGGLE] No artist and shouldBeSubscribed=false, nothing to do\")\n            }\n\n            Timber.d(\"[CHANNEL_TOGGLE] Calling syncUtils.subscribeChannel($channelId, $shouldBeSubscribed)\")\n            // Sync with YouTube (handles login check internally)\n            syncUtils.subscribeChannel(channelId, shouldBeSubscribed)\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/AutoPlaylistViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport android.content.Context\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.music.constants.HideExplicitKey\nimport com.metrolist.music.constants.HideVideoSongsKey\nimport com.metrolist.music.constants.SongSortDescendingKey\nimport com.metrolist.music.constants.SongSortType\nimport com.metrolist.music.constants.SongSortTypeKey\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.extensions.filterExplicit\nimport com.metrolist.music.extensions.filterVideoSongs\nimport com.metrolist.music.extensions.toEnum\nimport com.metrolist.music.utils.SyncUtils\nimport com.metrolist.music.utils.dataStore\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@OptIn(ExperimentalCoroutinesApi::class)\n@HiltViewModel\nclass AutoPlaylistViewModel\n@Inject\nconstructor(\n    @ApplicationContext context: Context,\n    private val database: MusicDatabase,\n    savedStateHandle: SavedStateHandle,\n    private val syncUtils: SyncUtils,\n) : ViewModel() {\n    val playlist = savedStateHandle.get<String>(\"playlist\")!!\n\n    private val _isRefreshing = MutableStateFlow(false)\n    val isRefreshing = _isRefreshing.asStateFlow()\n\n    @OptIn(ExperimentalCoroutinesApi::class)\n    val likedSongs =\n        context.dataStore.data\n            .map {\n                Triple(\n                    it[SongSortTypeKey].toEnum(SongSortType.CREATE_DATE) to (it[SongSortDescendingKey]\n                        ?: true),\n                    it[HideExplicitKey] ?: false,\n                    it[HideVideoSongsKey] ?: false\n                )\n            }\n            .distinctUntilChanged()\n            .flatMapLatest { (sortDesc, hideExplicit, hideVideoSongs) ->\n                val (sortType, descending) = sortDesc\n                when (playlist) {\n                    \"liked\" -> database.likedSongs(sortType, descending)\n                        .map { it.filterExplicit(hideExplicit).filterVideoSongs(hideVideoSongs) }\n\n                    \"downloaded\" -> database.downloadedSongs(sortType, descending)\n                        .map { it.filterExplicit(hideExplicit).filterVideoSongs(hideVideoSongs) }\n\n                    \"uploaded\" -> database.uploadedSongs(sortType, descending)\n                        .map { it.filterExplicit(hideExplicit).filterVideoSongs(hideVideoSongs) }\n\n                    else -> kotlinx.coroutines.flow.flowOf(emptyList())\n                }\n            }\n            .stateIn(viewModelScope, kotlinx.coroutines.flow.SharingStarted.Lazily, emptyList())\n\n    fun syncLikedSongs() {\n        viewModelScope.launch(Dispatchers.IO) { syncUtils.syncLikedSongs() }\n    }\n\n    fun syncUploadedSongs() {\n        viewModelScope.launch(Dispatchers.IO) { syncUtils.syncUploadedSongs() }\n    }\n\n    fun refresh() {\n        viewModelScope.launch(Dispatchers.IO) {\n            _isRefreshing.value = true\n            when (playlist) {\n                \"liked\" -> syncUtils.syncLikedSongsSuspend()\n                \"uploaded\" -> syncUtils.syncUploadedSongsSuspend()\n            }\n            _isRefreshing.value = false\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/BackupRestoreViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport android.widget.Toast\nimport androidx.datastore.preferences.core.edit\nimport androidx.lifecycle.ViewModel\nimport com.metrolist.innertube.utils.parseCookieString\nimport com.metrolist.innertube.utils.sha1\nimport com.metrolist.music.R\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.json.jsonArray\nimport kotlinx.serialization.json.jsonObject\nimport kotlinx.serialization.json.jsonPrimitive\nimport okhttp3.MediaType.Companion.toMediaType\nimport okhttp3.OkHttpClient\nimport okhttp3.Request\nimport okhttp3.RequestBody.Companion.toRequestBody\nimport com.metrolist.music.constants.DataSyncIdKey\nimport com.metrolist.music.constants.InnerTubeCookieKey\nimport com.metrolist.music.constants.VisitorDataKey\nimport com.metrolist.music.db.InternalDatabase\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.db.entities.ArtistEntity\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.db.entities.SongEntity\nimport com.metrolist.music.extensions.div\nimport com.metrolist.music.extensions.tryOrNull\nimport com.metrolist.music.extensions.zipInputStream\nimport com.metrolist.music.extensions.zipOutputStream\nimport com.metrolist.music.playback.MusicService\nimport com.metrolist.music.playback.MusicService.Companion.PERSISTENT_QUEUE_FILE\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.reportException\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.runBlocking\nimport timber.log.Timber\nimport java.io.FileInputStream\nimport java.io.FileOutputStream\nimport java.util.zip.ZipEntry\nimport javax.inject.Inject\n\ndata class BackupPreviewInfo(\n    val hasAuthData: Boolean = false,\n    val accountName: String? = null,\n    val accountEmail: String? = null,\n    val accountImageUrl: String? = null,\n    val cookie: String? = null,\n)\n\ndata class CsvImportState(\n    val previewRows: List<List<String>> = emptyList(),\n    val artistColumnIndex: Int = 0,\n    val titleColumnIndex: Int = 1,\n    val urlColumnIndex: Int = -1,\n    val hasHeader: Boolean = true,\n)\n\ndata class ConvertedSongLog(\n    val title: String,\n    val artists: String,\n)\n\n@HiltViewModel\nclass BackupRestoreViewModel @Inject constructor(\n    val database: MusicDatabase,\n) : ViewModel() {\n    fun backup(context: Context, uri: Uri) {\n        runCatching {\n            context.applicationContext.contentResolver.openOutputStream(uri)?.use {\n                it.buffered().zipOutputStream().use { outputStream ->\n                    (context.filesDir / \"datastore\" / SETTINGS_FILENAME).inputStream().buffered()\n                        .use { inputStream ->\n                            outputStream.putNextEntry(ZipEntry(SETTINGS_FILENAME))\n                            inputStream.copyTo(outputStream)\n                        }\n                    runBlocking(Dispatchers.IO) {\n                        database.checkpoint()\n                    }\n                    FileInputStream(database.openHelper.writableDatabase.path).use { inputStream ->\n                        outputStream.putNextEntry(ZipEntry(InternalDatabase.DB_NAME))\n                        inputStream.copyTo(outputStream)\n                    }\n                }\n            }\n        }.onSuccess {\n            Toast.makeText(context, R.string.backup_create_success, Toast.LENGTH_SHORT).show()\n        }.onFailure {\n            reportException(it)\n            Toast.makeText(context, R.string.backup_create_failed, Toast.LENGTH_SHORT).show()\n        }\n    }\n\n    fun restore(context: Context, uri: Uri, clearAuthData: Boolean = false) {\n        runCatching {\n            Timber.tag(\"RESTORE\").i(\"Starting restore from URI: $uri, clearAuthData: $clearAuthData\")\n            context.applicationContext.contentResolver.openInputStream(uri)?.use { raw ->\n                raw.zipInputStream().use { inputStream ->\n                    var entry = tryOrNull { inputStream.nextEntry } // prevent ZipException\n                    var foundAny = false\n                    while (entry != null) {\n                        Timber.tag(\"RESTORE\").i(\"Found zip entry: ${entry.name}\")\n                        when (entry.name) {\n                            SETTINGS_FILENAME -> {\n                                Timber.tag(\"RESTORE\").i(\"Restoring settings to datastore\")\n                                foundAny = true\n                                (context.filesDir / \"datastore\" / SETTINGS_FILENAME).outputStream()\n                                    .use { outputStream ->\n                                        inputStream.copyTo(outputStream)\n                                    }\n                            }\n                            InternalDatabase.DB_NAME -> {\n                                Timber.tag(\"RESTORE\").i(\"Restoring DB (entry = ${entry.name})\")\n                                foundAny = true\n                                // capture path before closing DB to avoid reopening race\n                                val dbPath = database.openHelper.writableDatabase.path\n                                runBlocking(Dispatchers.IO) { database.checkpoint() }\n                                database.close()\n                                Timber.tag(\"RESTORE\").i(\"Overwriting DB at path: $dbPath\")\n                                FileOutputStream(dbPath).use { outputStream ->\n                                    inputStream.copyTo(outputStream)\n                                }\n                                Timber.tag(\"RESTORE\").i(\"DB overwrite complete\")\n                            }\n                            else -> {\n                                Timber.tag(\"RESTORE\").i(\"Skipping unexpected entry: ${entry.name}\")\n                            }\n                        }\n                        entry = tryOrNull { inputStream.nextEntry } // prevent ZipException\n                    }\n                    if (!foundAny) {\n                        Timber.tag(\"RESTORE\").w(\"No expected entries found in archive\")\n                    }\n                }\n            } ?: run {\n                Timber.tag(\"RESTORE\").e(\"Could not open input stream for uri: $uri\")\n            }\n\n            // Clear stale auth data to prevent playback issues\n            if (clearAuthData) {\n                Timber.tag(\"RESTORE\").i(\"Clearing auth data to prevent stale session issues\")\n                runBlocking(Dispatchers.IO) {\n                    context.dataStore.edit { preferences ->\n                        preferences.remove(InnerTubeCookieKey)\n                        preferences.remove(VisitorDataKey)\n                        preferences.remove(DataSyncIdKey)\n                    }\n                }\n            }\n\n            context.stopService(Intent(context, MusicService::class.java))\n            context.filesDir.resolve(PERSISTENT_QUEUE_FILE).delete()\n            val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply {\n                addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)\n            }\n            context.startActivity(intent)\n            Runtime.getRuntime().exit(0)\n        }.onFailure {\n            reportException(it)\n            Timber.tag(\"RESTORE\").e(it, \"Restore failed\")\n            Toast.makeText(context, R.string.restore_failed, Toast.LENGTH_SHORT).show()\n        }\n    }\n\n    fun previewBackup(context: Context, uri: Uri): BackupPreviewInfo {\n        return runCatching {\n            context.applicationContext.contentResolver.openInputStream(uri)?.use { raw ->\n                raw.zipInputStream().use { inputStream ->\n                    var entry = tryOrNull { inputStream.nextEntry }\n                    while (entry != null) {\n                        if (entry.name == SETTINGS_FILENAME) {\n                            val bytes = inputStream.readBytes()\n                            val content = bytes.decodeToString(throwOnInvalidSequence = false)\n\n                            // Check for auth data (SAPISID cookie indicates logged in)\n                            val hasAuthData = content.contains(\"SAPISID=\")\n\n                            // Extract cookie string from backup\n                            val cookie = if (hasAuthData) {\n                                extractCookieFromPrefs(content)\n                            } else null\n\n                            return BackupPreviewInfo(\n                                hasAuthData = hasAuthData,\n                                accountName = null,\n                                accountEmail = null,\n                                accountImageUrl = null,\n                                cookie = cookie,\n                            )\n                        }\n                        entry = tryOrNull { inputStream.nextEntry }\n                    }\n                }\n            }\n            BackupPreviewInfo()\n        }.getOrElse {\n            Timber.tag(\"BACKUP_PREVIEW\").e(it, \"Failed to preview backup\")\n            BackupPreviewInfo()\n        }\n    }\n\n    private fun extractCookieFromPrefs(content: String): String? {\n        // Find innerTubeCookie key and extract the cookie value.\n        // The proto format has the key followed by type markers and then the string value.\n        val keyMarker = \"innerTubeCookie\"\n        val keyIndex = content.indexOf(keyMarker)\n        if (keyIndex == -1) return null\n\n        val afterKey = content.substring(keyIndex + keyMarker.length)\n\n        // Cookie starts after some proto markers and contains semicolon-separated values.\n        // Look for the first cookie key pattern like \"__Secure-\" or \"HSID=\" etc.\n        val cookiePatterns = listOf(\"__Secure-\", \"HSID=\", \"SSID=\", \"SID=\", \"SAPISID=\")\n        var cookieStart = -1\n        for (pattern in cookiePatterns) {\n            val idx = afterKey.indexOf(pattern)\n            if (idx != -1 && (cookieStart == -1 || idx < cookieStart)) {\n                cookieStart = idx\n            }\n        }\n        if (cookieStart == -1) return null\n\n        // Find the end of the cookie (next control character or next key).\n        val cookieContent = afterKey.substring(cookieStart)\n        val cookieEnd = cookieContent.indexOfFirst {\n            it.code < 32 && it != '\\t' && it != '\\n' && it != '\\r'\n        }\n\n        val rawCookie = if (cookieEnd > 0) {\n            cookieContent.substring(0, cookieEnd)\n        } else {\n            cookieContent.take(5000) // Reasonable max length\n        }\n        // Remove any control characters (newlines, etc.) that are invalid in HTTP headers.\n        return rawCookie.replace(Regex(\"[\\\\x00-\\\\x1F\\\\x7F]\"), \"\").trim()\n    }\n\n    suspend fun fetchAccountInfoFromBackup(cookie: String): BackupPreviewInfo? {\n        return runCatching {\n            // Parse cookie to get SAPISID for auth header\n            val cookieMap = parseCookieString(cookie)\n            val sapisid = cookieMap[\"SAPISID\"] ?: return@runCatching null\n\n            // Generate SAPISIDHASH auth header\n            val origin = \"https://music.youtube.com\"\n            val currentTime = System.currentTimeMillis() / 1000\n            val sapisidHash = sha1(\"$currentTime $sapisid $origin\")\n            val authHeader = \"SAPISIDHASH ${currentTime}_$sapisidHash\"\n\n            val client = OkHttpClient()\n            val requestBody = \"\"\"{\"context\":{\"client\":{\"clientName\":\"WEB_REMIX\",\"clientVersion\":\"1.20240101.01.00\"}}}\"\"\"\n                .toRequestBody(\"application/json\".toMediaType())\n\n            val request = Request.Builder()\n                .url(\"https://music.youtube.com/youtubei/v1/account/account_menu?prettyPrint=false\")\n                .post(requestBody)\n                .header(\"Cookie\", cookie)\n                .header(\"Authorization\", authHeader)\n                .header(\"Origin\", origin)\n                .header(\"Referer\", \"$origin/\")\n                .header(\"X-Origin\", origin)\n                .header(\"User-Agent\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\")\n                .build()\n\n            val response = client.newCall(request).execute()\n            val responseBody = response.body?.string() ?: return@runCatching null\n\n            // Parse the JSON response\n            val json = Json { ignoreUnknownKeys = true }\n            val jsonResponse = json.parseToJsonElement(responseBody).jsonObject\n\n            // Navigate to activeAccountHeaderRenderer\n            val header = jsonResponse[\"actions\"]\n                ?.jsonArray?.getOrNull(0)\n                ?.jsonObject?.get(\"openPopupAction\")\n                ?.jsonObject?.get(\"popup\")\n                ?.jsonObject?.get(\"multiPageMenuRenderer\")\n                ?.jsonObject?.get(\"header\")\n                ?.jsonObject?.get(\"activeAccountHeaderRenderer\")\n                ?.jsonObject\n\n            if (header != null) {\n                val name = header[\"accountName\"]\n                    ?.jsonObject?.get(\"runs\")\n                    ?.jsonArray?.getOrNull(0)\n                    ?.jsonObject?.get(\"text\")\n                    ?.jsonPrimitive?.content\n\n                val email = header[\"email\"]\n                    ?.jsonObject?.get(\"runs\")\n                    ?.jsonArray?.getOrNull(0)\n                    ?.jsonObject?.get(\"text\")\n                    ?.jsonPrimitive?.content\n\n                val thumbnailUrl = header[\"accountPhoto\"]\n                    ?.jsonObject?.get(\"thumbnails\")\n                    ?.jsonArray?.getOrNull(0)\n                    ?.jsonObject?.get(\"url\")\n                    ?.jsonPrimitive?.content\n\n                if (name != null) {\n                    BackupPreviewInfo(\n                        hasAuthData = true,\n                        accountName = name,\n                        accountEmail = email,\n                        accountImageUrl = thumbnailUrl,\n                        cookie = cookie,\n                    )\n                } else null\n            } else null\n        }.getOrElse {\n            Timber.tag(\"BACKUP_PREVIEW\").e(it, \"Failed to fetch account info from backup\")\n            null\n        }\n    }\n\n    fun previewCsvFile(context: Context, uri: Uri): CsvImportState {\n        val previewRows = mutableListOf<List<String>>()\n        val csvState: CsvImportState\n        runCatching {\n            context.contentResolver.openInputStream(uri)?.use { stream ->\n                val lines = stream.bufferedReader().readLines()\n                val rowsToPreview = lines.take(6).map { parseCsvLine(it) }\n                previewRows.addAll(rowsToPreview)\n\n                val hasHeader = lines.isNotEmpty() && lines[0].contains(\",\")\n                csvState = CsvImportState(\n                    previewRows = previewRows,\n                    hasHeader = hasHeader,\n                )\n                return csvState\n            }\n        }.onFailure {\n            reportException(it)\n            Toast.makeText(context, \"Failed to preview CSV file\", Toast.LENGTH_SHORT).show()\n        }\n        return CsvImportState()\n    }\n\n    suspend fun importPlaylistFromCsv(\n        context: Context,\n        uri: Uri,\n        columnMapping: CsvImportState,\n        onProgress: (Int) -> Unit = {},\n        onLogUpdate: (List<ConvertedSongLog>) -> Unit = {},\n    ): ArrayList<Song> = kotlinx.coroutines.withContext(Dispatchers.IO) {\n        val songs = arrayListOf<Song>()\n        val recentLogs = mutableListOf<ConvertedSongLog>()\n\n        runCatching {\n            context.contentResolver.openInputStream(uri)?.use { stream ->\n                val lines = stream.bufferedReader().readLines()\n                val startIndex = if (columnMapping.hasHeader) 1 else 0\n                val totalLines = lines.size - startIndex\n\n                lines.drop(startIndex).forEachIndexed { index, line ->\n                    val parts = parseCsvLine(line)\n\n                    if (parts.isNotEmpty()) {\n                        if (columnMapping.artistColumnIndex < parts.size && columnMapping.titleColumnIndex < parts.size) {\n                            val title = parts[columnMapping.titleColumnIndex].trim()\n                            val artistStr = parts[columnMapping.artistColumnIndex].trim()\n\n                            if (title.isNotEmpty() && artistStr.isNotEmpty()) {\n                                val artists = artistStr.split(\";\", \",\").map { it.trim() }\n                                    .filter { it.isNotEmpty() }\n                                    .map { ArtistEntity(id = \"\", name = it) }\n\n                                val mockSong = Song(\n                                    song = SongEntity(\n                                        id = \"\",\n                                        title = title,\n                                    ),\n                                    artists = artists,\n                                )\n                                songs.add(mockSong)\n\n                                val logEntry = ConvertedSongLog(\n                                    title = title,\n                                    artists = artists.joinToString(\", \") { it.name },\n                                )\n                                recentLogs.add(0, logEntry)\n                                if (recentLogs.size > 3) {\n                                    recentLogs.removeAt(recentLogs.size - 1)\n                                }\n                                onLogUpdate(recentLogs.toList())\n                            }\n                        }\n                    }\n\n                    val progress = ((index + 1) * 100) / totalLines\n                    onProgress(progress)\n                }\n            }\n        }.onFailure {\n            reportException(it)\n        }\n\n        songs\n    }\n\n    suspend fun importPlaylistFromCsv(context: Context, uri: Uri): ArrayList<Song> {\n        return importPlaylistFromCsv(context, uri, CsvImportState())\n    }\n\n    private fun parseCsvLine(line: String): List<String> {\n        val result = mutableListOf<String>()\n        var current = StringBuilder()\n        var inQuotes = false\n\n        for (char in line) {\n            when {\n                char == '\"' -> inQuotes = !inQuotes\n                char == ',' && !inQuotes -> {\n                    result.add(current.toString())\n                    current = StringBuilder()\n                }\n                else -> current.append(char)\n            }\n        }\n        result.add(current.toString())\n        return result.map { it.trim().trim('\"') }\n    }\n\n    fun loadM3UOnline(\n        context: Context,\n        uri: Uri,\n    ): ArrayList<Song> {\n        val songs = ArrayList<Song>()\n\n        runCatching {\n            context.applicationContext.contentResolver.openInputStream(uri)?.use { stream ->\n                val lines = stream.bufferedReader().readLines()\n                if (lines.isNotEmpty() && lines.first().startsWith(\"#EXTM3U\")) {\n                    lines.forEachIndexed { _, rawLine ->\n                        if (rawLine.startsWith(\"#EXTINF:\")) {\n                            val artists =\n                                rawLine.substringAfter(\"#EXTINF:\").substringAfter(',').substringBefore(\" - \").split(';')\n                            val title = rawLine.substringAfter(\"#EXTINF:\").substringAfter(',').substringAfter(\" - \")\n\n                            val mockSong = Song(\n                                song = SongEntity(\n                                    id = \"\",\n                                    title = title,\n                                ),\n                                artists = artists.map { ArtistEntity(\"\", it) },\n                            )\n                            songs.add(mockSong)\n                        }\n                    }\n                }\n            }\n        }\n\n        if (songs.isEmpty()) {\n            Toast.makeText(\n                context,\n                \"No songs found. Invalid file, or perhaps no song matches were found.\",\n                Toast.LENGTH_SHORT\n            ).show()\n        }\n        return songs\n    }\n\n    companion object {\n        const val SETTINGS_FILENAME = \"settings.preferences_pb\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/BrowseViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n \nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.YTItem\nimport com.metrolist.music.utils.reportException\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass BrowseViewModel @Inject constructor(\n    savedStateHandle: SavedStateHandle\n) : ViewModel() {\n    private val browseId: String? = savedStateHandle.get<String>(\"browseId\")\n \n    val items = MutableStateFlow<List<YTItem>?>(emptyList())\n    val title = MutableStateFlow<String?>(\"\")\n \n    init {\n        viewModelScope.launch {\n            browseId?.let {\n                YouTube.browse(browseId, null).onSuccess { result ->\n                    // Store the title\n                    title.value = result.title\n \n                    // Flatten the nested structure to get all YTItems\n                    val allItems = result.items.flatMap { it.items }\n                    items.value = allItems\n                }.onFailure {\n                    reportException(it)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/CachePlaylistViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport android.content.Context\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport androidx.media3.datasource.cache.SimpleCache\nimport com.metrolist.music.constants.HideExplicitKey\nimport com.metrolist.music.constants.HideVideoSongsKey\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.di.DownloadCache\nimport com.metrolist.music.di.PlayerCache\nimport com.metrolist.music.extensions.filterExplicit\nimport com.metrolist.music.extensions.filterVideoSongs\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.launch\nimport java.time.LocalDateTime\nimport javax.inject.Inject\n\n@HiltViewModel\nclass CachePlaylistViewModel @Inject constructor(\n    @ApplicationContext private val context: Context,\n    private val database: MusicDatabase,\n    @PlayerCache private val playerCache: SimpleCache,\n    @DownloadCache private val downloadCache: SimpleCache\n) : ViewModel() {\n\n    private val _cachedSongs = MutableStateFlow<List<Song>>(emptyList())\n    val cachedSongs: StateFlow<List<Song>> = _cachedSongs\n\n    init {\n        viewModelScope.launch {\n            while (true) {\n                val hideExplicit = context.dataStore.get(HideExplicitKey, false)\n                val hideVideoSongs = context.dataStore.get(HideVideoSongsKey, false)\n                val cachedIds = playerCache.keys.toSet()\n                val downloadedIds = downloadCache.keys.toSet()\n                val pureCacheIds = cachedIds.subtract(downloadedIds)\n\n                val songs = if (pureCacheIds.isNotEmpty()) {\n                    database.getSongsByIds(pureCacheIds.toList())\n                } else {\n                    emptyList()\n                }\n\n                val completeSongs = songs.filter {\n                    val contentLength = it.format?.contentLength\n                    contentLength != null && playerCache.isCached(it.song.id, 0, contentLength)\n                }\n\n                if (completeSongs.isNotEmpty()) {\n                    database.query {\n                        completeSongs.forEach {\n                            if (it.song.dateDownload == null) {\n                                update(it.song.copy(dateDownload = LocalDateTime.now()))\n                            }\n                        }\n                    }\n                }\n\n                _cachedSongs.value = completeSongs\n                    .filter { it.song.dateDownload != null }\n                    .sortedByDescending { it.song.dateDownload }\n                    .filterExplicit(hideExplicit)\n                    .filterVideoSongs(hideVideoSongs)\n\n                delay(1000)\n            }\n        }\n    }\n\n    fun removeSongFromCache(songId: String) {\n        playerCache.removeResource(songId)\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/ChartsViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.pages.ChartsPage\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass ChartsViewModel @Inject constructor() : ViewModel() {\n    private val _chartsPage = MutableStateFlow<ChartsPage?>(null)\n    val chartsPage = _chartsPage.asStateFlow()\n\n    private val _isLoading = MutableStateFlow(false)\n    val isLoading = _isLoading.asStateFlow()\n\n    private val _error = MutableStateFlow<String?>(null)\n    val error = _error.asStateFlow()\n\n    fun loadCharts() {\n        viewModelScope.launch {\n            _isLoading.value = true\n            _error.value = null\n            \n            YouTube.getChartsPage()\n                .onSuccess { page ->\n                    _chartsPage.value = page\n                }\n                .onFailure { e ->\n                    _error.value = \"Failed to load charts: ${e.message}\"\n                }\n            \n            _isLoading.value = false\n        }\n    }\n\n    fun loadMore() {\n        viewModelScope.launch {\n            _chartsPage.value?.continuation?.let { continuation ->\n                _isLoading.value = true\n                YouTube.getChartsPage(continuation)\n                    .onSuccess { newPage ->\n                        _chartsPage.value = _chartsPage.value?.copy(\n                            sections = _chartsPage.value?.sections.orEmpty() + newPage.sections,\n                            continuation = newPage.continuation\n                        )\n                    }\n                    .onFailure { e ->\n                        _error.value = \"Failed to load more: ${e.message}\"\n                    }\n                _isLoading.value = false\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/ExploreViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport android.content.Context\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.filterExplicit\nimport com.metrolist.innertube.pages.ExplorePage\nimport com.metrolist.music.constants.HideExplicitKey\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\nimport com.metrolist.music.utils.reportException\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass ExploreViewModel\n@Inject\nconstructor(\n    @ApplicationContext val context: Context,\n    val database: MusicDatabase,\n) : ViewModel() {\n    val explorePage = MutableStateFlow<ExplorePage?>(null)\n\n    private suspend fun load() {\n        YouTube\n            .explore()\n            .onSuccess { page ->\n                val artists: MutableMap<Int, String> = mutableMapOf()\n                val favouriteArtists: MutableMap<Int, String> = mutableMapOf()\n                database.allArtistsByPlayTime().first().let { list ->\n                    var favIndex = 0\n                    for ((artistsIndex, artist) in list.withIndex()) {\n                        artists[artistsIndex] = artist.id\n                        if (artist.artist.bookmarkedAt != null) {\n                            favouriteArtists[favIndex] = artist.id\n                            favIndex++\n                        }\n                    }\n                }\n                explorePage.value =\n                    page.copy(\n                        newReleaseAlbums =\n                        page.newReleaseAlbums\n                            .sortedBy { album ->\n                                val artistIds = album.artists.orEmpty().mapNotNull { it.id }\n                                val firstArtistKey =\n                                    artistIds.firstNotNullOfOrNull { artistId ->\n                                        if (artistId in favouriteArtists.values) {\n                                            favouriteArtists.entries.firstOrNull { it.value == artistId }?.key\n                                        } else {\n                                            artists.entries.firstOrNull { it.value == artistId }?.key\n                                        }\n                                    } ?: Int.MAX_VALUE\n                                firstArtistKey\n                            }.filterExplicit(context.dataStore.get(HideExplicitKey, false)),\n                    )\n            }.onFailure {\n                reportException(it)\n            }\n    }\n\n    init {\n        viewModelScope.launch(Dispatchers.IO) {\n            load()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/HistoryViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport android.content.Context\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.pages.HistoryPage\nimport com.metrolist.music.constants.HideVideoSongsKey\nimport com.metrolist.music.constants.HistorySource\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.reportException\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.launch\nimport java.time.DayOfWeek\nimport java.time.LocalDate\nimport java.time.temporal.ChronoUnit\nimport javax.inject.Inject\n\n@OptIn(ExperimentalCoroutinesApi::class)\n@HiltViewModel\nclass HistoryViewModel\n@Inject\nconstructor(\n    @ApplicationContext private val context: Context,\n    val database: MusicDatabase,\n) : ViewModel() {\n    var historySource = MutableStateFlow(HistorySource.LOCAL)\n\n    private val today = LocalDate.now()\n    private val thisMonday = today.with(DayOfWeek.MONDAY)\n    private val lastMonday = thisMonday.minusDays(7)\n\n    val historyPage = MutableStateFlow<HistoryPage?>(null)\n\n    val events =\n        context.dataStore.data\n            .map { it[HideVideoSongsKey] ?: false }\n            .distinctUntilChanged()\n            .flatMapLatest { hideVideoSongs ->\n                database\n                    .events()\n                    .map { events ->\n                        events\n                            .filter { !hideVideoSongs || !it.song.song.isVideo }\n                            .groupBy {\n                                val date = it.event.timestamp.toLocalDate()\n                                val daysAgo = ChronoUnit.DAYS.between(date, today).toInt()\n                                when {\n                                    daysAgo == 0 -> DateAgo.Today\n                                    daysAgo == 1 -> DateAgo.Yesterday\n                                    date >= thisMonday -> DateAgo.ThisWeek\n                                    date >= lastMonday -> DateAgo.LastWeek\n                                    else -> DateAgo.Other(date.withDayOfMonth(1))\n                                }\n                            }.toSortedMap(\n                                compareBy { dateAgo ->\n                                    when (dateAgo) {\n                                        DateAgo.Today -> 0L\n                                        DateAgo.Yesterday -> 1L\n                                        DateAgo.ThisWeek -> 2L\n                                        DateAgo.LastWeek -> 3L\n                                        is DateAgo.Other -> ChronoUnit.DAYS.between(dateAgo.date, today)\n                                    }\n                                },\n                            ).mapValues { entry ->\n                                entry.value.distinctBy { it.song.id }\n                            }\n                    }\n            }.stateIn(viewModelScope, SharingStarted.Lazily, emptyMap())\n\n    init {\n        fetchRemoteHistory()\n    }\n\n    fun fetchRemoteHistory() {\n        viewModelScope.launch(Dispatchers.IO) {\n            YouTube.musicHistory().onSuccess {\n                historyPage.value = it\n            }.onFailure {\n                reportException(it)\n            }\n        }\n    }\n}\n\nsealed class DateAgo {\n    data object Today : DateAgo()\n\n    data object Yesterday : DateAgo()\n\n    data object ThisWeek : DateAgo()\n\n    data object LastWeek : DateAgo()\n\n    class Other(\n        val date: LocalDate,\n    ) : DateAgo() {\n        override fun equals(other: Any?): Boolean {\n            if (other is Other) return date == other.date\n            return super.equals(other)\n        }\n\n        override fun hashCode(): Int = date.hashCode()\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/HomeViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport android.content.Context\nimport androidx.datastore.preferences.core.edit\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.Artist\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.SongItem\nimport kotlinx.coroutines.flow.combine\nimport com.metrolist.innertube.models.WatchEndpoint\nimport com.metrolist.innertube.models.BrowseEndpoint\nimport com.metrolist.innertube.models.YTItem\nimport com.metrolist.innertube.models.filterExplicit\nimport com.metrolist.innertube.models.filterVideoSongs\nimport com.metrolist.innertube.models.filterYoutubeShorts\nimport com.metrolist.innertube.pages.ExplorePage\nimport com.metrolist.innertube.pages.HomePage\nimport com.metrolist.innertube.utils.completed\nimport com.metrolist.music.constants.HideExplicitKey\nimport com.metrolist.music.constants.HideVideoSongsKey\nimport com.metrolist.music.constants.HideYoutubeShortsKey\nimport com.metrolist.music.constants.InnerTubeCookieKey\nimport com.metrolist.music.constants.QuickPicks\nimport com.metrolist.music.constants.QuickPicksKey\nimport com.metrolist.music.constants.ShowWrappedCardKey\nimport com.metrolist.music.constants.WrappedSeenKey\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.db.entities.Album\nimport com.metrolist.music.db.entities.LocalItem\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.db.entities.SpeedDialItem\nimport com.metrolist.music.extensions.filterVideoSongs\nimport com.metrolist.music.extensions.toEnum\nimport com.metrolist.music.models.SimilarRecommendation\nimport com.metrolist.music.ui.screens.wrapped.WrappedAudioService\nimport com.metrolist.music.ui.screens.wrapped.WrappedManager\nimport com.metrolist.music.utils.SyncUtils\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\nimport com.metrolist.music.utils.reportException\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.launch\nimport java.time.LocalDate\nimport javax.inject.Inject\nimport kotlin.random.Random\n\ndata class DailyDiscoverItem(\n    val seed: Song,\n    val recommendation: YTItem,\n    val relatedEndpoint: BrowseEndpoint?\n)\n\ndata class CommunityPlaylistItem(\n    val playlist: PlaylistItem,\n    val songs: List<SongItem>\n)\n\n@HiltViewModel\nclass HomeViewModel @Inject constructor(\n    @ApplicationContext val context: Context,\n    val database: MusicDatabase,\n    val syncUtils: SyncUtils,\n    val wrappedManager: WrappedManager,\n    private val wrappedAudioService: WrappedAudioService,\n) : ViewModel() {\n    val isRefreshing = MutableStateFlow(false)\n    val isLoading = MutableStateFlow(false)\n    val isRandomizing = MutableStateFlow(false)\n\n    private val quickPicksEnum = context.dataStore.data.map {\n        it[QuickPicksKey].toEnum(QuickPicks.QUICK_PICKS)\n    }.distinctUntilChanged()\n\n    val quickPicks = MutableStateFlow<List<Song>?>(null)\n    val dailyDiscover = MutableStateFlow<List<DailyDiscoverItem>?>(null)\n    val forgottenFavorites = MutableStateFlow<List<Song>?>(null)\n    val keepListening = MutableStateFlow<List<LocalItem>?>(null)\n    val similarRecommendations = MutableStateFlow<List<SimilarRecommendation>?>(null)\n    val accountPlaylists = MutableStateFlow<List<PlaylistItem>?>(null)\n    val homePage = MutableStateFlow<HomePage?>(null)\n    val explorePage = MutableStateFlow<ExplorePage?>(null)\n    val communityPlaylists = MutableStateFlow<List<CommunityPlaylistItem>?>(null)\n    val selectedChip = MutableStateFlow<HomePage.Chip?>(null)\n    private val previousHomePage = MutableStateFlow<HomePage?>(null)\n\n    // Official API data for podcast sections\n    val savedPodcastShows = MutableStateFlow<List<com.metrolist.innertube.models.PodcastItem>>(emptyList())\n    val episodesForLater = MutableStateFlow<List<SongItem>>(emptyList())\n\n    val allLocalItems = MutableStateFlow<List<LocalItem>>(emptyList())\n    val allYtItems = MutableStateFlow<List<YTItem>>(emptyList())\n\n    val pinnedSpeedDialItems: StateFlow<List<SpeedDialItem>> =\n        database.speedDialDao.getAll()\n            .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n\n    val speedDialItems: StateFlow<List<YTItem>> =\n        combine(\n            database.speedDialDao.getAll(),\n            keepListening,\n            quickPicks\n        ) { pinned, keepListening, quick ->\n            val pinnedItems = pinned.map { it.toYTItem() }\n            val filled = pinnedItems.toMutableList()\n            val targetSize = 27\n\n            if (filled.size < targetSize) {\n                // Keep Listening (History/Heavy Rotation)\n                keepListening?.let { k ->\n                    val needed = targetSize - filled.size\n                    val available = k.filter { item ->\n                        filled.none { p -> p.id == item.id }\n                    }.mapNotNull { item ->\n                        when (item) {\n                            is Song -> SongItem(\n                                id = item.id,\n                                title = item.title,\n                                artists = item.artists.map { Artist(name = it.name, id = it.id) },\n                                thumbnail = item.thumbnailUrl ?: \"\",\n                                explicit = false\n                            )\n                            is Album -> AlbumItem(\n                                browseId = item.id,\n                                playlistId = item.album.playlistId ?: \"\",\n                                title = item.title,\n                                artists = item.artists.map { Artist(name = it.name, id = it.id) },\n                                year = item.album.year,\n                                thumbnail = item.thumbnailUrl ?: \"\"\n                            )\n                            else -> null\n                        }\n                    }\n                    filled.addAll(available.take(needed))\n                }\n            }\n\n            if (filled.size < targetSize) {\n                // Quick Picks\n                quick?.let { q ->\n                    val needed = targetSize - filled.size\n                    val available = q.filter { song ->\n                        filled.none { p -> p.id == song.id }\n                    }.map { song ->\n                        SongItem(\n                            id = song.id,\n                            title = song.title,\n                            artists = song.artists.map { Artist(name = it.name, id = it.id) },\n                            thumbnail = song.thumbnailUrl ?: \"\",\n                            explicit = false\n                        )\n                    }\n                    filled.addAll(available.take(needed))\n                }\n            }\n            \n            filled.take(targetSize)\n        }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n\n    suspend fun getRandomItem(): YTItem? {\n        try {\n            isRandomizing.value = true\n            // Visual feedback for the animation\n            kotlinx.coroutines.delay(1000)\n\n            val userSongs = mutableListOf<YTItem>()\n            val otherSources = mutableListOf<YTItem>()\n\n            quickPicks.value?.let { songs ->\n                userSongs.addAll(songs.map { song ->\n                    SongItem(\n                        id = song.id,\n                        title = song.title,\n                        artists = song.artists.map { Artist(name = it.name, id = it.id) },\n                        thumbnail = song.thumbnailUrl ?: \"\",\n                        explicit = false\n                    )\n                })\n            }\n\n            keepListening.value?.let { items ->\n                items.forEach { item ->\n                    when (item) {\n                        is Song -> userSongs.add(SongItem(\n                            id = item.id,\n                            title = item.title,\n                            artists = item.artists.map { Artist(name = it.name, id = it.id) },\n                            thumbnail = item.thumbnailUrl ?: \"\",\n                            explicit = false\n                        ))\n                        is Album -> otherSources.add(AlbumItem(\n                            browseId = item.id,\n                            playlistId = item.album.playlistId ?: \"\",\n                            title = item.title,\n                            artists = item.artists.map { Artist(name = it.name, id = it.id) },\n                            year = item.album.year,\n                            thumbnail = item.thumbnailUrl ?: \"\"\n                        ))\n                        else -> {}\n                    }\n                }\n            }\n\n            otherSources.addAll(allYtItems.value)\n\n            // Probability: 80% User Songs, 20% Other Sources\n            val item = if (userSongs.isNotEmpty() && (otherSources.isEmpty() || Random.nextFloat() < 0.8f)) {\n                userSongs.distinctBy { it.id }.shuffled().firstOrNull()\n            } else {\n                otherSources.distinctBy { it.id }.shuffled().firstOrNull()\n            } ?: userSongs.firstOrNull() ?: otherSources.firstOrNull()\n\n            return item\n        } finally {\n            isRandomizing.value = false\n        }\n    }\n\n    val accountName = MutableStateFlow(\"Guest\")\n    val accountImageUrl = MutableStateFlow<String?>(null)\n\n\tval showWrappedCard: StateFlow<Boolean> = context.dataStore.data.map { prefs ->\n        val showWrappedPref = prefs[ShowWrappedCardKey] ?: false\n        val seen = prefs[WrappedSeenKey] ?: false\n        val isBeforeDate = LocalDate.now().isBefore(LocalDate.of(2026, 2, 1))\n\n        isBeforeDate && (!seen || showWrappedPref)\n    }.stateIn(viewModelScope, SharingStarted.Lazily, false)\n\n    val wrappedSeen: StateFlow<Boolean> = context.dataStore.data.map { prefs ->\n        prefs[WrappedSeenKey] ?: false\n    }.stateIn(viewModelScope, SharingStarted.Lazily, false)\n\n    fun togglePin(item: YTItem) {\n        viewModelScope.launch(Dispatchers.IO) {\n            val speedDialItem = SpeedDialItem.fromYTItem(item)\n            val isPinned = database.speedDialDao.isPinned(speedDialItem.id).first()\n            if (isPinned) {\n                database.speedDialDao.delete(speedDialItem.id)\n            } else {\n                database.speedDialDao.insert(speedDialItem)\n            }\n        }\n    }\n\n    fun markWrappedAsSeen() {\n        viewModelScope.launch(Dispatchers.IO) {\n            context.dataStore.edit {\n                it[WrappedSeenKey] = true\n            }\n        }\n    }\n    // Track last processed cookie to avoid unnecessary updates\n    private var lastProcessedCookie: String? = null\n    // Track if we're currently processing account data\n    private var isProcessingAccountData = false\n\n    private suspend fun getDailyDiscover() {\n        val hideVideoSongs = context.dataStore.get(HideVideoSongsKey, false)\n        val likedSongs = database.likedSongsByCreateDateAsc().first()\n        if (likedSongs.isEmpty()) return\n\n        val seeds = likedSongs.shuffled().distinctBy { it.id }.take(5)\n        \n        // Use a synchronized list to collect results safely from concurrent coroutines\n        val items = java.util.Collections.synchronizedList(mutableListOf<DailyDiscoverItem>())\n\n        kotlinx.coroutines.coroutineScope {\n            seeds.map { seed ->\n                launch(Dispatchers.IO) {\n                    val endpoint = YouTube.next(WatchEndpoint(videoId = seed.id)).getOrNull()?.relatedEndpoint\n                    if (endpoint != null) {\n                        YouTube.related(endpoint).onSuccess { page ->\n                            val recommendations = page.songs\n                                .filter { item ->\n                                    if (hideVideoSongs && item.isVideoSong) return@filter false\n                                    if (item.explicit) return@filter false\n                                    true\n                                }\n                                .shuffled()\n\n                            // Simple check to avoid immediate duplicate of seed\n                            val recommendation = recommendations.firstOrNull { rec ->\n                                rec.id != seed.id\n                            }\n\n                            if (recommendation != null) {\n                                items.add(\n                                    DailyDiscoverItem(\n                                        seed = seed,\n                                        recommendation = recommendation,\n                                        relatedEndpoint = endpoint\n                                    )\n                                )\n                            }\n                        }\n                    }\n                }\n            }.forEach { it.join() }\n        }\n        \n        // Final deduplication just in case multiple seeds recommended the same song\n        dailyDiscover.value = items.toList().distinctBy { it.recommendation.id }.shuffled()\n    }\n\n    private suspend fun getQuickPicks() {\n        val hideVideoSongs = context.dataStore.get(HideVideoSongsKey, false)\n        when (quickPicksEnum.first()) {\n            QuickPicks.QUICK_PICKS -> {\n                val relatedSongs = database.quickPicks().first().filterVideoSongs(hideVideoSongs)\n                val forgotten = database.forgottenFavorites().first().filterVideoSongs(hideVideoSongs).take(8)\n\n                // Get similar songs from YouTube based on recent listening\n                val recentSong = database.events().first().firstOrNull()?.song\n                val ytSimilarSongs = mutableListOf<Song>()\n\n                if (recentSong != null) {\n                    val endpoint = YouTube.next(WatchEndpoint(videoId = recentSong.id)).getOrNull()?.relatedEndpoint\n                    if (endpoint != null) {\n                        YouTube.related(endpoint).onSuccess { page ->\n                            // Convert YouTube songs to local Song format if they exist in database\n                            page.songs.take(10).forEach { ytSong ->\n                                database.song(ytSong.id).first()?.let { localSong ->\n                                    if (!hideVideoSongs || !localSong.song.isVideo) {\n                                        ytSimilarSongs.add(localSong)\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n\n                // Combine all sources and remove duplicates\n                val combined = (relatedSongs + forgotten + ytSimilarSongs)\n                    .distinctBy { it.id }\n                    .shuffled()\n                    .take(20)\n\n                quickPicks.value = combined.ifEmpty { relatedSongs.shuffled().take(20) }\n            }\n            QuickPicks.LAST_LISTEN -> {\n                val song = database.events().first().firstOrNull()?.song\n                if (song != null && database.hasRelatedSongs(song.id)) {\n                    quickPicks.value = database.getRelatedSongs(song.id).first().filterVideoSongs(hideVideoSongs).shuffled().take(20)\n                }\n            }\n        }\n    }\n\n    private suspend fun getCommunityPlaylists() {\n        val fromTimeStamp = System.currentTimeMillis() - 86400000L * 7 * 4\n        val artistSeeds = database.mostPlayedArtists(fromTimeStamp, limit = 10).first()\n            .filter { it.artist.isYouTubeArtist }\n            .shuffled().take(3)\n        val songSeeds = database.mostPlayedSongs(fromTimeStamp, limit = 5).first()\n            .shuffled().take(2)\n\n        val candidatePlaylists = java.util.Collections.synchronizedList(mutableListOf<PlaylistItem>())\n\n        kotlinx.coroutines.coroutineScope {\n            artistSeeds.map { seed ->\n                launch(Dispatchers.IO) {\n                    YouTube.artist(seed.id).onSuccess { page ->\n                        page.sections.forEach { section ->\n                            section.items.filterIsInstance<PlaylistItem>().forEach { playlist ->\n                                if (playlist.author?.name != \"YouTube Music\" && \n                                    playlist.author?.name != \"YouTube\" && \n                                    playlist.author?.name != \"Playlist\" &&\n                                    playlist.author?.name != seed.artist.name &&\n                                    !playlist.id.startsWith(\"RD\") &&\n                                    !playlist.id.startsWith(\"OLAK\")\n                                ) {\n                                    candidatePlaylists.add(playlist)\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n            \n            songSeeds.map { seed ->\n                launch(Dispatchers.IO) {\n                    val endpoint = YouTube.next(WatchEndpoint(videoId = seed.id)).getOrNull()?.relatedEndpoint\n                    if (endpoint != null) {\n                        YouTube.related(endpoint).onSuccess { page ->\n                            page.playlists.forEach { playlist ->\n                                if (playlist.author?.name != \"YouTube Music\" && \n                                    playlist.author?.name != \"YouTube\" && \n                                    playlist.author?.name != \"Playlist\" &&\n                                    !playlist.id.startsWith(\"RD\") &&\n                                    !playlist.id.startsWith(\"OLAK\")\n                                ) {\n                                    candidatePlaylists.add(playlist)\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n\n        val uniqueCandidates = candidatePlaylists.distinctBy { it.id }.shuffled().take(5)\n\n        val playlists = java.util.Collections.synchronizedList(mutableListOf<CommunityPlaylistItem>())\n\n        kotlinx.coroutines.coroutineScope {\n            uniqueCandidates.map { playlist ->\n                launch(Dispatchers.IO) {\n                    YouTube.playlist(playlist.id).onSuccess { page ->\n                        val songs = page.songs.take(10)\n                        if (songs.isNotEmpty()) {\n                            // Use song count from the playlist page if available, otherwise use original\n                            val songCountText = page.playlist.songCountText ?: playlist.songCountText\n                            val updatedPlaylist = playlist.copy(songCountText = songCountText)\n                            playlists.add(CommunityPlaylistItem(updatedPlaylist, songs))\n                        }\n                    }\n                }\n            }.forEach { it.join() }\n        }\n\n        communityPlaylists.value = playlists.shuffled()\n    }\n\n    private suspend fun load() {\n        isLoading.value = true\n        val hideExplicit = context.dataStore.get(HideExplicitKey, false)\n        val hideVideoSongs = context.dataStore.get(HideVideoSongsKey, false)\n        val hideYoutubeShorts = context.dataStore.get(HideYoutubeShortsKey, false)\n        val fromTimeStamp = System.currentTimeMillis() - 86400000L * 7 * 2\n\n        // Phase 1: Load essential sections in parallel — local DB (fast) + YouTube home page.\n        // isLoading is set to false as soon as all Phase 1 tasks complete so the UI appears quickly.\n        coroutineScope {\n            launch(Dispatchers.IO) { getQuickPicks() }\n\n            launch(Dispatchers.IO) {\n                forgottenFavorites.value = database.forgottenFavorites().first()\n                    .filterVideoSongs(hideVideoSongs).shuffled().take(20)\n            }\n\n            launch(Dispatchers.IO) {\n                val songs = database.mostPlayedSongs(fromTimeStamp, limit = 15, offset = 5).first()\n                    .filterVideoSongs(hideVideoSongs).shuffled().take(10)\n                val albums = database.mostPlayedAlbums(fromTimeStamp, limit = 8, offset = 2).first()\n                    .filter { it.album.thumbnailUrl != null }.shuffled().take(5)\n                val artists = database.mostPlayedArtists(fromTimeStamp).first()\n                    .filter { it.artist.isYouTubeArtist && it.artist.thumbnailUrl != null }.shuffled().take(5)\n                keepListening.value = (songs + albums + artists).shuffled()\n            }\n\n            launch(Dispatchers.IO) {\n                YouTube.home().onSuccess { page ->\n                    homePage.value = page.copy(\n                        sections = page.sections.mapNotNull { section ->\n                            val filtered = section.items\n                                .filterExplicit(hideExplicit)\n                                .filterVideoSongs(hideVideoSongs)\n                                .filterYoutubeShorts(hideYoutubeShorts)\n                            if (filtered.isEmpty()) null else section.copy(items = filtered)\n                        }\n                    )\n                }.onFailure { reportException(it) }\n            }\n\n            if (YouTube.cookie != null) {\n                launch(Dispatchers.IO) { loadAccountPlaylists() }\n            }\n        }\n\n        allLocalItems.value = (quickPicks.value.orEmpty() + forgottenFavorites.value.orEmpty() + keepListening.value.orEmpty())\n            .filter { it is Song || it is Album }\n        isLoading.value = false\n\n        // Phase 2: Heavy multi-request operations — run in background without blocking the UI.\n        viewModelScope.launch(Dispatchers.IO) { getDailyDiscover() }\n\n        viewModelScope.launch(Dispatchers.IO) { getCommunityPlaylists() }\n\n        viewModelScope.launch(Dispatchers.IO) {\n            YouTube.explore().onSuccess { page ->\n                explorePage.value = page.copy(\n                    newReleaseAlbums = page.newReleaseAlbums.filterExplicit(hideExplicit)\n                )\n            }.onFailure { reportException(it) }\n        }\n\n        viewModelScope.launch(Dispatchers.IO) {\n            val artistRecommendations = database.mostPlayedArtists(fromTimeStamp, limit = 15).first()\n                .filter { it.artist.isYouTubeArtist }\n                .shuffled().take(4)\n                .mapNotNull {\n                    val items = mutableListOf<YTItem>()\n                    YouTube.artist(it.id).onSuccess { page ->\n                        page.sections.takeLast(3).forEach { section -> items += section.items }\n                    }\n                    SimilarRecommendation(\n                        title = it,\n                        items = items\n                            .distinctBy { item -> item.id }\n                            .filterExplicit(hideExplicit)\n                            .filterVideoSongs(hideVideoSongs)\n                            .shuffled().take(12)\n                            .ifEmpty { return@mapNotNull null }\n                    )\n                }\n\n            val songRecommendations = database.mostPlayedSongs(fromTimeStamp, limit = 15).first()\n                .filter { it.album != null }\n                .shuffled().take(3)\n                .mapNotNull { song ->\n                    val endpoint = YouTube.next(WatchEndpoint(videoId = song.id)).getOrNull()?.relatedEndpoint\n                        ?: return@mapNotNull null\n                    val page = YouTube.related(endpoint).getOrNull() ?: return@mapNotNull null\n                    SimilarRecommendation(\n                        title = song,\n                        items = (page.songs.shuffled().take(10) +\n                                page.albums.shuffled().take(5) +\n                                page.artists.shuffled().take(3) +\n                                page.playlists.shuffled().take(3))\n                            .distinctBy { it.id }\n                            .filterExplicit(hideExplicit)\n                            .filterVideoSongs(hideVideoSongs)\n                            .shuffled()\n                            .ifEmpty { return@mapNotNull null }\n                    )\n                }\n\n            val albumRecommendations = database.mostPlayedAlbums(fromTimeStamp, limit = 10).first()\n                .filter { it.album.thumbnailUrl != null }\n                .shuffled().take(2)\n                .mapNotNull { album ->\n                    val items = mutableListOf<YTItem>()\n                    YouTube.album(album.id).onSuccess { page ->\n                        page.otherVersions.let { items += it }\n                    }\n                    album.artists.firstOrNull()?.id?.let { artistId ->\n                        YouTube.artist(artistId).onSuccess { page ->\n                            page.sections.lastOrNull()?.items?.let { items += it }\n                        }\n                    }\n                    SimilarRecommendation(\n                        title = album,\n                        items = items\n                            .distinctBy { it.id }\n                            .filterExplicit(hideExplicit)\n                            .filterVideoSongs(hideVideoSongs)\n                            .shuffled().take(10)\n                            .ifEmpty { return@mapNotNull null }\n                    )\n                }\n\n            similarRecommendations.value = (artistRecommendations + songRecommendations + albumRecommendations).shuffled()\n            allYtItems.value = similarRecommendations.value?.flatMap { it.items }.orEmpty() +\n                    homePage.value?.sections?.flatMap { it.items }.orEmpty()\n        }\n    }\n\n    private val _isLoadingMore = MutableStateFlow(false)\n    fun loadMoreYouTubeItems(continuation: String?) {\n        if (continuation == null || _isLoadingMore.value) return\n        val hideExplicit = context.dataStore.get(HideExplicitKey, false)\n        val hideVideoSongs = context.dataStore.get(HideVideoSongsKey, false)\n        val hideYoutubeShorts = context.dataStore.get(HideYoutubeShortsKey, false)\n\n        viewModelScope.launch(Dispatchers.IO) {\n            _isLoadingMore.value = true\n            val nextSections = YouTube.home(continuation).getOrNull() ?: run {\n                _isLoadingMore.value = false\n                return@launch\n            }\n\n            homePage.value = nextSections.copy(\n                chips = homePage.value?.chips,\n                sections = (homePage.value?.sections.orEmpty() + nextSections.sections).mapNotNull { section ->\n                    val filteredItems = section.items.filterExplicit(hideExplicit).filterVideoSongs(hideVideoSongs).filterYoutubeShorts(hideYoutubeShorts)\n                    if (filteredItems.isEmpty()) null else section.copy(items = filteredItems)\n                }\n            )\n            _isLoadingMore.value = false\n        }\n    }\n\n    fun toggleChip(chip: HomePage.Chip?) {\n        if (chip == null || chip == selectedChip.value && previousHomePage.value != null) {\n            homePage.value = previousHomePage.value\n            previousHomePage.value = null\n            selectedChip.value = null\n            return\n        }\n\n        if (selectedChip.value == null) {\n            previousHomePage.value = homePage.value\n        }\n\n        viewModelScope.launch(Dispatchers.IO) {\n            val hideExplicit = context.dataStore.get(HideExplicitKey, false)\n            val hideVideoSongs = context.dataStore.get(HideVideoSongsKey, false)\n            val hideYoutubeShorts = context.dataStore.get(HideYoutubeShortsKey, false)\n            val nextSections = YouTube.home(params = chip.endpoint?.params).getOrNull() ?: return@launch\n\n            homePage.value = nextSections.copy(\n                chips = homePage.value?.chips,\n                sections = nextSections.sections.map { section ->\n                    section.copy(items = section.items.filterExplicit(hideExplicit).filterVideoSongs(hideVideoSongs).filterYoutubeShorts(hideYoutubeShorts))\n                }\n            )\n            selectedChip.value = chip\n\n            // Fetch podcast-specific data when podcasts chip is selected\n            if (chip.title.contains(\"Podcast\", ignoreCase = true)) {\n                fetchPodcastData()\n            }\n        }\n    }\n\n    private suspend fun fetchPodcastData() {\n        // Fetch saved podcast shows from official API\n        YouTube.savedPodcastShows().onSuccess { shows ->\n            savedPodcastShows.value = shows\n        }.onFailure {\n            reportException(it)\n        }\n\n        // Fetch episodes for later from official API\n        YouTube.episodesForLater().onSuccess { episodes ->\n            episodesForLater.value = episodes\n        }.onFailure {\n            reportException(it)\n        }\n    }\n\n    private suspend fun loadAccountPlaylists() {\n        val hideYoutubeShorts = context.dataStore.get(HideYoutubeShortsKey, false)\n        YouTube.library(\"FEmusic_liked_playlists\").completed().onSuccess {\n            accountPlaylists.value = it.items.filterIsInstance<PlaylistItem>()\n                .filterNot { it.id == \"SE\" }\n                .filterYoutubeShorts(hideYoutubeShorts)\n        }.onFailure {\n            reportException(it)\n        }\n    }\n\n    fun refresh() {\n        if (isRefreshing.value) return\n        isRefreshing.value = true\n        viewModelScope.launch(Dispatchers.IO) {\n            // If a chip is selected, reload the chip's content instead of the default home\n            val currentChip = selectedChip.value\n            if (currentChip != null) {\n                val hideExplicit = context.dataStore.get(HideExplicitKey, false)\n                val hideVideoSongs = context.dataStore.get(HideVideoSongsKey, false)\n                val hideYoutubeShorts = context.dataStore.get(HideYoutubeShortsKey, false)\n                val nextSections = YouTube.home(params = currentChip.endpoint?.params).getOrNull()\n                if (nextSections != null) {\n                    homePage.value = nextSections.copy(\n                        chips = homePage.value?.chips,\n                        sections = nextSections.sections.map { section ->\n                            section.copy(items = section.items.filterExplicit(hideExplicit).filterVideoSongs(hideVideoSongs).filterYoutubeShorts(hideYoutubeShorts))\n                        }\n                    )\n                }\n            } else {\n                load()\n            }\n            isRefreshing.value = false\n        }\n        // Run sync when user manually refreshes\n        viewModelScope.launch(Dispatchers.IO) {\n            syncUtils.tryAutoSync()\n        }\n    }\n\n    init {\n        // Load home data\n        viewModelScope.launch(Dispatchers.IO) {\n            context.dataStore.data\n                .map { it[InnerTubeCookieKey] }\n                .distinctUntilChanged()\n                .first()\n\n            load()\n        }\n\n        // Run sync in separate coroutine with cooldown to avoid blocking UI\n        viewModelScope.launch(Dispatchers.IO) {\n            syncUtils.tryAutoSync()\n        }\n\n        // Prepare wrapped data in background\n        viewModelScope.launch(Dispatchers.IO) {\n            showWrappedCard.collect { shouldShow ->\n                if (shouldShow && !wrappedManager.state.value.isDataReady) {\n                    try {\n                        wrappedManager.prepare()\n                        val state = wrappedManager.state.first { it.isDataReady }\n                        val trackMap = state.trackMap\n                        if (trackMap.isNotEmpty()) {\n                            val firstTrackId = trackMap.entries.first().value\n                            wrappedAudioService.prepareTrack(firstTrackId)\n                        }\n                    } catch (e: Exception) {\n                        reportException(e)\n                    }\n                }\n            }\n        }\n\n        // Listen for cookie changes and reload account data\n        viewModelScope.launch(Dispatchers.IO) {\n            context.dataStore.data\n                .map { it[InnerTubeCookieKey] }\n                .collect { cookie ->\n                    // Avoid processing if already processing\n                    if (isProcessingAccountData) return@collect\n\n                    // Always process cookie changes, even if same value (for logout/login scenarios)\n                    lastProcessedCookie = cookie\n                    isProcessingAccountData = true\n\n                    try {\n                        if (cookie != null && cookie.isNotEmpty()) {\n\n                            // Update YouTube.cookie manually to ensure it's set\n                            YouTube.cookie = cookie\n\n                            // Fetch new account data\n                            YouTube.accountInfo().onSuccess { info ->\n                                accountName.value = info.name\n                                accountImageUrl.value = info.thumbnailUrl\n                            }.onFailure {\n                                reportException(it)\n                            }\n                        } else {\n                            accountName.value = \"Guest\"\n                            accountImageUrl.value = null\n                            accountPlaylists.value = null\n                        }\n                    } finally {\n                        isProcessingAccountData = false\n                    }\n                }\n        }\n\n        // Listen for HideYoutubeShorts preference changes and reload account playlists instantly\n        viewModelScope.launch(Dispatchers.IO) {\n            context.dataStore.data\n                .map { it[HideYoutubeShortsKey] ?: false }\n                .distinctUntilChanged()\n                .collect {\n                    if (YouTube.cookie != null && accountPlaylists.value != null) {\n                        loadAccountPlaylists()\n                    }\n                }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/LibraryViewModels.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\n@file:OptIn(ExperimentalCoroutinesApi::class)\n\npackage com.metrolist.music.viewmodels\n\nimport android.content.Context\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.ArtistItem\nimport com.metrolist.innertube.utils.completed\nimport com.metrolist.music.constants.AlbumFilter\nimport com.metrolist.music.constants.AlbumFilterKey\nimport com.metrolist.music.constants.AlbumSortDescendingKey\nimport com.metrolist.music.constants.AlbumSortType\nimport com.metrolist.music.constants.AlbumSortTypeKey\nimport com.metrolist.music.constants.ArtistFilter\nimport com.metrolist.music.constants.ArtistFilterKey\nimport com.metrolist.music.constants.ArtistSongSortDescendingKey\nimport com.metrolist.music.constants.ArtistSongSortType\nimport com.metrolist.music.constants.ArtistSongSortTypeKey\nimport com.metrolist.music.constants.ArtistSortDescendingKey\nimport com.metrolist.music.constants.ArtistSortType\nimport com.metrolist.music.constants.ArtistSortTypeKey\nimport com.metrolist.music.constants.HideExplicitKey\nimport com.metrolist.music.constants.HideVideoSongsKey\nimport com.metrolist.music.constants.HideYoutubeShortsKey\nimport com.metrolist.music.constants.LibraryFilter\nimport com.metrolist.music.constants.PlaylistSortDescendingKey\nimport com.metrolist.music.constants.PlaylistSortType\nimport com.metrolist.music.constants.PlaylistSortTypeKey\nimport com.metrolist.music.constants.SongFilter\nimport com.metrolist.music.constants.SongFilterKey\nimport com.metrolist.music.constants.SongSortDescendingKey\nimport com.metrolist.music.constants.SongSortType\nimport com.metrolist.music.constants.SongSortTypeKey\nimport com.metrolist.music.constants.TopSize\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.extensions.filterExplicit\nimport com.metrolist.music.extensions.filterExplicitAlbums\nimport com.metrolist.music.extensions.filterVideoSongs\nimport com.metrolist.music.extensions.filterYoutubeShorts\nimport com.metrolist.music.extensions.toEnum\nimport com.metrolist.music.playback.DownloadUtil\nimport com.metrolist.music.utils.PodcastRefreshTrigger\nimport com.metrolist.music.utils.SyncUtils\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.reportException\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.launch\nimport java.time.Duration\nimport java.time.LocalDateTime\nimport javax.inject.Inject\n\n@HiltViewModel\nclass LibrarySongsViewModel\n@Inject\nconstructor(\n    @ApplicationContext context: Context,\n    database: MusicDatabase,\n    downloadUtil: DownloadUtil,\n    private val syncUtils: SyncUtils,\n) : ViewModel() {\n    val allSongs =\n        context.dataStore.data\n            .map {\n                Triple(\n                    Triple(\n                        it[SongFilterKey].toEnum(SongFilter.LIKED),\n                        it[SongSortTypeKey].toEnum(SongSortType.CREATE_DATE),\n                        (it[SongSortDescendingKey] ?: true),\n                    ),\n                    it[HideExplicitKey] ?: false,\n                    it[HideVideoSongsKey] ?: false\n                )\n            }.distinctUntilChanged()\n            .flatMapLatest { (filterSort, hideExplicit, hideVideoSongs) ->\n                val (filter, sortType, descending) = filterSort\n                when (filter) {\n                    SongFilter.LIBRARY -> database.songs(sortType, descending).map { it.filterExplicit(hideExplicit).filterVideoSongs(hideVideoSongs) }\n                    SongFilter.LIKED -> database.likedSongs(sortType, descending).map { it.filterExplicit(hideExplicit).filterVideoSongs(hideVideoSongs) }\n                    SongFilter.DOWNLOADED -> database.downloadedSongs(sortType, descending).map { it.filterExplicit(hideExplicit).filterVideoSongs(hideVideoSongs) }\n                    SongFilter.UPLOADED -> database.uploadedSongs(sortType, descending).map { it.filterExplicit(hideExplicit).filterVideoSongs(hideVideoSongs) }\n                }\n            }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n\n    fun syncLikedSongs() {\n        viewModelScope.launch(Dispatchers.IO) { syncUtils.syncLikedSongs() }\n    }\n\n    fun syncLibrarySongs() {\n        viewModelScope.launch(Dispatchers.IO) { syncUtils.syncLibrarySongs() }\n    }\n\n    fun syncUploadedSongs() {\n        viewModelScope.launch(Dispatchers.IO) { syncUtils.syncUploadedSongs() }\n    }\n}\n\n@HiltViewModel\nclass LibraryArtistsViewModel\n@Inject\nconstructor(\n    @ApplicationContext context: Context,\n    database: MusicDatabase,\n    private val syncUtils: SyncUtils,\n) : ViewModel() {\n    val allArtists =\n        context.dataStore.data\n            .map {\n                Triple(\n                    it[ArtistFilterKey].toEnum(ArtistFilter.LIKED),\n                    it[ArtistSortTypeKey].toEnum(ArtistSortType.CREATE_DATE),\n                    it[ArtistSortDescendingKey] ?: true,\n                )\n            }.distinctUntilChanged()\n            .flatMapLatest { (filter, sortType, descending) ->\n                when (filter) {\n                    ArtistFilter.LIKED -> database.artistsBookmarked(sortType, descending)\n                    ArtistFilter.LIBRARY -> database.artists(sortType, descending)\n                }\n            }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n\n    fun sync() {\n        viewModelScope.launch(Dispatchers.IO) { syncUtils.syncArtistsSubscriptions() }\n    }\n\n    init {\n        viewModelScope.launch(Dispatchers.IO) {\n            allArtists.collect { artists ->\n                artists\n                    .map { it.artist }\n                    .filter {\n                        it.thumbnailUrl == null || Duration.between(\n                            it.lastUpdateTime,\n                            LocalDateTime.now()\n                        ) > Duration.ofDays(10)\n                    }.forEach { artist ->\n                        YouTube.artist(artist.id).onSuccess { artistPage ->\n                            database.query {\n                                update(artist, artistPage)\n                            }\n                        }\n                    }\n            }\n        }\n    }\n}\n\n@HiltViewModel\nclass LibraryAlbumsViewModel\n@Inject\nconstructor(\n    @ApplicationContext context: Context,\n    database: MusicDatabase,\n    private val syncUtils: SyncUtils,\n) : ViewModel() {\n    val allAlbums =\n        context.dataStore.data\n            .map {\n                Pair(\n                    Triple(\n                        it[AlbumFilterKey].toEnum(AlbumFilter.LIKED),\n                        it[AlbumSortTypeKey].toEnum(AlbumSortType.CREATE_DATE),\n                        it[AlbumSortDescendingKey] ?: true,\n                    ),\n                    it[HideExplicitKey] ?: false\n                )\n            }.distinctUntilChanged()\n            .flatMapLatest { (filterSort, hideExplicit) ->\n                val (filter, sortType, descending) = filterSort\n                when (filter) {\n                    AlbumFilter.LIKED -> database.albumsLiked(sortType, descending).map { it.filterExplicitAlbums(hideExplicit) }\n                    AlbumFilter.LIBRARY -> database.albums(sortType, descending).map { it.filterExplicitAlbums(hideExplicit) }\n                    AlbumFilter.UPLOADED -> database.albumsUploaded(sortType, descending).map { it.filterExplicitAlbums(hideExplicit) }\n                }\n            }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n\n    fun sync() {\n        viewModelScope.launch(Dispatchers.IO) { syncUtils.syncLikedAlbums() }\n    }\n\n    init {\n        viewModelScope.launch(Dispatchers.IO) {\n            allAlbums.collect { albums ->\n                albums\n                    .filter {\n                        it.album.songCount == 0\n                    }.forEach { album ->\n                        YouTube\n                            .album(album.id)\n                            .onSuccess { albumPage ->\n                                database.query {\n                                    update(album.album, albumPage, album.artists)\n                                }\n                            }.onFailure {\n                                reportException(it)\n                                if (it.message?.contains(\"NOT_FOUND\") == true) {\n                                    database.query {\n                                        delete(album.album)\n                                    }\n                                }\n                            }\n                    }\n            }\n        }\n    }\n}\n\n@HiltViewModel\nclass LibraryPlaylistsViewModel\n@Inject\nconstructor(\n    @ApplicationContext context: Context,\n    database: MusicDatabase,\n    private val syncUtils: SyncUtils,\n) : ViewModel() {\n    val allPlaylists =\n        context.dataStore.data\n            .map {\n                Triple(\n                    it[PlaylistSortTypeKey].toEnum(PlaylistSortType.CREATE_DATE),\n                    it[PlaylistSortDescendingKey] ?: true,\n                    it[HideYoutubeShortsKey] ?: false\n                )\n            }.distinctUntilChanged()\n            .flatMapLatest { (sortType, descending, hideYoutubeShorts) ->\n                database.playlists(sortType, descending).map { it.filterYoutubeShorts(hideYoutubeShorts) }\n            }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n\n    fun sync() {\n        viewModelScope.launch(Dispatchers.IO) { syncUtils.syncSavedPlaylists() }\n    }\n\n    val topValue =\n        context.dataStore.data\n            .map { it[TopSize] ?: \"50\" }\n            .distinctUntilChanged()\n}\n\n@HiltViewModel\nclass ArtistSongsViewModel\n@Inject\nconstructor(\n    @ApplicationContext context: Context,\n    database: MusicDatabase,\n    savedStateHandle: SavedStateHandle,\n) : ViewModel() {\n    private val artistId = savedStateHandle.get<String>(\"artistId\")!!\n    val artist =\n        database\n            .artist(artistId)\n            .stateIn(viewModelScope, SharingStarted.Lazily, null)\n\n    val songs =\n        context.dataStore.data\n            .map {\n                Triple(\n                    it[ArtistSongSortTypeKey].toEnum(ArtistSongSortType.CREATE_DATE) to (it[ArtistSongSortDescendingKey]\n                        ?: true),\n                    it[HideExplicitKey] ?: false,\n                    it[HideVideoSongsKey] ?: false\n                )\n            }.distinctUntilChanged()\n            .flatMapLatest { (sortDesc, hideExplicit, hideVideoSongs) ->\n                val (sortType, descending) = sortDesc\n                database.artistSongs(artistId, sortType, descending).map { it.filterExplicit(hideExplicit).filterVideoSongs(hideVideoSongs) }\n            }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n}\n\n@HiltViewModel\nclass LibraryMixViewModel\n@Inject\nconstructor(\n    @ApplicationContext context: Context,\n    database: MusicDatabase,\n    private val syncUtils: SyncUtils,\n) : ViewModel() {\n    private val _isRefreshing = MutableStateFlow(false)\n    val isRefreshing = _isRefreshing.asStateFlow()\n\n    val syncAllLibrary = {\n         viewModelScope.launch(Dispatchers.IO) {\n             syncUtils.tryAutoSync()\n         }\n    }\n\n    fun refresh() {\n        viewModelScope.launch(Dispatchers.IO) {\n            _isRefreshing.value = true\n            syncUtils.performFullSyncSuspend()\n            _isRefreshing.value = false\n        }\n    }\n\n    val topValue =\n        context.dataStore.data\n            .map { it[TopSize] ?: \"50\" }\n            .distinctUntilChanged()\n    var artists =\n        database\n            .artistsBookmarked(\n                ArtistSortType.CREATE_DATE,\n                true,\n            ).stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n    var albums = context.dataStore.data\n        .map { it[HideExplicitKey] ?: false }\n        .distinctUntilChanged()\n        .flatMapLatest { hideExplicit ->\n            database.albumsLiked(AlbumSortType.CREATE_DATE, true).map { it.filterExplicitAlbums(hideExplicit) }\n        }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n    var playlists = context.dataStore.data\n        .map { it[HideYoutubeShortsKey] ?: false }\n        .distinctUntilChanged()\n        .flatMapLatest { hideYoutubeShorts ->\n            database.playlists(PlaylistSortType.CREATE_DATE, true).map { it.filterYoutubeShorts(hideYoutubeShorts) }\n        }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n\n    init {\n        viewModelScope.launch(Dispatchers.IO) {\n            albums.collect { albums ->\n                albums\n                    .filter {\n                        it.album.songCount == 0\n                    }.forEach { album ->\n                        YouTube\n                            .album(album.id)\n                            .onSuccess { albumPage ->\n                                database.query {\n                                    update(album.album, albumPage, album.artists)\n                                }\n                            }.onFailure {\n                                reportException(it)\n                                if (it.message?.contains(\"NOT_FOUND\") == true) {\n                                    database.query {\n                                        delete(album.album)\n                                    }\n                                }\n                            }\n                    }\n            }\n        }\n        viewModelScope.launch(Dispatchers.IO) {\n            artists.collect { artists ->\n                artists\n                    .map { it.artist }\n                    .filter {\n                        it.thumbnailUrl == null ||\n                                Duration.between(\n                                    it.lastUpdateTime,\n                                    LocalDateTime.now(),\n                                ) > Duration.ofDays(10)\n                    }.forEach { artist ->\n                        YouTube.artist(artist.id).onSuccess { artistPage ->\n                            database.query {\n                                update(artist, artistPage)\n                            }\n                        }\n                    }\n            }\n        }\n    }\n}\n\n@HiltViewModel\nclass LibraryPodcastsViewModel\n@Inject\nconstructor(\n    @ApplicationContext context: Context,\n    private val database: MusicDatabase,\n    private val syncUtils: SyncUtils,\n) : ViewModel() {\n    // Subscribed podcast channels synced from YT Music\n    val subscribedChannels = database.subscribedPodcasts()\n        .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())\n\n    // SE \"Episodes for Later\" playlist fetched from YT Music (like AccountScreen)\n    private val _sePlaylist = MutableStateFlow<com.metrolist.innertube.models.PlaylistItem?>(null)\n    val sePlaylist = _sePlaylist.asStateFlow()\n\n    // RDPN \"New Episodes\" playlist fetched from YouTube Music (real thumbnail + episode count)\n    private val _rdpnPlaylist = MutableStateFlow<com.metrolist.innertube.models.PlaylistItem?>(null)\n    val rdpnPlaylist = _rdpnPlaylist.asStateFlow()\n\n    // Podcast host channels fetched from YT Music library/podcast_channels\n    private val _apiPodcastChannels = MutableStateFlow<List<ArtistItem>>(emptyList())\n\n    // Podcast channels: API subscriptions + locally bookmarked artists that have podcasts\n    // Only shows channels explicitly subscribed to (not derived from saved podcasts)\n    val podcastChannels = kotlinx.coroutines.flow.combine(\n        _apiPodcastChannels,\n        database.bookmarkedPodcastChannels()\n    ) { apiChannels, localPodcastChannels ->\n        // Convert locally bookmarked podcast channels to ArtistItem format\n        val localAsArtistItems = localPodcastChannels.map { artist ->\n            ArtistItem(\n                id = artist.id,\n                title = artist.artist.name,\n                thumbnail = artist.artist.thumbnailUrl,\n                shuffleEndpoint = null,\n                radioEndpoint = null,\n            )\n        }\n\n        // Combine and deduplicate by ID (prefer API version if exists)\n        val apiIds = apiChannels.map { it.id }.toSet()\n        val uniqueLocalChannels = localAsArtistItems.filter { it.id !in apiIds }\n        apiChannels + uniqueLocalChannels\n    }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())\n\n    // Downloaded podcast episodes\n    val downloadedEpisodes =\n        context.dataStore.data\n            .map {\n                Pair(\n                    it[SongSortTypeKey].toEnum(SongSortType.CREATE_DATE) to (it[SongSortDescendingKey] ?: true),\n                    it[HideExplicitKey] ?: false\n                )\n            }.distinctUntilChanged()\n            .flatMapLatest { (sortDesc, hideExplicit) ->\n                val (sortType, descending) = sortDesc\n                database.downloadedPodcastEpisodes(sortType, descending).map { it.filterExplicit(hideExplicit) }\n            }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n\n    // Saved podcast episodes (in library, not necessarily downloaded)\n    val savedEpisodes =\n        context.dataStore.data\n            .map {\n                Pair(\n                    it[SongSortTypeKey].toEnum(SongSortType.CREATE_DATE) to (it[SongSortDescendingKey] ?: true),\n                    it[HideExplicitKey] ?: false\n                )\n            }.distinctUntilChanged()\n            .flatMapLatest { (sortDesc, hideExplicit) ->\n                val (sortType, descending) = sortDesc\n                database.savedPodcastEpisodes(sortType, descending).map { it.filterExplicit(hideExplicit) }\n            }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())\n\n    private suspend fun fetchSePlaylist() {\n        YouTube.library(\"FEmusic_liked_playlists\").completed().onSuccess {\n            _sePlaylist.value = it.items\n                .filterIsInstance<com.metrolist.innertube.models.PlaylistItem>()\n                .find { it.id == \"SE\" }\n        }.onFailure {\n            timber.log.Timber.e(it, \"[PODCAST] Failed to fetch SE playlist\")\n        }\n    }\n\n    private suspend fun fetchPodcastChannels() {\n        YouTube.libraryPodcastChannels().onSuccess { page ->\n            val channels = page.items.filterIsInstance<ArtistItem>()\n            _apiPodcastChannels.value = channels\n            timber.log.Timber.d(\"[PODCAST] Fetched ${channels.size} podcast channels from YT Music\")\n        }.onFailure {\n            timber.log.Timber.e(it, \"[PODCAST] Failed to fetch podcast channels\")\n        }\n    }\n\n    private suspend fun fetchRdpnPlaylist() {\n        YouTube.newEpisodesPlaylistInfo().onSuccess { item ->\n            _rdpnPlaylist.value = item\n            timber.log.Timber.d(\"[PODCAST] RDPN playlist: ${item.title}, thumbnail: ${item.thumbnail}\")\n        }.onFailure {\n            timber.log.Timber.e(it, \"[PODCAST] Failed to fetch RDPN playlist info\")\n        }\n    }\n\n    init {\n        viewModelScope.launch(Dispatchers.IO) {\n            fetchSePlaylist()\n        }\n        viewModelScope.launch(Dispatchers.IO) {\n            fetchPodcastChannels()\n        }\n        viewModelScope.launch(Dispatchers.IO) {\n            fetchRdpnPlaylist()\n        }\n        viewModelScope.launch(Dispatchers.IO) {\n            syncUtils.syncPodcastSubscriptionsSuspend()\n        }\n        // Observe refresh trigger for auto-refresh after subscribe/unsubscribe\n        viewModelScope.launch(Dispatchers.IO) {\n            PodcastRefreshTrigger.refreshFlow.collect {\n                // Small delay to allow YouTube's backend to update\n                kotlinx.coroutines.delay(1500)\n                fetchPodcastChannels()\n            }\n        }\n    }\n\n    fun clearPodcastData() {\n        viewModelScope.launch(Dispatchers.IO) {\n            syncUtils.clearPodcastData()\n        }\n    }\n\n    suspend fun refreshAll() {\n        fetchSePlaylist()\n        fetchPodcastChannels()\n        fetchRdpnPlaylist()\n        syncUtils.syncPodcastSubscriptionsSuspend()\n        syncUtils.syncEpisodesForLaterSuspend()\n    }\n\n    /**\n     * Force refresh podcast channels. Called when screen becomes visible.\n     */\n    fun refreshChannels() {\n        viewModelScope.launch(Dispatchers.IO) {\n            fetchPodcastChannels()\n        }\n    }\n}\n\n@HiltViewModel\nclass LibraryViewModel\n@Inject\nconstructor() : ViewModel() {\n    private val curScreen = mutableStateOf(LibraryFilter.LIBRARY)\n    val filter: MutableState<LibraryFilter> = curScreen\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/ListenTogetherViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport androidx.lifecycle.ViewModel\nimport com.metrolist.music.listentogether.ListenTogetherManager\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport javax.inject.Inject\n\n@HiltViewModel\nclass ListenTogetherViewModel @Inject constructor(\n    private val manager: ListenTogetherManager\n) : ViewModel() {\n\n    val connectionState = manager.connectionState\n    val roomState = manager.roomState\n    val role = manager.role\n    val userId = manager.userId\n    val pendingJoinRequests = manager.pendingJoinRequests\n    val bufferingUsers = manager.bufferingUsers\n    val logs = manager.logs\n    val events = manager.events\n    val hasPersistedSession = manager.hasPersistedSession\n    val blockedUsernames = manager.blockedUsernames\n\n    init {\n        manager.initialize()\n    }\n\n    fun connect() {\n        manager.connect()\n    }\n\n    fun disconnect() {\n        manager.disconnect()\n    }\n\n    fun createRoom(username: String) {\n        manager.createRoom(username)\n    }\n\n    fun joinRoom(roomCode: String, username: String) {\n        manager.joinRoom(roomCode, username)\n    }\n\n    fun leaveRoom() {\n        manager.leaveRoom()\n    }\n\n    fun approveJoin(userId: String) {\n        manager.approveJoin(userId)\n    }\n\n    fun rejectJoin(userId: String, reason: String? = null) {\n        manager.rejectJoin(userId, reason)\n    }\n\n    fun kickUser(userId: String, reason: String? = null) {\n        manager.kickUser(userId, reason)\n    }\n\n    fun blockUser(username: String) {\n        manager.blockUser(username)\n    }\n\n    fun unblockUser(username: String) {\n        manager.unblockUser(username)\n    }\n\n    fun clearLogs() {\n        manager.clearLogs()\n    }\n\n    fun forceReconnect() {\n        manager.forceReconnect()\n    }\n    \n    fun reconnect() {\n        manager.forceReconnect()\n    }\n    \n    fun getPersistedRoomCode(): String? = manager.getPersistedRoomCode()\n    \n    fun getSessionAge(): Long = manager.getSessionAge()\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/LocalPlaylistViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport android.content.Context\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.music.constants.HideVideoSongsKey\nimport com.metrolist.music.constants.PlaylistSongSortDescendingKey\nimport com.metrolist.music.constants.PlaylistSongSortType\nimport com.metrolist.music.constants.PlaylistSongSortTypeKey\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.db.entities.PlaylistSong\nimport com.metrolist.music.extensions.reversed\nimport com.metrolist.music.extensions.toEnum\nimport com.metrolist.music.utils.dataStore\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.launch\nimport java.text.Collator\nimport java.util.Locale\nimport javax.inject.Inject\n\n@HiltViewModel\nclass LocalPlaylistViewModel\n@Inject\nconstructor(\n    @ApplicationContext context: Context,\n    database: MusicDatabase,\n    savedStateHandle: SavedStateHandle,\n) : ViewModel() {\n    val playlistId = savedStateHandle.get<String>(\"playlistId\")!!\n    val playlist =\n        database\n            .playlist(playlistId)\n            .stateIn(viewModelScope, SharingStarted.Lazily, null)\n    val playlistSongs: StateFlow<List<PlaylistSong>> =\n        combine(\n            database.playlistSongs(playlistId),\n            context.dataStore.data\n                .map {\n                    Triple(\n                        it[PlaylistSongSortTypeKey].toEnum(PlaylistSongSortType.CUSTOM),\n                        it[PlaylistSongSortDescendingKey] ?: true,\n                        it[HideVideoSongsKey] ?: false\n                    )\n                }.distinctUntilChanged(),\n        ) { songs, (sortType, sortDescending, hideVideoSongs) ->\n            val filteredSongs = if (hideVideoSongs) {\n                songs.filter { !it.song.song.isVideo }\n            } else {\n                songs\n            }\n            when (sortType) {\n                PlaylistSongSortType.CUSTOM -> filteredSongs\n                PlaylistSongSortType.CREATE_DATE -> filteredSongs.sortedBy { it.map.id }\n                PlaylistSongSortType.NAME -> {\n                    val collator = Collator.getInstance(Locale.getDefault())\n                    collator.strength = Collator.PRIMARY\n                    filteredSongs.sortedWith(compareBy(collator) { it.song.song.title })\n                }\n                PlaylistSongSortType.ARTIST -> {\n                    val collator = Collator.getInstance(Locale.getDefault())\n                    collator.strength = Collator.PRIMARY\n                    filteredSongs\n                        .sortedWith(compareBy(collator) { song -> song.song.artists.joinToString(\"\") { it.name } })\n                        .groupBy { it.song.album?.title }\n                        .flatMap { (_, songsByAlbum) ->\n                            songsByAlbum.sortedBy {\n                                it.song.artists.joinToString(\n                                    \"\"\n                                ) { it.name }\n                            }\n                        }\n                }\n\n                PlaylistSongSortType.PLAY_TIME -> filteredSongs.sortedBy { it.song.song.totalPlayTime }\n            }.reversed(sortDescending && sortType != PlaylistSongSortType.CUSTOM)\n        }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n\n    init {\n        viewModelScope.launch {\n            val sortedSongs =\n                playlistSongs.first().sortedWith(compareBy({ it.map.position }, { it.map.id }))\n            database.transaction {\n                sortedSongs.forEachIndexed { index, playlistSong ->\n                    if (playlistSong.map.position != index) {\n                        update(playlistSong.map.copy(position = index))\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/LocalSearchViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport android.content.Context\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.music.constants.HideVideoSongsKey\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.db.entities.Album\nimport com.metrolist.music.db.entities.Artist\nimport com.metrolist.music.db.entities.LocalItem\nimport com.metrolist.music.db.entities.Playlist\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.utils.dataStore\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.flow.flowOf\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.stateIn\nimport javax.inject.Inject\n\n@OptIn(ExperimentalCoroutinesApi::class)\n@HiltViewModel\nclass LocalSearchViewModel\n@Inject\nconstructor(\n    @ApplicationContext context: Context,\n    database: MusicDatabase,\n) : ViewModel() {\n    val query = MutableStateFlow(\"\")\n    val filter = MutableStateFlow(LocalFilter.ALL)\n\n    val result =\n        combine(\n            query,\n            filter,\n            context.dataStore.data.map { it[HideVideoSongsKey] ?: false }.distinctUntilChanged()\n        ) { query, filter, hideVideoSongs ->\n            Triple(query, filter, hideVideoSongs)\n        }.flatMapLatest { (query, filter, hideVideoSongs) ->\n            if (query.isEmpty()) {\n                flowOf(LocalSearchResult(\"\", filter, emptyMap()))\n            } else {\n                when (filter) {\n                    LocalFilter.ALL ->\n                        combine(\n                            database.searchSongs(query, PREVIEW_SIZE),\n                            database.searchAlbums(query, PREVIEW_SIZE),\n                            database.searchArtists(query, PREVIEW_SIZE),\n                            database.searchPlaylists(query, PREVIEW_SIZE),\n                        ) { songs, albums, artists, playlists ->\n                            val filteredSongs = if (hideVideoSongs) songs.filter { !it.song.isVideo } else songs\n                            filteredSongs + albums + artists + playlists\n                        }\n\n                    LocalFilter.SONG -> database.searchSongs(query).map { songs ->\n                        if (hideVideoSongs) songs.filter { !it.song.isVideo } else songs\n                    }\n                    LocalFilter.ALBUM -> database.searchAlbums(query)\n                    LocalFilter.ARTIST -> database.searchArtists(query)\n                    LocalFilter.PLAYLIST -> database.searchPlaylists(query)\n                }.map { list ->\n                    LocalSearchResult(\n                        query = query,\n                        filter = filter,\n                        map =\n                        list.groupBy {\n                            when (it) {\n                                is Song -> LocalFilter.SONG\n                                is Album -> LocalFilter.ALBUM\n                                is Artist -> LocalFilter.ARTIST\n                                is Playlist -> LocalFilter.PLAYLIST\n                            }\n                        },\n                    )\n                }\n            }\n        }.stateIn(\n            viewModelScope,\n            SharingStarted.Lazily,\n            LocalSearchResult(\"\", filter.value, emptyMap())\n        )\n\n    companion object {\n        const val PREVIEW_SIZE = 3\n    }\n}\n\nenum class LocalFilter {\n    ALL,\n    SONG,\n    ALBUM,\n    ARTIST,\n    PLAYLIST,\n}\n\ndata class LocalSearchResult(\n    val query: String,\n    val filter: LocalFilter,\n    val map: Map<LocalFilter, List<LocalItem>>,\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/LyricsMenuViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport androidx.compose.runtime.State\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.db.entities.LyricsEntity\nimport com.metrolist.music.db.entities.Song\nimport com.metrolist.music.lyrics.LyricsHelper\nimport com.metrolist.music.lyrics.LyricsResult\nimport com.metrolist.music.models.MediaMetadata\nimport com.metrolist.music.utils.NetworkConnectivityObserver\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.runBlocking\nimport javax.inject.Inject\n\n@HiltViewModel\nclass LyricsMenuViewModel\n@Inject\nconstructor(\n    private val lyricsHelper: LyricsHelper,\n    val database: MusicDatabase,\n    private val networkConnectivity: NetworkConnectivityObserver,\n) : ViewModel() {\n    private var job: Job? = null\n    val results = MutableStateFlow(emptyList<LyricsResult>())\n    val isLoading = MutableStateFlow(false)\n\n    private val _isNetworkAvailable = MutableStateFlow(false)\n    val isNetworkAvailable: StateFlow<Boolean> = _isNetworkAvailable.asStateFlow()\n\n    private val _currentSong = mutableStateOf<Song?>(null)\n    val currentSong: State<Song?> = _currentSong\n\n    init {\n        viewModelScope.launch {\n            networkConnectivity.networkStatus.collect { isConnected ->\n                _isNetworkAvailable.value = isConnected\n            }\n        }\n\n        _isNetworkAvailable.value = try {\n            networkConnectivity.isCurrentlyConnected()\n        } catch (e: Exception) {\n            true // Assume connected as fallback\n        }\n    }\n\n    fun setCurrentSong(song: Song) {\n        _currentSong.value = song\n    }\n\n    fun search(\n        mediaId: String,\n        title: String,\n        artist: String,\n        duration: Int,\n        album: String? = null,\n    ) {\n        isLoading.value = true\n        results.value = emptyList()\n        job?.cancel()\n        job =\n            viewModelScope.launch(Dispatchers.IO) {\n                lyricsHelper.getAllLyrics(mediaId, title, artist, duration, album) { result ->\n                    results.update {\n                        it + result\n                    }\n                }\n                isLoading.value = false\n            }\n    }\n\n    fun cancelSearch() {\n        job?.cancel()\n        job = null\n    }\n\n    fun refetchLyrics(\n        mediaMetadata: MediaMetadata,\n        lyricsEntity: LyricsEntity?,\n    ) {\n        database.query {\n            lyricsEntity?.let(::delete)\n            val lyricsWithProvider =\n                runBlocking {\n                    lyricsHelper.getLyrics(mediaMetadata)\n                }\n            upsert(LyricsEntity(mediaMetadata.id, lyricsWithProvider.lyrics, lyricsWithProvider.provider))\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/MoodAndGenresViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.pages.MoodAndGenres\nimport com.metrolist.music.utils.reportException\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass MoodAndGenresViewModel\n@Inject\nconstructor() : ViewModel() {\n    val moodAndGenres = MutableStateFlow<List<MoodAndGenres>?>(null)\n\n    init {\n        viewModelScope.launch {\n            YouTube\n                .moodAndGenres()\n                .onSuccess {\n                    moodAndGenres.value = it\n                }.onFailure {\n                    reportException(it)\n                }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/NewReleaseViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport android.content.Context\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.filterExplicit\nimport com.metrolist.music.constants.HideExplicitKey\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\nimport com.metrolist.music.utils.reportException\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass NewReleaseViewModel\n@Inject\nconstructor(\n    @ApplicationContext val context: Context,\n    database: MusicDatabase,\n) : ViewModel() {\n    private val _newReleaseAlbums = MutableStateFlow<List<AlbumItem>>(emptyList())\n    val newReleaseAlbums = _newReleaseAlbums.asStateFlow()\n\n    init {\n        viewModelScope.launch {\n            YouTube\n                .newReleaseAlbums()\n                .onSuccess { albums ->\n                    val artists: MutableMap<Int, String> = mutableMapOf()\n                    val favouriteArtists: MutableMap<Int, String> = mutableMapOf()\n                    database.allArtistsByPlayTime().first().let { list ->\n                        var favIndex = 0\n                        for ((artistsIndex, artist) in list.withIndex()) {\n                            artists[artistsIndex] = artist.id\n                            if (artist.artist.bookmarkedAt != null) {\n                                favouriteArtists[favIndex] = artist.id\n                                favIndex++\n                            }\n                        }\n                    }\n                    _newReleaseAlbums.value =\n                        albums\n                            .sortedBy { album ->\n                                val artistIds = album.artists.orEmpty().mapNotNull { it.id }\n                                val firstArtistKey =\n                                    artistIds.firstNotNullOfOrNull { artistId ->\n                                        if (artistId in favouriteArtists.values) {\n                                            favouriteArtists.entries.firstOrNull { it.value == artistId }?.key\n                                        } else {\n                                            artists.entries.firstOrNull { it.value == artistId }?.key\n                                        }\n                                    } ?: Int.MAX_VALUE\n                                firstArtistKey\n                            }.filterExplicit(context.dataStore.get(HideExplicitKey, false))\n                }.onFailure {\n                    reportException(it)\n                }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/OnlinePlaylistViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport android.content.Context\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.filterVideoSongs\nimport com.metrolist.music.constants.HideVideoSongsKey\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\nimport com.metrolist.music.utils.reportException\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.firstOrNull\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\nimport com.metrolist.music.constants.SongSortType\nimport com.metrolist.innertube.models.Artist\nimport com.metrolist.innertube.models.Album\nimport javax.inject.Inject\n\n@HiltViewModel\nclass OnlinePlaylistViewModel @Inject constructor(\n    @ApplicationContext private val context: Context,\n    savedStateHandle: SavedStateHandle,\n    private val database: MusicDatabase\n) : ViewModel() {\n    private val playlistId = savedStateHandle.get<String>(\"playlistId\")!!\n\n    // Check if this is a special podcast playlist (with or without VL prefix)\n    private val normalizedPlaylistId = playlistId.removePrefix(\"VL\")\n    val isPodcastPlaylist = normalizedPlaylistId == \"RDPN\" || normalizedPlaylistId == \"SE\"\n\n    val playlist = MutableStateFlow<PlaylistItem?>(null)\n    val playlistSongs = MutableStateFlow<List<SongItem>>(emptyList())\n\n    private val _isLoading = MutableStateFlow(true)\n    val isLoading = _isLoading.asStateFlow()\n\n    private val _error = MutableStateFlow<String?>(null)\n    val error = _error.asStateFlow()\n\n    private val _isLoadingMore = MutableStateFlow(false)\n    val isLoadingMore = _isLoadingMore.asStateFlow()\n\n    val dbPlaylist = database.playlistByBrowseId(playlistId)\n        .stateIn(viewModelScope, SharingStarted.Lazily, null)\n\n    var continuation: String? = null\n        private set\n\n    private var proactiveLoadJob: Job? = null\n\n    init {\n        fetchInitialPlaylistData()\n    }\n\n    private fun fetchInitialPlaylistData() {\n        viewModelScope.launch(Dispatchers.IO) {\n            _isLoading.value = true\n            _error.value = null\n            continuation = null\n            proactiveLoadJob?.cancel() // Cancel any ongoing proactive load\n\n            if (isPodcastPlaylist) {\n                // Use special podcast playlist APIs\n                fetchPodcastPlaylist()\n            } else {\n                // Use regular playlist API\n                fetchRegularPlaylist()\n            }\n        }\n    }\n\n    private suspend fun fetchPodcastPlaylist() {\n        when (normalizedPlaylistId) {\n            \"RDPN\" -> {\n                YouTube.newEpisodes()\n                    .onSuccess { episodes ->\n                        playlist.value = PlaylistItem(\n                            id = playlistId,\n                            title = \"New Episodes\",\n                            author = null,\n                            songCountText = \"${episodes.size} episodes\",\n                            thumbnail = episodes.firstOrNull()?.thumbnail ?: \"\",\n                            playEndpoint = null,\n                            shuffleEndpoint = null,\n                            radioEndpoint = null,\n                        )\n                        playlistSongs.value = applySongFilters(episodes)\n                        _isLoading.value = false\n                    }.onFailure { throwable ->\n                        _error.value = throwable.message ?: \"Failed to load new episodes\"\n                        _isLoading.value = false\n                        reportException(throwable)\n                    }\n            }\n            \"SE\" -> {\n                timber.log.Timber.d(\"[SE_LOCAL] Fetching SE playlist...\")\n                val result = YouTube.episodesForLater()\n                val episodes = result.getOrNull() ?: emptyList()\n                timber.log.Timber.d(\"[SE_LOCAL] YouTube API result: ${if (result.isSuccess) \"success\" else \"failed\"}, ${episodes.size} episodes\")\n\n                if (result.isSuccess && episodes.isNotEmpty()) {\n                    // Use YouTube episodes\n                    playlist.value = PlaylistItem(\n                        id = playlistId,\n                        title = \"Episodes for Later\",\n                        author = null,\n                        songCountText = \"${episodes.size} episodes\",\n                        thumbnail = episodes.firstOrNull()?.thumbnail ?: \"\",\n                        playEndpoint = null,\n                        shuffleEndpoint = null,\n                        radioEndpoint = null,\n                    )\n                    playlistSongs.value = applySongFilters(episodes)\n                    _isLoading.value = false\n                } else {\n                    // Fall back to local saved episodes when API fails or returns empty\n                    timber.log.Timber.d(\"[SE_LOCAL] Falling back to local saved episodes\")\n                    loadLocalSavedEpisodes()\n                }\n            }\n            else -> {\n                _error.value = \"Unknown podcast playlist\"\n                _isLoading.value = false\n            }\n        }\n    }\n\n    private suspend fun fetchRegularPlaylist() {\n        YouTube.playlist(playlistId)\n            .onSuccess { playlistPage ->\n                playlist.value = playlistPage.playlist\n                playlistSongs.value = applySongFilters(playlistPage.songs)\n                continuation = playlistPage.songsContinuation\n                _isLoading.value = false\n                if (continuation != null) {\n                    startProactiveBackgroundLoading()\n                }\n            }.onFailure { throwable ->\n                _error.value = throwable.message ?: \"Failed to load playlist\"\n                _isLoading.value = false\n                reportException(throwable)\n            }\n    }\n\n    private suspend fun loadLocalSavedEpisodes() {\n        timber.log.Timber.d(\"[SE_LOCAL] loadLocalSavedEpisodes called\")\n        val savedEpisodes = database.savedPodcastEpisodes(SongSortType.CREATE_DATE, true).firstOrNull() ?: emptyList()\n        timber.log.Timber.d(\"[SE_LOCAL] Found ${savedEpisodes.size} saved episodes\")\n        savedEpisodes.forEachIndexed { index, ep ->\n            timber.log.Timber.d(\"[SE_LOCAL] Episode $index: id=${ep.song.id}, title=${ep.song.title}, isEpisode=${ep.song.isEpisode}, inLibrary=${ep.song.inLibrary}\")\n        }\n        if (savedEpisodes.isNotEmpty()) {\n            // Convert local Song entities to SongItem format\n            val songItems = savedEpisodes.map { song ->\n                SongItem(\n                    id = song.song.id,\n                    title = song.song.title,\n                    artists = song.artists.map { Artist(it.id, it.name) },\n                    album = song.album?.let { com.metrolist.innertube.models.Album(it.id, it.title) },\n                    duration = song.song.duration,\n                    thumbnail = song.song.thumbnailUrl ?: \"\",\n                    explicit = song.song.explicit,\n                    endpoint = null,\n                )\n            }\n            timber.log.Timber.d(\"[SE_LOCAL] Converted to ${songItems.size} SongItems\")\n            playlist.value = PlaylistItem(\n                id = playlistId,\n                title = \"Episodes for Later\",\n                author = null,\n                songCountText = \"${songItems.size} episodes\",\n                thumbnail = songItems.firstOrNull()?.thumbnail ?: \"\",\n                playEndpoint = null,\n                shuffleEndpoint = null,\n                radioEndpoint = null,\n            )\n            val filtered = applySongFilters(songItems)\n            timber.log.Timber.d(\"[SE_LOCAL] After filter: ${filtered.size} episodes, setting playlistSongs\")\n            playlistSongs.value = filtered\n            _isLoading.value = false\n            timber.log.Timber.d(\"[SE_LOCAL] Done, isLoading=false\")\n        } else {\n            timber.log.Timber.d(\"[SE_LOCAL] No saved episodes found\")\n            _error.value = \"No saved episodes\"\n            _isLoading.value = false\n        }\n    }\n\n    private fun startProactiveBackgroundLoading() {\n        proactiveLoadJob?.cancel() // Cancel previous job if any\n        proactiveLoadJob = viewModelScope.launch(Dispatchers.IO) {\n            var currentProactiveToken = continuation\n            while (currentProactiveToken != null && isActive) {\n                // If a manual loadMore is happening, pause proactive loading\n                if (_isLoadingMore.value) {\n                    // Wait until manual load is finished, then re-evaluate\n                    // This simple break and restart strategy from loadMoreSongs is preferred\n                    break \n                }\n\n                YouTube.playlistContinuation(currentProactiveToken)\n                    .onSuccess { playlistContinuationPage ->\n                        val currentSongs = playlistSongs.value.toMutableList()\n                        currentSongs.addAll(playlistContinuationPage.songs)\n                        playlistSongs.value = applySongFilters(currentSongs)\n                        currentProactiveToken = playlistContinuationPage.continuation\n                        // Update the class-level continuation for manual loadMore if needed\n                        this@OnlinePlaylistViewModel.continuation = currentProactiveToken \n                    }.onFailure { throwable ->\n                        reportException(throwable)\n                        currentProactiveToken = null // Stop proactive loading on error\n                    }\n            }\n            // If loop finishes because currentProactiveToken is null, all songs are loaded proactively.\n        }\n    }\n\n    fun loadMoreSongs() {\n        if (_isLoadingMore.value) return // Already loading more (manually)\n        \n        val tokenForManualLoad = continuation ?: return // No more songs to load\n\n        proactiveLoadJob?.cancel() // Cancel proactive loading to prioritize manual scroll\n        _isLoadingMore.value = true\n\n        viewModelScope.launch(Dispatchers.IO) {\n            YouTube.playlistContinuation(tokenForManualLoad)\n                .onSuccess { playlistContinuationPage ->\n                    val currentSongs = playlistSongs.value.toMutableList()\n                    currentSongs.addAll(playlistContinuationPage.songs)\n                    playlistSongs.value = applySongFilters(currentSongs)\n                    continuation = playlistContinuationPage.continuation\n                }.onFailure { throwable ->\n                    reportException(throwable)\n                }.also {\n                    _isLoadingMore.value = false\n                    // Resume proactive loading if there's still a continuation\n                    if (continuation != null && isActive) {\n                        startProactiveBackgroundLoading()\n                    }\n                }\n        }\n    }\n\n    fun retry() {\n        proactiveLoadJob?.cancel()\n        fetchInitialPlaylistData() // This will also restart proactive loading if applicable\n    }\n\n    private fun applySongFilters(songs: List<SongItem>): List<SongItem> {\n        val hideVideoSongs = context.dataStore.get(HideVideoSongsKey, false)\n        return songs\n            .distinctBy { it.id }\n            .filterVideoSongs(hideVideoSongs)\n    }\n\n    override fun onCleared() {\n        super.onCleared()\n        proactiveLoadJob?.cancel()\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/OnlinePodcastViewModel.kt",
    "content": "package com.metrolist.music.viewmodels\n\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.EpisodeItem\nimport com.metrolist.innertube.models.PodcastItem\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.db.entities.PodcastEntity\nimport com.metrolist.music.utils.SyncUtils\nimport com.metrolist.music.utils.reportException\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.flow.flowOf\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.launch\nimport timber.log.Timber\nimport java.time.LocalDateTime\nimport javax.inject.Inject\n\n@OptIn(ExperimentalCoroutinesApi::class)\n@HiltViewModel\nclass OnlinePodcastViewModel @Inject constructor(\n    savedStateHandle: SavedStateHandle,\n    val database: MusicDatabase,\n    private val syncUtils: SyncUtils,\n) : ViewModel() {\n    private val podcastId = savedStateHandle.get<String>(\"podcastId\")!!\n\n    val podcast = MutableStateFlow<PodcastItem?>(null)\n    val episodes = MutableStateFlow<List<EpisodeItem>>(emptyList())\n\n    val libraryPodcast = podcast.flatMapLatest { p ->\n        p?.let { database.podcast(it.id) } ?: flowOf(null)\n    }.stateIn(viewModelScope, SharingStarted.Lazily, null)\n\n    private val _isLoading = MutableStateFlow(true)\n    val isLoading = _isLoading.asStateFlow()\n\n    private val _error = MutableStateFlow<String?>(null)\n    val error = _error.asStateFlow()\n\n    init {\n        Timber.d(\"ViewModel init with podcastId: $podcastId\")\n        fetchPodcastData()\n    }\n\n    private fun fetchPodcastData() {\n        viewModelScope.launch(Dispatchers.IO) {\n            Timber.d(\"fetchPodcastData called for: $podcastId\")\n            _isLoading.value = true\n            _error.value = null\n\n            YouTube.podcast(podcastId)\n                .onSuccess { podcastPage ->\n                    Timber.d(\"Success! Podcast: ${podcastPage.podcast.title}, Episodes: ${podcastPage.episodes.size}\")\n                    podcast.value = podcastPage.podcast\n                    episodes.value = podcastPage.episodes\n                    _isLoading.value = false\n                }.onFailure { throwable ->\n                    Timber.e(throwable, \"Failed to load podcast: ${throwable.message}\")\n                    _error.value = throwable.message ?: \"Failed to load podcast\"\n                    _isLoading.value = false\n                    reportException(throwable)\n                }\n        }\n    }\n\n    /**\n     * Toggle saving podcast to library.\n     */\n    fun toggleSubscription() {\n        val currentPodcast = podcast.value ?: run {\n            Timber.d(\"[PODCAST_TOGGLE] No podcast loaded, returning\")\n            return\n        }\n        val existingEntity = libraryPodcast.value\n        val isCurrentlySaved = existingEntity?.inLibrary == true\n        val shouldBeSaved = !isCurrentlySaved\n\n        val channelId = currentPodcast.channelId ?: currentPodcast.author?.id\n        Timber.d(\"[PODCAST_TOGGLE] toggleSubscription called: podcastId=${currentPodcast.id}, channelId=$channelId, authorId=${currentPodcast.author?.id}, isCurrentlySaved=$isCurrentlySaved, shouldBeSaved=$shouldBeSaved\")\n\n        viewModelScope.launch(Dispatchers.IO) {\n            Timber.d(\"[PODCAST_TOGGLE] Inside coroutine, updating database...\")\n\n            if (existingEntity != null) {\n                val updated = existingEntity.toggleBookmark()\n                Timber.d(\"[PODCAST_TOGGLE] Updating existing entity: bookmarkedAt=${updated.bookmarkedAt}\")\n                database.update(updated)\n            } else {\n                val newEntity = PodcastEntity(\n                    id = currentPodcast.id,\n                    title = currentPodcast.title,\n                    author = currentPodcast.author?.name,\n                    thumbnailUrl = currentPodcast.thumbnail,\n                    channelId = channelId,\n                    bookmarkedAt = LocalDateTime.now(),\n                )\n                Timber.d(\"[PODCAST_TOGGLE] Inserting new entity: ${newEntity.id}\")\n                database.insert(newEntity)\n            }\n\n            Timber.d(\"[PODCAST_TOGGLE] Database updated, calling syncUtils.savePodcast(${currentPodcast.id}, $shouldBeSaved)\")\n            // Sync with YouTube (handles login check internally)\n            syncUtils.savePodcast(currentPodcast.id, shouldBeSaved)\n        }\n    }\n\n    /**\n     * Legacy method - now calls toggleSubscription\n     */\n    fun toggleLibrary() = toggleSubscription()\n\n    fun retry() {\n        fetchPodcastData()\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/OnlineSearchSuggestionViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport android.content.Context\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.YTItem\nimport com.metrolist.innertube.models.filterExplicit\nimport com.metrolist.innertube.models.filterVideoSongs\nimport com.metrolist.music.constants.HideExplicitKey\nimport com.metrolist.music.constants.HideVideoSongsKey\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.db.entities.SearchHistory\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@OptIn(ExperimentalCoroutinesApi::class)\n@HiltViewModel\nclass OnlineSearchSuggestionViewModel\n@Inject\nconstructor(\n    @ApplicationContext val context: Context,\n    database: MusicDatabase,\n) : ViewModel() {\n    val query = MutableStateFlow(\"\")\n    private val _viewState = MutableStateFlow(SearchSuggestionViewState())\n    val viewState = _viewState.asStateFlow()\n\n    init {\n        viewModelScope.launch {\n            query\n                .flatMapLatest { query ->\n                    if (query.isEmpty()) {\n                        database.searchHistory().map { history ->\n                            SearchSuggestionViewState(\n                                history = history,\n                            )\n                        }\n                    } else {\n                        val result = YouTube.searchSuggestions(query).getOrNull()\n                        val hideExplicit = context.dataStore.get(HideExplicitKey, false)\n                        val hideVideoSongs = context.dataStore.get(HideVideoSongsKey, false)\n\n                        database\n                            .searchHistory(query)\n                            .map { it.take(3) }\n                            .map { history ->\n                                SearchSuggestionViewState(\n                                    history = history,\n                                    suggestions =\n                                    result\n                                        ?.queries\n                                        ?.filter { suggestionQuery ->\n                                            history.none { it.query == suggestionQuery }\n                                        }.orEmpty(),\n                                    items =\n                                    result\n                                        ?.recommendedItems\n                                        ?.distinctBy { it.id }\n                                        ?.filterExplicit(hideExplicit)\n                                        ?.filterVideoSongs(hideVideoSongs)\n                                        .orEmpty(),\n                                )\n                            }\n                    }\n                }.collect {\n                    _viewState.value = it\n                }\n        }\n    }\n}\n\ndata class SearchSuggestionViewState(\n    val history: List<SearchHistory> = emptyList(),\n    val suggestions: List<String> = emptyList(),\n    val items: List<YTItem> = emptyList(),\n)\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/OnlineSearchViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport android.content.Context\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateMapOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.filterExplicit\nimport com.metrolist.innertube.models.filterVideoSongs\nimport com.metrolist.innertube.models.filterYoutubeShorts\nimport com.metrolist.innertube.pages.SearchSummaryPage\nimport com.metrolist.music.constants.HideExplicitKey\nimport com.metrolist.music.constants.HideVideoSongsKey\nimport com.metrolist.music.constants.HideYoutubeShortsKey\nimport com.metrolist.music.models.ItemsPage\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\nimport com.metrolist.music.utils.reportException\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.launch\nimport java.net.URLDecoder\nimport javax.inject.Inject\n\n@HiltViewModel\nclass OnlineSearchViewModel\n@Inject\nconstructor(\n    @ApplicationContext val context: Context,\n    savedStateHandle: SavedStateHandle,\n) : ViewModel() {\n    val query = try {\n        URLDecoder.decode(savedStateHandle.get<String>(\"query\")!!, \"UTF-8\")\n    } catch (e: IllegalArgumentException) {\n        savedStateHandle.get<String>(\"query\")!!\n    }\n    val filter = MutableStateFlow<YouTube.SearchFilter?>(null)\n    var summaryPage by mutableStateOf<SearchSummaryPage?>(null)\n    val viewStateMap = mutableStateMapOf<String, ItemsPage?>()\n\n    private suspend fun loadSummaryPage() {\n        if (summaryPage == null) {\n            YouTube\n                .searchSummary(query)\n                .onSuccess {\n                    val hideExplicit = context.dataStore.get(HideExplicitKey, false)\n                    val hideVideoSongs = context.dataStore.get(HideVideoSongsKey, false)\n                    val hideYoutubeShorts = context.dataStore.get(HideYoutubeShortsKey, false)\n                    summaryPage =\n                        it.filterExplicit(hideExplicit)\n                          .filterVideoSongs(hideVideoSongs)\n                          .filterYoutubeShorts(hideYoutubeShorts)\n                }.onFailure {\n                    reportException(it)\n                }\n        }\n    }\n\n    init {\n        viewModelScope.launch {\n            filter.collect { filter ->\n                if (filter == null) {\n                    loadSummaryPage()\n                } else if (filter == YouTube.SearchFilter.FILTER_EPISODE) {\n                    // The FILTER_EPISODE API returns episodes in a format that differs from the\n                    // summary search: playlistItemData is absent and the subtitle structure is\n                    // different, making reliable isEpisode detection fail for many items.\n                    // Reuse the \"Episodes\" section from the summary page instead — it is already\n                    // parsed correctly by fromMusicResponsiveListItemRenderer and guaranteed to\n                    // show the same results as the episodes section in the \"All\" filter.\n                    if (viewStateMap[filter.value] == null) {\n                        loadSummaryPage()\n                        summaryPage?.let { page ->\n                            val episodes = page.summaries\n                                .firstOrNull { it.title == \"Episodes\" }\n                                ?.items\n                                .orEmpty()\n                            viewStateMap[filter.value] = ItemsPage(episodes, null)\n                        }\n                    }\n                } else {\n                    if (viewStateMap[filter.value] == null) {\n                        YouTube\n                            .search(query, filter)\n                            .onSuccess { result ->\n                                val hideExplicit = context.dataStore.get(HideExplicitKey, false)\n                                val hideVideoSongs = context.dataStore.get(HideVideoSongsKey, false)\n                                val hideYoutubeShorts = context.dataStore.get(HideYoutubeShortsKey, false)\n                                viewStateMap[filter.value] =\n                                    ItemsPage(\n                                        result.items\n                                            .distinctBy { it.id }\n                                            .filterExplicit(hideExplicit)\n                                            .filterVideoSongs(hideVideoSongs)\n                                            .filterYoutubeShorts(hideYoutubeShorts),\n                                        result.continuation,\n                                    )\n                            }.onFailure {\n                                reportException(it)\n                            }\n                    }\n                }\n            }\n        }\n    }\n\n    fun loadMore() {\n        val currentFilter = filter.value\n        val filterValue = currentFilter?.value ?: return\n        viewModelScope.launch {\n            val viewState = viewStateMap[filterValue] ?: return@launch\n            val continuation = viewState.continuation ?: return@launch\n            val searchResult =\n                YouTube.searchContinuation(continuation).getOrNull() ?: return@launch\n            val hideExplicit = context.dataStore.get(HideExplicitKey, false)\n            val hideVideoSongs = context.dataStore.get(HideVideoSongsKey, false)\n            val hideYoutubeShorts = context.dataStore.get(HideYoutubeShortsKey, false)\n            val newItems = searchResult.items\n                .filterExplicit(hideExplicit)\n                .filterVideoSongs(hideVideoSongs)\n                .filterYoutubeShorts(hideYoutubeShorts)\n            viewStateMap[filterValue] = ItemsPage(\n                (viewState.items + newItems).distinctBy { it.id },\n                searchResult.continuation\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/PlaylistsViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\n@file:OptIn(ExperimentalCoroutinesApi::class)\n\npackage com.metrolist.music.viewmodels\n\nimport android.content.Context\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.music.constants.AddToPlaylistSortDescendingKey\nimport com.metrolist.music.constants.AddToPlaylistSortTypeKey\nimport com.metrolist.music.constants.PlaylistSortType\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.extensions.toEnum\nimport com.metrolist.music.utils.SyncUtils\nimport com.metrolist.music.utils.dataStore\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.stateIn\nimport javax.inject.Inject\n\n@HiltViewModel\nclass PlaylistsViewModel\n@Inject\nconstructor(\n    @ApplicationContext context: Context,\n    database: MusicDatabase,\n    private val syncUtils: SyncUtils,\n) : ViewModel() {\n    val allPlaylists =\n        context.dataStore.data\n            .map {\n                it[AddToPlaylistSortTypeKey].toEnum(PlaylistSortType.CREATE_DATE) to (it[AddToPlaylistSortDescendingKey]\n                    ?: true)\n            }.distinctUntilChanged()\n            .flatMapLatest { (sortType, descending) ->\n                database.playlists(sortType, descending)\n            }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n\n    // Suspend function that waits for sync to complete\n    suspend fun sync() {\n        syncUtils.syncSavedPlaylists()\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/StatsViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport android.content.Context\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.snapshotFlow\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.Artist\nimport com.metrolist.music.constants.HideVideoSongsKey\nimport com.metrolist.music.constants.LastMonthlyMostPlaylistSyncKey\nimport com.metrolist.music.constants.LastWeeklyMostPlaylistSyncKey\nimport com.metrolist.music.constants.StatPeriod\nimport com.metrolist.music.constants.statToPeriod\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.db.entities.PlaylistEntity\nimport com.metrolist.music.db.entities.PlaylistSongMap\nimport com.metrolist.music.ui.screens.OptionStats\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.reportException\nimport androidx.datastore.preferences.core.edit\nimport com.metrolist.music.R\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.stateIn\nimport kotlinx.coroutines.launch\nimport java.time.Duration\nimport java.time.Instant\nimport java.time.LocalDateTime\nimport java.time.ZoneOffset\nimport javax.inject.Inject\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport kotlin.collections.emptyList\n\n@OptIn(ExperimentalCoroutinesApi::class)\n@HiltViewModel\nclass StatsViewModel\n@Inject\nconstructor(\n    @ApplicationContext private val context: Context,\n    val database: MusicDatabase,\n) : ViewModel() {\n    private val periodicMostPlaylistSyncMutex = Mutex()\n    val selectedOption = MutableStateFlow(OptionStats.CONTINUOUS)\n    val indexChips = MutableStateFlow(0)\n\n    val mostPlayedSongsStats =\n        combine(\n            selectedOption,\n            indexChips,\n            context.dataStore.data.map { it[HideVideoSongsKey] ?: false }.distinctUntilChanged()\n        ) { first, second, third -> Triple(first, second, third) }\n            .flatMapLatest { (selection, t, hideVideoSongs) ->\n                database\n                    .mostPlayedSongsStats(\n                        fromTimeStamp = statToPeriod(selection, t),\n                        limit = -1,\n                        toTimeStamp =\n                        if (selection == OptionStats.CONTINUOUS || t == 0) {\n                            LocalDateTime\n                                .now()\n                                .toInstant(\n                                    ZoneOffset.UTC,\n                                ).toEpochMilli()\n                        } else {\n                            statToPeriod(selection, t - 1)\n                        },\n                    ).map { songs ->\n                        if (hideVideoSongs) songs.filter { !it.isVideo } else songs\n                    }\n            }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n\n    val mostPlayedSongs =\n        combine(\n            selectedOption,\n            indexChips,\n            context.dataStore.data.map { it[HideVideoSongsKey] ?: false }.distinctUntilChanged()\n        ) { first, second, third -> Triple(first, second, third) }\n            .flatMapLatest { (selection, t, hideVideoSongs) ->\n                database\n                    .mostPlayedSongs(\n                        fromTimeStamp = statToPeriod(selection, t),\n                        limit = -1,\n                        toTimeStamp =\n                        if (selection == OptionStats.CONTINUOUS || t == 0) {\n                            LocalDateTime\n                                .now()\n                                .toInstant(\n                                    ZoneOffset.UTC,\n                                ).toEpochMilli()\n                        } else {\n                            statToPeriod(selection, t - 1)\n                        },\n                    ).map { songs ->\n                        if (hideVideoSongs) songs.filter { !it.song.isVideo } else songs\n                    }\n            }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n\n    val mostPlayedArtists =\n        combine(\n            selectedOption,\n            indexChips,\n        ) { first, second -> Pair(first, second) }\n            .flatMapLatest { (selection, t) ->\n                database\n                    .mostPlayedArtists(\n                        statToPeriod(selection, t),\n                        limit = -1,\n                        toTimeStamp =\n                        if (selection == OptionStats.CONTINUOUS || t == 0) {\n                            LocalDateTime\n                                .now()\n                                .toInstant(\n                                    ZoneOffset.UTC,\n                                ).toEpochMilli()\n                        } else {\n                            statToPeriod(selection, t - 1)\n                        },\n                    ).map { artists ->\n                        artists.filter { it.artist.isYouTubeArtist }\n                    }\n            }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n\n    val mostPlayedAlbums =\n        combine(\n            selectedOption,\n            indexChips,\n        ) { first, second -> Pair(first, second) }\n            .flatMapLatest { (selection, t) ->\n                database.mostPlayedAlbums(\n                    statToPeriod(selection, t),\n                    limit = -1,\n                    toTimeStamp =\n                    if (selection == OptionStats.CONTINUOUS || t == 0) {\n                        LocalDateTime\n                            .now()\n                            .toInstant(\n                                ZoneOffset.UTC,\n                            ).toEpochMilli()\n                    } else {\n                        statToPeriod(selection, t - 1)\n                    },\n                )\n            }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n\n    val firstEvent =\n        database\n            .firstEvent()\n            .stateIn(viewModelScope, SharingStarted.Lazily, null)\n\n    val selectedArtists = mutableStateListOf<Artist>() // Current artist selection\n\n    val filteredSongs = combine(\n        mostPlayedSongsStats, // Unfiltered songs\n        snapshotFlow { selectedArtists.toList() } // Selected artists\n    ) { songs, selected ->\n        if (selected.isEmpty()) {\n            songs\n        } else {\n            songs.filter { song ->\n                song.artists.any { artist -> selected.any { it.id == artist.id } }\n            }\n        }\n    }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n\n    val filteredArtists = combine(\n        mostPlayedArtists, // Unfiltered list of artists\n        snapshotFlow { selectedArtists.toList() } // Selected artists\n    ) { artists, selected ->\n        if (selected.isEmpty()) {\n            artists\n        } else {\n            artists.filter { artist ->\n                selected.any { it.id == artist.artist.id }\n            }\n        }\n    }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n\n    val filteredAlbums = combine(\n        mostPlayedAlbums, // Unfiltered list of albums\n        snapshotFlow { selectedArtists.toList() } // Selected artists\n    ) { albums, selected ->\n        if (selected.isEmpty()) {\n            albums\n        } else {\n            albums.filter { album ->\n                album.artists.any { artist ->\n                    selected.any { it.id == artist.id }\n                }\n            }\n        }\n    }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n\n    fun transferSongStats(fromSongId: String, toSongId: String, onDone: (() -> Unit)? = null) {\n        viewModelScope.launch {\n            try {\n                database.transferSongStats(fromSongId, toSongId)\n                syncMostPlaylistsIfNeeded(force = true)\n                onDone?.invoke()\n            } catch (t: Throwable) {\n                reportException(t)\n            }\n        }\n    }\n\n    val weeklyMostPlaylist =\n        database\n            .playlist(PlaylistEntity.WEEKLY_MOST_PLAYLIST_ID)\n            .stateIn(viewModelScope, SharingStarted.Lazily, null)\n\n    val monthlyMostPlaylist =\n        database\n            .playlist(PlaylistEntity.MONTHLY_MOST_PLAYLIST_ID)\n            .stateIn(viewModelScope, SharingStarted.Lazily, null)\n\n    val recapPlaylists =\n        database\n            .playlistsByNameAsc()\n            .map { playlists ->\n                playlists.filter { playlist ->\n                    playlist.playlist.browseId != null &&\n                        playlist.playlist.name.contains(\"recap\", ignoreCase = true)\n                }\n            }.distinctUntilChanged()\n            .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n\n    fun syncMostPlaylistsIfNeeded(force: Boolean = false) {\n        viewModelScope.launch(Dispatchers.IO) {\n            periodicMostPlaylistSyncMutex.withLock {\n                val now = LocalDateTime.now(ZoneOffset.UTC)\n                val nowEpochMillis = now.toInstant(ZoneOffset.UTC).toEpochMilli()\n                val preferences = context.dataStore.data.first()\n                val hideVideoSongs = preferences[HideVideoSongsKey] ?: false\n\n                val weeklyPlaylistExists =\n                    database.playlist(PlaylistEntity.WEEKLY_MOST_PLAYLIST_ID).first() != null\n                val monthlyPlaylistExists =\n                    database.playlist(PlaylistEntity.MONTHLY_MOST_PLAYLIST_ID).first() != null\n\n                val shouldSyncWeekly =\n                    force || !weeklyPlaylistExists || isWeeklySyncDue(\n                        lastSyncMillis = preferences[LastWeeklyMostPlaylistSyncKey],\n                        now = now,\n                    )\n                val shouldSyncMonthly =\n                    force || !monthlyPlaylistExists || isMonthlySyncDue(\n                        lastSyncMillis = preferences[LastMonthlyMostPlaylistSyncKey],\n                        now = now,\n                    )\n\n                if (!shouldSyncWeekly && !shouldSyncMonthly) {\n                    return@withLock\n                }\n\n                if (shouldSyncWeekly) {\n                    syncMostPlaylist(\n                        playlistId = PlaylistEntity.WEEKLY_MOST_PLAYLIST_ID,\n                        playlistName = context.getString(R.string.weekly_most_playlist_name),\n                        fromTimeStamp = StatPeriod.WEEK_1.toTimeMillis(),\n                        hideVideoSongs = hideVideoSongs,\n                        now = now,\n                    )\n                }\n\n                if (shouldSyncMonthly) {\n                    syncMostPlaylist(\n                        playlistId = PlaylistEntity.MONTHLY_MOST_PLAYLIST_ID,\n                        playlistName = context.getString(R.string.monthly_most_playlist_name),\n                        fromTimeStamp = StatPeriod.MONTH_1.toTimeMillis(),\n                        hideVideoSongs = hideVideoSongs,\n                        now = now,\n                    )\n                }\n\n                // Only write \"last sync\" when it was a scheduled sync, not a forced rebuild\n                if (!force) {\n                    context.dataStore.edit { settings ->\n                        if (shouldSyncWeekly) settings[LastWeeklyMostPlaylistSyncKey] = nowEpochMillis\n                        if (shouldSyncMonthly) settings[LastMonthlyMostPlaylistSyncKey] = nowEpochMillis\n                    }\n                }\n            }\n        }\n    }\n\n    private fun isWeeklySyncDue(\n        lastSyncMillis: Long?,\n        now: LocalDateTime,\n    ): Boolean {\n        if (lastSyncMillis == null || lastSyncMillis <= 0L) return true\n\n        val lastSyncAt = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastSyncMillis), ZoneOffset.UTC)\n        return !lastSyncAt.plusWeeks(1).isAfter(now)\n    }\n\n    private fun isMonthlySyncDue(\n        lastSyncMillis: Long?,\n        now: LocalDateTime,\n    ): Boolean {\n        if (lastSyncMillis == null || lastSyncMillis <= 0L) return true\n\n        val lastSyncAt = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastSyncMillis), ZoneOffset.UTC)\n        return !lastSyncAt.plusMonths(1).isAfter(now)\n    }\n\n    private suspend fun syncMostPlaylist(\n        playlistId: String,\n        playlistName: String,\n        fromTimeStamp: Long,\n        hideVideoSongs: Boolean,\n        now: LocalDateTime,\n    ) {\n        val songs =\n            database\n                .mostPlayedSongs(\n                    fromTimeStamp = fromTimeStamp,\n                    limit = -1,\n                    toTimeStamp = now.toInstant(ZoneOffset.UTC).toEpochMilli(),\n                ).first()\n                .let { mostPlayedSongs ->\n                    if (hideVideoSongs) {\n                        mostPlayedSongs.filter { !it.song.isVideo }\n                    } else {\n                        mostPlayedSongs\n                    }\n                }.distinctBy { it.song.id }\n\n        val existingPlaylist = database.playlist(playlistId).first()?.playlist\n        val playlistEntity =\n            existingPlaylist?.copy(\n                name = playlistName,\n                isEditable = true,\n                bookmarkedAt = existingPlaylist.bookmarkedAt ?: now,\n                lastUpdateTime = now,\n            ) ?: PlaylistEntity(\n                id = playlistId,\n                name = playlistName,\n                isEditable = true,\n                bookmarkedAt = now,\n                lastUpdateTime = now,\n            )\n\n        if (existingPlaylist == null) {\n            database.insert(playlistEntity)\n        } else {\n            database.update(playlistEntity)\n        }\n\n        database.clearPlaylist(playlistId)\n\n        songs.forEachIndexed { position, song ->\n            database.insert(\n                PlaylistSongMap(\n                    songId = song.song.id,\n                    playlistId = playlistId,\n                    position = position,\n                ),\n            )\n        }\n    }\n\n    init {\n        viewModelScope.launch {\n            mostPlayedArtists.collect { artists ->\n                artists\n                    .map { it.artist }\n                    .filter {\n                        it.thumbnailUrl == null || Duration.between(\n                            it.lastUpdateTime,\n                            LocalDateTime.now()\n                        ) > Duration.ofDays(10)\n                    }.forEach { artist ->\n                        YouTube.artist(artist.id).onSuccess { artistPage ->\n                            database.query {\n                                update(artist, artistPage)\n                            }\n                        }\n                    }\n            }\n        }\n        viewModelScope.launch {\n            mostPlayedAlbums.collect { albums ->\n                albums\n                    .filter {\n                        it.album.songCount == 0\n                    }.forEach { album ->\n                        YouTube\n                            .album(album.id)\n                            .onSuccess { albumPage ->\n                                database.query {\n                                    update(album.album, albumPage, album.artists)\n                                }\n                            }.onFailure {\n                                reportException(it)\n                                if (it.message?.contains(\"NOT_FOUND\") == true) {\n                                    database.query {\n                                        delete(album.album)\n                                    }\n                                }\n                            }\n                    }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/ThemeViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport androidx.lifecycle.ViewModel\nimport com.metrolist.music.ui.screens.settings.DarkMode\nimport com.metrolist.music.ui.theme.DefaultThemeColor\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\n\nclass ThemeViewModel : ViewModel() {\n    // Theme state flows\n    private val _darkMode = MutableStateFlow(DarkMode.AUTO)\n    val darkMode: StateFlow<DarkMode> = _darkMode.asStateFlow()\n\n    private val _pureBlack = MutableStateFlow(false)\n    val pureBlack: StateFlow<Boolean> = _pureBlack.asStateFlow()\n\n    private val _selectedThemeColorInt = MutableStateFlow(DefaultThemeColor.hashCode())\n    val selectedThemeColorInt: StateFlow<Int> = _selectedThemeColorInt.asStateFlow()\n\n    fun updateDarkMode(mode: DarkMode) {\n        _darkMode.value = mode\n    }\n\n    fun updatePureBlack(enabled: Boolean) {\n        _pureBlack.value = enabled\n    }\n\n    fun updateThemeColor(colorInt: Int) {\n        _selectedThemeColorInt.value = colorInt\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/TopPlaylistViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport android.content.Context\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.music.constants.HideVideoSongsKey\nimport com.metrolist.music.constants.MyTopFilter\nimport com.metrolist.music.db.MusicDatabase\nimport com.metrolist.music.utils.dataStore\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.SharingStarted\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.flow.flatMapLatest\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.flow.stateIn\nimport javax.inject.Inject\n\n@HiltViewModel\nclass TopPlaylistViewModel\n@Inject\nconstructor(\n    @ApplicationContext context: Context,\n    database: MusicDatabase,\n    savedStateHandle: SavedStateHandle,\n) : ViewModel() {\n    val top = savedStateHandle.get<String>(\"top\")!!\n\n    val topPeriod = MutableStateFlow(MyTopFilter.ALL_TIME)\n\n    @OptIn(ExperimentalCoroutinesApi::class)\n    val topSongs =\n        combine(\n            topPeriod,\n            context.dataStore.data.map { it[HideVideoSongsKey] ?: false }.distinctUntilChanged()\n        ) { period, hideVideoSongs -> period to hideVideoSongs }\n            .flatMapLatest { (period, hideVideoSongs) ->\n                database.mostPlayedSongs(period.toTimeMillis(), top.toInt()).map { songs ->\n                    if (hideVideoSongs) songs.filter { !it.song.isVideo } else songs\n                }\n            }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/viewmodels/YouTubeBrowseViewModel.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.viewmodels\n\nimport android.content.Context\nimport androidx.lifecycle.SavedStateHandle\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.models.filterYoutubeShorts\nimport com.metrolist.innertube.pages.BrowseResult\nimport com.metrolist.music.constants.HideExplicitKey\nimport com.metrolist.music.constants.HideVideoSongsKey\nimport com.metrolist.music.constants.HideYoutubeShortsKey\nimport com.metrolist.music.utils.dataStore\nimport com.metrolist.music.utils.get\nimport com.metrolist.music.utils.reportException\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass YouTubeBrowseViewModel\n@Inject\nconstructor(\n    @ApplicationContext val context: Context,\n    savedStateHandle: SavedStateHandle,\n) : ViewModel() {\n    private val browseId = savedStateHandle.get<String>(\"browseId\")!!\n    private val params = savedStateHandle.get<String>(\"params\")\n\n    val result = MutableStateFlow<BrowseResult?>(null)\n\n    init {\n        viewModelScope.launch {\n            val hideExplicit = context.dataStore.get(HideExplicitKey, false)\n            val hideVideoSongs = context.dataStore.get(HideVideoSongsKey, false)\n            val hideYoutubeShorts = context.dataStore.get(HideYoutubeShortsKey, false)\n            YouTube\n                .browse(browseId, params)\n                .onSuccess {\n                    result.value = it\n                        .filterExplicit(hideExplicit)\n                        .filterVideoSongs(hideVideoSongs)\n                        .filterYoutubeShorts(hideYoutubeShorts)\n                }.onFailure {\n                    reportException(it)\n                }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/widget/MetrolistWidgetManager.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.widget\n\nimport android.app.PendingIntent\nimport android.appwidget.AppWidgetManager\nimport android.content.ComponentName\nimport android.content.Context\nimport android.content.Intent\nimport android.graphics.Bitmap\nimport android.graphics.BitmapShader\nimport android.graphics.Canvas\nimport android.graphics.Paint\nimport android.graphics.RectF\nimport android.graphics.Shader\nimport android.os.Bundle\nimport android.widget.RemoteViews\nimport coil3.ImageLoader\nimport coil3.request.ImageRequest\nimport coil3.request.allowHardware\nimport coil3.request.crossfade\nimport coil3.toBitmap\nimport com.metrolist.music.MainActivity\nimport com.metrolist.music.R\nimport com.metrolist.music.db.MusicDatabase\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport javax.inject.Inject\nimport javax.inject.Singleton\n\n@Singleton\nclass MetrolistWidgetManager @Inject constructor(\n    @ApplicationContext private val context: Context,\n    private val database: MusicDatabase\n) {\n    private val imageLoader by lazy {\n        ImageLoader.Builder(context)\n            .crossfade(false)\n            .build()\n    }\n\n    // Cache for album art to avoid reloading\n    private var cachedArtworkUri: String? = null\n    private var cachedAlbumArt: Bitmap? = null\n    private var cachedCircularAlbumArt: Bitmap? = null\n\n    suspend fun updateWidgets(\n        title: String,\n        artist: String,\n        artworkUri: String?,\n        isPlaying: Boolean,\n        isLiked: Boolean,\n        duration: Long = 0,\n        currentPosition: Long = 0\n    ) {\n        val appWidgetManager = AppWidgetManager.getInstance(context)\n\n        // Use cached album art if URI hasn't changed, otherwise load new one\n        val albumArt: Bitmap?\n        val circularAlbumArt: Bitmap?\n        \n        if (artworkUri != null && artworkUri == cachedArtworkUri && cachedAlbumArt != null) {\n            albumArt = cachedAlbumArt\n            circularAlbumArt = cachedCircularAlbumArt\n        } else {\n            albumArt = artworkUri?.let { loadAlbumArt(it, 300) }\n            circularAlbumArt = albumArt?.let { getCircularBitmap(it) }\n            // Update cache\n            cachedArtworkUri = artworkUri\n            cachedAlbumArt = albumArt\n            cachedCircularAlbumArt = circularAlbumArt\n        }\n\n        // Update main music player widgets\n        val componentName = ComponentName(context, MusicWidgetReceiver::class.java)\n        val widgetIds = appWidgetManager.getAppWidgetIds(componentName)\n        if (widgetIds.isNotEmpty()) {\n            widgetIds.forEach { widgetId ->\n                val options = appWidgetManager.getAppWidgetOptions(widgetId)\n                val views = createRemoteViewsForSize(\n                    options,\n                    title,\n                    artist,\n                    albumArt,\n                    isPlaying,\n                    isLiked,\n                    duration,\n                    currentPosition\n                )\n                appWidgetManager.updateAppWidget(widgetId, views)\n            }\n        }\n\n        // Update turntable widgets\n        val turntableComponentName = ComponentName(context, TurntableWidgetReceiver::class.java)\n        val turntableWidgetIds = appWidgetManager.getAppWidgetIds(turntableComponentName)\n        if (turntableWidgetIds.isNotEmpty()) {\n            val turntableViews = createTurntableRemoteViews(\n                circularAlbumArt,\n                isPlaying,\n                isLiked\n            )\n            turntableWidgetIds.forEach { widgetId ->\n                appWidgetManager.updateAppWidget(widgetId, turntableViews)\n            }\n        }\n    }\n\n    private fun createRemoteViewsForSize(\n        options: Bundle,\n        title: String,\n        artist: String,\n        albumArt: Bitmap?,\n        isPlaying: Boolean,\n        isLiked: Boolean,\n        duration: Long,\n        currentPosition: Long\n    ): RemoteViews {\n        val minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)\n        val minHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)\n\n        // Determine widget size category\n        // 2x2: approximately 110dp x 110dp (compact square)\n        // 4x1: approximately 250dp x 40dp (wide single row)\n        // Full: approximately 250dp x 110dp (default)\n        return when {\n            minWidth < 180 && minHeight < 100 -> {\n                // 2x2 Compact - Only play button with album art\n                createCompactSquareRemoteViews(albumArt, isPlaying)\n            }\n            minWidth >= 180 && minHeight < 100 -> {\n                // 4x1 Wide - Single row with album art, song info, like and play buttons\n                createCompactWideRemoteViews(title, artist, albumArt, isPlaying, isLiked)\n            }\n            else -> {\n                // Full layout\n                createRemoteViews(title, artist, albumArt, isPlaying, isLiked, duration, currentPosition)\n            }\n        }\n    }\n\n    private fun createRemoteViews(\n        title: String,\n        artist: String,\n        albumArt: Bitmap?,\n        isPlaying: Boolean,\n        isLiked: Boolean,\n        duration: Long = 0,\n        currentPosition: Long = 0\n    ): RemoteViews {\n        val views = RemoteViews(context.packageName, R.layout.widget_music_player)\n\n        // Set song info\n        views.setTextViewText(R.id.widget_song_title, title)\n        views.setTextViewText(R.id.widget_artist_name, artist)\n\n        // Set album art with rounded corners\n        if (albumArt != null) {\n            val roundedAlbumArt = getRoundedCornerBitmap(albumArt, 48f)\n            views.setImageViewBitmap(R.id.widget_album_art, roundedAlbumArt)\n        } else {\n            views.setImageViewBitmap(R.id.widget_album_art, getRoundedDefaultIcon(48f))\n        }\n\n        // Set play/pause icon\n        val playPauseIcon = if (isPlaying) R.drawable.ic_widget_pause else R.drawable.ic_widget_play\n        views.setImageViewResource(R.id.widget_play_pause, playPauseIcon)\n\n        // Set like icon - using nav style (purple) for main widget\n        val likeIcon = if (isLiked) R.drawable.ic_widget_heart_nav else R.drawable.ic_widget_heart_outline_nav\n        views.setImageViewResource(R.id.widget_like_button, likeIcon)\n\n        // Set Progress Level\n        if (duration > 0) {\n            val level = ((currentPosition.toDouble() / duration.toDouble()) * 10000).toInt()\n            views.setInt(R.id.widget_progress_fill, \"setImageLevel\", level)\n        } else {\n            views.setInt(R.id.widget_progress_fill, \"setImageLevel\", 0)\n        }\n\n        // Set click intents\n        views.setOnClickPendingIntent(R.id.widget_album_art, getOpenAppIntent())\n        views.setOnClickPendingIntent(R.id.widget_play_pause_container, getPlayPauseIntent())\n        views.setOnClickPendingIntent(R.id.widget_like_button, getLikeIntent())\n\n        return views\n    }\n\n    private suspend fun loadAlbumArt(artworkUri: String, size: Int = 200): Bitmap? {\n        return withContext(Dispatchers.IO) {\n            try {\n                val request = ImageRequest.Builder(context)\n                    .data(artworkUri)\n                    .size(size, size)\n                    .allowHardware(false)\n                    .crossfade(300)\n                    .build()\n                val result = imageLoader.execute(request)\n                result.image?.toBitmap()\n            } catch (e: Exception) {\n                null\n            }\n        }\n    }\n\n    private fun getRoundedCornerBitmap(bitmap: Bitmap, cornerRadius: Float): Bitmap {\n        // Ensure the bitmap is square for thumbnails\n        val size = minOf(bitmap.width, bitmap.height)\n        val xOffset = (bitmap.width - size) / 2\n        val yOffset = (bitmap.height - size) / 2\n        val squareBitmap = Bitmap.createBitmap(bitmap, xOffset, yOffset, size, size)\n\n        val output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)\n        val canvas = Canvas(output)\n        val paint = Paint().apply {\n            isAntiAlias = true\n            isFilterBitmap = true\n            shader = BitmapShader(squareBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)\n        }\n        val rect = RectF(0f, 0f, size.toFloat(), size.toFloat())\n        canvas.drawRoundRect(rect, cornerRadius, cornerRadius, paint)\n        \n        if (squareBitmap != bitmap) {\n            squareBitmap.recycle()\n        }\n        \n        return output\n    }\n\n    private fun getCircularBitmap(bitmap: Bitmap): Bitmap {\n        val size = minOf(bitmap.width, bitmap.height)\n        \n        // First crop to square\n        val xOffset = (bitmap.width - size) / 2\n        val yOffset = (bitmap.height - size) / 2\n        val squareBitmap = Bitmap.createBitmap(bitmap, xOffset, yOffset, size, size)\n        \n        // Create circular output\n        val output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)\n        val canvas = Canvas(output)\n        val paint = Paint().apply {\n            isAntiAlias = true\n            isFilterBitmap = true\n            shader = BitmapShader(squareBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)\n        }\n        val radius = size / 2f\n        canvas.drawCircle(radius, radius, radius, paint)\n        \n        if (squareBitmap != bitmap) {\n            squareBitmap.recycle()\n        }\n        return output\n    }\n\n    private fun createCompactSquareRemoteViews(\n        albumArt: Bitmap?,\n        isPlaying: Boolean\n    ): RemoteViews {\n        val views = RemoteViews(context.packageName, R.layout.widget_compact_square)\n\n        // Set album art with rounded corners\n        if (albumArt != null) {\n            val roundedAlbumArt = getRoundedCornerBitmap(albumArt, 48f)\n            views.setImageViewBitmap(R.id.widget_compact_album_art, roundedAlbumArt)\n        } else {\n            views.setImageViewBitmap(R.id.widget_compact_album_art, getRoundedDefaultIcon(48f))\n        }\n\n        // Set play/pause icon - using low style icons\n        val playPauseIcon = if (isPlaying) R.drawable.ic_widget_pause_low else R.drawable.ic_widget_play_low\n        views.setImageViewResource(R.id.widget_compact_play_pause, playPauseIcon)\n\n        // Set click intents\n        views.setOnClickPendingIntent(R.id.widget_compact_album_art, getOpenAppIntent())\n        views.setOnClickPendingIntent(R.id.widget_compact_play_container, getPlayPauseIntent())\n\n        return views\n    }\n\n    private fun createCompactWideRemoteViews(\n        title: String,\n        artist: String,\n        albumArt: Bitmap?,\n        isPlaying: Boolean,\n        isLiked: Boolean\n    ): RemoteViews {\n        val views = RemoteViews(context.packageName, R.layout.widget_compact_wide)\n\n        // Set song info\n        views.setTextViewText(R.id.widget_wide_song_title, title)\n        views.setTextViewText(R.id.widget_wide_artist_name, artist)\n\n        // Set album art with rounded corners (48f to match 12dp at ~4x density for 48dp view)\n        if (albumArt != null) {\n            val roundedAlbumArt = getRoundedCornerBitmap(albumArt, 48f)\n            views.setImageViewBitmap(R.id.widget_wide_album_art, roundedAlbumArt)\n        } else {\n            // Create rounded default icon\n            views.setImageViewBitmap(R.id.widget_wide_album_art, getRoundedDefaultIcon(48f))\n        }\n\n        // Set play/pause icon - using low style icons\n        val playPauseIcon = if (isPlaying) R.drawable.ic_widget_pause_low else R.drawable.ic_widget_play_low\n        views.setImageViewResource(R.id.widget_wide_play_pause, playPauseIcon)\n\n        // Set like icon - using navigation style (purple)\n        val likeIcon = if (isLiked) R.drawable.ic_widget_heart_nav else R.drawable.ic_widget_heart_outline_nav\n        views.setImageViewResource(R.id.widget_wide_like_button, likeIcon)\n\n        // Set click intents\n        views.setOnClickPendingIntent(R.id.widget_wide_album_art, getOpenAppIntent())\n        views.setOnClickPendingIntent(R.id.widget_wide_play_container, getPlayPauseIntent())\n        views.setOnClickPendingIntent(R.id.widget_wide_like_button, getLikeIntent())\n\n        return views\n    }\n\n    private fun createTurntableRemoteViews(\n        circularAlbumArt: Bitmap?,\n        isPlaying: Boolean,\n        isLiked: Boolean\n    ): RemoteViews {\n        val views = RemoteViews(context.packageName, R.layout.widget_turntable)\n\n        // Set circular album art - create circular default icon if no album art\n        if (circularAlbumArt != null) {\n            views.setImageViewBitmap(R.id.widget_turntable_album_art, circularAlbumArt)\n        } else {\n            // Load and make the default icon circular\n            views.setImageViewBitmap(R.id.widget_turntable_album_art, getCircularDefaultIcon())\n        }\n\n        // Set play/pause icon - using secondary color icons for turntable\n        val playPauseIcon = if (isPlaying) R.drawable.ic_widget_pause_secondary else R.drawable.ic_widget_play_secondary\n        views.setImageViewResource(R.id.widget_turntable_play_pause, playPauseIcon)\n\n        // Set click intents\n        views.setOnClickPendingIntent(R.id.widget_turntable_album_art, getOpenAppIntent())\n        views.setOnClickPendingIntent(R.id.widget_turntable_play_container, getTurntablePlayPauseIntent())\n        views.setOnClickPendingIntent(R.id.widget_turntable_prev_button, getTurntablePreviousIntent())\n        views.setOnClickPendingIntent(R.id.widget_turntable_next_button, getTurntableNextIntent())\n\n        return views\n    }\n    \n    private fun getCircularDefaultIcon(): Bitmap {\n        // Get the launcher icon and make it circular\n        val drawable = context.packageManager.getApplicationIcon(context.packageName)\n        val size = 300\n        val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)\n        val canvas = Canvas(bitmap)\n        drawable.setBounds(0, 0, size, size)\n        drawable.draw(canvas)\n        return getCircularBitmap(bitmap)\n    }\n    \n    private fun getRoundedDefaultIcon(cornerRadius: Float): Bitmap {\n        // Get the launcher icon and make it rounded\n        val drawable = context.packageManager.getApplicationIcon(context.packageName)\n        val size = 300\n        val bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)\n        val canvas = Canvas(bitmap)\n        drawable.setBounds(0, 0, size, size)\n        drawable.draw(canvas)\n        return getRoundedCornerBitmap(bitmap, cornerRadius)\n    }\n\n    private fun getOpenAppIntent(): PendingIntent {\n        val intent = Intent(context, MainActivity::class.java)\n        return PendingIntent.getActivity(\n            context,\n            0,\n            intent,\n            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n        )\n    }\n\n    private fun getPlayPauseIntent(): PendingIntent {\n        val intent = Intent(context, MusicWidgetReceiver::class.java).apply {\n            action = MusicWidgetReceiver.ACTION_PLAY_PAUSE\n        }\n        return PendingIntent.getBroadcast(\n            context,\n            1,\n            intent,\n            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n        )\n    }\n\n    private fun getLikeIntent(): PendingIntent {\n        val intent = Intent(context, MusicWidgetReceiver::class.java).apply {\n            action = MusicWidgetReceiver.ACTION_LIKE\n        }\n        return PendingIntent.getBroadcast(\n            context,\n            2,\n            intent,\n            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n        )\n    }\n\n    private fun getTurntablePlayPauseIntent(): PendingIntent {\n        val intent = Intent(context, TurntableWidgetReceiver::class.java).apply {\n            action = TurntableWidgetReceiver.ACTION_TURNTABLE_PLAY_PAUSE\n        }\n        return PendingIntent.getBroadcast(\n            context,\n            3,\n            intent,\n            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n        )\n    }\n\n    private fun getTurntableNextIntent(): PendingIntent {\n        val intent = Intent(context, TurntableWidgetReceiver::class.java).apply {\n            action = TurntableWidgetReceiver.ACTION_TURNTABLE_NEXT\n        }\n        return PendingIntent.getBroadcast(\n            context,\n            4,\n            intent,\n            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n        )\n    }\n\n    private fun getTurntablePreviousIntent(): PendingIntent {\n        val intent = Intent(context, TurntableWidgetReceiver::class.java).apply {\n            action = TurntableWidgetReceiver.ACTION_TURNTABLE_PREVIOUS\n        }\n        return PendingIntent.getBroadcast(\n            context,\n            5,\n            intent,\n            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/widget/MusicRecognizerWidgetReceiver.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.widget\n\nimport android.app.PendingIntent\nimport android.appwidget.AppWidgetManager\nimport android.appwidget.AppWidgetProvider\nimport android.content.ComponentName\nimport android.content.Context\nimport android.content.Intent\nimport android.graphics.BitmapFactory\nimport android.os.Build\nimport android.os.Bundle\nimport android.view.View\nimport android.widget.RemoteViews\nimport com.metrolist.music.MainActivity\nimport com.metrolist.music.R\nimport com.metrolist.music.recognition.MusicRecognitionService\nimport com.metrolist.music.widget.MusicRecognizerWidgetService.Companion.ALBUM_ART_CACHE_FILE\nimport com.metrolist.music.widget.MusicRecognizerWidgetService.Companion.PREF_ARTIST_NAME\nimport com.metrolist.music.widget.MusicRecognizerWidgetService.Companion.PREF_COVER_ART_PATH\nimport com.metrolist.music.widget.MusicRecognizerWidgetService.Companion.PREF_ERROR_MESSAGE\nimport com.metrolist.music.widget.MusicRecognizerWidgetService.Companion.PREF_PULSE_FRAME\nimport com.metrolist.music.widget.MusicRecognizerWidgetService.Companion.PREF_SONG_TITLE\nimport com.metrolist.music.widget.MusicRecognizerWidgetService.Companion.PREF_STATE\nimport com.metrolist.music.widget.MusicRecognizerWidgetService.Companion.PREFS_NAME\nimport com.metrolist.music.widget.MusicRecognizerWidgetService.Companion.STATE_ERROR\nimport com.metrolist.music.widget.MusicRecognizerWidgetService.Companion.STATE_IDLE\nimport com.metrolist.music.widget.MusicRecognizerWidgetService.Companion.STATE_LISTENING\nimport com.metrolist.music.widget.MusicRecognizerWidgetService.Companion.STATE_NO_MATCH\nimport com.metrolist.music.widget.MusicRecognizerWidgetService.Companion.STATE_PROCESSING\nimport com.metrolist.music.widget.MusicRecognizerWidgetService.Companion.STATE_SUCCESS\nimport java.io.File\n\n/**\n * AppWidgetProvider for the Music Recognizer Widget.\n *\n * Sizes:\n *  - 1×1 (minWidth < 110dp): Only the animated mic circle\n *  - 1×3 (minWidth 110–229dp): Album art + song info + mic button (compact)\n *  - 1×4 (minWidth ≥ 230dp): Album art + song info + mic button (wide, default)\n *\n * Click behaviour:\n *  - Mic button  → start / stop recognition\n *  - Album art / text area (SUCCESS) → open app on recognition history screen\n *  - Album art / text area (other)   → open app on recognition screen\n */\nclass MusicRecognizerWidgetReceiver : AppWidgetProvider() {\n\n    override fun onUpdate(\n        context: Context,\n        appWidgetManager: AppWidgetManager,\n        appWidgetIds: IntArray\n    ) {\n        updateAllWidgets(context, appWidgetManager)\n    }\n\n    override fun onAppWidgetOptionsChanged(\n        context: Context,\n        appWidgetManager: AppWidgetManager,\n        appWidgetId: Int,\n        newOptions: Bundle\n    ) {\n        super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)\n        updateAllWidgets(context, appWidgetManager)\n    }\n\n    override fun onReceive(context: Context, intent: Intent) {\n        super.onReceive(context, intent)\n        when (intent.action) {\n            ACTION_START_RECOGNITION -> handleStartRecognition(context)\n            ACTION_UPDATE_WIDGET -> updateAllWidgets(context, AppWidgetManager.getInstance(context))\n            ACTION_RESET_STATE -> {\n                context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit()\n                    .putInt(PREF_STATE, STATE_IDLE)\n                    .putString(PREF_SONG_TITLE, \"\")\n                    .putString(PREF_ARTIST_NAME, \"\")\n                    .putString(PREF_ERROR_MESSAGE, \"\")\n                    .putString(PREF_COVER_ART_PATH, \"\")\n                    .putInt(PREF_PULSE_FRAME, 0)\n                    .apply()\n                File(context.cacheDir, ALBUM_ART_CACHE_FILE).delete()\n                updateAllWidgets(context, AppWidgetManager.getInstance(context))\n            }\n        }\n    }\n\n    // ─── Recognition start / stop ─────────────────────────────────────────────\n\n    private fun handleStartRecognition(context: Context) {\n        val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)\n        val currentState = prefs.getInt(PREF_STATE, STATE_IDLE)\n\n        // If active → stop\n        if (currentState == STATE_LISTENING || currentState == STATE_PROCESSING) {\n            context.startService(\n                Intent(context, MusicRecognizerWidgetService::class.java).apply {\n                    action = MusicRecognizerWidgetService.ACTION_STOP_RECOGNITION\n                }\n            )\n            return\n        }\n\n        // Showing a result/error → clear it before starting a new search\n        if (currentState == STATE_SUCCESS || currentState == STATE_NO_MATCH || currentState == STATE_ERROR) {\n            prefs.edit().putInt(PREF_STATE, STATE_IDLE).apply()\n        }\n\n        // No mic permission → open the app so the user can grant it\n        if (!MusicRecognitionService.hasRecordPermission(context)) {\n            context.startActivity(\n                Intent(context, MainActivity::class.java).apply {\n                    action = MainActivity.ACTION_RECOGNITION\n                    flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP\n                }\n            )\n            return\n        }\n\n        // Start recognition foreground service\n        val serviceIntent = Intent(context, MusicRecognizerWidgetService::class.java).apply {\n            action = MusicRecognizerWidgetService.ACTION_START_RECOGNITION\n        }\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            context.startForegroundService(serviceIntent)\n        } else {\n            context.startService(serviceIntent)\n        }\n    }\n\n    // ─── Widget update ────────────────────────────────────────────────────────\n\n    private fun updateAllWidgets(context: Context, appWidgetManager: AppWidgetManager) {\n        val componentName = ComponentName(context, MusicRecognizerWidgetReceiver::class.java)\n        val widgetIds = appWidgetManager.getAppWidgetIds(componentName)\n\n        val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)\n        val state = prefs.getInt(PREF_STATE, STATE_IDLE)\n        val songTitle = prefs.getString(PREF_SONG_TITLE, \"\") ?: \"\"\n        val artistName = prefs.getString(PREF_ARTIST_NAME, \"\") ?: \"\"\n        val errorMessage = prefs.getString(PREF_ERROR_MESSAGE, \"\") ?: \"\"\n        val coverArtPath = prefs.getString(PREF_COVER_ART_PATH, \"\") ?: \"\"\n        val pulseFrame = prefs.getInt(PREF_PULSE_FRAME, 0)\n\n        // Load album art bitmap from the cached file (synchronous, already on disk)\n        val albumArtBitmap = if (state == STATE_SUCCESS && coverArtPath.isNotEmpty()) {\n            try { BitmapFactory.decodeFile(coverArtPath) } catch (_: Exception) { null }\n        } else null\n\n        widgetIds.forEach { widgetId ->\n            val options = appWidgetManager.getAppWidgetOptions(widgetId)\n            val minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)\n\n            val views = when {\n                minWidth < 110 -> createTinyViews(context, state, pulseFrame)\n                minWidth < 230 -> createCompactViews(context, state, songTitle, artistName, errorMessage, albumArtBitmap, pulseFrame)\n                else -> createWideViews(context, state, songTitle, artistName, errorMessage, albumArtBitmap, pulseFrame)\n            }\n\n            appWidgetManager.updateAppWidget(widgetId, views)\n        }\n    }\n\n    // ─── Layout builders ──────────────────────────────────────────────────────\n\n    private fun createWideViews(\n        context: Context,\n        state: Int,\n        songTitle: String,\n        artistName: String,\n        errorMessage: String,\n        albumArt: android.graphics.Bitmap?,\n        pulseFrame: Int\n    ): RemoteViews {\n        val views = RemoteViews(context.packageName, R.layout.widget_recognizer_wide)\n        applyAlbumArt(views, state, albumArt)\n        applyTextState(context, views, state, songTitle, artistName, errorMessage)\n        applyMicState(views, state, pulseFrame, R.id.widget_recognizer_mic_container, R.id.widget_recognizer_pulse)\n        views.setOnClickPendingIntent(R.id.widget_recognizer_mic_container, getMicIntent(context))\n        val infoIntent = getInfoAreaIntent(context)\n        views.setOnClickPendingIntent(R.id.widget_recognizer_text_area, infoIntent)\n        views.setOnClickPendingIntent(R.id.widget_recognizer_album_art, infoIntent)\n        return views\n    }\n\n    private fun createCompactViews(\n        context: Context,\n        state: Int,\n        songTitle: String,\n        artistName: String,\n        errorMessage: String,\n        albumArt: android.graphics.Bitmap?,\n        pulseFrame: Int\n    ): RemoteViews {\n        val views = RemoteViews(context.packageName, R.layout.widget_recognizer_compact)\n        applyAlbumArt(views, state, albumArt)\n        applyTextState(context, views, state, songTitle, artistName, errorMessage)\n        applyMicState(views, state, pulseFrame, R.id.widget_recognizer_mic_container, R.id.widget_recognizer_pulse)\n        views.setOnClickPendingIntent(R.id.widget_recognizer_mic_container, getMicIntent(context))\n        val infoIntent = getInfoAreaIntent(context)\n        views.setOnClickPendingIntent(R.id.widget_recognizer_text_area, infoIntent)\n        views.setOnClickPendingIntent(R.id.widget_recognizer_album_art, infoIntent)\n        return views\n    }\n\n    private fun createTinyViews(\n        context: Context,\n        state: Int,\n        pulseFrame: Int\n    ): RemoteViews {\n        val views = RemoteViews(context.packageName, R.layout.widget_recognizer_tiny)\n        applyMicState(views, state, pulseFrame, R.id.widget_recognizer_tiny_mic_container, R.id.widget_recognizer_tiny_pulse)\n        views.setOnClickPendingIntent(R.id.widget_recognizer_tiny_root, getMicIntent(context))\n        return views\n    }\n\n    // ─── State helpers ────────────────────────────────────────────────────────\n\n    private fun applyAlbumArt(\n        views: RemoteViews,\n        state: Int,\n        albumArt: android.graphics.Bitmap?\n    ) {\n        if (state == STATE_SUCCESS && albumArt != null) {\n            views.setImageViewBitmap(R.id.widget_recognizer_album_art, albumArt)\n            views.setViewVisibility(R.id.widget_recognizer_album_art, View.VISIBLE)\n        } else {\n            views.setViewVisibility(R.id.widget_recognizer_album_art, View.GONE)\n        }\n    }\n\n    private fun applyTextState(\n        context: Context,\n        views: RemoteViews,\n        state: Int,\n        songTitle: String,\n        artistName: String,\n        errorMessage: String\n    ) {\n        when (state) {\n            STATE_IDLE -> {\n                views.setTextViewText(R.id.widget_recognizer_song_title,\n                    context.getString(R.string.widget_recognizer_tap_to_search))\n                views.setViewVisibility(R.id.widget_recognizer_artist_name, View.GONE)\n            }\n            STATE_LISTENING -> {\n                views.setTextViewText(R.id.widget_recognizer_song_title,\n                    context.getString(R.string.widget_recognizer_listening))\n                views.setViewVisibility(R.id.widget_recognizer_artist_name, View.GONE)\n            }\n            STATE_PROCESSING -> {\n                views.setTextViewText(R.id.widget_recognizer_song_title,\n                    context.getString(R.string.widget_recognizer_processing))\n                views.setViewVisibility(R.id.widget_recognizer_artist_name, View.GONE)\n            }\n            STATE_SUCCESS -> {\n                views.setTextViewText(R.id.widget_recognizer_song_title,\n                    songTitle.ifEmpty { context.getString(R.string.widget_recognizer_unknown_song) })\n                views.setTextViewText(R.id.widget_recognizer_artist_name,\n                    artistName.ifEmpty { context.getString(R.string.widget_recognizer_unknown_artist) })\n                views.setViewVisibility(R.id.widget_recognizer_artist_name, View.VISIBLE)\n            }\n            STATE_NO_MATCH -> {\n                views.setTextViewText(R.id.widget_recognizer_song_title,\n                    context.getString(R.string.widget_recognizer_no_match))\n                views.setViewVisibility(R.id.widget_recognizer_artist_name, View.GONE)\n            }\n            STATE_ERROR -> {\n                views.setTextViewText(R.id.widget_recognizer_song_title,\n                    context.getString(R.string.widget_recognizer_error))\n                views.setTextViewText(R.id.widget_recognizer_artist_name,\n                    errorMessage.ifEmpty { context.getString(R.string.widget_recognizer_error_generic) })\n                views.setViewVisibility(R.id.widget_recognizer_artist_name, View.VISIBLE)\n            }\n        }\n    }\n\n    private fun applyMicState(\n        views: RemoteViews,\n        state: Int,\n        pulseFrame: Int,\n        micContainerId: Int,\n        pulseViewId: Int\n    ) {\n        val isActive = state == STATE_LISTENING || state == STATE_PROCESSING\n\n        views.setInt(\n            micContainerId, \"setBackgroundResource\",\n            if (isActive) R.drawable.widget_mic_button_bg_active else R.drawable.widget_mic_button_bg\n        )\n\n        val pulseDrawable = if (isActive) {\n            when (pulseFrame % 4) {\n                0 -> R.drawable.widget_mic_pulse_1\n                1 -> R.drawable.widget_mic_pulse_2\n                2 -> R.drawable.widget_mic_pulse_3\n                else -> R.drawable.widget_mic_pulse_4\n            }\n        } else R.drawable.widget_mic_pulse_idle\n\n        views.setImageViewResource(pulseViewId, pulseDrawable)\n    }\n\n    // ─── PendingIntents ───────────────────────────────────────────────────────\n\n    /** Tap on mic button → start or stop recognition */\n    private fun getMicIntent(context: Context): PendingIntent =\n        PendingIntent.getBroadcast(\n            context, 20,\n            Intent(context, MusicRecognizerWidgetReceiver::class.java).apply {\n                action = ACTION_START_RECOGNITION\n            },\n            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n        )\n\n    /**\n     * Tap on song info area / album art → always open the recognition screen.\n     * On SUCCESS the screen will show the result that the widget service already\n     * set on [MusicRecognitionService.recognitionStatus].\n     */\n    private fun getInfoAreaIntent(context: Context): PendingIntent =\n        PendingIntent.getActivity(\n            context, 21,\n            Intent(context, MainActivity::class.java).apply {\n                action = MainActivity.ACTION_RECOGNITION\n                flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP\n            },\n            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n        )\n\n    companion object {\n        const val ACTION_START_RECOGNITION = \"com.metrolist.music.widget.recognizer.TAP_MIC\"\n        const val ACTION_UPDATE_WIDGET = \"com.metrolist.music.widget.recognizer.UPDATE\"\n        const val ACTION_RESET_STATE = \"com.metrolist.music.widget.recognizer.RESET\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/widget/MusicRecognizerWidgetService.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.widget\n\nimport android.app.NotificationChannel\nimport android.app.NotificationManager\nimport android.app.PendingIntent\nimport android.app.Service\nimport android.content.Context\nimport android.content.Intent\nimport android.graphics.Bitmap\nimport android.graphics.BitmapShader\nimport android.graphics.Canvas\nimport android.graphics.Paint\nimport android.graphics.RectF\nimport android.graphics.Shader\nimport android.os.Build\nimport android.os.IBinder\nimport androidx.core.app.NotificationCompat\nimport coil3.ImageLoader\nimport coil3.request.ImageRequest\nimport coil3.request.allowHardware\nimport coil3.request.crossfade\nimport coil3.toBitmap\nimport com.metrolist.music.MainActivity\nimport com.metrolist.music.R\nimport com.metrolist.music.db.DatabaseDao\nimport com.metrolist.music.db.entities.RecognitionHistory\nimport com.metrolist.music.recognition.MusicRecognitionService\nimport com.metrolist.shazamkit.models.RecognitionStatus\nimport dagger.hilt.EntryPoint\nimport dagger.hilt.InstallIn\nimport dagger.hilt.android.EntryPointAccessors\nimport dagger.hilt.components.SingletonComponent\nimport java.time.LocalDateTime\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.cancel\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport java.io.File\nimport java.io.FileOutputStream\n\n@EntryPoint\n@InstallIn(SingletonComponent::class)\ninterface RecognizerWidgetEntryPoint {\n    fun databaseDao(): DatabaseDao\n}\n\n/**\n * Foreground service that handles music recognition for the widget.\n * Runs recognition in the foreground to allow microphone access,\n * downloads & caches the album art, then broadcasts results back to\n * the widget receiver.\n */\nclass MusicRecognizerWidgetService : Service() {\n\n    private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())\n    private var recognitionJob: Job? = null\n    private var pulseJob: Job? = null\n\n    private val imageLoader by lazy {\n        ImageLoader.Builder(this).crossfade(false).build()\n    }\n\n    override fun onBind(intent: Intent?): IBinder? = null\n\n    override fun onCreate() {\n        super.onCreate()\n        createNotificationChannel()\n    }\n\n    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\n        when (intent?.action) {\n            ACTION_START_RECOGNITION -> {\n                startForegroundNotification()\n                startRecognition()\n            }\n            ACTION_STOP_RECOGNITION -> stopRecognitionAndService()\n        }\n        return START_NOT_STICKY\n    }\n\n    // ─── Foreground notification ──────────────────────────────────────────────\n\n    private fun startForegroundNotification() {\n        val openAppIntent = PendingIntent.getActivity(\n            this, 0,\n            Intent(this, MainActivity::class.java),\n            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n        )\n        val stopIntent = PendingIntent.getService(\n            this, 1,\n            Intent(this, MusicRecognizerWidgetService::class.java).apply {\n                action = ACTION_STOP_RECOGNITION\n            },\n            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n        )\n\n        val notification = NotificationCompat.Builder(this, CHANNEL_ID)\n            .setSmallIcon(R.drawable.ic_widget_mic)\n            .setContentTitle(getString(R.string.widget_recognizer_listening))\n            .setContentText(getString(R.string.widget_recognizer_notification_text))\n            .setContentIntent(openAppIntent)\n            .addAction(\n                android.R.drawable.ic_menu_close_clear_cancel,\n                getString(android.R.string.cancel),\n                stopIntent\n            )\n            .setOngoing(true)\n            .setPriority(NotificationCompat.PRIORITY_LOW)\n            .build()\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {\n            startForeground(\n                NOTIFICATION_ID, notification,\n                android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE\n            )\n        } else {\n            startForeground(NOTIFICATION_ID, notification)\n        }\n    }\n\n    // ─── Recognition flow ─────────────────────────────────────────────────────\n\n    private fun startRecognition() {\n        saveState(STATE_LISTENING)\n        updateAllWidgets()\n\n        // Animate pulse rings while active\n        pulseJob = serviceScope.launch {\n            var frame = 0\n            while (isActive) {\n                savePulseFrame(frame)\n                updateAllWidgets()\n                frame = (frame + 1) % PULSE_FRAME_COUNT\n                delay(PULSE_INTERVAL_MS)\n            }\n        }\n\n        recognitionJob = serviceScope.launch {\n            try {\n                val result = MusicRecognitionService.recognize(this@MusicRecognizerWidgetService)\n                pulseJob?.cancel()\n\n                val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)\n                when (result) {\n                    is RecognitionStatus.Success -> {\n                        val artPath = downloadAndCacheAlbumArt(\n                            result.result.coverArtHqUrl ?: result.result.coverArtUrl\n                        )?.absolutePath ?: \"\"\n                        prefs.edit()\n                            .putInt(PREF_STATE, STATE_SUCCESS)\n                            .putString(PREF_SONG_TITLE, result.result.title)\n                            .putString(PREF_ARTIST_NAME, result.result.artist)\n                            .putString(PREF_COVER_ART_PATH, artPath)\n                            .putInt(PREF_PULSE_FRAME, 0)\n                            .apply()\n                        // Save to history so the result is persisted even if the user\n                        // never opens the recognition screen after seeing the widget result.\n                        try {\n                            val dao = EntryPointAccessors.fromApplication(\n                                applicationContext,\n                                RecognizerWidgetEntryPoint::class.java\n                            ).databaseDao()\n                            dao.insert(\n                                RecognitionHistory(\n                                    trackId = result.result.trackId,\n                                    title = result.result.title,\n                                    artist = result.result.artist,\n                                    album = result.result.album,\n                                    coverArtUrl = result.result.coverArtUrl,\n                                    coverArtHqUrl = result.result.coverArtHqUrl,\n                                    genre = result.result.genre,\n                                    releaseDate = result.result.releaseDate,\n                                    label = result.result.label,\n                                    shazamUrl = result.result.shazamUrl,\n                                    appleMusicUrl = result.result.appleMusicUrl,\n                                    spotifyUrl = result.result.spotifyUrl,\n                                    isrc = result.result.isrc,\n                                    youtubeVideoId = result.result.youtubeVideoId,\n                                    recognizedAt = LocalDateTime.now()\n                                )\n                            )\n                            // Tell RecognitionScreen not to save again (avoid duplicate entry)\n                            MusicRecognitionService.resultSavedExternally = true\n                        } catch (_: Exception) {\n                            // Non-fatal – widget result is still displayed\n                        }\n                    }\n                    is RecognitionStatus.NoMatch -> {\n                        prefs.edit()\n                            .putInt(PREF_STATE, STATE_NO_MATCH)\n                            .putString(PREF_ERROR_MESSAGE, result.message)\n                            .putString(PREF_COVER_ART_PATH, \"\")\n                            .putInt(PREF_PULSE_FRAME, 0)\n                            .apply()\n                    }\n                    is RecognitionStatus.Error -> {\n                        prefs.edit()\n                            .putInt(PREF_STATE, STATE_ERROR)\n                            .putString(PREF_ERROR_MESSAGE, result.message)\n                            .putString(PREF_COVER_ART_PATH, \"\")\n                            .putInt(PREF_PULSE_FRAME, 0)\n                            .apply()\n                    }\n                    else -> {\n                        prefs.edit()\n                            .putInt(PREF_STATE, STATE_IDLE)\n                            .putString(PREF_COVER_ART_PATH, \"\")\n                            .putInt(PREF_PULSE_FRAME, 0)\n                            .apply()\n                    }\n                }\n            } catch (e: Exception) {\n                pulseJob?.cancel()\n                getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit()\n                    .putInt(PREF_STATE, STATE_ERROR)\n                    .putString(PREF_ERROR_MESSAGE, e.message ?: getString(R.string.widget_recognizer_error))\n                    .putString(PREF_COVER_ART_PATH, \"\")\n                    .putInt(PREF_PULSE_FRAME, 0)\n                    .apply()\n            } finally {\n                updateAllWidgets()\n                stopForeground(STOP_FOREGROUND_REMOVE)\n                stopSelf()\n            }\n        }\n    }\n\n    /**\n     * Downloads [url], clips it to a rounded square (corner 24dp), and writes\n     * the result to [ALBUM_ART_CACHE_FILE] inside the app cache directory.\n     * Returns the file on success or null on any failure.\n     */\n    private suspend fun downloadAndCacheAlbumArt(url: String?): File? {\n        if (url.isNullOrBlank()) return null\n        return withContext(Dispatchers.IO) {\n            try {\n                val request = ImageRequest.Builder(this@MusicRecognizerWidgetService)\n                    .data(url)\n                    .size(200, 200)\n                    .allowHardware(false)\n                    .build()\n                val bitmap = imageLoader.execute(request).image?.toBitmap() ?: return@withContext null\n                val rounded = getRoundedCornerBitmap(bitmap, 24f)\n                val file = File(cacheDir, ALBUM_ART_CACHE_FILE)\n                FileOutputStream(file).use { rounded.compress(Bitmap.CompressFormat.PNG, 90, it) }\n                file\n            } catch (_: Exception) {\n                null\n            }\n        }\n    }\n\n    private fun getRoundedCornerBitmap(bitmap: Bitmap, cornerRadius: Float): Bitmap {\n        val size = minOf(bitmap.width, bitmap.height)\n        val xOff = (bitmap.width - size) / 2\n        val yOff = (bitmap.height - size) / 2\n        val square = Bitmap.createBitmap(bitmap, xOff, yOff, size, size)\n        val output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)\n        val canvas = Canvas(output)\n        val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG).apply {\n            shader = BitmapShader(square, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)\n        }\n        canvas.drawRoundRect(RectF(0f, 0f, size.toFloat(), size.toFloat()), cornerRadius, cornerRadius, paint)\n        if (square != bitmap) square.recycle()\n        return output\n    }\n\n    // ─── Helpers ─────────────────────────────────────────────────────────────\n\n    private fun stopRecognitionAndService() {\n        recognitionJob?.cancel()\n        pulseJob?.cancel()\n        saveState(STATE_IDLE)\n        savePulseFrame(0)\n        updateAllWidgets()\n        stopForeground(STOP_FOREGROUND_REMOVE)\n        stopSelf()\n    }\n\n    private fun saveState(state: Int) {\n        getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)\n            .edit().putInt(PREF_STATE, state).apply()\n    }\n\n    private fun savePulseFrame(frame: Int) {\n        getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)\n            .edit().putInt(PREF_PULSE_FRAME, frame).apply()\n    }\n\n    private fun updateAllWidgets() {\n        sendBroadcast(\n            Intent(this, MusicRecognizerWidgetReceiver::class.java).apply {\n                action = MusicRecognizerWidgetReceiver.ACTION_UPDATE_WIDGET\n            }\n        )\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        serviceScope.cancel()\n    }\n\n    private fun createNotificationChannel() {\n        val channel = NotificationChannel(\n            CHANNEL_ID,\n            getString(R.string.widget_recognizer_channel_name),\n            NotificationManager.IMPORTANCE_LOW\n        ).apply {\n            description = getString(R.string.widget_recognizer_channel_desc)\n            setShowBadge(false)\n        }\n        getSystemService(NotificationManager::class.java).createNotificationChannel(channel)\n    }\n\n    // ─── Constants ────────────────────────────────────────────────────────────\n\n    companion object {\n        const val ACTION_START_RECOGNITION = \"com.metrolist.music.widget.recognizer.START\"\n        const val ACTION_STOP_RECOGNITION = \"com.metrolist.music.widget.recognizer.STOP\"\n\n        const val PREFS_NAME = \"recognizer_widget_prefs\"\n        const val PREF_STATE = \"state\"\n        const val PREF_SONG_TITLE = \"song_title\"\n        const val PREF_ARTIST_NAME = \"artist_name\"\n        const val PREF_ERROR_MESSAGE = \"error_message\"\n        const val PREF_PULSE_FRAME = \"pulse_frame\"\n        const val PREF_COVER_ART_PATH = \"cover_art_path\"\n\n        const val STATE_IDLE = 0\n        const val STATE_LISTENING = 1\n        const val STATE_PROCESSING = 2\n        const val STATE_SUCCESS = 3\n        const val STATE_NO_MATCH = 4\n        const val STATE_ERROR = 5\n\n        const val ALBUM_ART_CACHE_FILE = \"recognizer_widget_art.png\"\n\n        private const val PULSE_FRAME_COUNT = 4\n        private const val PULSE_INTERVAL_MS = 600L\n        private const val CHANNEL_ID = \"music_recognizer_widget\"\n        private const val NOTIFICATION_ID = 9001\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/widget/MusicWidgetReceiver.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.widget\n\nimport android.appwidget.AppWidgetManager\nimport android.appwidget.AppWidgetProvider\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Build\nimport android.os.Bundle\nimport com.metrolist.music.playback.MusicService\n\nclass MusicWidgetReceiver : AppWidgetProvider() {\n\n    override fun onUpdate(\n        context: Context,\n        appWidgetManager: AppWidgetManager,\n        appWidgetIds: IntArray\n    ) {\n        // Only trigger update through MusicService if it's already running\n        // This prevents BackgroundServiceStartNotAllowedException on Android 14+\n        if (MusicService.isRunning) {\n            val intent = Intent(context, MusicService::class.java).apply {\n                action = ACTION_UPDATE_WIDGET\n            }\n            try {\n                context.startService(intent)\n            } catch (e: Exception) {\n                // Service might be restricted in background\n            }\n        }\n        // If service is not running, widget shows default layout until user opens app\n    }\n\n    override fun onAppWidgetOptionsChanged(\n        context: Context,\n        appWidgetManager: AppWidgetManager,\n        appWidgetId: Int,\n        newOptions: Bundle\n    ) {\n        super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)\n        // Trigger widget update when size changes\n        if (MusicService.isRunning) {\n            val intent = Intent(context, MusicService::class.java).apply {\n                action = ACTION_UPDATE_WIDGET\n            }\n            try {\n                context.startService(intent)\n            } catch (e: Exception) {\n                // Service might be restricted in background\n            }\n        }\n    }\n\n    override fun onReceive(context: Context, intent: Intent) {\n        super.onReceive(context, intent)\n\n        when (intent.action) {\n            ACTION_PLAY_PAUSE, ACTION_LIKE, ACTION_NEXT, ACTION_PREVIOUS -> {\n                // User interactions from widget buttons can start the service\n                // Android allows starting FGS from widget PendingIntent clicks\n                val serviceIntent = Intent(context, MusicService::class.java).apply {\n                    action = intent.action\n                    putExtras(intent)\n                }\n                try {\n                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n                        context.startService(serviceIntent)\n                    } else {\n                        context.startService(serviceIntent)\n                    }\n                } catch (e: Exception) {\n                    // Service might be restricted in background\n                }\n            }\n        }\n    }\n\n    companion object {\n        const val ACTION_PLAY_PAUSE = \"com.metrolist.music.widget.PLAY_PAUSE\"\n        const val ACTION_LIKE = \"com.metrolist.music.widget.LIKE\"\n        const val ACTION_NEXT = \"com.metrolist.music.widget.NEXT\"\n        const val ACTION_PREVIOUS = \"com.metrolist.music.widget.PREVIOUS\"\n        const val ACTION_UPDATE_WIDGET = \"com.metrolist.music.widget.UPDATE_WIDGET\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/kotlin/com/metrolist/music/widget/TurntableWidgetReceiver.kt",
    "content": "/**\n * Metrolist Project (C) 2026\n * Licensed under GPL-3.0 | See git history for contributors\n */\n\npackage com.metrolist.music.widget\n\nimport android.appwidget.AppWidgetManager\nimport android.appwidget.AppWidgetProvider\nimport android.content.Context\nimport android.content.Intent\nimport com.metrolist.music.playback.MusicService\n\nclass TurntableWidgetReceiver : AppWidgetProvider() {\n\n    override fun onUpdate(\n        context: Context,\n        appWidgetManager: AppWidgetManager,\n        appWidgetIds: IntArray\n    ) {\n        // Only trigger update through MusicService if it's already running\n        // This prevents BackgroundServiceStartNotAllowedException on Android 14+\n        if (MusicService.isRunning) {\n            val intent = Intent(context, MusicService::class.java).apply {\n                action = ACTION_UPDATE_TURNTABLE_WIDGET\n            }\n            try {\n                context.startService(intent)\n            } catch (e: Exception) {\n                // Service might be restricted in background\n            }\n        }\n        // If service is not running, widget shows default layout until user opens app\n    }\n\n    override fun onReceive(context: Context, intent: Intent) {\n        super.onReceive(context, intent)\n\n        when (intent.action) {\n            ACTION_TURNTABLE_PLAY_PAUSE, ACTION_TURNTABLE_NEXT, ACTION_TURNTABLE_PREVIOUS -> {\n                // User interactions from widget buttons can start the service\n                // Android allows starting FGS from widget PendingIntent clicks\n                val serviceIntent = Intent(context, MusicService::class.java).apply {\n                    action = when (intent.action) {\n                        ACTION_TURNTABLE_PLAY_PAUSE -> MusicWidgetReceiver.ACTION_PLAY_PAUSE\n                        ACTION_TURNTABLE_NEXT -> MusicWidgetReceiver.ACTION_NEXT\n                        ACTION_TURNTABLE_PREVIOUS -> MusicWidgetReceiver.ACTION_PREVIOUS\n                        else -> intent.action\n                    }\n                    putExtras(intent)\n                }\n                try {\n                    context.startService(serviceIntent)\n                } catch (e: Exception) {\n                    // Service might be restricted in background\n                }\n            }\n        }\n    }\n\n    companion object {\n        const val ACTION_TURNTABLE_PLAY_PAUSE = \"com.metrolist.music.widget.TURNTABLE_PLAY_PAUSE\"\n        const val ACTION_TURNTABLE_NEXT = \"com.metrolist.music.widget.TURNTABLE_NEXT\"\n        const val ACTION_TURNTABLE_PREVIOUS = \"com.metrolist.music.widget.TURNTABLE_PREVIOUS\"\n        const val ACTION_UPDATE_TURNTABLE_WIDGET = \"com.metrolist.music.widget.UPDATE_TURNTABLE_WIDGET\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/res/drawable/account.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:width=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M234,684q51,-39 114,-61.5T480,600q69,0 132,22.5T726,684q35,-41 54.5,-93T800,480q0,-133 -93.5,-226.5T480,160q-133,0 -226.5,93.5T160,480q0,59 19.5,111t54.5,93ZM480,520q-59,0 -99.5,-40.5T340,380q0,-59 40.5,-99.5T480,240q59,0 99.5,40.5T620,380q0,59 -40.5,99.5T480,520ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM480,800q53,0 100,-15.5t86,-44.5q-39,-29 -86,-44.5T480,680q-53,0 -100,15.5T294,740q39,29 86,44.5T480,800ZM480,440q26,0 43,-17t17,-43q0,-26 -17,-43t-43,-17q-26,0 -43,17t-17,43q0,26 17,43t43,17ZM480,380ZM480,740Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/add.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M440,760L440,520L200,520L200,440L440,440L440,200L520,200L520,440L760,440L760,520L520,520L520,760L440,760Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/add_circle.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:width=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M440,680h80v-160h160v-80L520,440v-160h-80v160L280,440v80h160v160ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM480,800q134,0 227,-93t93,-227q0,-134 -93,-227t-227,-93q-134,0 -227,93t-93,227q0,134 93,227t227,93ZM480,480Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/album.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:tint=\"@android:color/white\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"M12,3c4.96,0 9,4.04 9,9s-4.04,9 -9,9s-9,-4.04 -9,-9S7.04,3 12,3M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2L12,2z\"\n        android:strokeWidth=\"1.0\"\n        android:strokeColor=\"@android:color/white\" />\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M12,7c-2.77,0 -5,2.23 -5,5s2.23,5 5,5s5,-2.23 5,-5S14.77,7 12,7zM12,13.11c-0.61,0 -1.11,-0.5 -1.11,-1.11s0.5,-1.11 1.11,-1.11s1.11,0.5 1.11,1.11S12.61,13.11 12,13.11z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/alphabet_cyrillic.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M16,6C14.36,6 13,7.36 13,9V15C13,16.65 14.36,18 16,18H17C18.65,18 20,16.65 20,15V12C20,10.36 18.65,9 17,9H15C15,8.44 15.44,8 16,8H18C19.09,8 20,7.09 20,6M5,9V11H8C8.57,11 9,11.43 9,12H7C5.36,12 4,13.36 4,15C4,16.65 5.36,18 7,18H11V12C11,10.36 9.65,9 8,9M15,11H17C17.57,11 18,11.43 18,12V15C18,15.57 17.57,16 17,16H16C15.43,16 15,15.57 15,15M7,14H9V16H7C6.43,16 6,15.57 6,15C6,14.43 6.43,14 7,14Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/app_logo.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"1000dp\"\n    android:height=\"1000dp\"\n    android:viewportWidth=\"1000\"\n    android:viewportHeight=\"1000\">\n  <path\n      android:pathData=\"M0.5,0L1000,0.5L999.5,1000L0,999.5L0.5,0Z\"\n      android:strokeAlpha=\"0\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#000000\"\n      android:strokeColor=\"#000000\"\n      android:fillAlpha=\"0\"/>\n  <path\n      android:pathData=\"M553.5,135L578,135.5L592.5,139L614,149.5Q631.5,161.5 644.5,178L726,258.5L726.5,260L852,384.5L766.5,471L758.5,462L757,461.5L748.5,452L676,381.5L662.5,367L655,360.5L641.5,346L634,339.5L632.5,337L631,336.5L627.5,332L624,329.5L621.5,326L602.5,308L602,571.5L602,637.5Q599.9,673.9 580.5,693L553.5,721L441.5,833L430,843.5L429.5,845L410,857.5Q399.9,864.4 384.5,866L380,866.5L364.5,867L360.5,866L352.5,865Q333.3,860.3 320,849.5L302,833.5L300.5,831L295,826.5L294.5,825L279,810.5L278.5,809L172,703.5Q163.4,693.6 158,680.5Q154,673.5 153,663.5L151.5,652L151,641.5L154,625.5Q158.5,603.5 172,590.5L200,561.5L296.5,465L300,462.5L315.5,446L320,448.5L326,454.5L337.5,467L346,474.5L358.5,488L367,495.5L379.5,509L388,516.5L390.5,520L395,523.5L398.5,528L403,531.5L387,546.5L384.5,550L381,552.5L378.5,556L309,623.5L287,646.5L350.5,710L366,725.5L370,729.5Q371.3,732.3 372.5,730L377,725.5L394,708.5L461.5,641L467,636.5L467.5,635L475,628.5L475.5,627L478,625.5L480,622.5L481,617.5L481,603.5L481,546.5L481,321.5L481,235.5L481.5,214L484,197.5L487.5,188L493,176.5Q500.2,164.2 510.5,155L530,142.5L544,137.5L553.5,135Z\"\n      android:strokeAlpha=\"0.99607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#FEFEFE\"\n      android:strokeColor=\"#FEFEFE\"\n      android:fillAlpha=\"0.99607843\"/>\n  <path\n      android:pathData=\"M618.5,152L620.5,155L618.5,152Z\"\n      android:strokeAlpha=\"0.6784314\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#ADADAD\"\n      android:strokeColor=\"#ADADAD\"\n      android:fillAlpha=\"0.6784314\"/>\n  <path\n      android:pathData=\"M512.5,153L511.5,155L512.5,153Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M504.5,160L503.5,162L504.5,160Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M503.5,162L500.5,166L503.5,162Z\"\n      android:strokeAlpha=\"0.7137255\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#B6B6B6\"\n      android:strokeColor=\"#B6B6B6\"\n      android:fillAlpha=\"0.7137255\"/>\n  <path\n      android:pathData=\"M499.5,166L498.5,168L499.5,166Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M498.5,168L497.5,170L498.5,168Z\"\n      android:strokeAlpha=\"0.7137255\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#B6B6B6\"\n      android:strokeColor=\"#B6B6B6\"\n      android:fillAlpha=\"0.7137255\"/>\n  <path\n      android:pathData=\"M496.5,170L495.5,172L496.5,170Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M645.5,178L646.5,180L645.5,178Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M650.5,183L652.5,186L650.5,183Z\"\n      android:strokeAlpha=\"0.47843137\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#7A7A7A\"\n      android:strokeColor=\"#7A7A7A\"\n      android:fillAlpha=\"0.47843137\"/>\n  <path\n      android:pathData=\"M653.5,186L654.5,188L653.5,186Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M657.5,190L658.5,192L657.5,190Z\"\n      android:strokeAlpha=\"0.6784314\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#ADADAD\"\n      android:strokeColor=\"#ADADAD\"\n      android:fillAlpha=\"0.6784314\"/>\n  <path\n      android:pathData=\"M666.5,199L668.5,202L666.5,199Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M672.5,205L673.5,207L672.5,205Z\"\n      android:strokeAlpha=\"0.47843137\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#7A7A7A\"\n      android:strokeColor=\"#7A7A7A\"\n      android:fillAlpha=\"0.47843137\"/>\n  <path\n      android:pathData=\"M674.5,207L678.5,212L674.5,207Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M481.5,209L482,213.5L481,213.5L481.5,209Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M681.5,214L684.5,218L681.5,214Z\"\n      android:strokeAlpha=\"0.6784314\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#ADADAD\"\n      android:strokeColor=\"#ADADAD\"\n      android:fillAlpha=\"0.6784314\"/>\n  <path\n      android:pathData=\"M685.5,218L689.5,223L685.5,218Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M690.5,223L691.5,225L690.5,223Z\"\n      android:strokeAlpha=\"0.47843137\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#7A7A7A\"\n      android:strokeColor=\"#7A7A7A\"\n      android:fillAlpha=\"0.47843137\"/>\n  <path\n      android:pathData=\"M693.5,226L694.5,228L693.5,226Z\"\n      android:strokeAlpha=\"0.47843137\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#7A7A7A\"\n      android:strokeColor=\"#7A7A7A\"\n      android:fillAlpha=\"0.47843137\"/>\n  <path\n      android:pathData=\"M695.5,228L699.5,233L695.5,228Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M700.5,233L703.5,237L700.5,233Z\"\n      android:strokeAlpha=\"0.6784314\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#ADADAD\"\n      android:strokeColor=\"#ADADAD\"\n      android:fillAlpha=\"0.6784314\"/>\n  <path\n      android:pathData=\"M706.5,239L710.5,244L706.5,239Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M711.5,244L712.5,246L711.5,244Z\"\n      android:strokeAlpha=\"0.47843137\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#7A7A7A\"\n      android:strokeColor=\"#7A7A7A\"\n      android:fillAlpha=\"0.47843137\"/>\n  <path\n      android:pathData=\"M713.5,246L714.5,248L713.5,246Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M716.5,249L721.5,255L716.5,249Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M603.5,309L609.5,316L605,311.5L602,310.5L603.5,309Z\"\n      android:strokeAlpha=\"0.31764707\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#515151\"\n      android:strokeColor=\"#515151\"\n      android:fillAlpha=\"0.31764707\"/>\n  <path\n      android:pathData=\"M610.5,316L615.5,322L610.5,316Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M788.5,320L792.5,325L788.5,320Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M602.5,322L603,326.5L602,326.5L602.5,322Z\"\n      android:strokeAlpha=\"0.6784314\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#ADADAD\"\n      android:strokeColor=\"#ADADAD\"\n      android:fillAlpha=\"0.6784314\"/>\n  <path\n      android:pathData=\"M621.5,326L623.5,329L621.5,326Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M627.5,332L630.5,336L627.5,332Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M800.5,332L801.5,334L800.5,332Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M803.5,335L807.5,340L803.5,335Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M632.5,337L633.5,339L632.5,337Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M809.5,341L811.5,344L809.5,341Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M813.5,345L814.5,347L813.5,345Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M641.5,346L654.5,360L641.5,346Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M815.5,347L820.5,353L815.5,347Z\"\n      android:strokeAlpha=\"0.47843137\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#7A7A7A\"\n      android:strokeColor=\"#7A7A7A\"\n      android:fillAlpha=\"0.47843137\"/>\n  <path\n      android:pathData=\"M821.5,353L826.5,359L821.5,353Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M828.5,360L832.5,365L828.5,360Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M834.5,366L835.5,368L834.5,366Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M662.5,367L675.5,381L662.5,367Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M836.5,368L842.5,375L836.5,368Z\"\n      android:strokeAlpha=\"0.47843137\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#7A7A7A\"\n      android:strokeColor=\"#7A7A7A\"\n      android:fillAlpha=\"0.47843137\"/>\n  <path\n      android:pathData=\"M843.5,375L847.5,380L843.5,375Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M849.5,381L852.5,385L849.5,381Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M683.5,388L684.5,390L683.5,388Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M846.5,391L844.5,394L846.5,391Z\"\n      android:strokeAlpha=\"0.6784314\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#ADADAD\"\n      android:strokeColor=\"#ADADAD\"\n      android:fillAlpha=\"0.6784314\"/>\n  <path\n      android:pathData=\"M688.5,393L691.5,397L688.5,393Z\"\n      android:strokeAlpha=\"0.31764707\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#515151\"\n      android:strokeColor=\"#515151\"\n      android:fillAlpha=\"0.31764707\"/>\n  <path\n      android:pathData=\"M694.5,399L695.5,401L694.5,399Z\"\n      android:strokeAlpha=\"0.31764707\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#515151\"\n      android:strokeColor=\"#515151\"\n      android:fillAlpha=\"0.31764707\"/>\n  <path\n      android:pathData=\"M838.5,399L836.5,402L838.5,399Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M696.5,401L703.5,409L696.5,401Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M835.5,402L834.5,404L835.5,402Z\"\n      android:strokeAlpha=\"0.47843137\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#7A7A7A\"\n      android:strokeColor=\"#7A7A7A\"\n      android:fillAlpha=\"0.47843137\"/>\n  <path\n      android:pathData=\"M833.5,404L832.5,406L833.5,404Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M831.5,406L826.5,412L831.5,406Z\"\n      android:strokeAlpha=\"0.6784314\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#ADADAD\"\n      android:strokeColor=\"#ADADAD\"\n      android:fillAlpha=\"0.6784314\"/>\n  <path\n      android:pathData=\"M706.5,411L716.5,422L706.5,411Z\"\n      android:strokeAlpha=\"0.31764707\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#515151\"\n      android:strokeColor=\"#515151\"\n      android:fillAlpha=\"0.31764707\"/>\n  <path\n      android:pathData=\"M824.5,413L823.5,415L824.5,413Z\"\n      android:strokeAlpha=\"0.6784314\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#ADADAD\"\n      android:strokeColor=\"#ADADAD\"\n      android:fillAlpha=\"0.6784314\"/>\n  <path\n      android:pathData=\"M820.5,417L813.5,425L820.5,417Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M717.5,422L724.5,430L717.5,422Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M807.5,430L805.5,433L807.5,430Z\"\n      android:strokeAlpha=\"0.6784314\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#ADADAD\"\n      android:strokeColor=\"#ADADAD\"\n      android:fillAlpha=\"0.6784314\"/>\n  <path\n      android:pathData=\"M727.5,432L729.5,435L727.5,432Z\"\n      android:strokeAlpha=\"0.31764707\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#515151\"\n      android:strokeColor=\"#515151\"\n      android:fillAlpha=\"0.31764707\"/>\n  <path\n      android:pathData=\"M731.5,436L735.5,441L731.5,436Z\"\n      android:strokeAlpha=\"0.31764707\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#515151\"\n      android:strokeColor=\"#515151\"\n      android:fillAlpha=\"0.31764707\"/>\n  <path\n      android:pathData=\"M801.5,436L797.5,441L801.5,436Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M738.5,443L741.5,447L738.5,443Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M316.5,445L317.5,447L316.5,445Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M748.5,452L756.5,461L748.5,452Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M783.5,454L781.5,457L783.5,454Z\"\n      android:strokeAlpha=\"0.6784314\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#ADADAD\"\n      android:strokeColor=\"#ADADAD\"\n      android:fillAlpha=\"0.6784314\"/>\n  <path\n      android:pathData=\"M326.5,455L337.5,467L326.5,455Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M776.5,461L773.5,465L776.5,461Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M299.5,462L298.5,464L299.5,462Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M771.5,466L770.5,468L771.5,466Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M769.5,468L767.5,471L769.5,468Z\"\n      android:strokeAlpha=\"0.6784314\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#ADADAD\"\n      android:strokeColor=\"#ADADAD\"\n      android:fillAlpha=\"0.6784314\"/>\n  <path\n      android:pathData=\"M764.5,469L765.5,471L764.5,469Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M346.5,475L358.5,488L346.5,475Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M367.5,496L379.5,509L367.5,496Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M388.5,517L390.5,520L388.5,517Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M392.5,520L394.5,523L392.5,520Z\"\n      android:strokeAlpha=\"0.31764707\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#515151\"\n      android:strokeColor=\"#515151\"\n      android:fillAlpha=\"0.31764707\"/>\n  <path\n      android:pathData=\"M395.5,524L398.5,528L395.5,524Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M400.5,528L402.5,531L400.5,528Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M398.5,535L391.5,543L398.5,535Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M389.5,544L387.5,547L389.5,544Z\"\n      android:strokeAlpha=\"0.31764707\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#515151\"\n      android:strokeColor=\"#515151\"\n      android:fillAlpha=\"0.31764707\"/>\n  <path\n      android:pathData=\"M383.5,550L381.5,553L383.5,550Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M377.5,556L376.5,558L377.5,556Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M375.5,558L371.5,563L375.5,558Z\"\n      android:strokeAlpha=\"0.31764707\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#515151\"\n      android:strokeColor=\"#515151\"\n      android:fillAlpha=\"0.31764707\"/>\n  <path\n      android:pathData=\"M368.5,565L366.5,568L368.5,565Z\"\n      android:strokeAlpha=\"0.31764707\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#515151\"\n      android:strokeColor=\"#515151\"\n      android:fillAlpha=\"0.31764707\"/>\n  <path\n      android:pathData=\"M365.5,568L359.5,575L365.5,568Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M358.5,575L345.5,589L358.5,575Z\"\n      android:strokeAlpha=\"0.31764707\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#515151\"\n      android:strokeColor=\"#515151\"\n      android:fillAlpha=\"0.31764707\"/>\n  <path\n      android:pathData=\"M344.5,589L339.5,595L344.5,589Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M171.5,590L169.5,593L171.5,590Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M338.5,595L325.5,609L338.5,595Z\"\n      android:strokeAlpha=\"0.31764707\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#515151\"\n      android:strokeColor=\"#515151\"\n      android:fillAlpha=\"0.31764707\"/>\n  <path\n      android:pathData=\"M324.5,609L318.5,616L324.5,609Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M317.5,616L304.5,630L317.5,616Z\"\n      android:strokeAlpha=\"0.31764707\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#515151\"\n      android:strokeColor=\"#515151\"\n      android:fillAlpha=\"0.31764707\"/>\n  <path\n      android:pathData=\"M311.5,621L309.5,624L311.5,621Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M479.5,622L478.5,624L479.5,622Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M153.5,625L154,627.5L153,627.5L153.5,625Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M474.5,628L473.5,630L474.5,628Z\"\n      android:strokeAlpha=\"0.7137255\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#B6B6B6\"\n      android:strokeColor=\"#B6B6B6\"\n      android:fillAlpha=\"0.7137255\"/>\n  <path\n      android:pathData=\"M303.5,630L297.5,637L303.5,630Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M472.5,630L468.5,635L472.5,630Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M151.5,636L152,640.5L151,640.5L151.5,636Z\"\n      android:strokeAlpha=\"0.7137255\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#B6B6B6\"\n      android:strokeColor=\"#B6B6B6\"\n      android:fillAlpha=\"0.7137255\"/>\n  <path\n      android:pathData=\"M466.5,636L464.5,639L466.5,636Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M294.5,639L290,644.5Q285.8,646.8 290.5,649L296.5,656L289.5,649Q285,647 288.5,645L294.5,639Z\"\n      android:strokeAlpha=\"0.31764707\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#515151\"\n      android:strokeColor=\"#515151\"\n      android:fillAlpha=\"0.31764707\"/>\n  <path\n      android:pathData=\"M463.5,639L462.5,641L463.5,639Z\"\n      android:strokeAlpha=\"0.7137255\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#B6B6B6\"\n      android:strokeColor=\"#B6B6B6\"\n      android:fillAlpha=\"0.7137255\"/>\n  <path\n      android:pathData=\"M460.5,641L452.5,650L460.5,641Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M601.5,644L602,646.5L601,646.5L601.5,644Z\"\n      android:strokeAlpha=\"0.6784314\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#ADADAD\"\n      android:strokeColor=\"#ADADAD\"\n      android:fillAlpha=\"0.6784314\"/>\n  <path\n      android:pathData=\"M151.5,653L152,656.5L151,656.5L151.5,653Z\"\n      android:strokeAlpha=\"0.7137255\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#B6B6B6\"\n      android:strokeColor=\"#B6B6B6\"\n      android:fillAlpha=\"0.7137255\"/>\n  <path\n      android:pathData=\"M297.5,656L303.5,663L297.5,656Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M441.5,660L431.5,671L441.5,660Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M304.5,663L317.5,677L304.5,663Z\"\n      android:strokeAlpha=\"0.31764707\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#515151\"\n      android:strokeColor=\"#515151\"\n      android:fillAlpha=\"0.31764707\"/>\n  <path\n      android:pathData=\"M153.5,665L154,667.5L153,667.5L153.5,665Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M318.5,677L324.5,684L318.5,677Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M420.5,681L410.5,692L420.5,681Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M325.5,684L338.5,698L325.5,684Z\"\n      android:strokeAlpha=\"0.31764707\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#515151\"\n      android:strokeColor=\"#515151\"\n      android:fillAlpha=\"0.31764707\"/>\n  <path\n      android:pathData=\"M581.5,692L580.5,694L581.5,692Z\"\n      android:strokeAlpha=\"0.7137255\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#B6B6B6\"\n      android:strokeColor=\"#B6B6B6\"\n      android:fillAlpha=\"0.7137255\"/>\n  <path\n      android:pathData=\"M339.5,698L344.5,704L339.5,698Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M169.5,700L171.5,703L169.5,700Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M399.5,702L389.5,713L399.5,702Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M572.5,702L571.5,704L572.5,702Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M345.5,704L358.5,718L345.5,704Z\"\n      android:strokeAlpha=\"0.31764707\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#515151\"\n      android:strokeColor=\"#515151\"\n      android:fillAlpha=\"0.31764707\"/>\n  <path\n      android:pathData=\"M570.5,704L567.5,708L570.5,704Z\"\n      android:strokeAlpha=\"0.7137255\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#B6B6B6\"\n      android:strokeColor=\"#B6B6B6\"\n      android:fillAlpha=\"0.7137255\"/>\n  <path\n      android:pathData=\"M566.5,708L565.5,710L566.5,708Z\"\n      android:strokeAlpha=\"0.6784314\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#ADADAD\"\n      android:strokeColor=\"#ADADAD\"\n      android:fillAlpha=\"0.6784314\"/>\n  <path\n      android:pathData=\"M563.5,711L559.5,716L563.5,711Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M359.5,718L365.5,725L359.5,718Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M378.5,723L372.5,730L378.5,723Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M366.5,725L370.5,730L366.5,725Z\"\n      android:strokeAlpha=\"0.31764707\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#515151\"\n      android:strokeColor=\"#515151\"\n      android:fillAlpha=\"0.31764707\"/>\n  <path\n      android:pathData=\"M548.5,726L538.5,737L548.5,726Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M528.5,746L517.5,758L528.5,746Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M507.5,767L496.5,779L507.5,767Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M486.5,788L476.5,799L486.5,788Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M463.5,811L455.5,820L463.5,811Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M444.5,830L438.5,837L444.5,830Z\"\n      android:strokeAlpha=\"0.19607843\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#323232\"\n      android:strokeColor=\"#323232\"\n      android:fillAlpha=\"0.19607843\"/>\n  <path\n      android:pathData=\"M439.5,834L438.5,836L439.5,834Z\"\n      android:strokeAlpha=\"0.7137255\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#B6B6B6\"\n      android:strokeColor=\"#B6B6B6\"\n      android:fillAlpha=\"0.7137255\"/>\n  <path\n      android:pathData=\"M304.5,835L311.5,843L304.5,835Z\"\n      android:strokeAlpha=\"0.47843137\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#7A7A7A\"\n      android:strokeColor=\"#7A7A7A\"\n      android:fillAlpha=\"0.47843137\"/>\n  <path\n      android:pathData=\"M436.5,837L434.5,840L436.5,837Z\"\n      android:strokeAlpha=\"0.7137255\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#B6B6B6\"\n      android:strokeColor=\"#B6B6B6\"\n      android:fillAlpha=\"0.7137255\"/>\n  <path\n      android:pathData=\"M433.5,840L430.5,844L433.5,840Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M314.5,845L315.5,847L314.5,845Z\"\n      android:strokeAlpha=\"0.53333336\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#888888\"\n      android:strokeColor=\"#888888\"\n      android:fillAlpha=\"0.53333336\"/>\n  <path\n      android:pathData=\"M428.5,845L427.5,847L428.5,845Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M424.5,848L423.5,850L424.5,848Z\"\n      android:strokeAlpha=\"0.8392157\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#D6D6D6\"\n      android:strokeColor=\"#D6D6D6\"\n      android:fillAlpha=\"0.8392157\"/>\n  <path\n      android:pathData=\"M321.5,850L322.5,852L321.5,850Z\"\n      android:strokeAlpha=\"0.6784314\"\n      android:strokeWidth=\"1\"\n      android:fillColor=\"#ADADAD\"\n      android:strokeColor=\"#ADADAD\"\n      android:fillAlpha=\"0.6784314\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/arrow_back.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:autoMirrored=\"true\"\n    android:viewportWidth=\"68.0\"\n    android:viewportHeight=\"68.0\">\n    <group\n        android:scaleX=\"3.0\"\n        android:scaleY=\"3.0\"\n        android:translateX=\"-1.5\"\n        android:translateY=\"-2.5\">\n        <path\n            android:fillColor=\"@android:color/white\"\n            android:pathData=\"M20,11L7.8,11l5.6,-5.6L12,4l-8,8l8,8l1.4,-1.4L7.8,13L20,13L20,11z\" />\n    </group>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/arrow_downward.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M480,800L160,480L216,423L440,647L440,160L520,160L520,647L744,423L800,480L480,800Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/arrow_forward.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:autoMirrored=\"true\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M647,520L160,520L160,440L647,440L423,216L480,160L800,480L480,800L423,744L647,520Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/arrow_top_left.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M744,800L280,336L280,600L200,600L200,200L600,200L600,280L336,280L800,744L744,800Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/arrow_upward.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M440,800L440,313L216,537L160,480L480,160L800,480L744,537L520,313L520,800L440,800Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/artist.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M740,400h140v80h-80v220q0,42 -29,71t-71,29q-42,0 -71,-29t-29,-71q0,-42 29,-71t71,-29q8,0 18,1.5t22,6.5v-208ZM120,800v-112q0,-35 17.5,-63t46.5,-43q62,-31 126,-46.5T440,520q42,0 83.5,6.5T607,546q-20,12 -36,29t-28,37q-26,-6 -51.5,-9t-51.5,-3q-57,0 -112,14t-108,40q-9,5 -14.5,14t-5.5,20v32h321q2,20 9.5,40t20.5,40L120,800ZM440,480q-66,0 -113,-47t-47,-113q0,-66 47,-113t113,-47q66,0 113,47t47,113q0,66 -47,113t-113,47ZM440,400q33,0 56.5,-23.5T520,320q0,-33 -23.5,-56.5T440,240q-33,0 -56.5,23.5T360,320q0,33 23.5,56.5T440,400ZM440,320ZM440,720Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/backup.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M260,800q-91,0 -155.5,-63T40,583q0,-78 47,-139t123,-78q25,-92 100,-149t170,-57q117,0 198.5,81.5T760,440q69,8 114.5,59.5T920,620q0,75 -52.5,127.5T740,800L520,800q-33,0 -56.5,-23.5T440,720v-206l-64,62 -56,-56 160,-160 160,160 -56,56 -64,-62v206h220q42,0 71,-29t29,-71q0,-42 -29,-71t-71,-29h-60v-80q0,-83 -58.5,-141.5T480,240q-83,0 -141.5,58.5T280,440h-20q-58,0 -99,41t-41,99q0,58 41,99t99,41h100v80L260,800ZM480,520Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/baseline_event_repeat_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#000000\" android:viewportHeight=\"24\" android:viewportWidth=\"24\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M21,12V6c0,-1.1 -0.9,-2 -2,-2h-1V2h-2v2H8V2H6v2H5C3.9,4 3,4.9 3,6v14c0,1.1 0.9,2 2,2h7v-2H5V10h14v2H21zM15.64,20c0.43,1.45 1.77,2.5 3.36,2.5c1.93,0 3.5,-1.57 3.5,-3.5s-1.57,-3.5 -3.5,-3.5c-0.95,0 -1.82,0.38 -2.45,1l1.45,0V18h-4v-4h1.5l0,1.43C16.4,14.55 17.64,14 19,14c2.76,0 5,2.24 5,5s-2.24,5 -5,5c-2.42,0 -4.44,-1.72 -4.9,-4L15.64,20z\"/>\n    \n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/bedtime.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M480,840q-150,0 -255,-105T120,480q0,-150 105,-255t255,-105q14,0 27.5,1t26.5,3q-41,29 -65.5,75.5T444,300q0,90 63,153t153,63q55,0 101,-24.5t75,-65.5q2,13 3,26.5t1,27.5q0,150 -105,255T480,840ZM480,760q88,0 158,-48.5T740,585q-20,5 -40,8t-40,3q-123,0 -209.5,-86.5T364,300q0,-20 3,-40t8,-40q-78,32 -126.5,102T200,480q0,116 82,198t198,82ZM470,490Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/bluetooth.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M17.71,7.71L12,2h-1v7.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L11,14.41V22h1l5.71-5.71L13.41,12L17.71,7.71z M13,5.83l1.88,1.88L13,9.59V5.83z M13,18.17v-3.76l1.88,1.88L13,18.17z\"/>\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/bug_report.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M480,760Q546,760 593,713Q640,666 640,600L640,440Q640,374 593,327Q546,280 480,280Q414,280 367,327Q320,374 320,440L320,600Q320,666 367,713Q414,760 480,760ZM400,640L560,640L560,560L400,560L400,640ZM400,480L560,480L560,400L400,400L400,480ZM480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520L480,520Q480,520 480,520Q480,520 480,520ZM480,840Q415,840 359.5,808Q304,776 272,720L160,720L160,640L244,640Q241,620 240.5,600Q240,580 240,560L160,560L160,480L240,480Q240,460 240.5,440Q241,420 244,400L160,400L160,320L272,320Q286,297 303.5,277Q321,257 344,242L280,176L336,120L422,206Q450,197 479,197Q508,197 536,206L624,120L680,176L614,242Q637,257 655.5,276.5Q674,296 688,320L800,320L800,400L716,400Q719,420 719.5,440Q720,460 720,480L800,480L800,560L720,560Q720,580 719.5,600Q719,620 716,640L800,640L800,720L688,720Q656,776 600.5,808Q545,840 480,840Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/buymeacoffee.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"16.59dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"884\"\n    android:viewportHeight=\"1279\">\n    <path\n        android:fillColor=\"#0D0C22\"\n        android:pathData=\"M791.1,297.5L790.2,297L788.2,296.4C789,297.1 790,297.5 791.1,297.5V297.5Z\" />\n    <path\n        android:fillColor=\"#0D0C22\"\n        android:pathData=\"M803.9,388.9L802.9,389.2L803.9,388.9Z\" />\n    <path\n        android:fillColor=\"#0D0C22\"\n        android:pathData=\"M791.5,297.4C791.4,297.4 791.2,297.3 791.1,297.3C791.1,297.4 791.1,297.5 791.1,297.5C791.3,297.5 791.4,297.5 791.5,297.4V297.4Z\" />\n    <path\n        android:fillColor=\"#0D0C22\"\n        android:pathData=\"M791.1,297.5H791.2V297.4L791.1,297.5Z\" />\n    <path\n        android:fillColor=\"#0D0C22\"\n        android:pathData=\"M803.1,388.7L804.6,387.9L805.1,387.6L805.6,387C804.7,387.4 803.8,388 803.1,388.7V388.7Z\" />\n    <path\n        android:fillColor=\"#0D0C22\"\n        android:pathData=\"M793.7,299.5L792.2,298.1L791.2,297.6C791.8,298.5 792.6,299.2 793.7,299.5V299.5Z\" />\n    <path\n        android:fillColor=\"#0D0C22\"\n        android:pathData=\"M430,1186.2C428.9,1186.7 427.9,1187.5 427.1,1188.4L428,1187.9C428.6,1187.3 429.5,1186.6 430,1186.2Z\" />\n    <path\n        android:fillColor=\"#0D0C22\"\n        android:pathData=\"M641.2,1144.6C641.2,1143.3 640.6,1143.6 640.7,1148.2C640.7,1147.8 640.9,1147.5 640.9,1147.1C641,1146.3 641.1,1145.5 641.2,1144.6Z\" />\n    <path\n        android:fillColor=\"#0D0C22\"\n        android:pathData=\"M619.3,1186.2C618.1,1186.7 617.1,1187.5 616.3,1188.4L617.3,1187.9C617.9,1187.3 618.8,1186.6 619.3,1186.2Z\" />\n    <path\n        android:fillColor=\"#0D0C22\"\n        android:pathData=\"M281.3,1196.1C280.4,1195.3 279.4,1194.8 278.2,1194.6C279.1,1195.1 280.1,1195.5 280.7,1195.8L281.3,1196.1Z\" />\n    <path\n        android:fillColor=\"#0D0C22\"\n        android:pathData=\"M247.8,1164C247.7,1162.7 247.3,1161.3 246.6,1160.2C247.1,1161.4 247.5,1162.7 247.8,1163.9L247.8,1164Z\" />\n    <path\n        android:fillColor=\"#FFDD00\"\n        android:pathData=\"M472.6,590.8C426.7,610.5 374.5,632.8 307,632.8C278.7,632.7 250.6,628.9 223.4,621.3L270.1,1101.1C271.7,1121.1 280.9,1139.8 295.7,1153.5C310.5,1167.1 329.9,1174.7 350,1174.7C350,1174.7 416.3,1178.1 438.4,1178.1C462.2,1178.1 533.5,1174.7 533.5,1174.7C553.6,1174.7 573,1167.1 587.8,1153.4C602.6,1139.8 611.8,1121.1 613.4,1101.1L663.5,570.9C641.1,563.2 618.5,558.2 593.1,558.2C549.1,558.1 513.6,573.3 472.6,590.8Z\" />\n    <path\n        android:fillColor=\"#0D0C22\"\n        android:pathData=\"M78.7,386.1L79.5,386.9L80,387.2C79.6,386.8 79.2,386.4 78.7,386.1V386.1Z\" />\n    <path\n        android:fillColor=\"#0D0C22\"\n        android:pathData=\"M879.6,341.8L872.5,306.4C866.2,274.5 851.9,244.4 819.2,232.9C808.7,229.2 796.8,227.6 788.8,220C780.8,212.4 778.4,200.6 776.5,189.6C773.1,169.4 769.8,149.3 766.3,129.1C763.3,111.8 760.9,92.4 752.9,76.6C742.6,55.3 721.2,42.8 699.9,34.6C689,30.5 677.8,27 666.5,24.2C613.3,10.2 557.3,5 502.6,2.1C436.9,-1.5 371,-0.4 305.4,5.4C256.6,9.8 205.2,15.2 158.9,32C141.9,38.2 124.4,45.6 111.6,58.7C95.7,74.8 90.6,99.7 102.1,119.8C110.3,134 124.2,144.1 139,150.7C158.2,159.3 178.3,165.8 198.8,170.2C256.1,182.9 315.5,187.9 374,190C438.9,192.6 503.9,190.5 568.4,183.6C584.4,181.9 600.3,179.8 616.3,177.3C635,174.4 647,149.9 641.5,132.9C634.9,112.5 617.1,104.5 597.1,107.6C594.1,108.1 591.2,108.5 588.2,108.9L586.1,109.3C579.3,110.1 572.5,110.9 565.7,111.7C551.6,113.2 537.5,114.4 523.4,115.4C491.8,117.6 460.1,118.6 428.4,118.6C397.2,118.6 366.1,117.8 335,115.7C320.8,114.8 306.7,113.6 292.6,112.2C286.1,111.5 279.7,110.8 273.3,110L267.2,109.2L265.9,109L259.6,108.1C246.7,106.2 233.8,104 221,101.3C219.7,101 218.6,100.2 217.8,99.2C216.9,98.2 216.5,96.9 216.5,95.6C216.5,94.3 216.9,93 217.8,92C218.6,90.9 219.7,90.2 221,89.9H221.3C232.3,87.6 243.5,85.6 254.7,83.8C258.4,83.2 262.1,82.6 265.9,82.1H266C273,81.6 280,80.4 287,79.5C347.6,73.2 408.6,71.1 469.5,73.1C499.1,74 528.7,75.7 558.1,78.7C564.4,79.3 570.7,80 577,80.8C579.5,81.1 581.9,81.4 584.3,81.7L589.2,82.4C603.4,84.6 617.6,87.1 631.7,90.2C652.6,94.7 679.4,96.2 688.7,119.1C691.7,126.3 693,134.4 694.6,142L696.7,151.8C696.8,151.9 696.8,152.1 696.9,152.3C701.8,175.2 706.7,198.2 711.6,221.1C712,222.8 712,224.6 711.7,226.3C711.3,228 710.6,229.6 709.6,231C708.6,232.4 707.4,233.6 705.9,234.5C704.4,235.4 702.8,236 701,236.2H700.9L697.9,236.6L694.9,237C685.5,238.3 676,239.4 666.6,240.5C648,242.6 629.3,244.4 610.6,246C573.5,249.1 536.4,251.1 499.1,252.1C480.1,252.6 461.1,252.8 442.2,252.8C366.6,252.7 291.2,248.3 216.2,239.6C208.1,238.7 199.9,237.6 191.8,236.6C198.1,237.4 187.2,236 185,235.7C179.9,234.9 174.7,234.2 169.5,233.4C152.2,230.8 135,227.6 117.7,224.8C96.8,221.4 76.8,223.1 57.9,233.4C42.4,241.9 29.8,254.9 21.9,270.7C13.7,287.6 11.3,306 7.6,324.1C4,342.2 -1.7,361.7 0.5,380.3C5.1,420.4 33.2,453.1 73.5,460.4C111.5,467.2 149.7,472.8 188,477.6C338.4,496 490.3,498.2 641.2,484.1C653.4,483 665.7,481.7 678,480.4C681.8,480 685.7,480.4 689.3,481.7C692.9,482.9 696.2,485 699,487.7C701.7,490.4 703.8,493.7 705.1,497.3C706.4,501 706.8,504.8 706.5,508.7L702.6,545.8C694.9,620.8 687.2,695.9 679.5,770.9C671.5,849.7 663.4,928.4 655.3,1007.2C653,1029.4 650.7,1051.6 648.4,1073.7C646.2,1095.6 645.9,1118.1 641.8,1139.7C635.2,1173.6 612.2,1194.4 578.7,1202.1C548,1209.1 516.7,1212.7 485.2,1213C450.2,1213.2 415.4,1211.7 380.4,1211.8C343.2,1212.1 297.5,1208.6 268.8,1180.9C243.5,1156.5 240,1118.4 236.5,1085.4C232,1041.7 227.4,998 222.9,954.4L197.6,711.6L181.2,554.5C181,551.9 180.7,549.4 180.4,546.8C178.5,528 165.2,509.7 144.3,510.6C126.4,511.4 106.1,526.6 108.2,546.8L120.3,663.2L145.4,904.1C152.5,972.5 159.7,1041 166.8,1109.4C168.1,1122.5 169.4,1135.7 170.9,1148.8C178.7,1220.4 233.5,1259 301.2,1269.9C340.8,1276.3 381.3,1277.6 421.5,1278.2C473,1279.1 525,1281.1 575.6,1271.7C650.7,1257.9 707,1207.8 715,1130.1C717.3,1107.7 719.6,1085.3 721.9,1062.8C729.5,988.6 737.1,914.3 744.7,840.1L769.6,597.5L781,486.3C781.6,480.7 783.9,475.6 787.6,471.5C791.4,467.4 796.4,464.6 801.8,463.6C823.3,459.4 843.8,452.2 859,435.9C883.3,409.9 888.2,376 879.6,341.8ZM72.4,365.8C72.8,365.7 72.2,368.5 71.9,369.8C71.8,367.8 71.9,366.1 72.4,365.8ZM74.5,381.9C74.7,381.8 75.2,382.5 75.7,383.3C74.9,382.6 74.4,382 74.5,381.9H74.5ZM76.6,384.6C77.3,385.9 77.7,386.7 76.6,384.6V384.6ZM80.7,388H80.8C80.8,388.1 81,388.2 81,388.3C80.9,388.2 80.8,388.1 80.7,388H80.7ZM800.8,383C793.1,390.3 781.5,393.7 770,395.4C641.3,414.5 510.7,424.2 380.6,419.9C287.5,416.7 195.3,406.4 103.1,393.4C94.1,392.1 84.3,390.5 78.1,383.8C66.4,371.2 72.2,345.9 75.2,330.8C78,316.9 83.3,298.3 99.9,296.4C125.7,293.3 155.6,304.2 181.2,308.1C211.9,312.8 242.8,316.5 273.7,319.4C405.9,331.4 540.3,329.5 671.9,311.9C695.9,308.7 719.8,304.9 743.6,300.7C764.8,296.9 788.4,289.7 801.2,311.7C810,326.7 811.1,346.7 809.8,363.6C809.4,371 806.1,377.9 800.8,383H800.8Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/cached.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M482,800Q348,800 254,707Q160,614 160,480L160,473L96,537L40,481L200,321L360,481L304,537L240,473L240,480Q240,580 310.5,650Q381,720 482,720Q508,720 533,714Q558,708 582,696L642,756Q604,778 564,789Q524,800 482,800ZM760,639L600,479L656,423L720,487L720,480Q720,380 649.5,310Q579,240 478,240Q452,240 427,246Q402,252 378,264L318,204Q356,182 396,171Q436,160 478,160Q612,160 706,253Q800,346 800,480L800,487L864,423L920,479L760,639Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/cast.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480ZM800,800L600,800Q600,780 598.5,760Q597,740 594,720L800,720Q800,720 800,720Q800,720 800,720L800,240Q800,240 800,240Q800,240 800,240L160,240Q160,240 160,240Q160,240 160,240L160,286Q140,283 120,281.5Q100,280 80,280L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800ZM80,800L80,680Q130,680 165,715Q200,750 200,800L80,800ZM280,800Q280,717 221.5,658.5Q163,600 80,600L80,520Q197,520 278.5,601.5Q360,683 360,800L280,800ZM440,800Q440,725 411.5,659.5Q383,594 334.5,545.5Q286,497 220.5,468.5Q155,440 80,440L80,360Q171,360 251,394.5Q331,429 391,489Q451,549 485.5,629Q520,709 520,800L440,800Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/cast_connected.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M720,640L575,640Q540,531 463.5,448Q387,365 281,320L720,320L720,640ZM80,800L80,680Q130,680 165,715Q200,750 200,800L80,800ZM280,800Q280,717 221.5,658.5Q163,600 80,600L80,520Q197,520 278.5,601.5Q360,683 360,800L280,800ZM440,800Q440,725 411.5,659.5Q383,594 334.5,545.5Q286,497 220.5,468.5Q155,440 80,440L80,360Q171,360 251,394.5Q331,429 391,489Q451,549 485.5,629Q520,709 520,800L440,800ZM800,800L600,800Q600,780 598.5,760Q597,740 594,720L800,720Q800,720 800,720Q800,720 800,720L800,240Q800,240 800,240Q800,240 800,240L160,240Q160,240 160,240Q160,240 160,240L160,286Q140,283 120,281.5Q100,280 80,280L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/check.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:width=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/clear_all.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M120,680L120,600L680,600L680,680L120,680ZM200,520L200,440L760,440L760,520L200,520ZM280,360L280,280L840,280L840,360L280,360Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/close.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:width=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/cloud.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M260,800Q169,800 104.5,737Q40,674 40,583Q40,505 87,444Q134,383 210,366Q235,274 310,217Q385,160 480,160Q597,160 678.5,241.5Q760,323 760,440L760,440L760,440Q829,448 874.5,499.5Q920,551 920,620Q920,695 867.5,747.5Q815,800 740,800L260,800ZM260,720L740,720Q782,720 811,691Q840,662 840,620Q840,578 811,549Q782,520 740,520L680,520L680,440Q680,357 621.5,298.5Q563,240 480,240Q397,240 338.5,298.5Q280,357 280,440L280,440L260,440Q202,440 161,481Q120,522 120,580Q120,638 161,679Q202,720 260,720ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/content_copy.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M360,720Q327,720 303.5,696.5Q280,673 280,640L280,160Q280,127 303.5,103.5Q327,80 360,80L720,80Q753,80 776.5,103.5Q800,127 800,160L800,640Q800,673 776.5,696.5Q753,720 720,720L360,720ZM360,640L720,640Q720,640 720,640Q720,640 720,640L720,160Q720,160 720,160Q720,160 720,160L360,160Q360,160 360,160Q360,160 360,160L360,640Q360,640 360,640Q360,640 360,640ZM200,880Q167,880 143.5,856.5Q120,833 120,800L120,240L200,240L200,800Q200,800 200,800Q200,800 200,800L640,800L640,880L200,880ZM360,640Q360,640 360,640Q360,640 360,640L360,160Q360,160 360,160Q360,160 360,160L360,160Q360,160 360,160Q360,160 360,160L360,640Q360,640 360,640Q360,640 360,640Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/contrast.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M12,22c5.52,0 10,-4.48 10,-10S17.52,2 12,2S2,6.48 2,12S6.48,22 12,22zM13,4.07c3.94,0.49 7,3.85 7,7.93s-3.05,7.44 -7,7.93V4.07z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/crop.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M17,15h2V7c0,-1.1 -0.9,-2 -2,-2H9v2h8v8zM7,17V1H5v4H1v2h4v10c0,1.1 0.9,2 2,2h10v4h2v-4h4v-2H7z\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/crown.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M200,800L200,720L760,720L760,800L200,800ZM200,660L149,339Q147,339 144.5,339.5Q142,340 140,340Q115,340 97.5,322.5Q80,305 80,280Q80,255 97.5,237.5Q115,220 140,220Q165,220 182.5,237.5Q200,255 200,280Q200,287 198.5,293Q197,299 195,304L320,360L445,189Q434,181 427,168Q420,155 420,140Q420,115 437.5,97.5Q455,80 480,80Q505,80 522.5,97.5Q540,115 540,140Q540,155 533,168Q526,181 515,189L640,360L765,304Q763,299 761.5,293Q760,287 760,280Q760,255 777.5,237.5Q795,220 820,220Q845,220 862.5,237.5Q880,255 880,280Q880,305 862.5,322.5Q845,340 820,340Q818,340 815.5,339.5Q813,339 811,339L760,660L200,660ZM268,580L692,580L718,413L613,459L480,276L347,459L242,413L268,580ZM480,580L480,580L480,580L480,580L480,580L480,580L480,580Z\"/>\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/delete.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M280,840q-33,0 -56.5,-23.5T200,760v-520h-40v-80h200v-40h240v40h200v80h-40v520q0,33 -23.5,56.5T680,840L280,840ZM680,240L280,240v520h400v-520ZM360,680h80v-360h-80v360ZM520,680h80v-360h-80v360ZM280,240v520,-520Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/delete_history.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M656,840L600,784L684,700L600,616L656,560L740,644L824,560L880,616L797,700L880,784L824,840L740,757L656,840ZM480,840Q342,840 239.5,748.5Q137,657 122,520L204,520Q218,624 296.5,692Q375,760 480,760Q491,760 500.5,759.5Q510,759 520,757L520,838Q510,839 500.5,839.5Q491,840 480,840ZM120,400L120,160L200,160L200,254Q251,190 324.5,155Q398,120 480,120Q630,120 735,225Q840,330 840,480Q840,480 840,480Q840,480 840,480L760,480Q760,480 760,480Q760,480 760,480Q760,363 678.5,281.5Q597,200 480,200Q411,200 351,232Q291,264 250,320L360,320L360,400L120,400ZM534,590L440,496L440,280L520,280L520,464L576,520L534,590Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/discord.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"#ffffff\"\n        android:pathData=\"M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.028zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/discover_tune.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M517.5,357.5L517.5,282.5L637.5,282.5L637.5,127.5L712.5,127.5L712.5,282.5L832.5,282.5L832.5,357.5L517.5,357.5ZM637.5,832.5L637.5,442.5L712.5,442.5L712.5,832.5L637.5,832.5ZM247.5,832.5L247.5,677.5L127.5,677.5L127.5,602.5L442.5,602.5L442.5,677.5L322.5,677.5L322.5,832.5L247.5,832.5ZM247.5,517.5L247.5,127.5L322.5,127.5L322.5,517.5L247.5,517.5Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/dock_to_top.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M19,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5C21,3.9 20.1,3 19,3zM19,19H5V8h14V19z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/done.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M382,720 L154,492l57,-57 171,171 367,-367 57,57 -424,424Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/download.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M240,800Q207,800 183.5,776.5Q160,753 160,720L160,600L240,600L240,720Q240,720 240,720Q240,720 240,720L720,720Q720,720 720,720Q720,720 720,720L720,600L800,600L800,720Q800,753 776.5,776.5Q753,800 720,800L240,800ZM480,640L280,440L336,382L440,486L440,160L520,160L520,486L624,382L680,440L480,640Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/drag_handle.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M160,600L160,520L800,520L800,600L160,600ZM160,440L160,360L800,360L800,440L160,440Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/edit.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M200,760L257,760L648,369L591,312L200,703L200,760ZM120,840L120,670L648,143Q660,132 674.5,126Q689,120 705,120Q721,120 736,126Q751,132 762,144L817,200Q829,211 834.5,226Q840,241 840,256Q840,272 834.5,286.5Q829,301 817,313L290,840L120,840ZM760,256L760,256L704,200L704,200L760,256ZM619,341L591,312L591,312L648,369L648,369L619,341Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/equalizer.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M160,800L160,480L320,480L320,800L160,800ZM400,800L400,160L560,160L560,800L400,800ZM640,800L640,360L800,360L800,800L640,800Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/error.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:tint=\"#ffc62828\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M480,680Q497,680 508.5,668.5Q520,657 520,640Q520,623 508.5,611.5Q497,600 480,600Q463,600 451.5,611.5Q440,623 440,640Q440,657 451.5,668.5Q463,680 480,680ZM440,520L520,520L520,280L440,280L440,520ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/expand_less.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M296,615L240,559L480,319L720,559L664,615L480,431L296,615Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/expand_more.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M480,615L240,375L296,319L480,503L664,319L720,375L480,615Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/explicit.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M360,680h240v-80L440,600v-80h160v-80L440,440v-80h160v-80L360,280v400ZM200,840q-33,0 -56.5,-23.5T120,760v-560q0,-33 23.5,-56.5T200,120h560q33,0 56.5,23.5T840,200v560q0,33 -23.5,56.5T760,840L200,840ZM200,760h560v-560L200,200v560ZM200,200v560,-560Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/explore_outlined.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"@android:color/white\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M9.8,9.8l-3.83,8.23l8.23,-3.83l3.83,-8.23L9.8,9.8zM13.08,12.77c-0.21,0.29 -0.51,0.48 -0.86,0.54c-0.07,0.01 -0.15,0.02 -0.22,0.02c-0.28,0 -0.54,-0.08 -0.77,-0.25c-0.29,-0.21 -0.48,-0.51 -0.54,-0.86c-0.06,-0.35 0.02,-0.71 0.23,-0.99c0.21,-0.29 0.51,-0.48 0.86,-0.54c0.35,-0.06 0.7,0.02 0.99,0.23c0.29,0.21 0.48,0.51 0.54,0.86C13.37,12.13 13.29,12.48 13.08,12.77z\" />\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M12,3c4.96,0 9,4.04 9,9s-4.04,9 -9,9s-9,-4.04 -9,-9S7.04,3 12,3M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2L12,2z\"\n        android:strokeWidth=\"1.0\"\n        android:strokeColor=\"@android:color/white\"\n        android:strokeLineCap=\"butt\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/fast_forward.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M100,720L100,240L460,480L100,720ZM500,720L500,240L860,480L500,720ZM180,480L180,480L180,480ZM580,480L580,480L580,480ZM180,570L316,480L180,390L180,570ZM580,570L716,480L580,390L580,570Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/favorite.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/favorite_border.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M16.5,3c-1.74,0 -3.41,0.81 -4.5,2.09C10.91,3.81 9.24,3 7.5,3 4.42,3 2,5.42 2,8.5c0,3.78 3.4,6.86 8.55,11.54L12,21.35l1.45,-1.32C18.6,15.36 22,12.28 22,8.5 22,5.42 19.58,3 16.5,3zM12.1,18.55l-0.1,0.1 -0.1,-0.1C7.14,14.24 4,11.39 4,8.5 4,6.5 5.5,5 7.5,5c1.54,0 3.04,0.99 3.57,2.36h1.87C13.46,5.99 14.96,5 16.5,5c2,0 3.5,1.5 3.5,3.5 0,2.89 -3.14,5.74 -7.9,10.05z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/fullscreen.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/github.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:width=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M12,2A10,10 0,0 0,2 12c0,4.42 2.87,8.17 6.84,9.5c0.5,0.08 0.66,-0.23 0.66,-0.5c0,-0.23 0,-0.86 0,-1.69c-2.77,0.6 -3.36,-1.34 -3.36,-1.34c-0.46,-1.16 -1.11,-1.47 -1.11,-1.47c-0.91,-0.62 0.07,-0.6 0.07,-0.6c1,0.07 1.53,1.03 1.53,1.03c0.87,1.52 2.34,1.07 2.91,0.83c0.09,-0.65 0.35,-1.09 0.63,-1.34c-2.22,-0.25 -4.55,-1.11 -4.55,-4.92c0,-1.11 0.38,-2 1.03,-2.71c-0.1,-0.25 -0.45,-1.29 0.1,-2.64c0,0 0.84,-0.27 2.75,1.02c0.79,-0.22 1.65,-0.33 2.5,-0.33c0.85,0 1.71,0.11 2.5,0.33c1.91,-1.29 2.75,-1.02 2.75,-1.02c0.55,1.35 0.2,2.39 0.1,2.64c0.65,0.71 1.03,1.6 1.03,2.71c0,3.82 -2.34,4.66 -4.57,4.91c0.36,0.31 0.69,0.92 0.69,1.85c0,1.34 0,2.42 0,2.74c0,0.27 0.16,0.59 0.67,0.5C19.14,20.16 22,16.42 22,12A10,10 0,0 0,12 2Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/gradient.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:tint=\"@android:color/white\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M11,9h2v2h-2zM9,11h2v2L9,13zM13,11h2v2h-2zM15,9h2v2h-2zM7,9h2v2L7,11zM19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM9,18L7,18v-2h2v2zM13,18h-2v-2h2v2zM17,18h-2v-2h2v2zM19,11h-2v2h2v2h-2v-2h-2v2h-2v-2h-2v2L9,15v-2L7,13v2L5,15v-2h2v-2L5,11L5,5h14v6z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/graphic_eq.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M280,720L280,240L360,240L360,720L280,720ZM440,880L440,80L520,80L520,880L440,880ZM120,560L120,400L200,400L200,560L120,560ZM600,720L600,240L680,240L680,720L600,720ZM760,560L760,400L840,400L840,560L760,560Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/grid_view.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M120,440L120,120L440,120L440,440L120,440ZM120,840L120,520L440,520L440,840L120,840ZM520,440L520,120L840,120L840,440L520,440ZM520,840L520,520L840,520L840,840L520,840ZM200,360L360,360L360,200L200,200L200,360ZM600,360L760,360L760,200L600,200L600,360ZM600,760L760,760L760,600L600,600L600,760ZM200,760L360,760L360,600L200,600L200,760ZM600,360L600,360L600,360L600,360L600,360ZM600,600L600,600L600,600L600,600L600,600ZM360,600L360,600L360,600L360,600L360,600ZM360,360L360,360L360,360L360,360L360,360Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/group.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M40,800L40,688Q40,654 57.5,625.5Q75,597 104,582Q166,551 230,535.5Q294,520 360,520Q426,520 490,535.5Q554,551 616,582Q645,597 662.5,625.5Q680,654 680,688L680,800L40,800ZM760,800L760,680Q760,636 735.5,595.5Q711,555 666,526Q717,532 762,546.5Q807,561 846,582Q882,602 901,626.5Q920,651 920,680L920,800L760,800ZM360,480Q294,480 247,433Q200,386 200,320Q200,254 247,207Q294,160 360,160Q426,160 473,207Q520,254 520,320Q520,386 473,433Q426,480 360,480ZM760,320Q760,386 713,433Q666,480 600,480Q589,480 572,477.5Q555,475 544,472Q571,440 585.5,401Q600,362 600,320Q600,278 585.5,239Q571,200 544,168Q558,163 572,161.5Q586,160 600,160Q666,160 713,207Q760,254 760,320ZM120,720L600,720L600,688Q600,677 594.5,668Q589,659 580,654Q526,627 471,613.5Q416,600 360,600Q304,600 249,613.5Q194,627 140,654Q131,659 125.5,668Q120,677 120,688L120,720ZM360,400Q393,400 416.5,376.5Q440,353 440,320Q440,287 416.5,263.5Q393,240 360,240Q327,240 303.5,263.5Q280,287 280,320Q280,353 303.5,376.5Q327,400 360,400ZM360,720L360,720L360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720Q360,720 360,720ZM360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Q360,320 360,320Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/group_add.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M500,478Q529,446 544.5,405Q560,364 560,320Q560,276 544.5,235Q529,194 500,162Q500,162 500,162Q500,162 500,162Q560,170 600,215Q640,260 640,320Q640,380 600,425Q560,470 500,478Q500,478 500,478Q500,478 500,478ZM720,800L720,680Q720,644 704,611.5Q688,579 662,554Q713,572 756.5,600.5Q800,629 800,680L800,800L720,800ZM800,520L800,440L720,440L720,360L800,360L800,280L880,280L880,360L960,360L960,440L880,440L880,520L800,520ZM320,480Q254,480 207,433Q160,386 160,320Q160,254 207,207Q254,160 320,160Q386,160 433,207Q480,254 480,320Q480,386 433,433Q386,480 320,480ZM0,800L0,688Q0,654 17.5,625.5Q35,597 64,582Q126,551 190,535.5Q254,520 320,520Q386,520 450,535.5Q514,551 576,582Q605,597 622.5,625.5Q640,654 640,688L640,800L0,800ZM320,400Q353,400 376.5,376.5Q400,353 400,320Q400,287 376.5,263.5Q353,240 320,240Q287,240 263.5,263.5Q240,287 240,320Q240,353 263.5,376.5Q287,400 320,400ZM80,720L560,720L560,688Q560,677 554.5,668Q549,659 540,654Q486,627 431,613.5Q376,600 320,600Q264,600 209,613.5Q154,627 100,654Q91,659 85.5,668Q80,677 80,688L80,720ZM320,320Q320,320 320,320Q320,320 320,320Q320,320 320,320Q320,320 320,320Q320,320 320,320Q320,320 320,320Q320,320 320,320Q320,320 320,320ZM320,720L320,720Q320,720 320,720Q320,720 320,720Q320,720 320,720Q320,720 320,720Q320,720 320,720Q320,720 320,720Q320,720 320,720Q320,720 320,720L320,720Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/group_filled.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M40,800v-112q0,-34 17.5,-62.5T104,582q62,-31 126,-46.5T360,520q66,0 130,15.5T616,582q29,15 46.5,43.5T680,688v112H40ZM760,800v-120q0,-44 -24.5,-84.5T666,526q51,6 96,20.5t84,35.5q36,20 55,44.5t19,53.5v120H760ZM360,480q-66,0 -113,-47t-47,-113q0,-66 47,-113t113,-47q66,0 113,47t47,113q0,66 -47,113t-113,47ZM760,320q0,66 -47,113t-113,47q-11,0 -28,-2.5t-28,-5.5q27,-32 41.5,-71t14.5,-81q0,-42 -14.5,-81T544,168q14,-5 28,-6.5t28,-1.5q66,0 113,47t47,113Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/group_outlined.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M40,800v-112q0,-34 17.5,-62.5T104,582q62,-31 126,-46.5T360,520q66,0 130,15.5T616,582q29,15 46.5,43.5T680,688v112H40ZM760,800v-120q0,-44 -24.5,-84.5T666,526q51,6 96,20.5t84,35.5q36,20 55,44.5t19,53.5v120H760ZM360,480q-66,0 -113,-47t-47,-113q0,-66 47,-113t113,-47q66,0 113,47t47,113q0,66 -47,113t-113,47ZM760,320q0,66 -47,113t-113,47q-11,0 -28,-2.5t-28,-5.5q27,-32 41.5,-71t14.5,-81q0,-42 -14.5,-81T544,168q14,-5 28,-6.5t28,-1.5q66,0 113,47t47,113ZM120,720h480v-32q0,-11 -5.5,-20T580,654q-54,-27 -109,-40.5T360,600q-56,0 -111,13.5T140,654q-9,5 -14.5,14t-5.5,20v32ZM360,400q33,0 56.5,-23.5T440,320q0,-33 -23.5,-56.5T360,240q-33,0 -56.5,23.5T280,320q0,33 23.5,56.5T360,400ZM360,320ZM360,720Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/hide_image.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M840,726 l-80,-80v-446L314,200l-80,-80h526q33,0 56.5,23.5T840,200v526ZM792,904l-64,-64L200,840q-33,0 -56.5,-23.5T120,760v-528l-64,-64 56,-56 736,736 -56,56ZM240,680l120,-160 90,120 33,-44 -283,-283v447h447l-80,-80L240,680ZM537,423ZM424,536Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/history.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:width=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M480,840q-138,0 -240.5,-91.5T122,520h82q14,104 92.5,172T480,760q117,0 198.5,-81.5T760,480q0,-117 -81.5,-198.5T480,200q-69,0 -129,32t-101,88h110v80L120,400v-240h80v94q51,-64 124.5,-99T480,120q75,0 140.5,28.5t114,77q48.5,48.5 77,114T840,480q0,75 -28.5,140.5t-77,114q-48.5,48.5 -114,77T480,840ZM592,648L440,496v-216h80v184l128,128 -56,56Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/home_filled.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M12,3.12L4,10.08V21h6v-6h4v6h6V10.08L12,3.12z\" />\n</vector>\n\n"
  },
  {
    "path": "app/src/main/res/drawable/home_outlined.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"@android:color/white\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M12,4.44l7,6.09V20h-4v-5v-1h-1h-4H9v1v5H5v-9.47L12,4.44M12,3.12l-8,6.96V21h6v-6h4v6h6V10.08L12,3.12L12,3.12z\"\n        android:strokeWidth=\"1.0\"\n        android:strokeColor=\"@android:color/white\"\n        android:strokeLineCap=\"butt\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_android_auto.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <!-- Android Auto external arrow -->\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:strokeColor=\"@android:color/white\"\n        android:strokeWidth=\"1.5\"\n        android:strokeLineCap=\"round\"\n        android:strokeLineJoin=\"round\"\n        android:pathData=\"\n            M7.0,18.25 L3.26,18.25\n            C2.87,18.25 2.63,17.83 2.83,17.49\n            L11.57,2.96\n            C11.77,2.64 12.23,2.64 12.43,2.96\n            L21.17,17.49\n            C21.37,17.49 21.13,18.25 20.74,18.25\n            L17.0,18.25\" />\n\n    <!-- Android Auto internal arrow -->\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"\n            M5.25,21.25\n            L12.0,9.75\n            L18.75,21.25\n            L12.0,18.25\n            Z\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_dynamic_icon.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM12,6c-3.31,0 -6,2.69 -6,6s2.69,6 6,6 6,-2.69 6,-6 -2.69,-6 -6,-6z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_heart.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#000000\" android:viewportHeight=\"24\" android:viewportWidth=\"24\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z\"/>\n    \n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_heart_outline.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#000000\" android:viewportHeight=\"24\" android:viewportWidth=\"24\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M16.5,3c-1.74,0 -3.41,0.81 -4.5,2.09C10.91,3.81 9.24,3 7.5,3 4.42,3 2,5.42 2,8.5c0,3.78 3.4,6.86 8.55,11.54L12,21.35l1.45,-1.32C18.6,15.36 22,12.28 22,8.5 22,5.42 19.58,3 16.5,3zM12.1,18.55l-0.1,0.1 -0.1,-0.1C7.14,14.24 4,11.39 4,8.5 4,6.5 5.5,5 7.5,5c1.54,0 3.04,0.99 3.57,2.36h1.87C13.46,5.99 14.96,5 16.5,5c2,0 3.5,1.5 3.5,3.5 0,2.89 -3.14,5.74 -7.9,10.05z\"/>\n    \n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_background_v31.xml",
    "content": "<!-- Moved to drawable-v31. Keep a fallback gradient using app colors for <31 if referenced by name. -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <gradient\n        android:angle=\"45.0\"\n        android:endColor=\"@color/teal_200\"\n        android:startColor=\"@color/teal_700\"\n        android:type=\"linear\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"108dp\"\n    android:width=\"108dp\"\n    android:viewportWidth=\"30.0\"\n    android:viewportHeight=\"32.0\">\n    <group\n        android:scaleX=\"0.47\"\n        android:scaleY=\"0.5\"\n        android:translateX=\"7.8\"\n        android:translateY=\"8.0\">\n        <path\n            android:fillColor=\"#00000000\"\n            android:pathData=\"M9.5,15.5L3.707,21.293C3.317,21.684 3.317,22.317 3.707,22.707L9.293,28.293C9.683,28.684 10.317,28.684 10.707,28.293L16.707,22.293C16.895,22.105 17,21.851 17,21.586V4.468C17,3.578 18.074,3.132 18.705,3.759L28,13\"\n            android:strokeColor=\"#FFFFFFFF\"\n            android:strokeWidth=\"5.0\" />\n    </group>\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_foreground_v31.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"108dp\"\n    android:width=\"108dp\"\n    android:viewportWidth=\"30.0\"\n    android:viewportHeight=\"32.0\">\n    <group\n        android:scaleX=\"0.47\"\n        android:scaleY=\"0.5\"\n        android:translateX=\"7.8\"\n        android:translateY=\"8.0\">\n        <path\n            android:fillColor=\"#00000000\"\n            android:pathData=\"M9.5,15.5L3.707,21.293C3.317,21.684 3.317,22.317 3.707,22.707L9.293,28.293C9.683,28.684 10.317,28.684 10.707,28.293L16.707,22.293C16.895,22.105 17,21.851 17,21.586V4.468C17,3.578 18.074,3.132 18.705,3.759L28,13\"\n            android:strokeColor=\"#FFFFFFFF\"\n            android:strokeWidth=\"5.0\" />\n    </group>\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_monochrome.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"108dp\"\n    android:width=\"108dp\"\n    android:viewportWidth=\"30.0\"\n    android:viewportHeight=\"32.0\">\n    <group\n        android:scaleX=\"0.47\"\n        android:scaleY=\"0.5\"\n        android:translateX=\"7.8\"\n        android:translateY=\"8.0\">\n        <path\n            android:fillColor=\"#00000000\"\n            android:pathData=\"M9.5,15.5L3.707,21.293C3.317,21.684 3.317,22.317 3.707,22.707L9.293,28.293C9.683,28.684 10.317,28.684 10.707,28.293L16.707,22.293C16.895,22.105 17,21.851 17,21.586V4.468C17,3.578 18.074,3.132 18.705,3.759L28,13\"\n            android:strokeColor=\"#FF000000\"\n            android:strokeWidth=\"5.0\" />\n    </group>\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_push_pin.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M16,12V4H17V2H7V4H8V12L6,14V16H11.2V22H12.8V16H18V14L16,12Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_widget_heart_nav.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:width=\"24dp\">\n    <path\n        android:fillColor=\"@color/widget_on_tertiary_container\"\n        android:pathData=\"M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_widget_heart_outline_nav.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:width=\"24dp\">\n    <path\n        android:fillColor=\"@color/widget_on_tertiary_container\"\n        android:pathData=\"M16.5,3c-1.74,0 -3.41,0.81 -4.5,2.09C10.91,3.81 9.24,3 7.5,3 4.42,3 2,5.42 2,8.5c0,3.78 3.4,6.86 8.55,11.54L12,21.35l1.45,-1.32C18.6,15.36 22,12.28 22,8.5 22,5.42 19.58,3 16.5,3zM12.1,18.55l-0.1,0.1 -0.1,-0.1C7.14,14.24 4,11.39 4,8.5 4,6.5 5.5,5 7.5,5c1.54,0 3.04,0.99 3.57,2.36h1.87C13.46,5.99 14.96,5 16.5,5c2,0 3.5,1.5 3.5,3.5 0,2.89 -3.14,5.74 -7.9,10.05z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_widget_mic.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Microphone icon for the Music Recognizer Widget -->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"@color/widget_on_primary_container\">\n\n    <!-- Mic body -->\n    <path\n        android:fillColor=\"@color/widget_on_primary_container\"\n        android:pathData=\"M12,14c1.66,0 3,-1.34 3,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5l0,6c0,1.66 1.34,3 3,3z\" />\n\n    <!-- Mic stand arm -->\n    <path\n        android:fillColor=\"@color/widget_on_primary_container\"\n        android:pathData=\"M17,11c0,2.76 -2.24,5 -5,5s-5,-2.24 -5,-5L5,11c0,3.53 2.61,6.43 6,6.92L11,21L9,21l0,2l6,0l0,-2l-2,0l0,-3.08c3.39,-0.49 6,-3.39 6,-6.92l-2,0z\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_widget_pause.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:width=\"24dp\">\n    <path\n        android:fillColor=\"#FFFFFF\"\n        android:pathData=\"M6,19h4V5H6v14zM14,5v14h4V5h-4z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_widget_pause_low.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:width=\"24dp\">\n    <path\n        android:fillColor=\"@color/widget_play_button_low_icon\"\n        android:pathData=\"M6,19h4V5H6v14zM14,5v14h4V5h-4z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_widget_pause_secondary.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:width=\"24dp\">\n    <path\n        android:fillColor=\"@color/widget_on_primary_container\"\n        android:pathData=\"M6,19h4V5H6v14zM14,5v14h4V5h-4z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_widget_play.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:width=\"24dp\">\n    <path\n        android:fillColor=\"#FFFFFF\"\n        android:pathData=\"M8,5v14l11,-7z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_widget_play_low.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:width=\"24dp\">\n    <path\n        android:fillColor=\"@color/widget_play_button_low_icon\"\n        android:pathData=\"M8,5v14l11,-7z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_widget_play_secondary.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:width=\"24dp\">\n    <path\n        android:fillColor=\"@color/widget_on_primary_container\"\n        android:pathData=\"M8,5v14l11,-7z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_widget_skip_next.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:width=\"24dp\">\n    <path\n        android:fillColor=\"@color/widget_text_primary\"\n        android:pathData=\"M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_widget_skip_previous.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\"\n    android:width=\"24dp\">\n    <path\n        android:fillColor=\"@color/widget_text_primary\"\n        android:pathData=\"M6,6h2v12L6,18zM9.5,12l8.5,6L18,6z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/info.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M440,680h80v-240h-80v240ZM480,360q17,0 28.5,-11.5T520,320q0,-17 -11.5,-28.5T480,280q-17,0 -28.5,11.5T440,320q0,17 11.5,28.5T480,360ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM480,800q134,0 227,-93t93,-227q0,-134 -93,-227t-227,-93q-134,0 -227,93t-93,227q0,134 93,227t227,93ZM480,480Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/insert_photo.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M19,5v14L5,19L5,5h14m0,-2L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM14.14,11.86l-3,3.87L9,13.14 6,17h12l-3.86,-5.14z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/instagram.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"#ffffff\"\n        android:pathData=\"M12,2.163c3.204,0,3.584,0.012,4.85,0.07c1.17,0.054,1.805,0.249,2.227,0.413c0.559,0.217,0.957,0.477,1.377,0.897c0.42,0.42,0.68,0.818,0.897,1.377c0.164,0.422,0.359,1.057,0.413,2.227c0.058,1.266,0.07,1.646,0.07,4.85s-0.012,3.584-0.07,4.85c-0.054,1.17-0.249,1.805-0.413,2.227c-0.217,0.559-0.477,0.957-0.897,1.377c-0.42,0.42-0.818,0.68-1.377,0.897c-0.422,0.164-1.057,0.359-2.227,0.413c-1.266,0.058-1.646,0.07-4.85,0.07s-3.584-0.012-4.85-0.07c-1.17-0.054-1.805-0.249-2.227-0.413c-0.559-0.217-0.957-0.477-1.377-0.897c-0.42-0.42-0.68-0.818-0.897-1.377c-0.164-0.422-0.359-1.057-0.413-2.227c-0.058-1.266-0.07-1.646-0.07-4.85s0.012-3.584,0.07-4.85c0.054-1.17,0.249-1.805,0.413-2.227c0.217-0.559,0.477-0.957,0.897-1.377c0.42-0.42,0.818-0.68,1.377-0.897c0.422-0.164,1.057-0.359,2.227-0.413C8.416,2.175,8.796,2.163,12,2.163 M12,0C8.741,0,8.332,0.014,7.052,0.072C5.775,0.131,4.903,0.334,4.14,0.631C3.351,0.938,2.682,1.346,2.016,2.013C1.349,2.68,0.941,3.349,0.634,4.138C0.337,4.902,0.134,5.774,0.075,7.052C0.017,8.332,0,8.741,0,12c0,3.259,0.017,3.668,0.075,4.948c0.059,1.278,0.262,2.15,0.559,2.913c0.307,0.789,0.715,1.458,1.382,2.125c0.666,0.667,1.335,1.075,2.124,1.382c0.763,0.297,1.635,0.5,2.912,0.559C8.332,23.986,8.741,24,12,24s3.668-0.014,4.948-0.072c1.277-0.06,2.149-0.262,2.912-0.559c0.789-0.307,1.458-0.715,2.125-1.382c0.667-0.667,1.075-1.335,1.382-2.124c0.297-0.763,0.5-1.635,0.559-2.912C23.986,15.668,24,15.259,24,12s-0.014-3.668-0.072-4.948c-0.06-1.277-0.262-2.149-0.559-2.912c-0.307-0.789-0.715-1.458-1.382-2.125c-0.666-0.667-1.335-1.075-2.124-1.382c-0.763-0.297-1.635-0.5-2.912-0.559C15.668,0.017,15.259,0,12,0L12,0z M12,5.838c-3.403,0-6.162,2.759-6.162,6.162c0,3.403,2.759,6.162,6.162,6.162s6.162-2.759,6.162-6.162C18.162,8.597,15.403,5.838,12,5.838z M12,16c-2.209,0-4-1.791-4-4c0-2.209,1.791-4,4-4s4,1.791,4,4C16,14.209,14.209,16,12,16z M18.406,4.237c-0.795,0-1.44,0.645-1.44,1.44c0,0.795,0.645,1.44,1.44,1.44s1.44-0.645,1.44-1.44C19.846,4.882,19.201,4.237,18.406,4.237z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/integration.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"> \n    <path \n        android:fillColor=\"@android:color/white\" \n        android:pathData=\"M10.5,4.5c0.28,0 0.5,0.22 0.5,0.5v2h6v6h2c0.28,0 0.5,0.22 0.5,0.5s-0.22,0.5 -0.5,0.5h-2v6h-2.12c-0.68,-1.75 -2.39,-3 -4.38,-3s-3.7,1.25 -4.38,3H4v-2.12c1.75,-0.68 3,-2.39 3,-4.38 0,-1.99 -1.24,-3.7 -2.99,-4.38L4,7h6V5c0,-0.28 0.22,-0.5 0.5,-0.5m0,-2C9.12,2.5 8,3.62 8,5H4c-1.1,0 -1.99,0.9 -1.99,2v3.8h0.29c1.49,0 2.7,1.21 2.7,2.7s-1.21,2.7 -2.7,2.7H2V20c0,1.1 0.9,2 2,2h3.8v-0.3c0,-1.49 1.21,-2.7 2.7,-2.7s2.7,1.21 2.7,2.7v0.3H17c1.1,0 2,-0.9 2,-2v-4c1.38,0 2.5,-1.12 2.5,-2.5S20.38,11 19,11V7c0,-1.1 -0.9,-2 -2,-2h-4c0,-1.38 -1.12,-2.5 -2.5,-2.5z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/key.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M7,14c-1.66,0 -3,-1.34 -3,-3 0,-1.66 1.34,-3 3,-3 1.66,0 3,1.34 3,3 0,1.66 -1.34,3 -3,3zM18.8,3H5.2C4.04,3 3,4.04 3,5.2v13.6C3,19.96 4.04,21 5.2,21h13.6c1.16,0 2.2,-1.04 2.2,-2.2V5.2C21,4.04 19.96,3 18.8,3zM7,15c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM18,17h-7v-2h7V17z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/language.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M480,880Q398,880 325,848.5Q252,817 197.5,762.5Q143,708 111.5,635Q80,562 80,480Q80,397 111.5,324.5Q143,252 197.5,197.5Q252,143 325,111.5Q398,80 480,80Q563,80 635.5,111.5Q708,143 762.5,197.5Q817,252 848.5,324.5Q880,397 880,480Q880,562 848.5,635Q817,708 762.5,762.5Q708,817 635.5,848.5Q563,880 480,880ZM480,798Q506,762 525,723Q544,684 556,640L404,640Q416,684 435,723Q454,762 480,798ZM376,782Q358,749 344.5,713.5Q331,678 322,640L204,640Q233,690 276.5,727Q320,764 376,782ZM584,782Q640,764 683.5,727Q727,690 756,640L638,640Q629,678 615.5,713.5Q602,749 584,782ZM170,560L306,560Q303,540 301.5,520.5Q300,501 300,480Q300,459 301.5,439.5Q303,420 306,400L170,400Q165,420 162.5,439.5Q160,459 160,480Q160,501 162.5,520.5Q165,540 170,560ZM386,560L574,560Q577,540 578.5,520.5Q580,501 580,480Q580,459 578.5,439.5Q577,420 574,400L386,400Q383,420 381.5,439.5Q380,459 380,480Q380,501 381.5,520.5Q383,540 386,560ZM654,560L790,560Q795,540 797.5,520.5Q800,501 800,480Q800,459 797.5,439.5Q795,420 790,400L654,400Q657,420 658.5,439.5Q660,459 660,480Q660,501 658.5,520.5Q657,540 654,560ZM638,320L756,320Q727,270 683.5,233Q640,196 584,178Q602,211 615.5,246.5Q629,282 638,320ZM404,320L556,320Q544,276 525,237Q506,198 480,162Q454,198 435,237Q416,276 404,320ZM204,320L322,320Q331,282 344.5,246.5Q358,211 376,178Q320,196 276.5,233Q233,270 204,320Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/language_japanese_latin.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M326,720L296,672Q376,664 421,629Q466,594 466,539Q466,509 445.5,484Q425,459 392,448Q369,505 337.5,550Q306,595 268,628Q271,640 274.5,652Q278,664 282,676L232,691Q229,681 227,673.5Q225,666 223,660Q197,674 174,681.5Q151,689 129,689Q97,689 77,668Q57,647 57,612Q57,559 97,507Q137,455 200,425Q201,406 202,387.5Q203,369 205,350Q177,351 146,349.5Q115,348 79,345L78,292Q104,297 134,298.5Q164,300 211,300Q213,282 215.5,264.5Q218,247 216,229L276,230Q269,247 266,264.5Q263,282 260,299Q318,296 367,290Q416,284 459,274L460,326Q407,334 356.5,339.5Q306,345 255,348Q253,362 252.5,377Q252,392 250,406Q278,398 304.5,395Q331,392 357,394Q360,384 361.5,374Q363,364 364,354L421,368Q418,376 414.5,384Q411,392 408,403Q459,417 489.5,455Q520,493 520,540Q520,610 468.5,657.5Q417,705 326,720ZM138,635Q155,635 173,628Q191,621 211,607Q204,569 201,538Q198,507 198,479Q160,503 135,538Q110,573 110,604Q110,617 118.5,626Q127,635 138,635ZM256,570Q285,542 306.5,509.5Q328,477 342,440Q319,440 295.5,444Q272,448 248,456Q246,482 248.5,510Q251,538 256,570ZM702,626Q730,626 756.5,613Q783,600 805,576L805,470Q782,473 762.5,477Q743,481 726,486Q681,500 658.5,521Q636,542 636,570Q636,596 654,611Q672,626 702,626ZM679,694Q622,694 589,661.5Q556,629 556,573Q556,521 589,488Q622,455 695,435Q718,429 745.5,424Q773,419 805,415L805,415Q803,368 783,346.5Q763,325 721,325Q695,325 669.5,334.5Q644,344 604,368L572,312Q605,287 649.5,271.5Q694,256 740,256Q811,256 848,300Q885,344 885,428L885,685L818,685L812,640Q784,665 750.5,679.5Q717,694 679,694Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/language_korean_latin.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M216,503Q242,503 260.5,491.5Q279,480 279,457Q279,434 260.5,422Q242,410 216,410Q190,410 171.5,422Q153,434 153,457Q153,480 171.5,491.5Q190,503 216,503ZM75,352L75,308L189,308L189,252L241,252L241,308L356,308L356,352L75,352ZM216,547Q169,547 135.5,523.5Q102,500 102,457Q102,413 135.5,390Q169,367 216,367Q264,367 297.5,390Q331,413 331,457Q331,501 297.5,524Q264,547 216,547ZM143,708L143,568L196,568L196,664L460,664L460,708L143,708ZM388,603L388,252L439,252L439,402L508,402L508,446L440,446L440,603L388,603ZM702,626Q730,626 756.5,613Q783,600 805,576L805,470Q782,473 762.5,477Q743,481 726,486Q681,500 658.5,521Q636,542 636,570Q636,596 654,611Q672,626 702,626ZM679,694Q622,694 589,661.5Q556,629 556,573Q556,521 589,488Q622,455 695,435Q718,429 745.5,424Q773,419 805,415L805,415Q803,368 783,346.5Q763,325 721,325Q695,325 669.5,334.5Q644,344 604,368L572,312Q605,287 649.5,271.5Q694,256 740,256Q811,256 848,300Q885,344 885,428L885,685L818,685L812,640Q784,665 750.5,679.5Q717,694 679,694Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/library_add.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:tint=\"@android:color/white\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M4,20h14v1H3V6h1V20zM21,3v15H6V3H21zM20,4H7v13h13V4z\"\n        android:strokeWidth=\"1.0\"\n        android:strokeColor=\"@android:color/white\"\n        android:strokeLineCap=\"butt\" />\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M18,10h-4V6h-1v4H9v1h4v4h1v-4h4V10z\"\n        android:strokeWidth=\"0.5\"\n        android:strokeColor=\"@android:color/white\"\n        android:strokeLineCap=\"butt\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/library_add_check.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"@android:color/white\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M17.58,8.41l-1.41,-1.41l-4.18,4.18l-1.58,-1.58L9,11.01L11.99,14L17.58,8.41z\" />\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M4,20h14v1H3V6h1V20zM21,3v15H6V3H21zM20,4H7v13h13V4z\"\n        android:strokeWidth=\"1.0\"\n        android:strokeColor=\"@android:color/white\"\n        android:strokeLineCap=\"butt\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/library_music.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"@android:color/white\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M16,6v2h-2v5c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2s0.9,-2 2,-2c0.37,0 0.7,0.11 1,0.28V6H16z\" />\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M18,20H4V6H3v15h15V20zM21,3H6v15h15V3zM7,4h13v13H7V4z\"\n        android:strokeWidth=\"1.0\"\n        android:strokeColor=\"@android:color/white\"\n        android:strokeLineCap=\"butt\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/library_music_filled.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:tint=\"@android:color/white\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M18,21H3V6h1v14h14V21z\"\n        android:strokeWidth=\"1.0\"\n        android:strokeColor=\"@android:color/white\"\n        android:strokeLineCap=\"butt\" />\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M21,3v15H6V3H21zM16,6h-3v5.28C12.7,11.11 12.37,11 12,11c-1.1,0 -2,0.9 -2,2s0.9,2 2,2s2,-0.9 2,-2V8h2V6z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/library_music_outlined.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"@android:color/white\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M16,6v2h-2v5c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2s0.9,-2 2,-2c0.37,0 0.7,0.11 1,0.28V6H16z\" />\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M18,20H4V6H3v15h15V20zM21,3H6v15h15V3zM7,4h13v13H7V4z\"\n        android:strokeWidth=\"1.0\"\n        android:strokeColor=\"@android:color/white\"\n        android:strokeLineCap=\"butt\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/linear_scale.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M19.5,9.5c-1.03,0 -1.9,0.62 -2.29,1.5h-2.92c-0.39,-0.88 -1.26,-1.5 -2.29,-1.5s-1.9,0.62 -2.29,1.5H6.79c-0.39,-0.88 -1.26,-1.5 -2.29,-1.5C3.12,9.5 2,10.62 2,12s1.12,2.5 2.5,2.5c1.03,0 1.9,-0.62 2.29,-1.5h2.92c0.39,0.88 1.26,1.5 2.29,1.5s1.9,-0.62 2.29,-1.5h2.92c0.39,0.88 1.26,1.5 2.29,1.5c1.38,0 2.5,-1.12 2.5,-2.5S20.88,9.5 19.5,9.5z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/link.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M680,800v-120L560,680v-80h120v-120h80v120h120v80L760,680v120h-80ZM440,680L280,680q-83,0 -141.5,-58.5T80,480q0,-83 58.5,-141.5T280,280h160v80L280,360q-50,0 -85,35t-35,85q0,50 35,85t85,35h160v80ZM320,520v-80h320v80L320,520ZM880,480h-80q0,-50 -35,-85t-85,-35L520,360v-80h160q83,0 141.5,58.5T880,480Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/list.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:autoMirrored=\"true\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M280,360L280,280L840,280L840,360L280,360ZM280,520L280,440L840,440L840,520L280,520ZM280,680L280,600L840,600L840,680L280,680ZM160,360Q143,360 131.5,348.5Q120,337 120,320Q120,303 131.5,291.5Q143,280 160,280Q177,280 188.5,291.5Q200,303 200,320Q200,337 188.5,348.5Q177,360 160,360ZM160,520Q143,520 131.5,508.5Q120,497 120,480Q120,463 131.5,451.5Q143,440 160,440Q177,440 188.5,451.5Q200,463 200,480Q200,497 188.5,508.5Q177,520 160,520ZM160,680Q143,680 131.5,668.5Q120,657 120,640Q120,623 131.5,611.5Q143,600 160,600Q177,600 188.5,611.5Q200,623 200,640Q200,657 188.5,668.5Q177,680 160,680Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/location_on.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M480,480q33,0 56.5,-23.5T560,400q0,-33 -23.5,-56.5T480,320q-33,0 -56.5,23.5T400,400q0,33 23.5,56.5T480,480ZM480,774q122,-112 181,-203.5T720,408q0,-109 -69.5,-178.5T480,160q-101,0 -170.5,69.5T240,408q0,71 59,162.5T480,774ZM480,880Q319,743 239.5,625.5T160,408q0,-150 96.5,-239T480,80q127,0 223.5,89T800,408q0,100 -79.5,217.5T480,880ZM480,400Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/lock.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M240,880Q207,880 183.5,856.5Q160,833 160,800L160,400Q160,367 183.5,343.5Q207,320 240,320L280,320L280,240Q280,157 338.5,98.5Q397,40 480,40Q563,40 621.5,98.5Q680,157 680,240L680,320L720,320Q753,320 776.5,343.5Q800,367 800,400L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880ZM240,800L720,800Q720,800 720,800Q720,800 720,800L720,400Q720,400 720,400Q720,400 720,400L240,400Q240,400 240,400Q240,400 240,400L240,800Q240,800 240,800Q240,800 240,800ZM480,680Q513,680 536.5,656.5Q560,633 560,600Q560,567 536.5,543.5Q513,520 480,520Q447,520 423.5,543.5Q400,567 400,600Q400,633 423.5,656.5Q447,680 480,680ZM360,320L600,320L600,240Q600,190 565,155Q530,120 480,120Q430,120 395,155Q360,190 360,240L360,320ZM240,800Q240,800 240,800Q240,800 240,800L240,400Q240,400 240,400Q240,400 240,400L240,400Q240,400 240,400Q240,400 240,400L240,800Q240,800 240,800Q240,800 240,800L240,800Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/lock_open.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M240,320L600,320L600,240Q600,190 565,155Q530,120 480,120Q430,120 395,155Q360,190 360,240L280,240Q280,157 338.5,98.5Q397,40 480,40Q563,40 621.5,98.5Q680,157 680,240L680,320L720,320Q753,320 776.5,343.5Q800,367 800,400L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880Q207,880 183.5,856.5Q160,833 160,800L160,400Q160,367 183.5,343.5Q207,320 240,320ZM240,800L720,800Q720,800 720,800Q720,800 720,800L720,400Q720,400 720,400Q720,400 720,400L240,400Q240,400 240,400Q240,400 240,400L240,800Q240,800 240,800Q240,800 240,800ZM480,680Q513,680 536.5,656.5Q560,633 560,600Q560,567 536.5,543.5Q513,520 480,520Q447,520 423.5,543.5Q400,567 400,600Q400,633 423.5,656.5Q447,680 480,680ZM240,800Q240,800 240,800Q240,800 240,800L240,400Q240,400 240,400Q240,400 240,400L240,400Q240,400 240,400Q240,400 240,400L240,800Q240,800 240,800Q240,800 240,800L240,800Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/login.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:autoMirrored=\"true\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M480,840L480,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L480,200L480,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L480,840ZM400,680L345,622L447,520L120,520L120,440L447,440L345,338L400,280L600,480L400,680Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/logout.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M200,840q-33,0 -56.5,-23.5T120,760v-560q0,-33 23.5,-56.5T200,120h280v80L200,200v560h280v80L200,840ZM640,680 L585,622 687,520L360,520v-80h327L585,338l55,-58 200,200 -200,200Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/lyrics.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"30.0\"\n    android:viewportHeight=\"30.0\">\n    <path android:pathData=\"M0,0h30v30h-30z M 0,0\" />\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M9.5,6L20.432,6\"\n        android:strokeWidth=\"2.5\"\n        android:strokeColor=\"@android:color/white\"\n        android:strokeLineCap=\"butt\" />\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M11,23.052L19.03,23.052\"\n        android:strokeWidth=\"2.5\"\n        android:strokeColor=\"@android:color/white\"\n        android:strokeLineCap=\"butt\" />\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M6.694,14.526L23.238,14.526\"\n        android:strokeWidth=\"2.5\"\n        android:strokeColor=\"@android:color/white\"\n        android:strokeLineCap=\"butt\" />\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M4.500,18.802L25.577,18.802\"\n        android:strokeWidth=\"2.0\"\n        android:strokeColor=\"@android:color/white\"\n        android:strokeLineCap=\"butt\" />\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M4.500,10.252L25.577,10.252\"\n        android:strokeWidth=\"2.0\"\n        android:strokeColor=\"@android:color/white\"\n        android:strokeLineCap=\"butt\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/manage_search.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M80,760L80,680L480,680L480,760L80,760ZM80,560L80,480L280,480L280,560L80,560ZM80,360L80,280L280,280L280,360L80,360ZM824,760L670,606Q646,623 617.5,631.5Q589,640 560,640Q477,640 418.5,581.5Q360,523 360,440Q360,357 418.5,298.5Q477,240 560,240Q643,240 701.5,298.5Q760,357 760,440Q760,469 751.5,497.5Q743,526 726,550L880,704L824,760ZM560,560Q610,560 645,525Q680,490 680,440Q680,390 645,355Q610,320 560,320Q510,320 475,355Q440,390 440,440Q440,490 475,525Q510,560 560,560Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/mic.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11L5,11c0,3.41 2.72,6.23 6,6.72L11,21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/more_horiz.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M240,560Q207,560 183.5,536.5Q160,513 160,480Q160,447 183.5,423.5Q207,400 240,400Q273,400 296.5,423.5Q320,447 320,480Q320,513 296.5,536.5Q273,560 240,560ZM480,560Q447,560 423.5,536.5Q400,513 400,480Q400,447 423.5,423.5Q447,400 480,400Q513,400 536.5,423.5Q560,447 560,480Q560,513 536.5,536.5Q513,560 480,560ZM720,560Q687,560 663.5,536.5Q640,513 640,480Q640,447 663.5,423.5Q687,400 720,400Q753,400 776.5,423.5Q800,447 800,480Q800,513 776.5,536.5Q753,560 720,560Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/more_time.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M299.5,812Q234,784 185,735Q136,686 108,620.5Q80,555 80,480Q80,405 108,339.5Q136,274 185,225Q234,176 299.5,148Q365,120 440,120Q461,120 480.5,122.5Q500,125 520,130L520,212Q500,206 480.5,203Q461,200 440,200Q322,200 241,281Q160,362 160,480Q160,598 241,679Q322,760 440,760Q558,760 639,679Q720,598 720,480Q720,469 719,460Q718,451 716,440L798,440Q800,451 800,460Q800,469 800,480Q800,555 772,620.5Q744,686 695,735Q646,784 580.5,812Q515,840 440,840Q365,840 299.5,812ZM552,648L400,496L400,280L480,280L480,464L608,592L552,648ZM720,360L720,240L600,240L600,160L720,160L720,40L800,40L800,160L920,160L920,240L800,240L800,360L720,360Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/more_vert.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M480,800Q447,800 423.5,776.5Q400,753 400,720Q400,687 423.5,663.5Q447,640 480,640Q513,640 536.5,663.5Q560,687 560,720Q560,753 536.5,776.5Q513,800 480,800ZM480,560Q447,560 423.5,536.5Q400,513 400,480Q400,447 423.5,423.5Q447,400 480,400Q513,400 536.5,423.5Q560,447 560,480Q560,513 536.5,536.5Q513,560 480,560ZM480,320Q447,320 423.5,296.5Q400,273 400,240Q400,207 423.5,183.5Q447,160 480,160Q513,160 536.5,183.5Q560,207 560,240Q560,273 536.5,296.5Q513,320 480,320Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/music_note.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M400,840Q334,840 287,793Q240,746 240,680Q240,614 287,567Q334,520 400,520Q423,520 442.5,525.5Q462,531 480,542L480,120L720,120L720,280L560,280L560,680Q560,746 513,793Q466,840 400,840Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/nav_bar.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M200,840q-33,0 -56.5,-23.5T120,760v-560q0,-33 23.5,-56.5T200,120h560q33,0 56.5,23.5T840,200v560q0,33 -23.5,56.5T760,840L200,840ZM200,600h560v-400L200,200v400ZM200,680v80h560v-80L200,680ZM200,680v80,-80Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/navigate_next.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:autoMirrored=\"true\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M376,720L320,664L504,480L320,296L376,240L616,480L376,720Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/newspaper.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M80,840L80,120L147,187L213,120L280,187L347,120L413,187L480,120L547,187L613,120L680,187L747,120L813,187L880,120L880,840L80,840ZM160,760L440,760L440,520L160,520L160,760ZM520,760L800,760L800,680L520,680L520,760ZM520,600L800,600L800,520L520,520L520,600ZM160,440L800,440L800,320L160,320L160,440Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/notification.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:width=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M160,760v-80h80v-280q0,-83 50,-147.5T420,168v-28q0,-25 17.5,-42.5T480,80q25,0 42.5,17.5T540,140v28q80,20 130,84.5T720,400v280h80v80L160,760ZM480,460ZM480,880q-33,0 -56.5,-23.5T400,800h160q0,33 -23.5,56.5T480,880ZM320,680h320v-280q0,-66 -47,-113t-113,-47q-66,0 -113,47t-47,113v280Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/offline.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M320,680h320v-80L320,600v80ZM438,560 L664,334 607,279 438,448 352,362 296,418 438,560ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM480,800q134,0 227,-93t93,-227q0,-134 -93,-227t-227,-93q-134,0 -227,93t-93,227q0,134 93,227t227,93ZM480,480Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/palette.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M480,880q-82,0 -155,-31.5t-127.5,-86Q143,708 111.5,635T80,480q0,-83 32.5,-156t88,-127Q256,143 330,111.5T488,80q80,0 151,27.5t124.5,76q53.5,48.5 85,115T880,442q0,115 -70,176.5T640,680h-74q-9,0 -12.5,5t-3.5,11q0,12 15,34.5t15,51.5q0,50 -27.5,74T480,880ZM480,480ZM260,520q26,0 43,-17t17,-43q0,-26 -17,-43t-43,-17q-26,0 -43,17t-17,43q0,26 17,43t43,17ZM380,360q26,0 43,-17t17,-43q0,-26 -17,-43t-43,-17q-26,0 -43,17t-17,43q0,26 17,43t43,17ZM580,360q26,0 43,-17t17,-43q0,-26 -17,-43t-43,-17q-26,0 -43,17t-17,43q0,26 17,43t43,17ZM700,520q26,0 43,-17t17,-43q0,-26 -17,-43t-43,-17q-26,0 -43,17t-17,43q0,26 17,43t43,17ZM480,800q9,0 14.5,-5t5.5,-13q0,-14 -15,-33t-15,-57q0,-42 29,-67t71,-25h70q66,0 113,-38.5T800,442q0,-121 -92.5,-201.5T488,160q-136,0 -232,93t-96,227q0,133 93.5,226.5T480,800Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/pause.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M520,760v-560h240v560L520,760ZM200,760v-560h240v560L200,760ZM600,680h80v-400h-80v400ZM280,680h80v-400h-80v400ZM280,280v400,-400ZM600,280v400,-400Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/person.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:fillType=\"evenOdd\"\n        android:pathData=\"M12,4C9.79,4 8,5.79 8,8C8,10.21 9.79,12 12,12C14.21,12 16,10.21 16,8C16,5.79 14.21,4 12,4ZM14,8C14,6.9 13.1,6 12,6C10.9,6 10,6.9 10,8C10,9.1 10.9,10 12,10C13.1,10 14,9.1 14,8ZM18,18C17.8,17.29 14.7,16 12,16C9.31,16 6.23,17.28 6,18H18ZM4,18C4,15.34 9.33,14 12,14C14.67,14 20,15.34 20,18V20H4V18Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/play.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M320,760v-560l440,280 -440,280ZM400,480ZM400,614 L610,480 400,346v268Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/playlist_add.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M120,640L120,560L400,560L400,640L120,640ZM120,480L120,400L560,400L560,480L120,480ZM120,320L120,240L560,240L560,320L120,320ZM640,800L640,640L480,640L480,560L640,560L640,400L720,400L720,560L880,560L880,640L720,640L720,800L640,800Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/playlist_play.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M100,640L100,560L420,560L420,640L100,640ZM100,480L100,400L580,400L580,480L100,480ZM100,320L100,240L580,240L580,320L100,320ZM620,840L620,520L860,680L620,840Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/queue_music.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:autoMirrored=\"false\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M640,800Q590,800 555,765Q520,730 520,680Q520,630 555,595Q590,560 640,560Q651,560 661,561.5Q671,563 680,568L680,240L880,240L880,320L760,320L760,680Q760,730 725,765Q690,800 640,800ZM120,640L120,560L440,560L440,640L120,640ZM120,480L120,400L600,400L600,480L120,480ZM120,320L120,240L600,240L600,320L120,320Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/radio.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:width=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M7.76,16.24C6.67,15.16 6,13.66 6,12s0.67,-3.16 1.76,-4.24l1.42,1.42C8.45,9.9 8,10.9 8,12c0,1.1 0.45,2.1 1.17,2.83L7.76,16.24zM16.24,16.24C17.33,15.16 18,13.66 18,12s-0.67,-3.16 -1.76,-4.24l-1.42,1.42C15.55,9.9 16,10.9 16,12c0,1.1 -0.45,2.1 -1.17,2.83L16.24,16.24zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2s2,-0.9 2,-2S13.1,10 12,10zM20,12c0,2.21 -0.9,4.21 -2.35,5.65l1.42,1.42C20.88,17.26 22,14.76 22,12s-1.12,-5.26 -2.93,-7.07l-1.42,1.42C19.1,7.79 20,9.79 20,12zM6.35,6.35L4.93,4.93C3.12,6.74 2,9.24 2,12s1.12,5.26 2.93,7.07l1.42,-1.42C4.9,16.21 4,14.21 4,12S4.9,7.79 6.35,6.35z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/radio_button_checked.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M480,680Q563,680 621.5,621.5Q680,563 680,480Q680,397 621.5,338.5Q563,280 480,280Q397,280 338.5,338.5Q280,397 280,480Q280,563 338.5,621.5Q397,680 480,680ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/radio_button_unchecked.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/refresh.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M480,800Q346,800 253,707Q160,614 160,480Q160,346 253,253Q346,160 480,160Q549,160 612,188.5Q675,217 720,270L720,160L800,160L800,440L520,440L520,360L688,360Q656,304 600.5,272Q545,240 480,240Q380,240 310,310Q240,380 240,480Q240,580 310,650Q380,720 480,720Q557,720 619,676Q681,632 706,560L790,560Q762,666 676,733Q590,800 480,800Z\"/>\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/remove.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M200,520L200,440L760,440L760,520L200,520Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/repeat.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M280,880L120,720L280,560L336,618L274,680L680,680L680,520L760,520L760,760L274,760L336,822L280,880ZM200,440L200,200L686,200L624,138L680,80L840,240L680,400L624,342L686,280L280,280L280,440L200,440Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/repeat_on.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M120,920Q87,920 63.5,896.5Q40,873 40,840L40,120Q40,87 63.5,63.5Q87,40 120,40L840,40Q873,40 896.5,63.5Q920,87 920,120L920,840Q920,873 896.5,896.5Q873,920 840,920L120,920ZM280,880L336,822L274,760L760,760L760,520L680,520L680,680L274,680L336,618L280,560L120,720L280,880ZM200,440L280,440L280,280L686,280L624,342L680,400L840,240L680,80L624,138L686,200L200,200L200,440Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/repeat_one.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M280,880L120,720L280,560L336,618L274,680L680,680L680,520L760,520L760,760L274,760L336,822L280,880ZM460,600L460,420L400,420L400,360L520,360L520,600L460,600ZM200,440L200,200L686,200L624,138L680,80L840,240L680,400L624,342L686,280L280,280L280,440L200,440Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/repeat_one_on.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M120,920Q87,920 63.5,896.5Q40,873 40,840L40,120Q40,87 63.5,63.5Q87,40 120,40L840,40Q873,40 896.5,63.5Q920,87 920,120L920,840Q920,873 896.5,896.5Q873,920 840,920L120,920ZM280,880L336,822L274,760L760,760L760,520L680,520L680,680L274,680L336,618L280,560L120,720L280,880ZM460,600L520,600L520,360L400,360L400,420L460,420L460,600ZM200,440L280,440L280,280L686,280L624,342L680,400L840,240L680,80L624,138L686,200L200,200L200,440Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/replay.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M480,880Q405,880 339.5,851.5Q274,823 225.5,774.5Q177,726 148.5,660.5Q120,595 120,520L200,520Q200,637 281.5,718.5Q363,800 480,800Q597,800 678.5,718.5Q760,637 760,520Q760,403 678.5,321.5Q597,240 480,240L474,240L536,302L480,360L320,200L480,40L536,98L474,160L480,160Q555,160 620.5,188.5Q686,217 734.5,265.5Q783,314 811.5,379.5Q840,445 840,520Q840,595 811.5,660.5Q783,726 734.5,774.5Q686,823 620.5,851.5Q555,880 480,880Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/restore.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M260,800q-91,0 -155.5,-63T40,583q0,-78 47,-139t123,-78q17,-72 85,-137t145,-65q33,0 56.5,23.5T520,244v242l64,-62 56,56 -160,160 -160,-160 56,-56 64,62v-242q-76,14 -118,73.5T280,440h-20q-58,0 -99,41t-41,99q0,58 41,99t99,41h480q42,0 71,-29t29,-71q0,-42 -29,-71t-71,-29h-60v-80q0,-48 -22,-89.5T600,280v-93q74,35 117,103.5T760,440q69,8 114.5,59.5T920,620q0,75 -52.5,127.5T740,800L260,800ZM480,442Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/screenshot.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M480,680L480,620L580,620L580,520L640,520L640,680L480,680ZM320,440L320,280L480,280L480,340L380,340L380,440L320,440ZM280,920Q247,920 223.5,896.5Q200,873 200,840L200,120Q200,87 223.5,63.5Q247,40 280,40L680,40Q713,40 736.5,63.5Q760,87 760,120L760,840Q760,873 736.5,896.5Q713,920 680,920L280,920ZM280,800L280,840Q280,840 280,840Q280,840 280,840L680,840Q680,840 680,840Q680,840 680,840L680,800L280,800ZM280,720L680,720L680,240L280,240L280,720ZM280,160L680,160L680,120Q680,120 680,120Q680,120 680,120L280,120Q280,120 280,120Q280,120 280,120L280,160ZM280,160L280,120Q280,120 280,120Q280,120 280,120L280,120Q280,120 280,120Q280,120 280,120L280,160ZM280,800L280,800L280,840Q280,840 280,840Q280,840 280,840L280,840Q280,840 280,840Q280,840 280,840L280,800Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/search.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/search_off.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M280,880Q197,880 138.5,821.5Q80,763 80,680Q80,597 138.5,538.5Q197,480 280,480Q363,480 421.5,538.5Q480,597 480,680Q480,763 421.5,821.5Q363,880 280,880ZM824,840L568,584Q568,584 568,584Q568,584 568,584Q556,571 542.5,557.5Q529,544 516,532Q554,508 577,468Q600,428 600,380Q600,305 547.5,252.5Q495,200 420,200Q345,200 292.5,252.5Q240,305 240,380Q240,386 240.5,391.5Q241,397 242,403Q224,405 202.5,411Q181,417 164,425Q162,414 161,403Q160,392 160,380Q160,271 235.5,195.5Q311,120 420,120Q529,120 604.5,195.5Q680,271 680,380Q680,423 666.5,461.5Q653,500 629,532L880,784L824,840ZM209,779L280,708L350,779L379,751L308,680L379,609L351,581L280,652L209,581L181,609L252,680L181,751L209,779Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/security.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M480,880Q341,845 250.5,720.5Q160,596 160,444L160,200L480,80L800,200L800,444Q800,596 709.5,720.5Q619,845 480,880ZM480,796Q577,766 642,677.5Q707,589 718,480L480,480L480,165L240,255L240,444Q240,455 240,462Q240,469 242,480L480,480L480,796Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/select_all.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M280,680v-400h400v400L280,680ZM360,600h240v-240L360,360v240ZM200,760v80q-33,0 -56.5,-23.5T120,760h80ZM120,680v-80h80v80h-80ZM120,520v-80h80v80h-80ZM120,360v-80h80v80h-80ZM200,200h-80q0,-33 23.5,-56.5T200,120v80ZM280,840v-80h80v80h-80ZM280,200v-80h80v80h-80ZM440,840v-80h80v80h-80ZM440,200v-80h80v80h-80ZM600,840v-80h80v80h-80ZM600,200v-80h80v80h-80ZM760,840v-80h80q0,33 -23.5,56.5T760,840ZM760,680v-80h80v80h-80ZM760,520v-80h80v80h-80ZM760,360v-80h80v80h-80ZM760,200v-80q33,0 56.5,23.5T840,200h-80Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/settings.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98 0,-0.34 -0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.09,-0.16 -0.26,-0.25 -0.44,-0.25 -0.06,0 -0.12,0.01 -0.17,0.03l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.06,-0.02 -0.12,-0.03 -0.18,-0.03 -0.17,0 -0.34,0.09 -0.43,0.25l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98 0,0.33 0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.09,0.16 0.26,0.25 0.44,0.25 0.06,0 0.12,-0.01 0.17,-0.03l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.06,0.02 0.12,0.03 0.18,0.03 0.17,0 0.34,-0.09 0.43,-0.25l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM17.45,11.27c0.04,0.31 0.05,0.52 0.05,0.73 0,0.21 -0.02,0.43 -0.05,0.73l-0.14,1.13 0.89,0.7 1.08,0.84 -0.7,1.21 -1.27,-0.51 -1.04,-0.42 -0.9,0.68c-0.43,0.32 -0.84,0.56 -1.25,0.73l-1.06,0.43 -0.16,1.13 -0.2,1.35h-1.4l-0.19,-1.35 -0.16,-1.13 -1.06,-0.43c-0.43,-0.18 -0.83,-0.41 -1.23,-0.71l-0.91,-0.7 -1.06,0.43 -1.27,0.51 -0.7,-1.21 1.08,-0.84 0.89,-0.7 -0.14,-1.13c-0.03,-0.31 -0.05,-0.54 -0.05,-0.74s0.02,-0.43 0.05,-0.73l0.14,-1.13 -0.89,-0.7 -1.08,-0.84 0.7,-1.21 1.27,0.51 1.04,0.42 0.9,-0.68c0.43,-0.32 0.84,-0.56 1.25,-0.73l1.06,-0.43 0.16,-1.13 0.2,-1.35h1.39l0.19,1.35 0.16,1.13 1.06,0.43c0.43,0.18 0.83,0.41 1.23,0.71l0.91,0.7 1.06,-0.43 1.27,-0.51 0.7,1.21 -1.07,0.85 -0.89,0.7 0.14,1.13zM12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM12,14c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/share.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:autoMirrored=\"true\"\n    android:tint=\"@android:color/white\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M15,5.63L20.66,12L15,18.37V15v-1h-1c-3.96,0 -7.14,1 -9.75,3.09c1.84,-4.07 5.11,-6.4 9.89,-7.1L15,9.86V9V5.63M14,3v6C6.22,10.13 3.11,15.33 2,21c2.78,-3.97 6.44,-6 12,-6v6l8,-9L14,3L14,3z\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#ff000000\"\n        android:strokeLineCap=\"butt\"\n        android:strokeLineJoin=\"miter\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/shortcut_library.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"#000000\"\n        android:pathData=\"M18,21H3V6h1v14h14V21z\"\n        android:strokeWidth=\"1.0\"\n        android:strokeColor=\"#000000\"\n        android:strokeLineCap=\"butt\" />\n    <path\n        android:fillColor=\"#000000\"\n        android:pathData=\"M21,3v15H6V3H21zM16,6h-3v5.28C12.7,11.11 12.37,11 12,11c-1.1,0 -2,0.9 -2,2s0.9,2 2,2s2,-0.9 2,-2V8h2V6z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/shortcut_search.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:autoMirrored=\"true\"\n    android:viewportWidth=\"1024.0\"\n    android:viewportHeight=\"1024.0\">\n    <path\n        android:fillColor=\"#000000\"\n        android:pathData=\"m795.9,750.7 l125,124.9a32,32 0,0 0,-45.2 45.2L750.7,795.9a416,416 0,1 1,45.2 -45.2zM900.485,877.071l2.829,2.828 -20.415,20.415 -2.828,-2.829 1.414,-1.414zM480,832a352,352 0,1 0,0 -704,352 352,0 0,0 0,704z\"\n        android:strokeWidth=\"20.0\"\n        android:strokeColor=\"#000000\"\n        android:strokeLineCap=\"butt\"\n        android:strokeLineJoin=\"miter\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/shuffle.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M560,800L560,720L664,720L536,592L593,535L720,662L720,560L800,560L800,800L560,800ZM216,800L160,744L664,240L560,240L560,160L800,160L800,400L720,400L720,296L216,800ZM367,423L160,216L216,160L423,367L367,423Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/shuffle_on.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M120,920Q87,920 63.5,896.5Q40,873 40,840L40,120Q40,87 63.5,63.5Q87,40 120,40L840,40Q873,40 896.5,63.5Q920,87 920,120L920,840Q920,873 896.5,896.5Q873,920 840,920L120,920ZM560,800L800,800L800,560L720,560L720,662L593,535L536,592L664,720L560,720L560,800ZM216,800L720,296L720,400L800,400L800,160L560,160L560,240L664,240L160,744L216,800ZM367,423L423,367L216,160L160,216L367,423Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/similar.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24.0\"\n    android:viewportHeight=\"24.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M20,10V8h-4V4h-2v4h-4V4H8v4H4v2h4v4H4v2h4v4h2v-4h4v4h2v-4h4v-2h-4v-4H20zM14,14h-4v-4h4V14z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/skip_next.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M660,720v-480h80v480h-80ZM220,720v-480l360,240 -360,240ZM300,480ZM300,570 L436,480 300,390v180Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/skip_previous.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M220,720v-480h80v480h-80ZM740,720L380,480l360,-240v480ZM660,480ZM660,570v-180l-136,90 136,90Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/sliders.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M200,600Q150,600 115,565Q80,530 80,480Q80,430 115,395Q150,360 200,360L760,360Q810,360 845,395Q880,430 880,480Q880,530 845,565Q810,600 760,600L200,600ZM560,520L760,520Q777,520 788.5,508.5Q800,497 800,480Q800,463 788.5,451.5Q777,440 760,440L560,440L560,520Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/slow_motion_video.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M170,732Q132,688 109,634Q86,580 80,520L162,520Q168,564 184,603.5Q200,643 226,676L170,732ZM80,440Q88,380 110,326Q132,272 170,228L226,284Q200,317 184,356.5Q168,396 162,440L80,440ZM438,878Q378,872 324.5,849Q271,826 226,790L282,732Q317,758 355.5,775Q394,792 438,798L438,878ZM284,228L226,170Q271,134 324.5,111Q378,88 440,82L440,162Q395,168 356,185Q317,202 284,228ZM380,660L380,300L660,480L380,660ZM520,878L520,798Q641,781 720.5,691Q800,601 800,480Q800,359 720.5,269Q641,179 520,162L520,82Q674,99 777,212Q880,325 880,480Q880,635 777,748Q674,861 520,878Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/small_icon.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"200dp\"\n    android:width=\"200dp\"\n    android:viewportWidth=\"35.0\"\n    android:viewportHeight=\"35.0\">\n    <group\n        android:scaleX=\"0.7\"\n        android:scaleY=\"0.7\"\n        android:translateX=\"5.2\"\n        android:translateY=\"5.3\">\n        <path\n            android:fillColor=\"#00000000\"\n            android:pathData=\"M11.7,17.037L5.907,22.83C5.517,23.22 5.517,23.853 5.907,24.244L11.493,29.83C11.883,30.22 12.517,30.22 12.907,29.83L18.907,23.83C19.095,23.642 19.2,23.388 19.2,23.122V6.004C19.2,5.115 20.274,4.668 20.905,5.295L30.2,14.536\"\n            android:strokeColor=\"#FFFFFFFF\"\n            android:strokeWidth=\"5.0\" />\n    </group>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/speed.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M418,620Q442,644 480,643.5Q518,643 536,616L760,280L424,504Q397,522 395.5,559Q394,596 418,620ZM480,160Q539,160 593.5,176.5Q648,193 696,226L620,274Q587,257 551.5,248.5Q516,240 480,240Q347,240 253.5,333.5Q160,427 160,560Q160,602 171.5,643Q183,684 204,720L756,720Q779,682 789.5,641Q800,600 800,556Q800,520 791.5,486Q783,452 766,420L814,344Q844,391 861.5,444Q879,497 880,554Q881,611 867,663Q853,715 826,762Q815,780 796,790Q777,800 756,800L204,800Q183,800 164,790Q145,780 134,762Q108,717 94,666.5Q80,616 80,560Q80,477 111.5,404.5Q143,332 197.5,277.5Q252,223 325,191.5Q398,160 480,160ZM487,473L487,473Q487,473 487,473Q487,473 487,473Q487,473 487,473Q487,473 487,473Q487,473 487,473Q487,473 487,473L487,473L487,473L487,473Q487,473 487,473Q487,473 487,473Q487,473 487,473Q487,473 487,473Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/star.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M354,673L480,597L606,674L573,530L684,434L538,421L480,285L422,420L276,433L387,530L354,673ZM233,840L298,559L80,370L368,345L480,80L592,345L880,370L662,559L727,840L480,691L233,840ZM480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490L480,490Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/stats.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:width=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M480,880q-66,0 -127.5,-20.5T240,800l58,-58q42,29 88,43.5t94,14.5q133,0 226.5,-93.5T800,480q0,-133 -93.5,-226.5T480,160q-133,0 -226.5,93.5T160,480L80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 155.5,31.5t127,86q54.5,54.5 86,127T880,480q0,82 -31.5,155t-86,127.5q-54.5,54.5 -127,86T480,880ZM159,717l163,-163 120,100 198,-198v104h80v-240L480,320v80h104L438,546 318,446 117,647q11,23 19.5,37.5T159,717ZM480,480Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/storage.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M120,800L120,640L840,640L840,800L120,800ZM200,760L280,760L280,680L200,680L200,760ZM120,320L120,160L840,160L840,320L120,320ZM200,280L280,280L280,200L200,200L200,280ZM120,560L120,400L840,400L840,560L120,560ZM200,520L280,520L280,440L200,440L200,520Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/subscribe.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:width=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M480,480q-66,0 -113,-47t-47,-113q0,-66 47,-113t113,-47q66,0 113,47t47,113q0,66 -47,113t-113,47ZM160,800v-112q0,-34 17.5,-62.5T224,582q62,-31 126,-46.5T480,520q66,0 130,15.5T736,582q29,15 46.5,43.5T800,688v112L160,800ZM240,720h480v-32q0,-11 -5.5,-20T700,654q-54,-27 -109,-40.5T480,600q-56,0 -111,13.5T260,654q-9,5 -14.5,14t-5.5,20v32ZM480,400q33,0 56.5,-23.5T560,320q0,-33 -23.5,-56.5T480,240q-33,0 -56.5,23.5T400,320q0,33 23.5,56.5T480,400ZM480,320ZM480,720Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/subscribed.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:height=\"24dp\"\n    android:width=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M80,800v-112q0,-33 17,-62t47,-44q51,-26 115,-44t141,-18q30,0 58.5,3t55.5,9l-70,70q-11,-2 -21.5,-2L400,600q-71,0 -127.5,17T180,654q-9,5 -14.5,14t-5.5,20v32h250l80,80L80,800ZM622,816L484,678l56,-56 82,82 202,-202 56,56 -258,258ZM400,480q-66,0 -113,-47t-47,-113q0,-66 47,-113t113,-47q66,0 113,47t47,113q0,66 -47,113t-113,47ZM410,720ZM400,400q33,0 56.5,-23.5T480,320q0,-33 -23.5,-56.5T400,240q-33,0 -56.5,23.5T320,320q0,33 23.5,56.5T400,400ZM400,320Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/swipe.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"@android:color/white\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M18.89,14.75l-4.09,-2.04c-0.28,-0.14 -0.58,-0.21 -0.89,-0.21H13v-6C13,5.67 12.33,5 11.5,5S10,5.67 10,6.5v10.74L6.75,16.5c-0.33,-0.07 -0.68,0.03 -0.92,0.28L5,17.62l4.54,4.79C9.92,22.79 10.68,23 11.21,23h6.16c1,0 1.84,-0.73 1.98,-1.72l0.63,-4.46C20.1,15.97 19.66,15.14 18.89,14.75z\" />\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M20.13,3.87C18.69,2.17 15.6,1 12,1S5.31,2.17 3.87,3.87L2,2v5h5L4.93,4.93c1,-1.29 3.7,-2.43 7.07,-2.43s6.07,1.14 7.07,2.43L17,7h5V2L20.13,3.87z\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/sync.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M160,800L160,720L270,720L254,706Q205,657 182.5,599.5Q160,542 160,482Q160,371 226.5,284.5Q293,198 400,170L400,254Q328,280 284,342.5Q240,405 240,482Q240,527 257,569.5Q274,612 310,648L320,658L320,560L400,560L400,800L160,800ZM560,790L560,706Q632,680 676,617.5Q720,555 720,478Q720,433 703,390.5Q686,348 650,312L640,302L640,400L560,400L560,160L800,160L800,240L690,240L706,254Q755,303 777.5,360.5Q800,418 800,478Q800,589 733.5,675.5Q667,762 560,790Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/tab.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M160,720L800,720Q800,720 800,720Q800,720 800,720L800,400L520,400L520,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720ZM160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800ZM160,720Q160,720 160,720Q160,720 160,720L160,240Q160,240 160,240Q160,240 160,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720L160,720Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/telegram.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:pathData=\"M21.198,2.433 C21.198,2.433 23.049,1.728 22.898,3.479 C22.847,4.184 22.395,6.691 22.043,9.399L20.896,16.797 C20.896,16.797 20.795,17.954 19.899,18.156 C19.002,18.357 17.554,17.401 17.303,17.199 C17.102,17.048 13.503,14.741 12.206,13.585 C11.804,13.233 11.352,12.528 12.256,11.723L17.604,6.59 C18.208,5.985 18.811,4.577 16.307,6.289L9.009,11.222 C9.009,11.222 8.213,11.724 6.721,11.273L3.523,10.267 C3.523,10.267 2.326,9.511 4.378,8.756 C9.428,6.388 15.629,3.97 21.198,2.433Z\"\n        android:fillColor=\"#ffffff\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/time_auto.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M320,680L394,680L417,610L541,610L564,680L640,680L520,342L440,342L320,680ZM436,552L480,420L523,552L436,552ZM360,120L360,40L600,40L600,120L360,120ZM340.5,851.5Q275,823 226,774Q177,725 148.5,659.5Q120,594 120,520Q120,446 148.5,380.5Q177,315 226,266Q275,217 340.5,188.5Q406,160 480,160Q542,160 599,180Q656,200 706,238L762,182L818,238L762,294Q800,344 820,401Q840,458 840,520Q840,594 811.5,659.5Q783,725 734,774Q685,823 619.5,851.5Q554,880 480,880Q406,880 340.5,851.5ZM678,718Q760,636 760,520Q760,404 678,322Q596,240 480,240Q364,240 282,322Q200,404 200,520Q200,636 282,718Q364,800 480,800Q596,800 678,718ZM480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Q480,520 480,520Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/timer.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M15,1H9v2h6V1zm-4,13h2v-6h-2v6zm8.03,-6.61l1.42,-1.42c-0.43,-0.51 -0.9,-0.99 -1.41,-1.41l-1.42,1.42C16.07,4.74 14.12,4 12,4c-4.97,0 -9,4.03 -9,9c0,4.97 4.03,9 9,9s9,-4.03 9,-9c0,-2.12 -0.74,-4.07 -1.97,-5.61zM12,20c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7s7,3.13 7,7s-3.13,7 -7,7z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/timer_arrow_down.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\"\n    android:tint=\"?attr/colorControlNormal\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M127.5,712.5Q40,625 40,500Q40,375 127.5,287.5Q215,200 340,200Q392,200 438,216.5Q484,233 522,262L564,220L620,276L578,318Q607,356 623.5,402.5Q640,449 640,500Q640,625 552.5,712.5Q465,800 340,800Q215,800 127.5,712.5ZM780,800L640,660L696,604L740,648L740,160L820,160L820,647L863,604L920,660L780,800ZM240,160L240,80L440,80L440,160L240,160ZM496,656Q560,592 560,500Q560,408 496,344Q432,280 340,280Q248,280 184,344Q120,408 120,500Q120,592 184,656Q248,720 340,720Q432,720 496,656ZM300,540L380,540L380,340L300,340L300,540ZM340,500Q340,500 340,500Q340,500 340,500Q340,500 340,500Q340,500 340,500Q340,500 340,500Q340,500 340,500Q340,500 340,500Q340,500 340,500Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/token.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M480,880 L120,680v-400l360,-200 360,200v400L480,880ZM364,370q23,-24 53,-37t63,-13q33,0 63,13t53,37l120,-67 -236,-131 -236,131 120,67ZM440,766v-131q-54,-14 -87,-57t-33,-98q0,-11 1,-20.5t4,-19.5l-125,-70v263l240,133ZM480,560q33,0 56.5,-23.5T560,480q0,-33 -23.5,-56.5T480,400q-33,0 -56.5,23.5T400,480q0,33 23.5,56.5T480,560ZM520,766 L760,633v-263l-125,70q3,10 4,19.5t1,20.5q0,55 -33,98t-87,57v131Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/translate.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M12.87,15.07L10.33,12.56L10.41,12.48C12.11,10.58 13.31,8.39 13.92,6H17V4H10V2H8V4H1V6H11.91C11.39,7.91 10.36,9.65 8.95,11.15C8.03,10.16 7.27,9.07 6.69,7.91H4.69C5.37,9.55 6.4,11.08 7.73,12.42L2.91,17.21L4.33,18.63L9.12,13.84L12.01,16.7L12.87,15.07M18.5,10H16.5L12,22H14L15.12,19H19.87L21,22H23L18.5,10M15.87,17L17.5,12.67L19.12,17H15.87Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/trending_up.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:autoMirrored=\"false\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M136,720L80,664L376,366L536,526L744,320L640,320L640,240L880,240L880,480L800,480L800,376L536,640L376,480L136,720Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/tune.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M440,840L440,600L520,600L520,680L840,680L840,760L520,760L520,840L440,840ZM120,760L120,680L360,680L360,760L120,760ZM280,600L280,520L120,520L120,440L280,440L280,360L360,360L360,600L280,600ZM440,520L440,440L840,440L840,520L440,520ZM600,360L600,120L680,120L680,200L840,200L840,280L680,280L680,360L600,360ZM120,280L120,200L520,200L520,280L120,280Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/update.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M480,840Q405,840 339.5,811.5Q274,783 225.5,734.5Q177,686 148.5,620.5Q120,555 120,480Q120,405 148.5,339.5Q177,274 225.5,225.5Q274,177 339.5,148.5Q405,120 480,120Q562,120 635.5,155Q709,190 760,254L760,160L840,160L840,400L600,400L600,320L710,320Q669,264 609,232Q549,200 480,200Q363,200 281.5,281.5Q200,363 200,480Q200,597 281.5,678.5Q363,760 480,760Q585,760 663.5,692Q742,624 756,520L838,520Q823,657 720.5,748.5Q618,840 480,840ZM592,648L440,496L440,280L520,280L520,464L648,592L592,648Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/upload.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"?attr/colorControlNormal\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M9,16h6v-6h4l-7,-7 -7,7h4v6zM5,18h14v2H5z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/volume_down.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:autoMirrored=\"true\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M200,600v-240h160l200,-200v640L360,600L200,600ZM560,640v-322q47,22 73.5,66t26.5,96q0,51 -26.5,94.5T560,640ZM480,354l-86,86L280,440v80h114l86,86v-252ZM380,480Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/volume_mute.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:autoMirrored=\"true\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M280,600v-240h160l200,-200v640L440,600L280,600ZM560,354l-86,86L360,440v80h114l86,86v-252ZM460,480Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/volume_off.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:autoMirrored=\"false\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M792,904 L671,783q-25,16 -53,27.5T560,829v-82q14,-5 27.5,-10t25.5,-12L480,592v208L280,600L120,600v-240h128L56,168l56,-56 736,736 -56,56ZM784,672 L726,614q17,-31 25.5,-65t8.5,-70q0,-94 -55,-168T560,211v-82q124,28 202,125.5T840,479q0,53 -14.5,102T784,672ZM650,538l-90,-90v-130q47,22 73.5,66t26.5,96q0,15 -2.5,29.5T650,538ZM480,368 L376,264l104,-104v208ZM400,606v-94l-72,-72L200,440v80h114l86,86ZM364,476Z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/volume_off_pause.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:autoMirrored=\"false\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <group>\n        <clip-path\n                android:pathData=\"M50,0h960v960Z\" />\n        <path\n                android:fillColor=\"@android:color/white\"\n                android:pathData=\"M520,760v-560h240v560L520,760ZM200,760v-560h240v560L200,760ZM600,680h80v-400h-80v400ZM280,680h80v-400h-80v400ZM280,280v400,-400ZM600,280v400,-400Z\"/>\n\n    </group>\n    <group>\n        <clip-path\n                android:pathData=\"M0,0v960h960Z\" />\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M792,904 L671,783q-25,16 -53,27.5T560,829v-82q14,-5 27.5,-10t25.5,-12L480,592v208L280,600L120,600v-240h128L56,168l56,-56 736,736 -56,56ZM784,672 L726,614q17,-31 25.5,-65t8.5,-70q0,-94 -55,-168T560,211v-82q124,28 202,125.5T840,479q0,53 -14.5,102T784,672ZM650,538l-90,-90v-130q47,22 73.5,66t26.5,96q0,15 -2.5,29.5T650,538ZM480,368 L376,264l104,-104v208ZM400,606v-94l-72,-72L200,440v80h114l86,86ZM364,476Z\" />\n    </group>\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/volume_up.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24.0dip\"\n    android:height=\"24.0dip\"\n    android:autoMirrored=\"false\"\n    android:viewportWidth=\"960.0\"\n    android:viewportHeight=\"960.0\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M560,829v-82q90,-26 145,-100t55,-168q0,-94 -55,-168T560,211v-82q124,28 202,125.5T840,479q0,127 -78,224.5T560,829ZM120,600v-240h160l200,-200v640L280,600L120,600ZM560,640v-322q47,22 73.5,66t26.5,96q0,51 -26.5,94.5T560,640ZM400,354l-86,86L200,440v80h114l86,86v-252ZM300,480Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/warning.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M40,840L480,80L920,840L40,840ZM178,760L782,760L480,240L178,760ZM480,720Q497,720 508.5,708.5Q520,697 520,680Q520,663 508.5,651.5Q497,640 480,640Q463,640 451.5,651.5Q440,663 440,680Q440,697 451.5,708.5Q463,720 480,720ZM440,600L520,600L520,400L440,400L440,600ZM480,500L480,500L480,500Z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/widget_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Widget background - uses Material 3 surface colors -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@android:color/system_accent1_100\"\n        tools:targetApi=\"31\" />\n    <corners android:radius=\"28dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/widget_like_button_bg.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"oval\">\n    <solid android:color=\"@color/widget_tertiary_container\" />\n    <size android:width=\"48dp\" android:height=\"48dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/widget_mic_button_bg.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Mic button background – idle state (solid accent circle) -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"oval\">\n    <solid android:color=\"@color/widget_primary_container\" />\n    <size android:width=\"44dp\" android:height=\"44dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/widget_mic_button_bg_active.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Mic button background – active/listening state (brighter accent) -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"oval\">\n    <solid android:color=\"@color/widget_mic_active_bg\" />\n    <size android:width=\"44dp\" android:height=\"44dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/widget_mic_pulse_1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Mic pulse ring – frame 1 (ring appears just outside button, full opacity) -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"ring\"\n    android:useLevel=\"false\"\n    android:innerRadius=\"22dp\"\n    android:thickness=\"2dp\">\n    <solid android:color=\"@color/widget_mic_pulse_ring\" />\n    <size android:width=\"56dp\" android:height=\"56dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/widget_mic_pulse_2.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Mic pulse ring – frame 2 (ring expands outward, high opacity) -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"ring\"\n    android:useLevel=\"false\"\n    android:innerRadius=\"24dp\"\n    android:thickness=\"2dp\">\n    <solid android:color=\"@color/widget_mic_pulse_ring_mid\" />\n    <size android:width=\"56dp\" android:height=\"56dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/widget_mic_pulse_3.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Mic pulse ring – frame 3 (ring expanding further, medium opacity) -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"ring\"\n    android:useLevel=\"false\"\n    android:innerRadius=\"26dp\"\n    android:thickness=\"2dp\">\n    <solid android:color=\"@color/widget_mic_pulse_ring_low\" />\n    <size android:width=\"56dp\" android:height=\"56dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/widget_mic_pulse_4.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Mic pulse ring – frame 4 (ring at outer edge, low opacity – about to reset) -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"ring\"\n    android:useLevel=\"false\"\n    android:innerRadius=\"26dp\"\n    android:thickness=\"1dp\">\n    <solid android:color=\"@color/widget_mic_pulse_ring_fade\" />\n    <size android:width=\"56dp\" android:height=\"56dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/widget_mic_pulse_idle.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Mic pulse ring – idle state (transparent, no ring) -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"oval\">\n    <solid android:color=\"@android:color/transparent\" />\n    <size android:width=\"56dp\" android:height=\"56dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/widget_play_button_circular.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@color/widget_play_button_low_bg\" />\n    <corners android:radius=\"12dp\" />\n    <size android:width=\"56dp\" android:height=\"56dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/widget_play_pill_bg.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"#D0BCFF\" />\n    <corners android:radius=\"24dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/widget_progress_clip.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<clip xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:drawable=\"@drawable/widget_progress_fill\"\n    android:clipOrientation=\"horizontal\"\n    android:gravity=\"left\" />\n"
  },
  {
    "path": "app/src/main/res/drawable/widget_progress_fill.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"#6750A4\" />\n    <corners android:radius=\"4dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/widget_progress_track.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"#CAC4D0\" />\n    <corners android:radius=\"4dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/widget_turntable_default_art.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Default circular art for turntable widget preview -->\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <!-- Circular gradient background -->\n    <item>\n        <shape android:shape=\"oval\">\n            <gradient\n                android:angle=\"135\"\n                android:startColor=\"#283A4D\"\n                android:centerColor=\"#405571\"\n                android:endColor=\"#6B8099\"\n                android:type=\"linear\" />\n        </shape>\n    </item>\n    <!-- Centered app icon -->\n    <item\n        android:drawable=\"@drawable/ic_launcher_foreground\"\n        android:gravity=\"center\" />\n</layer-list>\n"
  },
  {
    "path": "app/src/main/res/drawable/widget_turntable_nav_bg.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@color/widget_tertiary_container\" />\n    <corners android:radius=\"18dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/widget_turntable_play_bg.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@color/widget_primary_container\" />\n    <corners android:radius=\"12dp\" />\n    <size android:width=\"52dp\" android:height=\"52dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable/wifi_proxy.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M700,780L750,780L750,730L700,730L700,780ZM700,670L750,670L750,620L700,620L700,670ZM810,780L860,780L860,730L810,730L810,780ZM640,840L640,560L810,560L810,670L920,670L920,840L640,840ZM480,840L0,360Q95,263 219.5,211.5Q344,160 480,160Q616,160 740.5,211.5Q865,263 960,360L822,497Q808,483 794,468.5Q780,454 766,440L844,362Q765,302 672,271Q579,240 480,240Q381,240 288,271Q195,302 116,362L480,726L520,686Q534,700 548.5,714.5Q563,729 577,743L480,840ZM480,483L480,483Q480,483 480,483Q480,483 480,483Q480,483 480,483Q480,483 480,483L480,483Q480,483 480,483Q480,483 480,483Q480,483 480,483Q480,483 480,483Q480,483 480,483Q480,483 480,483Z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-night/widget_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Widget background night mode - uses Material 3 surface colors -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@android:color/system_accent1_800\"\n        tools:targetApi=\"31\" />\n    <corners android:radius=\"28dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-night/widget_play_pill_bg.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"#4F378B\" />\n    <corners android:radius=\"24dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-night/widget_progress_fill.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"#D0BCFF\" />\n    <corners android:radius=\"4dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-night/widget_progress_track.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"#49454F\" />\n    <corners android:radius=\"4dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-night/widget_turntable_nav_bg.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@color/widget_tertiary_container\" />\n    <corners android:radius=\"18dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-night/widget_turntable_play_bg.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"#4F378B\" />\n    <corners android:radius=\"12dp\" />\n    <size android:width=\"52dp\" android:height=\"52dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-night-v31/widget_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Widget background night - uses system dynamic colors for Android 12+ -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@android:color/system_accent2_800\" />\n    <corners android:radius=\"@android:dimen/system_app_widget_background_radius\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-night-v31/widget_play_pill_bg.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@android:color/system_accent1_700\"\n        tools:targetApi=\"31\" />\n    <corners android:radius=\"24dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-night-v31/widget_progress_fill.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@android:color/system_accent1_200\" />\n    <corners android:radius=\"4dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-night-v31/widget_progress_track.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@android:color/system_accent2_700\" />\n    <corners android:radius=\"4dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-night-v31/widget_turntable_nav_bg.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@android:color/system_accent3_700\" />\n    <corners android:radius=\"18dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-night-v31/widget_turntable_play_bg.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@android:color/system_accent1_700\" />\n    <corners android:radius=\"12dp\" />\n    <size android:width=\"52dp\" android:height=\"52dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-v31/ic_launcher_background_v31.xml",
    "content": "<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <gradient\n        android:angle=\"45.0\"\n        android:endColor=\"@android:color/system_accent2_400\"\n        android:startColor=\"@android:color/system_accent1_800\"\n        android:type=\"linear\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-v31/ic_widget_mic.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Microphone icon – API 31+ (Material You dynamic: onPrimaryContainer = accent1-700) -->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\"\n    android:tint=\"@android:color/system_accent1_700\">\n\n    <!-- Mic body -->\n    <path\n        android:fillColor=\"@android:color/system_accent1_700\"\n        android:pathData=\"M12,14c1.66,0 3,-1.34 3,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5l0,6c0,1.66 1.34,3 3,3z\" />\n\n    <!-- Mic stand arm -->\n    <path\n        android:fillColor=\"@android:color/system_accent1_700\"\n        android:pathData=\"M17,11c0,2.76 -2.24,5 -5,5s-5,-2.24 -5,-5L5,11c0,3.53 2.61,6.43 6,6.92L11,21L9,21l0,2l6,0l0,-2l-2,0l0,-3.08c3.39,-0.49 6,-3.39 6,-6.92l-2,0z\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-v31/widget_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Widget background - uses system dynamic colors for Android 12+ -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@android:color/system_accent2_100\" />\n    <corners android:radius=\"@android:dimen/system_app_widget_background_radius\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-v31/widget_mic_button_bg.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Mic button background – idle (API 31+ Material You: accent1-100 = primaryContainer) -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"oval\">\n    <solid android:color=\"@android:color/system_accent1_100\" />\n    <size android:width=\"44dp\" android:height=\"44dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-v31/widget_mic_button_bg_active.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Mic button background – active/listening (API 31+ Material You: accent1-300 = primary) -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"oval\">\n    <solid android:color=\"@android:color/system_accent1_300\" />\n    <size android:width=\"44dp\" android:height=\"44dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-v31/widget_mic_pulse_1.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Pulse ring frame 1 – API 31+ (accent1-200 with high alpha) -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"ring\"\n    android:useLevel=\"false\"\n    android:innerRadius=\"22dp\"\n    android:thickness=\"2dp\">\n    <solid android:color=\"@android:color/system_accent1_200\" />\n    <size android:width=\"56dp\" android:height=\"56dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-v31/widget_mic_pulse_2.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Pulse ring frame 2 – API 31+ (expanding ring, accent1-100) -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"ring\"\n    android:useLevel=\"false\"\n    android:innerRadius=\"24dp\"\n    android:thickness=\"2dp\">\n    <solid android:color=\"@android:color/system_accent1_100\" />\n    <size android:width=\"56dp\" android:height=\"56dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-v31/widget_mic_pulse_3.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Pulse ring frame 3 – API 31+ (wider ring, softer accent) -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"ring\"\n    android:useLevel=\"false\"\n    android:innerRadius=\"26dp\"\n    android:thickness=\"2dp\">\n    <solid android:color=\"@android:color/system_accent2_100\" />\n    <size android:width=\"56dp\" android:height=\"56dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-v31/widget_mic_pulse_4.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Pulse ring frame 4 – API 31+ (outermost ring, fading) -->\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"ring\"\n    android:useLevel=\"false\"\n    android:innerRadius=\"26dp\"\n    android:thickness=\"1dp\">\n    <solid android:color=\"@android:color/system_neutral1_200\" />\n    <size android:width=\"56dp\" android:height=\"56dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-v31/widget_play_pill_bg.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@android:color/system_accent1_200\"\n        tools:targetApi=\"31\" />\n    <corners android:radius=\"24dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-v31/widget_progress_fill.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@android:color/system_accent1_600\" />\n    <corners android:radius=\"4dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-v31/widget_progress_track.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@android:color/system_accent2_200\" />\n    <corners android:radius=\"4dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-v31/widget_turntable_nav_bg.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@android:color/system_accent3_100\" />\n    <corners android:radius=\"18dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/drawable-v31/widget_turntable_play_bg.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@android:color/system_accent1_200\" />\n    <corners android:radius=\"12dp\" />\n    <size android:width=\"52dp\" android:height=\"52dp\" />\n</shape>\n"
  },
  {
    "path": "app/src/main/res/font/bbh_bartle.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<font-family xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\n    <font\n        android:font=\"@font/bbh_bartle_regular\"\n        android:fontStyle=\"normal\"\n        android:fontWeight=\"400\"\n        app:font=\"@font/bbh_bartle_regular\"\n        app:fontStyle=\"normal\"\n        app:fontWeight=\"400\" />\n</font-family>\n"
  },
  {
    "path": "app/src/main/res/layout/widget_compact_square.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Compact 2x2 Widget - Album art with play button overlay -->\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@android:id/background\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"@drawable/widget_background\"\n    android:padding=\"12dp\"\n    android:theme=\"@style/Theme.Widget.Metrolist\">\n\n    <!-- Album Art Container -->\n    <FrameLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\">\n\n        <ImageView\n            android:id=\"@+id/widget_compact_album_art\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:scaleType=\"centerCrop\"\n            android:src=\"@mipmap/ic_launcher\"\n            android:contentDescription=\"@string/album_art\" />\n\n        <!-- Play Button Overlay - Bottom Center -->\n        <FrameLayout\n            android:id=\"@+id/widget_compact_play_container\"\n            android:layout_width=\"56dp\"\n            android:layout_height=\"56dp\"\n            android:layout_gravity=\"bottom|center_horizontal\"\n            android:layout_marginBottom=\"8dp\"\n            android:background=\"@drawable/widget_play_button_circular\">\n\n            <ImageView\n                android:id=\"@+id/widget_compact_play_pause\"\n                android:layout_width=\"28dp\"\n                android:layout_height=\"28dp\"\n                android:layout_gravity=\"center\"\n                android:src=\"@drawable/ic_widget_play_low\"\n                android:contentDescription=\"@string/play_pause\" />\n        </FrameLayout>\n\n    </FrameLayout>\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/widget_compact_wide.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Compact 4x1 Widget - Single row with album art, song info, like and play buttons -->\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@android:id/background\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"horizontal\"\n    android:background=\"@drawable/widget_background\"\n    android:paddingStart=\"12dp\"\n    android:paddingEnd=\"12dp\"\n    android:paddingTop=\"8dp\"\n    android:paddingBottom=\"8dp\"\n    android:gravity=\"center_vertical\"\n    android:theme=\"@style/Theme.Widget.Metrolist\">\n\n    <!-- Album Art - Square -->\n    <ImageView\n        android:id=\"@+id/widget_wide_album_art\"\n        android:layout_width=\"48dp\"\n        android:layout_height=\"48dp\"\n        android:scaleType=\"centerCrop\"\n        android:src=\"@mipmap/ic_launcher\"\n        android:contentDescription=\"@string/album_art\" />\n\n    <!-- Song Info -->\n    <LinearLayout\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_weight=\"1\"\n        android:layout_marginStart=\"12dp\"\n        android:layout_marginEnd=\"12dp\"\n        android:orientation=\"vertical\"\n        android:gravity=\"center_vertical\">\n\n        <TextView\n            android:id=\"@+id/widget_wide_song_title\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/not_playing\"\n            android:textSize=\"14sp\"\n            android:textStyle=\"bold\"\n            android:textColor=\"@color/widget_text_primary\"\n            android:maxLines=\"1\"\n            android:ellipsize=\"end\" />\n\n        <TextView\n            android:id=\"@+id/widget_wide_artist_name\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:text=\"@string/tap_to_play\"\n            android:textSize=\"12sp\"\n            android:textColor=\"@color/widget_text_secondary\"\n            android:maxLines=\"1\"\n            android:ellipsize=\"end\"\n            android:layout_marginTop=\"2dp\" />\n    </LinearLayout>\n\n    <!-- Like Button -->\n    <FrameLayout\n        android:layout_width=\"40dp\"\n        android:layout_height=\"40dp\"\n        android:layout_marginEnd=\"8dp\"\n        android:background=\"@drawable/widget_like_button_bg\">\n\n        <ImageButton\n            android:id=\"@+id/widget_wide_like_button\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:padding=\"10dp\"\n            android:scaleType=\"fitCenter\"\n            android:background=\"@android:color/transparent\"\n            android:src=\"@drawable/ic_widget_heart_outline_nav\"\n            android:contentDescription=\"@string/like\" />\n    </FrameLayout>\n\n    <!-- Play Button -->\n    <FrameLayout\n        android:id=\"@+id/widget_wide_play_container\"\n        android:layout_width=\"44dp\"\n        android:layout_height=\"44dp\"\n        android:background=\"@drawable/widget_play_button_circular\">\n\n        <ImageView\n            android:id=\"@+id/widget_wide_play_pause\"\n            android:layout_width=\"22dp\"\n            android:layout_height=\"22dp\"\n            android:layout_gravity=\"center\"\n            android:src=\"@drawable/ic_widget_play_low\"\n            android:contentDescription=\"@string/play_pause\" />\n    </FrameLayout>\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/widget_music_player.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@android:id/background\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"horizontal\"\n    android:background=\"@drawable/widget_background\"\n    android:padding=\"16dp\"\n    android:gravity=\"center_vertical\"\n    android:theme=\"@style/Theme.Widget.Metrolist\">\n\n    <!-- Album Art Section (Left) -->\n    <ImageView\n        android:id=\"@+id/widget_album_art\"\n        android:layout_width=\"88dp\"\n        android:layout_height=\"88dp\"\n        android:scaleType=\"centerCrop\"\n        android:src=\"@mipmap/ic_launcher\"\n        android:contentDescription=\"@string/album_art\" />\n\n    <!-- Right Content Column -->\n    <LinearLayout\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_weight=\"1\"\n        android:layout_marginStart=\"16dp\"\n        android:orientation=\"vertical\">\n\n        <!-- Top Row: Song Info -->\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"vertical\"\n            android:layout_marginBottom=\"12dp\">\n\n            <TextView\n                android:id=\"@+id/widget_song_title\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/not_playing\"\n                android:textSize=\"16sp\"\n                android:textStyle=\"bold\"\n                android:textColor=\"@color/widget_text_primary\"\n                android:maxLines=\"1\"\n                android:ellipsize=\"end\" />\n\n            <TextView\n                android:id=\"@+id/widget_artist_name\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:text=\"@string/tap_to_play\"\n                android:textSize=\"13sp\"\n                android:textColor=\"@color/widget_text_secondary\"\n                android:maxLines=\"1\"\n                android:ellipsize=\"end\"\n                android:layout_marginTop=\"2dp\" />\n        </LinearLayout>\n\n        <!-- Middle Row: Interactive Elements -->\n        <LinearLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:orientation=\"horizontal\"\n            android:gravity=\"center_vertical\">\n\n            <!-- Like Button -->\n            <FrameLayout\n                android:layout_width=\"48dp\"\n                android:layout_height=\"48dp\"\n                android:background=\"@drawable/widget_like_button_bg\">\n\n                <ImageButton\n                    android:id=\"@+id/widget_like_button\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"match_parent\"\n                    android:padding=\"12dp\"\n                    android:scaleType=\"fitCenter\"\n                    android:background=\"@android:color/transparent\"\n                    android:src=\"@drawable/ic_widget_heart_outline_nav\"\n                    android:contentDescription=\"@string/like\" />\n            </FrameLayout>\n\n            <!-- Play/Pause Container -->\n            <FrameLayout\n                android:id=\"@+id/widget_play_pause_container\"\n                android:layout_width=\"0dp\"\n                android:layout_height=\"48dp\"\n                android:layout_weight=\"1\"\n                android:layout_marginStart=\"12dp\"\n                android:background=\"@drawable/widget_play_pill_bg\">\n\n                <ImageView\n                    android:id=\"@+id/widget_play_pause\"\n                    android:layout_width=\"28dp\"\n                    android:layout_height=\"28dp\"\n                    android:layout_gravity=\"center\"\n                    android:src=\"@drawable/ic_widget_play\"\n                    android:contentDescription=\"@string/play_pause\" />\n            </FrameLayout>\n\n        </LinearLayout>\n\n        <!-- Bottom Row: Progress Bar -->\n        <FrameLayout\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"6dp\"\n            android:layout_marginTop=\"12dp\">\n\n            <ImageView\n                android:id=\"@+id/widget_progress_track\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:src=\"@drawable/widget_progress_track\"\n                android:scaleType=\"fitXY\" />\n\n            <ImageView\n                android:id=\"@+id/widget_progress_fill\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:src=\"@drawable/widget_progress_clip\"\n                android:scaleType=\"fitXY\" />\n        </FrameLayout>\n\n    </LinearLayout>\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/widget_recognizer_compact.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Music Recognizer Widget – Compact (1×3) -->\n<!-- Left: album art + song title + artist  |  Right: animated mic button (smaller padding) -->\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@android:id/background\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"horizontal\"\n    android:background=\"@drawable/widget_background\"\n    android:paddingStart=\"10dp\"\n    android:paddingEnd=\"10dp\"\n    android:paddingTop=\"8dp\"\n    android:paddingBottom=\"8dp\"\n    android:gravity=\"center_vertical\"\n    android:theme=\"@style/Theme.Widget.Metrolist\">\n\n    <!-- Album art (only visible in SUCCESS state) -->\n    <ImageView\n        android:id=\"@+id/widget_recognizer_album_art\"\n        android:layout_width=\"44dp\"\n        android:layout_height=\"44dp\"\n        android:scaleType=\"centerCrop\"\n        android:visibility=\"gone\"\n        android:contentDescription=\"@string/album_art\" />\n\n    <!-- Text area (left) -->\n    <LinearLayout\n        android:id=\"@+id/widget_recognizer_text_area\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_weight=\"1\"\n        android:orientation=\"vertical\"\n        android:gravity=\"center_vertical\"\n        android:layout_marginStart=\"10dp\"\n        android:layout_marginEnd=\"8dp\">\n\n        <TextView\n            android:id=\"@+id/widget_recognizer_song_title\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:textSize=\"13sp\"\n            android:textStyle=\"bold\"\n            android:textColor=\"@color/widget_text_primary\"\n            android:maxLines=\"1\"\n            android:ellipsize=\"end\"\n            android:text=\"@string/widget_recognizer_tap_to_search\" />\n\n        <TextView\n            android:id=\"@+id/widget_recognizer_artist_name\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:textSize=\"11sp\"\n            android:textColor=\"@color/widget_text_secondary\"\n            android:maxLines=\"1\"\n            android:ellipsize=\"end\"\n            android:layout_marginTop=\"2dp\"\n            android:visibility=\"gone\" />\n\n    </LinearLayout>\n\n    <!-- Mic button area (right) -->\n    <FrameLayout\n        android:layout_width=\"52dp\"\n        android:layout_height=\"52dp\">\n\n        <!-- Pulse ring -->\n        <ImageView\n            android:id=\"@+id/widget_recognizer_pulse\"\n            android:layout_width=\"52dp\"\n            android:layout_height=\"52dp\"\n            android:layout_gravity=\"center\"\n            android:src=\"@drawable/widget_mic_pulse_idle\"\n            android:contentDescription=\"@null\" />\n\n        <!-- Mic circle button -->\n        <FrameLayout\n            android:id=\"@+id/widget_recognizer_mic_container\"\n            android:layout_width=\"40dp\"\n            android:layout_height=\"40dp\"\n            android:layout_gravity=\"center\"\n            android:background=\"@drawable/widget_mic_button_bg\">\n\n            <ImageView\n                android:layout_width=\"20dp\"\n                android:layout_height=\"20dp\"\n                android:layout_gravity=\"center\"\n                android:src=\"@drawable/ic_widget_mic\"\n                android:contentDescription=\"@string/widget_recognizer_mic_desc\" />\n\n        </FrameLayout>\n\n    </FrameLayout>\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/widget_recognizer_tiny.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Music Recognizer Widget – Tiny (1×1) -->\n<!-- Only animated mic circle, no text -->\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/widget_recognizer_tiny_root\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"@drawable/widget_background\"\n    android:theme=\"@style/Theme.Widget.Metrolist\">\n\n    <!-- Pulse ring (behind the button) -->\n    <ImageView\n        android:id=\"@+id/widget_recognizer_tiny_pulse\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_gravity=\"center\"\n        android:src=\"@drawable/widget_mic_pulse_idle\"\n        android:contentDescription=\"@null\" />\n\n    <!-- Mic circle button (centred, 56dp to leave room for pulse) -->\n    <FrameLayout\n        android:id=\"@+id/widget_recognizer_tiny_mic_container\"\n        android:layout_width=\"50dp\"\n        android:layout_height=\"50dp\"\n        android:layout_gravity=\"center\"\n        android:background=\"@drawable/widget_mic_button_bg\">\n\n        <ImageView\n            android:layout_width=\"24dp\"\n            android:layout_height=\"24dp\"\n            android:layout_gravity=\"center\"\n            android:src=\"@drawable/ic_widget_mic\"\n            android:contentDescription=\"@string/widget_recognizer_mic_desc\" />\n\n    </FrameLayout>\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/widget_recognizer_wide.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Music Recognizer Widget – Wide (1×4 default) -->\n<!-- Left: album art + song title + artist  |  Right: animated mic button -->\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@android:id/background\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"horizontal\"\n    android:background=\"@drawable/widget_background\"\n    android:paddingStart=\"12dp\"\n    android:paddingEnd=\"12dp\"\n    android:paddingTop=\"8dp\"\n    android:paddingBottom=\"8dp\"\n    android:gravity=\"center_vertical\"\n    android:theme=\"@style/Theme.Widget.Metrolist\">\n\n    <!-- Album art (only visible in SUCCESS state) -->\n    <ImageView\n        android:id=\"@+id/widget_recognizer_album_art\"\n        android:layout_width=\"48dp\"\n        android:layout_height=\"48dp\"\n        android:scaleType=\"centerCrop\"\n        android:visibility=\"gone\"\n        android:contentDescription=\"@string/album_art\" />\n\n    <!-- Text area (takes remaining space, clickable) -->\n    <LinearLayout\n        android:id=\"@+id/widget_recognizer_text_area\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"wrap_content\"\n        android:layout_weight=\"1\"\n        android:orientation=\"vertical\"\n        android:gravity=\"center_vertical\"\n        android:layout_marginStart=\"12dp\"\n        android:layout_marginEnd=\"12dp\">\n\n        <TextView\n            android:id=\"@+id/widget_recognizer_song_title\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:textSize=\"14sp\"\n            android:textStyle=\"bold\"\n            android:textColor=\"@color/widget_text_primary\"\n            android:maxLines=\"1\"\n            android:ellipsize=\"end\"\n            android:text=\"@string/widget_recognizer_tap_to_search\" />\n\n        <TextView\n            android:id=\"@+id/widget_recognizer_artist_name\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:textSize=\"12sp\"\n            android:textColor=\"@color/widget_text_secondary\"\n            android:maxLines=\"1\"\n            android:ellipsize=\"end\"\n            android:layout_marginTop=\"2dp\"\n            android:visibility=\"gone\" />\n\n    </LinearLayout>\n\n    <!-- Mic button area (right) – pulse ring behind circle -->\n    <FrameLayout\n        android:layout_width=\"56dp\"\n        android:layout_height=\"56dp\">\n\n        <!-- Pulse ring (animated, cycles through drawables) -->\n        <ImageView\n            android:id=\"@+id/widget_recognizer_pulse\"\n            android:layout_width=\"56dp\"\n            android:layout_height=\"56dp\"\n            android:layout_gravity=\"center\"\n            android:src=\"@drawable/widget_mic_pulse_idle\"\n            android:contentDescription=\"@null\" />\n\n        <!-- Mic circle button -->\n        <FrameLayout\n            android:id=\"@+id/widget_recognizer_mic_container\"\n            android:layout_width=\"44dp\"\n            android:layout_height=\"44dp\"\n            android:layout_gravity=\"center\"\n            android:background=\"@drawable/widget_mic_button_bg\">\n\n            <ImageView\n                android:layout_width=\"22dp\"\n                android:layout_height=\"22dp\"\n                android:layout_gravity=\"center\"\n                android:src=\"@drawable/ic_widget_mic\"\n                android:contentDescription=\"@string/widget_recognizer_mic_desc\" />\n\n        </FrameLayout>\n\n    </FrameLayout>\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/widget_turntable.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- Turntable Widget - Circular design with play and navigation buttons -->\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@android:id/background\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:padding=\"4dp\"\n    android:theme=\"@style/Theme.Widget.Metrolist\">\n\n    <!-- Album Art - circular bitmap set programmatically -->\n    <ImageView\n        android:id=\"@+id/widget_turntable_album_art\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_gravity=\"center\"\n        android:scaleType=\"fitCenter\"\n        android:src=\"@drawable/widget_turntable_default_art\"\n        android:contentDescription=\"@string/album_art\" />\n\n    <!-- Navigation Buttons Container - Top Right (Previous & Next) -->\n    <LinearLayout\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"36dp\"\n        android:layout_gravity=\"top|end\"\n        android:layout_marginTop=\"20dp\"\n        android:layout_marginEnd=\"6dp\"\n        android:orientation=\"horizontal\"\n        android:background=\"@drawable/widget_turntable_nav_bg\"\n        android:gravity=\"center_vertical\">\n\n        <!-- Previous Button -->\n        <ImageButton\n            android:id=\"@+id/widget_turntable_prev_button\"\n            android:layout_width=\"36dp\"\n            android:layout_height=\"match_parent\"\n            android:padding=\"8dp\"\n            android:scaleType=\"fitCenter\"\n            android:background=\"@android:color/transparent\"\n            android:src=\"@drawable/ic_widget_skip_previous\"\n            android:contentDescription=\"@string/previous\" />\n\n        <!-- Next Button -->\n        <ImageButton\n            android:id=\"@+id/widget_turntable_next_button\"\n            android:layout_width=\"36dp\"\n            android:layout_height=\"match_parent\"\n            android:padding=\"8dp\"\n            android:scaleType=\"fitCenter\"\n            android:background=\"@android:color/transparent\"\n            android:src=\"@drawable/ic_widget_skip_next\"\n            android:contentDescription=\"@string/next\" />\n\n    </LinearLayout>\n\n    <!-- Play Button - Bottom Left -->\n    <FrameLayout\n        android:id=\"@+id/widget_turntable_play_container\"\n        android:layout_width=\"44dp\"\n        android:layout_height=\"44dp\"\n        android:layout_gravity=\"bottom|start\"\n        android:layout_marginBottom=\"12dp\"\n        android:layout_marginStart=\"12dp\"\n        android:background=\"@drawable/widget_turntable_play_bg\">\n\n        <ImageView\n            android:id=\"@+id/widget_turntable_play_pause\"\n            android:layout_width=\"22dp\"\n            android:layout_height=\"22dp\"\n            android:layout_gravity=\"center\"\n            android:src=\"@drawable/ic_widget_play_secondary\"\n            android:contentDescription=\"@string/play_pause\" />\n    </FrameLayout>\n\n</FrameLayout>\n"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n    <monochrome android:drawable=\"@drawable/ic_launcher_monochrome\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n    <monochrome android:drawable=\"@drawable/ic_launcher_monochrome\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi/ic_launcher_static.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_static_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_static_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi/ic_launcher_static_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_static_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_static_foreground\" />\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v31/ic_launcher.xml",
    "content": "<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background_v31\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground_v31\" />\n    <monochrome android:drawable=\"@drawable/ic_launcher_monochrome\" />\n</adaptive-icon>\n"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v31/ic_launcher_round.xml",
    "content": "<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background_v31\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground_v31\" />\n    <monochrome android:drawable=\"@drawable/ic_launcher_monochrome\" />\n</adaptive-icon>\n"
  },
  {
    "path": "app/src/main/res/resources.properties",
    "content": "unqualifiedResLocale=en\n"
  },
  {
    "path": "app/src/main/res/values/app_name.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"app_name\">Metrolist</string>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_static_background\">#000000</color>\n    \n    <!-- Widget colors for light mode -->\n    <color name=\"widget_text_primary\">#1C1B1F</color>\n    <color name=\"widget_text_secondary\">#49454F</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_background\">#000000</color>\n    <color name=\"teal_200\">#80CBC4</color>\n    <color name=\"teal_700\">#00796B</color>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">Local</string>\n    <string name=\"remote_history\">Remote</string>\n\n    <string name=\"charts\">Charts</string>\n    <string name=\"back_button_desc\">Back</string>\n    <string name=\"album_cover_desc\">Album cover</string>\n    <string name=\"top_music_videos\">Top music videos</string>\n    <string name=\"trending\">Trending</string>\n\n    <string name=\"weeks\">Weeks</string>\n    <string name=\"months\">Months</string>\n    <string name=\"years\">Years</string>\n    <string name=\"continuous\">Continuous</string>\n\n    <string name=\"liked\">Liked</string>\n    <string name=\"offline\">Downloaded</string>\n    <string name=\"my_top\">My top</string>\n    <string name=\"weekly_most_playlist_name\">Weekly Most</string>\n    <string name=\"monthly_most_playlist_name\">Monthly Most</string>\n    <string name=\"cached_playlist\">Cached</string>\n    <string name=\"uploaded_playlist\">Uploaded</string>\n    <string name=\"filter_uploaded\">Uploaded</string>\n    <string name=\"sync_playlist\">Sync playlist</string>\n    <string name=\"sync_disabled\">Sync disabled</string>\n    <string name=\"allows_for_sync_witch_youtube\">Note: This allows for syncing with YouTube Music. This is NOT changeable later.</string>\n    <string name=\"generating_image\">Generating image</string>\n    <string name=\"please_wait\">Please wait</string>\n    <string name=\"cancel\">Cancel</string>\n    <string name=\"enable\">Enable</string>\n    <string name=\"share_lyrics\">Share lyrics</string>\n    <string name=\"share_as_text\">Share as text</string>\n    <string name=\"share_as_image\">Share as image</string>\n    <string name=\"max_selection_limit\">Max selection limit</string>\n    <string name=\"share_selected\">Share selected</string>\n    <string name=\"customize_colors\">Customize colors</string>\n    <string name=\"text_color\">Text color</string>\n    <string name=\"secondary_text_color\">Secondary text color</string>\n    <string name=\"background_color\">Background color</string>\n\n    <string name=\"remove_from_cache\">Remove from cache</string>\n\n    <!-- Artist about section -->\n    <string name=\"about_artist\">About</string>\n    <string name=\"show_more\">Show more</string>\n    <string name=\"show_less\">Show less</string>\n    <string name=\"artist_page_settings\">Artist page</string>\n    <string name=\"show_artist_description\">Show artist description</string>\n    <string name=\"show_artist_subscriber_count\">Show subscriber count</string>\n    <string name=\"show_artist_monthly_listeners\">Show monthly listeners</string>\n\n    <!-- Menu descriptions -->\n    <string name=\"download_playlist_desc\">Download all songs for offline playback</string>\n    <string name=\"remove_download_playlist_desc\">Remove all downloaded songs from this playlist</string>\n    <string name=\"download_in_progress_desc\">Download is in progress</string>\n    <string name=\"share_playlist_desc\">Share this playlist with others</string>\n    <string name=\"delete_playlist_desc\">Remove this playlist permanently</string>\n    <string name=\"sync_playlist_desc\">Sync playlist with YouTube Music</string>\n    <string name=\"copy_link\">Copy link</string>\n    <string name=\"select\">Select all</string>\n    <string name=\"like_all\">Like all</string>\n    <string name=\"dislike_all\">Dislike all</string>\n    <string name=\"sort_by_last_updated\">Date updated</string>\n    <string name=\"link_copied\">Link copied to clipboard</string>\n    <string name=\"starting_radio\">Starting radio</string>\n    <string name=\"now_playing\">Now Playing</string>\n\n    <string name=\"lyrics\">Lyrics</string>\n    <string name=\"close\">Close</string>\n    <string name=\"hide_player_thumbnail\">Hide Player Thumbnail</string>\n    <string name=\"hide_player_thumbnail_desc\">Replace album artwork with app logo in player</string>\n    <string name=\"crop_album_art\">Crop Album Art</string>\n    <string name=\"crop_album_art_desc\">Force a square aspect ratio by cropping video thumbnails</string>\n\n    <string name=\"already_in_playlist\">Already in playlist:</string>\n\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d time</item>\n        <item quantity=\"other\">%d times</item>\n    </plurals>\n\n    <string name=\"seek_forward_dynamic\">+%1$d seconds forwards</string>\n    <string name=\"seek_backward_dynamic\">-%1$d seconds backwards</string>\n    <string name=\"seek_seconds_addup\">Progressive seek</string>\n    <string name=\"seek_seconds_addup_description\">If enabled,  Adds up 5 extra seconds incrementally on each seek skip</string>\n\n    <string name=\"similar_content\">Similar content</string>\n\n    <string name=\"player_background_style\">Player background style</string>\n    <string name=\"player_background_solid\">Solid</string>\n    <string name=\"follow_theme\">Follow theme</string>\n    <string name=\"gradient\">Gradient</string>\n    <string name=\"new_player_design\">New player design</string>\n    <string name=\"new_mini_player_design\">New mini player design</string>\n    <string name=\"player_background_blur\">Blur</string>\n    <string name=\"player_buttons_style\">Player button colors</string>\n    <string name=\"default_style\">Default</string>\n    <string name=\"primary_color_style\">Primary color</string>\n    <string name=\"tertiary_color_style\">Tertiary color</string>\n    <string name=\"display_density\">Display density</string>\n    <string name=\"restart\">Restart</string>\n    <string name=\"restart_required\">Restart required</string>\n    <string name=\"density_restart_message\">The display density change will take effect after restarting the app. Do you want to restart now?</string>\n    <string name=\"wavy\">Wavy</string>\n    <string name=\"enable_swipe_thumbnail\">Enable swipe to change song</string>\n    <string name=\"swipe_song_to_add\">Swipe song to the left to add it to the queue or to the right to play it next</string>\n    <string name=\"swipe_song_to_remove\">Swipe song to remove it from the playlist</string>\n    <string name=\"lyrics_click_change\">Change lyrics on click</string>\n    <string name=\"lyrics_auto_scroll\">Auto scroll lyrics</string>\n    <string name=\"lyrics_glow_effect\">Enable glowing lyrics effect</string>\n    <string name=\"lyrics_glow_effect_desc\">Add glowing animation and bounce effect to active lyrics</string>\n    <string name=\"enable_lrclib_desc\">Community-driven synchronized lyrics database</string>\n    <string name=\"enable_kugou_desc\">Takes lyrics from KuGou, a popular Chinese music platform</string>\n    <string name=\"youtube_music_lyrics_note\">NOTE: Lyrics from YouTube Music will be automatically shown when other lyrics are not available. Lyrics from YTM are usually not synchronized.</string>\n    <string name=\"enable_better_lyrics\">Enable Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">Syllable-synced lyrics for any song, for karaoke</string>\n    <string name=\"enable_simpmusic\">Enable SimpMusic Lyrics</string>\n    <string name=\"enable_simpmusic_desc\">Automatically sourced lyrics from Musixmatch and YouTube Transcript</string>\n    <string name=\"enable_lyricsplus\">Enable LyricsPlus</string>\n    <string name=\"enable_lyricsplus_desc\">Synchronized lyrics from multiple sources</string>\n    <string name=\"lyrics_provider_selection\">Provider selection</string>\n    <string name=\"lyrics_provider_selection_desc\">Choose which lyrics providers are enabled</string>\n    <string name=\"auto_scroll\">Re-sync</string>\n    <string name=\"slim\">Slim</string>\n    <string name=\"slim_navbar\">Slim bottom navigation bar</string>\n    <string name=\"auto_playlists\">Auto playlists</string>\n    <string name=\"show_liked_playlist\">Show \\\"Liked\\\" playlist</string>\n    <string name=\"show_downloaded_playlist\">Show \\\"Downloaded\\\" playlist</string>\n    <string name=\"show_top_playlist\">Show \\\"Top\\\" playlist</string>\n    <string name=\"show_cached_playlist\">Show \\\"Cached\\\" playlist</string>\n    <string name=\"enable_song_cache\">Enable song cache</string>\n    <string name=\"enable_song_cache_desc\">Automatically cache songs for future playback</string>\n    <string name=\"show_uploaded_playlist\">Show \\\"Uploaded\\\" playlist</string>\n    <string name=\"shuffle_playlist_first\">Shuffle playlist/album first</string>\n    <string name=\"shuffle_playlist_first_desc\">When shuffling, play all songs from the original playlist/album first, then similar content</string>\n    <string name=\"prevent_duplicate_tracks_in_queue\">Prevent duplicate tracks in queue</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">When adding a track to queue, remove it from its previous position if already present</string>\n    <string name=\"show_wrapped_card\">Show Wrapped card</string>\n    <string name=\"skip_silence_desc\">Fast forward through silent parts of songs</string>\n    <string name=\"skip_silence_instant\">Instantly skip silence</string>\n    <string name=\"skip_silence_instant_desc\">Jump ahead during silent moments instead of speeding up playback</string>\n\n    <string name=\"advanced_login\">Login with token</string>\n    <string name=\"token_hidden\">Tap to show token</string>\n    <string name=\"token_shown\">Tap again to copy or edit</string>\n    <string name=\"token_adv_login_description\">This is an ADVANCED login method. As an alternative to the web portal, you may directly enter or update your login token here. For example, this can speed up logging in on multiple devices. Please note that any invalid token formats the app fails to parse will not be accepted</string>\n    <string name=\"yt_sync\">Auto sync with account</string>\n    <string name=\"more_content\">More content</string>\n\n    <string name=\"sort_ascending\">Sort ascending</string>\n    <string name=\"sort_descending\">Sort descending</string>\n    \n    <!-- Playlist cover edit -->\n    <string name=\"edit_playlist_cover\">Edit playlist cover</string>\n    <string name=\"edit_playlist_cover_note\">Note: Your account must be linked to a phone number and verified on YouTube Music to change playlist cover.</string>\n    <string name=\"edit_playlist_cover_note_wait\">After selecting an image, please wait a moment for the new cover to appear in your playlist.</string>\n    <string name=\"choose_from_library\">Choose from library</string>\n    <string name=\"remove_custom_image\">Remove custom image</string>\n\n    <!-- General -->\n    <string name=\"general\">General</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"default_lib_chips\">Change default library chip</string>\n    <string name=\"set_quick_picks\">Set quick picks</string>\n    <string name=\"last_song_listened\">Based on last song listened</string>\n    <string name=\"app_language\">App language</string>\n    <string name=\"lyrics_provider_priority\">Lyrics provider priority</string>\n    <string name=\"lyrics_provider_priority_desc\">Drag to reorder providers by preference. Higher position -> higher priority.</string>\n    <string name=\"config_proxy\">Configure proxy</string>\n    <string name=\"proxy_username\">Proxy username</string>\n    <string name=\"proxy_password\">Proxy password</string>\n    <string name=\"enable_authentication\">Enable authentication</string>\n    <string name=\"discord_use_details\">Use details instead of state</string>\n    <string name=\"discord_use_details_description\">Show song title prominently instead of artist names</string>\n\n    <string name=\"enable_similar_content\">Enable similar content</string>\n    <string name=\"similar_content_desc\">Automatically add more similar songs when the end of the queue is reached</string>\n    <string name=\"persistent_shuffle_title\">Persistent shuffle</string>\n    <string name=\"persistent_shuffle_desc\">Keep shuffle enabled when starting new songs or playlists</string>\n    <string name=\"remember_shuffle_and_repeat\">Remember shuffle and repeat</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Remember shuffle and repeat mode when restarting the app</string>\n    <string name=\"percentage_format\">%d%%</string>\n\n    <string name=\"import_online\">Import a \\\"m3u\\\" playlists</string>\n    <string name=\"import_csv\">Import a \"csv\" playlists</string>\n    <string name=\"playlist_add_local_to_synced_note\">Note: Adding local songs to synced/remote playlists is unsupported. Any other combination is valid</string>\n    <string name=\"export_playlist\">Export playlist</string>\n    <string name=\"export_as_csv\">Export as CSV</string>\n    <string name=\"export_as_m3u\">Export as M3U</string>\n    <string name=\"export_success\">Playlist exported successfully</string>\n    <string name=\"export_failed\">Failed to export playlist</string>\n    <string name=\"export_option_share\">Share</string>\n    <string name=\"export_option_save\">Save to Documents</string>\n\n    <string name=\"auto_download_on_like\">Auto download on like</string>\n    <string name=\"auto_download_on_like_desc\">Automatically download songs when you like them</string>\n\n    <string name=\"swipe_sensitivity\">Mini player swipe sensitivity</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n\n    <string name=\"clear_song_cache_dialog\">  Are you sure you want to clear all cached songs?</string>\n    <string name=\"clear_image_cache_dialog\">  Are you sure you want to clear all cached image?</string>\n    <string name=\"clear_downloads_dialog\">  Are you sure you want to clear all downloads?</string>\n\n    <string name=\"disable\">Disable</string>\n\n    <string name=\"not_logged_in_youtube\">Not logged in to YouTube</string>\n\n    <string name=\"default_links\">Open supported links</string>\n    <string name=\"open_app_settings_error\">Couldn\\'t open app settings</string>\n\n    <string name=\"release_notes\">Release notes</string>\n    <string name=\"changelog\">Changelog</string>\n    <string name=\"changelog_empty\">No changelogs available</string>\n    <string name=\"github_releases_url\">https://github.com/MetrolistGroup/Metrolist/releases</string>\n    <string name=\"list_bullet\">•</string>\n    <string name=\"view_on_github\">View on GitHub</string>\n    <string name=\"current_version\">Current version</string>\n    <string name=\"version_format\">Version: %s</string>\n    <string name=\"update_settings\">Update settings</string>\n    <string name=\"check_for_updates_title\">Check for updates</string>\n    <string name=\"checking_for_updates\">Checking for updates…</string>\n    <string name=\"latest_version_format\">Latest: %s</string>\n    <string name=\"check_for_updates_button\">Check for updates</string>\n    <string name=\"hide_changelog\">Hide changelog</string>\n    <string name=\"view_changelog\">View changelog</string>\n    <string name=\"failed_to_check_updates\">Failed to check for updates: %s</string>\n\n    <string name=\"all_time\">All time</string>\n    <string name=\"past_24_hours\">Past 24 hours</string>\n    <string name=\"past_week\">Past week</string>\n    <string name=\"past_month\">Past month</string>\n    <string name=\"past_year\">Past year</string>\n    <string name=\"top_length\">My Top list length</string>\n    <string name=\"history_duration\">History duration</string>\n    <string name=\"information\">Information</string>\n    <string name=\"description\">Description</string>\n    <string name=\"views\">Views</string>\n    <string name=\"likes\">Likes</string>\n    <string name=\"dislikes\">Dislikes</string>\n    <string name=\"subscribe\">Subscribe</string>\n    <string name=\"subscribed\">Subscribed</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">1 second</item>\n        <item quantity=\"other\">%d seconds</item>\n    </plurals>\n\n    <!-- Sleep timer -->\n    <string name=\"set_as_default\">Set as default</string>\n    <string name=\"sleep_timer_default_set\">Sleep timer default set to %d min</string>\n    <string name=\"enable_automatic_sleeptimer\">Enable automatic sleep timer</string>\n    <string name=\"sleeptimer_description\">Enables the sleep timer automatically with the default value by a custom time</string>\n    <string name=\"sleep_timer_repeat_description\">Set a custom day and time when the sleep timer should automatically activate</string>\n    <string name=\"sleep_timer_repeat\">Repeat</string>\n    <string name=\"sleep_timer_daily\">Daily</string>\n    <string name=\"sleep_timer_weekdays\">Monday to Friday</string>\n    <string name=\"sleep_timer_weekdays_weekends\">Weekdays / Weekends</string>\n    <string name=\"sleep_timer_weekends\">Weekends (Sat–Sun)</string>\n    <string name=\"sleep_timer_custom\">Custom</string>\n    <string name=\"sleep_timer_start_time\">Start time</string>\n    <string name=\"sleep_timer_end_time\">End time</string>\n    <string name=\"sleep_timer_monday\">Monday</string>\n    <string name=\"sleep_timer_tuesday\">Tuesday</string>\n    <string name=\"sleep_timer_wednesday\">Wednesday</string>\n    <string name=\"sleep_timer_thursday\">Thursday</string>\n    <string name=\"sleep_timer_friday\">Friday</string>\n    <string name=\"sleep_timer_saturday\">Saturday</string>\n    <string name=\"sleep_timer_sunday\">Sunday</string>\n    <string name=\"sleep_timer_stop_after_current_song_title\">Stop at end of current song</string>\n    <string name=\"sleep_timer_stop_after_current_song_description\">Stops the sleep timer at the end of the current playing song</string>\n    <string name=\"sleep_timer_fade_out_title\">Fade out</string>\n    <string name=\"sleep_timer_fade_out_description\">Fades out the volume in the final minute</string>\n\n    <!-- Alarm -->\n    <string name=\"alarm\">Alarm</string>\n    <string name=\"alarm_enabled\">Enable alarm</string>\n    <string name=\"alarm_time\">Alarm time</string>\n    <string name=\"alarm_playlist\">Playlist</string>\n    <string name=\"alarm_select_playlist\">Select playlist</string>\n    <string name=\"alarm_no_playlists\">No playlists found</string>\n    <plurals name=\"alarm_playlist_song_count\">\n        <item quantity=\"one\">%d song</item>\n        <item quantity=\"other\">%d songs</item>\n    </plurals>\n    <string name=\"alarm_selected\">Selected</string>\n    <string name=\"alarm_playlist_helper\">Choose which playlist the alarm should play.</string>\n    <string name=\"alarm_add\">Add alarm</string>\n    <string name=\"alarm_empty\">No alarms yet. Add one to start scheduled playback.</string>\n    <string name=\"alarm_disabled\">Disabled</string>\n    <string name=\"alarm_random_enabled\">Random</string>\n    <string name=\"alarm_random_disabled\">In order</string>\n    <string name=\"alarm_next_prefix\">Next: %s</string>\n    <string name=\"alarm_new\">New alarm</string>\n    <string name=\"alarm_edit\">Edit alarm</string>\n    <string name=\"alarm_save\">Save</string>\n    <string name=\"alarm_time_picker_value\">Time: %s</string>\n    <string name=\"alarm_delete\">Delete alarm</string>\n    <string name=\"alarm_duplicate_time_warning\">Another alarm already exists at this time. If both trigger together, only one may effectively play.</string>\n    <string name=\"alarm_random_song\">Play random song from playlist</string>\n    <string name=\"alarm_next_trigger\">Next trigger</string>\n    <string name=\"alarm_not_scheduled\">Not scheduled</string>\n    <string name=\"alarm_exact_permission_title\">Allow exact alarms</string>\n    <string name=\"alarm_exact_permission_desc\">Exact alarm permission improves reliability when the app is in the background.</string>\n    <string name=\"alarm_battery_optimization_title\">Battery optimization</string>\n    <string name=\"alarm_battery_optimization_desc\">Disable battery optimization for better alarm reliability in the background.</string>\n\n    <!-- Player Settings -->\n    <string name=\"disable_load_more_when_repeat_all\">Disable load more when repeat all</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Don\\'t auto load more songs and similar content when repeat all mode is enabled</string>\n    <string name=\"pause_music_when_media_is_muted\">Pause music when media is muted</string>\n    <string name=\"resume_on_bluetooth_connect\">Resume on Bluetooth connect</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">Keep screen on when player is expanded</string>\n\n    <string name=\"crossfade\">Crossfade</string>\n    <string name=\"crossfade_desc\">Crossfade between songs</string>\n    <string name=\"crossfade_duration\">Crossfade duration</string>\n    <string name=\"crossfade_gapless\">Disable for gapless albums</string>\n    <string name=\"crossfade_gapless_desc\">Don\\'t crossfade if the album is gapless</string>\n    <string name=\"crossfade_beta_title\">Beta Feature</string>\n    <string name=\"crossfade_beta_message\">Crossfade is a new feature and may have bugs. If you experience any issues, please report them.\\n\\nThis feature disables audio offload due to technical limitations.</string>\n\n    <!-- Romanization Settings -->\n    <string name=\"lyrics_romanization_cyrillic\">Cyrillic</string>\n    <string name=\"lyrics_romanize_title\">Romanization</string>\n    <string name=\"lyrics_romanization\">Lyrics romanization</string>\n    <string name=\"lyrics_romanize_japanese\">Romanize Japanese lyrics</string>\n    <string name=\"lyrics_romanize_korean\">Romanize Korean lyrics</string>\n    <string name=\"lyrics_romanize_chinese\">Romanize Chinese lyrics</string>\n    <string name=\"lyrics_romanize_hindi\">Romanize Hindi lyrics</string>\n    <string name=\"lyrics_romanize_punjabi\">Romanize Punjabi lyrics</string>\n    <string name=\"lyrics_romanize_russian\">Romanize Russian lyrics</string>\n    <string name=\"lyrics_romanize_ukrainian\">Romanize Ukrainian lyrics</string>\n    <string name=\"lyrics_romanize_belarusian\">Romanize Belarusian lyrics</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Romanize Kyrgyz lyrics</string>\n    <string name=\"lyrics_romanize_serbian\">Romanize Serbian lyrics</string>\n    <string name=\"lyrics_romanize_bulgarian\">Romanize Bulgarian lyrics</string>\n    <string name=\"line_by_line_option_title\">Detect language line by line</string>\n    <string name=\"line_by_line_option_desc\">The Cyrillic language will be detected line by line instead of the entire song.</string>\n    <string name=\"line_by_line_dialog_title\">Are you sure?</string>\n    <string name=\"line_by_line_dialog_desc\">This is a hit or miss experimental feature. \\n\\nBy default, language is determined from the whole song, but with this option on, it will be determined line by line instead. This will allow multi-language songs to work BUT the language might not always be correct (for example if there is a Ukrainian lyric that doesn\\'t contain any Ukrainian-specific letters, it might be romanized as Russian instead). \\n\\nIf you do not have issues, it is recommended to keep this option off.</string>\n    <string name=\"romanize_current_track\">Romanize current track</string>\n\n    <!-- Lyrics Offset Settings -->\n    <string name=\"lyrics_offset\">Lyrics Offset</string>\n\n    <!-- Material 3 Settings Sections -->\n    <string name=\"settings_section_ui\">Interface</string>\n    <string name=\"settings_section_privacy\">Privacy &amp; Security</string>\n    <string name=\"settings_section_player_content\">Player &amp; Content</string>\n    <string name=\"settings_section_storage\">Storage &amp; Data</string>\n    <string name=\"settings_section_system\">System &amp; About</string>\n\n    <!-- Updater -->\n    <string name=\"updater\">Updater</string>\n    <string name=\"check_for_updates\">Automatically check for updates</string>\n    <string name=\"update_notifications\">Enable update notifications</string>\n    <string name=\"update_available_title\">Update available</string>\n    <string name=\"update_channel_name\">App updates</string>\n    <string name=\"update_channel_desc\">Notifications about new versions</string>\n\n    <!-- Offload -->\n    <string name=\"audio_offload\">Enable offload</string>\n    <string name=\"audio_offload_description\">Use the offload audio path for audio playback. Disabling this may increase power usage but can be useful if you experience issues with audio playback or post processing</string>\n    <string name=\"audio_offload_disabled_by_crossfade\">Disabled because Crossfade is active</string>\n\n    <!-- Google Cast -->\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"google_cast_description\">Enable casting audio to Chromecast and other Cast-enabled devices</string>\n\n    <string name=\"lyrics_romanize_macedonian\">Romanize Macedonian lyrics</string>\n    <string name=\"lyrics_romanize_as_main\">Show romanized lyrics as main</string>\n\n    <string name=\"integrations\">Integrations</string>\n    <string name=\"username\">Username</string>\n    <string name=\"password\">Password</string>\n\n    <!-- Last.fm -->\n    <string name=\"lastfm_integration\">Last.fm Integration</string>\n    <string name=\"enable_scrobbling\">Enable scrobbling</string>\n    <string name=\"lastfm_now_playing\">Send Now Playing</string>\n    <string name=\"last_fm_send_likes\">Send Likes/Unlikes</string>\n    <string name=\"last_fm_send_likes_description\">Love/Unlove songs in Last.fm when they are Liked/Unliked in Metrolist</string>\n    <string name=\"logging_in\">Logging in…</string>\n\n    <string name=\"scrobbling_configuration\">Scrobbling Configuration</string>\n    <string name=\"scrobble_min_track_duration\">Scrobble songs longer than</string>\n    <string name=\"scrobble_delay_percent\">Scrobble delay percent</string>\n    <string name=\"scrobble_delay_minutes\">Scrobble delay minutes</string>\n    <string name=\"hide_video_songs\">Hide video songs</string>\n    <string name=\"hide_youtube_shorts\">Hide YouTube Shorts</string>\n\n    <string name=\"details_desc\">View the song\\'s information</string>\n    <string name=\"edit_desc\">Change the title or artist</string>\n    <string name=\"start_radio_desc\">Create a station based on this item</string>\n    <string name=\"play_next_desc\">Add to the top of your queue</string>\n    <string name=\"add_to_queue_desc\">Add to the bottom of your queue</string>\n    <string name=\"add_to_library_desc\">Save to your library</string>\n    <string name=\"download_desc\">Make available for offline playback</string>\n    <string name=\"add_to_playlist_desc\">Add to one of your playlists</string>\n    <string name=\"refetch_desc\">Fetch the latest metadata from YouTube Music</string>\n    <string name=\"share_desc\">Share a link to this item</string>\n    <string name=\"delete_desc\">Permanently remove this item</string>\n    <string name=\"advanced_desc\">Change the song\\'s tempo and pitch</string>\n    <string name=\"equalizer_desc\">Adjust the audio equalizer</string>\n    <string name=\"enable_dynamic_icon\">Enable dynamic icon</string>\n    <string name=\"mini_player\">Mini-player</string>\n    <string name=\"pure_black_mini_player\">Pure black mini-player</string>\n    <string name=\"cache_size_warning_title\">Hold on!</string>\n    <string name=\"cache_size_warning_message\">You\\'ve chosen a cache size limit smaller than what the app is currently using (%1$s). If you continue, the app may remove some cached %2$s to match the new limit. Proceed anyway?</string>\n    <string name=\"cache_size_warning_confirm\">Continue</string>\n\n    <!-- Lyrics Animation Styles -->\n    <string name=\"lyrics_animation_style\">Word-by-word animation style</string>\n    <string name=\"none\">None</string>\n    <string name=\"fade\">Fade</string>\n    <string name=\"glow\">Glow</string>\n    <string name=\"slide\">Slide</string>\n    <string name=\"karaoke\">Karaoke</string>\n    <string name=\"apple_music_style\">Apple Music</string>\n    <string name=\"lyrics_text_size\">Lyrics text size</string>\n    <string name=\"lyrics_line_spacing\">Lyrics line spacing</string>\n    <string name=\"album_art_for\">Album art for %s</string>\n    <string name=\"wrapped_total_albums_title\">You\\'ve listened to</string>\n    <string name=\"wrapped_total_albums_subtitle\">unique albums</string>\n    <string name=\"wrapped_top_album_title\">Your top album is</string>\n    <string name=\"wrapped_playlist_ready\">Your personal playlist is ready</string>\n    <string name=\"wrapped_top_5_albums_title\">Your top 5 albums</string>\n    <string name=\"wrapped_album_listening_time\">You\\'ve listened to this album for %d minutes</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d minutes</string>\n    <string name=\"wrapped_no_data\">No data</string>\n    <string name=\"wrapped_top_5_artists_title\">Your top artists of the year</string>\n    <string name=\"wrapped_artist_listening_time\">%d minutes</string>\n    <string name=\"wrapped_top_5_songs_title\">Your top songs of the year</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">Album art</string>\n    <string name=\"wrapped_top_artist_title\">Your top artist of the year is</string>\n    <string name=\"wrapped_top_artist_image_content_description\">Top artist image</string>\n    <string name=\"wrapped_top_artist_listening_time\">You\\'ve listened to them for %d minutes</string>\n    <string name=\"wrapped_top_song_title\">Your most played song is</string>\n    <string name=\"wrapped_top_song_listening_time\">You\\'ve listened for %d minutes</string>\n    <string name=\"wrapped_total_artists_title\">You listened to</string>\n    <string name=\"wrapped_total_artists_subtitle\">unique artists</string>\n    <string name=\"wrapped_total_songs_title\">You listened to</string>\n    <string name=\"wrapped_total_songs_subtitle\">unique songs</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_intro_subtitle\">it\\'s time to see what you\\'ve been listening to</string>\n    <string name=\"wrapped_intro_button\">let\\'s go!</string>\n    <string name=\"wrapped_logo_content_description\">Metrolist Logo</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"wrapped_ready_title\">YOUR WRAPPED IS READY!</string>\n    <string name=\"wrapped_ready_subtitle\">Time to see what you loved this year.</string>\n    <string name=\"wrapped_thank_you\">Thank you for listening</string>\n    <string name=\"wrapped_special_thanks\">Special thanks to MO Agamy for creating Metrolist</string>\n    <string name=\"wrapped_close\">Close wrapped</string>\n    <string name=\"wrapped_playlist_title\">Your %s Wrapped</string>\n    <string name=\"wrapped_create_playlist\">Create playlist</string>\n    <string name=\"wrapped_playlist_saved\">Playlist saved</string>\n\n    <!-- Equalizer UI -->\n    <plurals name=\"profiles_count\">\n        <item quantity=\"one\">%d Profile</item>\n        <item quantity=\"other\">%d Profiles</item>\n    </plurals>\n    <string name=\"equalizer_header\">Equalizer</string>\n    <string name=\"no_profiles\">No equalizer profiles</string>\n    <string name=\"import_profile\">Import Profile</string>\n    <string name=\"system_equalizer\">System Equalizer</string>\n    <string name=\"eq_disabled\">Disabled</string>\n    <plurals name=\"band_count\">\n        <item quantity=\"one\">%d band</item>\n        <item quantity=\"other\">%d bands</item>\n    </plurals>\n    <string name=\"delete_profile_desc\">Delete Profile</string>\n    <string name=\"delete_profile_confirmation\">Are you sure you want to delete \"%1$s\"? This action\n        cannot be undone.\n    </string>\n    <string name=\"error_file_read\">Could not read file</string>\n    <string name=\"error_file_open\">Failed to open file: %1$s</string>\n    <string name=\"import_error_title\">Import Error</string>\n    <string name=\"error_title\">Error</string>\n    <string name=\"error_eq_apply_failed\">Failed to apply EQ profile: %1$s</string>\n\n    <!-- Hardcoded strings -->\n    <string name=\"found_in_settings_content\">Found in Settings &gt; Content</string>\n    <string name=\"plays\">plays</string>\n    <string name=\"casting_to\">Casting to %s</string>\n    <string name=\"progress_percent\">Progress %s%%</string>\n    <string name=\"listening_to_metrolist\">Listening to Metrolist</string>\n    <string name=\"open\">Open</string>\n    <string name=\"failed_to_create_image\">Failed to create image: %s</string>\n    <string name=\"copied_title\">Copied Title</string>\n    <string name=\"copied_artist\">Copied Artist</string>\n    <string name=\"error_playing\">Error playing</string>\n    <string name=\"failed_to_parse_proxy\">Failed to parse proxy url.</string>\n\n    <!-- Playback errors -->\n    <string name=\"error_playback_failed\">Playback failed</string>\n    <string name=\"error_episode_save\">Failed to save episode</string>\n    <string name=\"error_episode_remove\">Failed to remove episode</string>\n    <string name=\"error_podcast_subscribe\">Failed to subscribe to podcast</string>\n    <string name=\"error_podcast_unsubscribe\">Failed to unsubscribe from podcast</string>\n    <string name=\"view_channel\">View Channel</string>\n\n    <!-- Widget strings -->\n    <string name=\"album_art\">Album art</string>\n    <string name=\"no_song_playing\">No song playing</string>\n    <string name=\"tap_to_open\">Tap to open Metrolist</string>\n    <string name=\"previous\">Previous</string>\n    <string name=\"play_pause\">Play/Pause</string>\n    <string name=\"next\">Next</string>\n    <string name=\"like\">Like</string>\n    <string name=\"not_playing\">No song playing</string>\n    <string name=\"tap_to_play\">Tap to open Metrolist</string>\n    <string name=\"widget_description\">Music player widget with playback controls</string>\n    <string name=\"turntable_widget_description\">Circular music widget with play and like controls</string>\n    <string name=\"widget_music_player\">Music Player</string>\n    <string name=\"widget_turntable\">Turntable</string>\n\n    <!-- Music Recognizer Widget strings -->\n    <string name=\"widget_recognizer_name\">Music Recognizer</string>\n    <string name=\"widget_recognizer_description\">Identify songs playing around you directly from your home screen</string>\n    <string name=\"widget_recognizer_tap_to_search\">Tap to identify song</string>\n    <string name=\"widget_recognizer_listening\">Listening…</string>\n    <string name=\"widget_recognizer_processing\">Identifying…</string>\n    <string name=\"widget_recognizer_no_match\">No match found. Try again</string>\n    <string name=\"widget_recognizer_error\">Recognition failed</string>\n    <string name=\"widget_recognizer_error_generic\">An error occurred. Please try again</string>\n    <string name=\"widget_recognizer_unknown_song\">Unknown song</string>\n    <string name=\"widget_recognizer_unknown_artist\">Unknown artist</string>\n    <string name=\"widget_recognizer_mic_desc\">Identify song</string>\n    <string name=\"widget_recognizer_channel_name\">Music Recognition</string>\n    <string name=\"widget_recognizer_channel_desc\">Shows a notification while identifying a song from the widget</string>\n    <string name=\"widget_recognizer_notification_text\">Recording audio to identify the song…</string>\n\n    <!-- Player Client Settings -->\n\n    <!-- Decryption Library Settings -->\n\n    <!-- Listen Together -->\n    <string name=\"together\">Together</string>\n    <string name=\"listen_together\">Listen Together</string>\n    <string name=\"listen_together_server_url\">Server URL</string>\n    <string name=\"listen_together_choose_server\">Choose server</string>\n    <string name=\"listen_together_custom_server\">Custom server</string>\n    <string name=\"listen_together_use_custom_server\">Use custom server</string>\n    <string name=\"listen_together_username\">Username</string>\n    <string name=\"listen_together_connected\">Connected</string>\n    <string name=\"listen_together_reconnecting\">Reconnecting…</string>\n    <string name=\"listen_together_disconnected\">Disconnected</string>\n    <string name=\"listen_together_connecting\">Connecting…</string>\n    <string name=\"listen_together_error\">Connection error</string>\n    <string name=\"listen_together_create_room\">Create room</string>\n    <string name=\"listen_together_create_room_desc\">Create a room and share the code with friends</string>\n    <string name=\"listen_together_join_room\">Join room</string>\n    <string name=\"listen_together_room_code\">Room code</string>\n    <string name=\"listen_together_you_are_host\">You are the host</string>\n    <string name=\"listen_together_you_are_guest\">You are a guest</string>\n    <string name=\"mute\">Mute</string>\n    <string name=\"unmute\">Unmute</string>\n    <string name=\"listen_together_join_requests\">Join requests</string>\n    <string name=\"listen_together_view_logs\">View logs</string>\n    <string name=\"listen_together_view_logs_desc\">Debug connection and messages</string>\n    <string name=\"listen_together_logs\">Connection logs</string>\n    <string name=\"listen_together_no_logs\">No logs yet</string>\n    <string name=\"listen_together_auto_approval_joins\">Auto-approve join requests</string>\n    <string name=\"listen_together_auto_approval_joins_desc\">Automatically approve join requests instead of reviewing them manually</string>\n    <string name=\"listen_together_auto_approval_suggestions\">Auto-approve song suggestions</string>\n    <string name=\"listen_together_auto_approval_suggestions_desc\">Automatically approve and queue song suggestions from guests</string>\n    <string name=\"listen_together_sync_volume\">Sync host volume</string>\n    <string name=\"listen_together_sync_volume_desc\">Guests follow the host volume level</string>\n    <string name=\"listen_together_in_top_bar\">Listen Together in top bar</string>\n    <string name=\"listen_together_in_top_bar_desc\">Show Listen Together in top app bar instead of navigation bar</string>\n    <string name=\"listen_together_description\">Listen to music with your friends in real-time. Create a room to be the host or join an existing room with a code.</string>\n    <string name=\"listen_together_background_disconnect_note\">Note: You may get disconnected if you create a room while no music is playing and then switch to another app.</string>\n    <string name=\"listen_together_not_configured\">Listen Together is not configured. Please set up the server URL in Settings → Integrations → Listen Together.</string>\n    <string name=\"listen_together_suggestion_received\">%1$s requested %2$s</string>\n    <string name=\"listen_together_suggestion_sent\">Suggestion sent to host!</string>\n    <string name=\"listen_together_join_request_notification\">%1$s wants to join the room</string>\n    <string name=\"listen_together_notification_channel_name\">Listen Together</string>\n    <string name=\"listen_together_notification_channel_desc\">Notifications for Listen Together events</string>\n    <string name=\"listen_together_room_created\">Room created: %s</string>\n    <string name=\"listen_together_cannot_edit_username_in_room\">Cannot edit username while in a room</string>\n    <string name=\"waiting_for_approval\">Waiting for approval from host</string>\n    <string name=\"invalid_room_code\">Invalid room code</string>\n    <string name=\"join_request_denied\">Join request denied</string>\n    <string name=\"join_existing_room\">Join existing room</string>\n    <string name=\"room_code\">Room code</string>\n    <string name=\"leave_room\">Leave room</string>\n    <string name=\"join_room\">Join</string>\n    <string name=\"create_room\">Create</string>\n    <string name=\"joining_room\">Joining room %s…</string>\n    <string name=\"creating_room\">Creating room…</string>\n    <string name=\"connect\">Connect</string>\n    <string name=\"disconnect\">Disconnect</string>\n    <string name=\"create\">Create</string>\n    <string name=\"join\">Join</string>\n    <string name=\"approve\">Approve</string>\n    <string name=\"reject\">Reject</string>\n    <string name=\"clear\">Clear</string>\n    <string name=\"copy\">Copy</string>\n    <string name=\"copy_all_lyrics\">Copy all lyrics</string>\n    <string name=\"change\">Change</string>\n    <string name=\"copied_to_clipboard\">Copied to clipboard</string>\n    <string name=\"not_set\">Not set</string>\n    <string name=\"hosting_room\">Hosting room</string>\n    <string name=\"in_room\">In room</string>\n    <string name=\"pending_requests\">Pending requests</string>\n    <string name=\"pending_suggestions\">Pending suggestions</string>\n    <string name=\"suggest_to_host\">Suggest to host</string>\n    <string name=\"kick_user\">Kick</string>\n    <string name=\"host_label\">Host</string>\n    <string name=\"you_label\">You</string>\n    <string name=\"connected_users\">Connected users</string>\n    <string name=\"enter_username\">Enter username</string>\n    <string name=\"enter_room_code\">Enter room code</string>\n    <string name=\"listen_together_settings_desc\">Configure server, username, and more</string>\n    <string name=\"error_username_empty\">Username is required.</string>\n    <string name=\"resync\">Resync</string>\n    <string name=\"copy_code\">Copy code</string>\n    <string name=\"kick_user_desc\">Remove this person from the session</string>\n    <string name=\"permanently_kick_user\">Permanently Block</string>\n    <string name=\"permanently_kick_user_desc\">Block this person\\'s join requests and hide their suggestions</string>\n    <string name=\"transfer_ownership\">Transfer Ownership</string>\n    <string name=\"transfer_ownership_desc\">Make this person the host of the room</string>\n    <string name=\"manage_user\">Manage User</string>\n    <string name=\"listen_together_blocked_users\">Blocked Users</string>\n    <string name=\"listen_together_blocked_users_count\">%d user(s) blocked</string>\n    <string name=\"listen_together_no_blocked_users\">No blocked users</string>\n    <string name=\"unblock\">Unblock</string>\n    <string name=\"user_blocked_by_host\">User blocked by host</string>\n\n    <!-- AI Lyrics Translation -->\n    <string name=\"ai_lyrics_translation\">AI Lyrics Translation</string>\n    <string name=\"ai_translating_lyrics\">Translating lyrics...</string>\n    <string name=\"ai_lyrics_translated\">Lyrics translated</string>\n    <string name=\"ai_provider\">Provider</string>\n    <string name=\"ai_base_url\">Base URL</string>\n    <string name=\"ai_api_key\">API Key</string>\n    <string name=\"ai_model\">Model</string>\n    <string name=\"ai_translation_mode\">Translation Mode</string>\n    <string name=\"ai_target_language\">Target Language</string>\n    <string name=\"ai_setup_guide\">API Credentials</string>\n    <string name=\"ai_translation_literal\">Translation</string>\n    <string name=\"ai_translation_literal_desc\">Translate meaning to target language</string>\n    <string name=\"ai_translation_transcribed\">Transcription</string>\n    <string name=\"ai_translation_transcribed_desc\">Convert pronunciation to target script</string>\n    <string name=\"ai_api_key_required\">API Key Required</string>\n    <string name=\"ai_error_api_key_required\">API key is required</string>\n    <string name=\"ai_error_no_lyrics\">No lyrics to translate</string>\n    <string name=\"ai_error_lyrics_empty\">Lyrics are empty</string>\n    <string name=\"ai_error_language_required\">Target language is required</string>\n    <string name=\"ai_error_unexpected\">Unexpected translation result</string>\n    <string name=\"ai_error_unknown\">Unknown error occurred</string>\n    <string name=\"ai_error_translation_failed\">Translation failed</string>\n    <string name=\"ai_provider_help\">Get API Keys</string>\n    <string name=\"ai_provider_openrouter_help\">Visit https://openrouter.ai for free and paid models</string>\n    <string name=\"ai_provider_openai_help\">Visit https://platform.openai.com/api-keys</string>\n    <string name=\"ai_provider_claude_help\">Visit https://console.anthropic.com/settings/keys</string>\n    <string name=\"ai_provider_gemini_help\">Visit https://aistudio.google.com/apikey</string>\n    <string name=\"ai_provider_perplexity_help\">Visit https://perplexity.ai/settings/api</string>\n    <string name=\"ai_provider_xai_help\">Visit https://console.x.ai</string>\n    <string name=\"ai_provider_deepl_help\">Visit https://deepl.com/pro-api for free and paid keys</string>\n    <string name=\"ai_provider_mistral_help\">Visit https://console.mistral.ai/api-keys</string>\n    <string name=\"ai_deepl_formality\">Formality</string>\n    <string name=\"ai_deepl_formality_default\">Default</string>\n    <string name=\"ai_deepl_formality_more\">More Formal</string>\n    <string name=\"ai_deepl_formality_less\">Less Formal</string>\n    <string name=\"ai_system_prompt\">System Prompt</string>\n    <string name=\"ai_system_prompt_desc\">Custom instructions sent to the AI model. Leave empty to use the default prompt.</string>\n    <string name=\"ai_system_prompt_placeholder\">Use {lineCount} as a placeholder for the number of lines to translate.</string>\n    <string name=\"ai_system_prompt_default\">Default</string>\n    <string name=\"ai_system_prompt_reset\">Reset to Default</string>\n    \n    <!-- Crash Handler -->\n    <string name=\"crash_title\">App Crashed</string>\n    <string name=\"crash_description\">An unexpected error occurred. Please share the crash report to help us fix the issue.</string>\n    <string name=\"crash_share_logs\">Share Logs</string>\n    <string name=\"crash_share_title\">Share crash report</string>\n    <string name=\"crash_report_subject\">Metrolist Crash Report</string>\n    <string name=\"crash_close\">Close</string>\n    <string name=\"crash_no_log\">No crash log available</string>\n\n    <!-- Theme Palette Names -->\n    <string name=\"palette_dynamic\">Dynamic</string>\n    <string name=\"palette_crimson\">Crimson</string>\n    <string name=\"palette_rose\">Rose</string>\n    <string name=\"palette_purple\">Purple</string>\n    <string name=\"palette_deep_purple\">Deep Purple</string>\n    <string name=\"palette_indigo\">Indigo</string>\n    <string name=\"palette_blue\">Blue</string>\n    <string name=\"palette_sky_blue\">Sky Blue</string>\n    <string name=\"palette_cyan\">Cyan</string>\n    <string name=\"palette_teal\">Teal</string>\n    <string name=\"palette_green\">Green</string>\n    <string name=\"palette_light_green\">Light Green</string>\n    <string name=\"palette_lime\">Lime</string>\n    <string name=\"palette_yellow\">Yellow</string>\n    <string name=\"palette_amber\">Amber</string>\n    <string name=\"palette_orange\">Orange</string>\n    <string name=\"palette_deep_orange\">Deep Orange</string>\n    <string name=\"palette_brown\">Brown</string>\n    <string name=\"palette_grey\">Grey</string>\n    <string name=\"palette_blue_grey\">Blue Grey</string>\n\n    <!-- Theme Content Descriptions -->\n    <string name=\"cd_back\">Back</string>\n    <string name=\"cd_pure_black_mode\">Pure Black mode</string>\n    <string name=\"cd_light_mode\">Light mode</string>\n    <string name=\"cd_dark_mode\">Dark mode</string>\n    <string name=\"cd_system_mode\">System mode</string>\n    <string name=\"cd_palette_item\">%1$s palette</string>\n    <string name=\"play_all\">Play all</string>\n    <string name=\"enable_high_refresh_rate\">Enable high refresh rate</string>\n    <string name=\"enable_high_refresh_rate_desc\">Force the display to run at the highest supported refresh rate (e.g. 120Hz)</string>\n\n    <!-- Music Recognition -->\n    <string name=\"recognize_music\">Recognize Music</string>\n    <string name=\"tap_to_recognize\">Tap to recognize</string>\n    <string name=\"listening\">Listening…</string>\n    <string name=\"processing\">Processing…</string>\n    <string name=\"no_match_found\">No match found</string>\n    <string name=\"recognition_error\">Recognition error</string>\n    <string name=\"try_again\">Try again</string>\n    <string name=\"recognition_history\">Recognition History</string>\n    <string name=\"clear_recognition_history\">Clear recognition history</string>\n    <string name=\"clear_recognition_history_confirm\">Are you sure you want to clear all recognition history?</string>\n    <string name=\"delete_from_history\">Delete from history</string>\n    <string name=\"re_listen\">Re-listen</string>\n    <string name=\"play_on_app\">Play on Metrolist</string>\n    <string name=\"qs_tile_music_recognizer\">Recognize Music</string>\n    <string name=\"recognition_notification_channel_name\">Music Recognition</string>\n    <string name=\"recognition_notification_channel_desc\">Shows a notification while identifying a song from Quick Settings</string>\n    <string name=\"recognition_notification_listening\">Listening for music...</string>\n    <string name=\"recognition_notification_processing\">Identifying...</string>\n    <string name=\"recognition_notification_no_match\">No match found</string>\n    <string name=\"recognition_notification_failed\">Recognition failed</string>\n    <string name=\"listen_on_metrolist\">Listen on Metrolist</string>\n\n    <!-- CSV Import Strings -->\n    <string name=\"map_csv_columns\">Map CSV Columns</string>\n    <string name=\"first_row_is_header\">First row is header</string>\n    <string name=\"artist_name_column\">Artist Name Column</string>\n    <string name=\"song_title_column\">Song Title Column</string>\n    <string name=\"youtube_url_column\">YouTube URL Column (Optional)</string>\n    <string name=\"continue_action\">Continue</string>\n    <string name=\"importing_csv\">Importing CSV</string>\n    <string name=\"importing_playlist\">Importing Playlist</string>\n\n    <string name=\"recently_converted\">Recently Converted</string>\n    <string name=\"column_label\">Col %d</string>\n\n    <!-- Discord Integration Redesign -->\n    <string name=\"discord_status\">Status</string>\n    <string name=\"discord_status_online\">Online</string>\n    <string name=\"discord_status_idle\">Idle</string>\n    <string name=\"discord_status_dnd\">Do Not Disturb</string>\n    <string name=\"discord_buttons\">Buttons</string>\n    <string name=\"discord_button_1\">Button 1</string>\n    <string name=\"discord_button_2\">Button 2</string>\n    <string name=\"login_successful\">Login successful!</string>\n    <string name=\"discord_information_warning\">This feature uses the KizzyRPC library to connect to Discord\\'s Gateway and set your Rich Presence status. While no known account suspensions have occurred from similar usage, this method is not officially supported by Discord and may be considered a Terms of Service violation. Your token is extracted locally and never sent to third-party servers. Proceed at your own discretion.</string>\n    <string name=\"discord_activity_type\">Activity type</string>\n    <string name=\"discord_activity_playing\">Playing</string>\n    <string name=\"discord_activity_listening\">Listening</string>\n    <string name=\"discord_activity_watching\">Watching</string>\n    <string name=\"discord_activity_competing\">Competing</string>\n    <string name=\"discord_button_text_variables\">Variables: {song_name}, {artist_name}, {album_name}</string>\n    <string name=\"discord_rpc_preview\">Rich Presence Preview</string>\n    <string name=\"discord_presence\">Presence</string>\n    <string name=\"discord_connect_description\">Sign in with Discord to share what you\\'re listening to</string>\n    <string name=\"discord_playing_metrolist\">Playing Metrolist</string>\n    <string name=\"discord_watching_metrolist\">Watching Metrolist</string>\n    <string name=\"discord_competing_metrolist\">Competing in Metrolist</string>\n    <string name=\"discord_activity_name\">Activity name</string>\n    <string name=\"discord_activity_name_description\">Custom name for the activity (leave empty for default)</string>\n    <string name=\"discord_advanced_mode\">Advanced mode</string>\n    <string name=\"discord_advanced_mode_description\">Show additional customization options for Rich Presence</string>\n    <string name=\"speed_dial\">Speed dial</string>\n    <string name=\"pin_to_speed_dial\">Pin to Speed dial</string>\n    <string name=\"unpin_from_speed_dial\">Unpin from Speed dial</string>\n    <string name=\"randomize_home_order\">Randomize Home Screen Order</string>\n    <string name=\"randomize_home_order_desc\">Randomly reorder home screen sections with weighted priorities</string>\n\n    <!-- Daily Discover -->\n    <string name=\"daily_discover_sounds_like\">Sounds like %1$s</string>\n    <string name=\"daily_discover_because_you_listen_to\">Because you listen to %1$s</string>\n    <string name=\"daily_discover_similar_to\">Similar to %1$s</string>\n    <string name=\"daily_discover_based_on\">Based on %1$s</string>\n    <string name=\"daily_discover_for_fans_of\">For fans of %1$s</string>\n    <string name=\"from_the_community\">From the community</string>\n\n    <!-- Logout dialog -->\n    <string name=\"logout_dialog_title\">Keep library data?</string>\n    <string name=\"logout_dialog_message\">Do you want to keep your playlists and library data? Downloaded songs will be kept regardless.</string>\n    <string name=\"logout_keep\">Keep</string>\n    <string name=\"logout_clear\">Clear</string>\n    <!-- Credits / About Screen -->\n    <string name=\"credits_lead_developer\">Lead Developer</string>\n    <string name=\"credits_collaborator\">Collaborator</string>\n    <string name=\"credits_collaborators_section\">Collaborators</string>\n    <string name=\"credits_license_name\">GNU General Public License v3.0</string>\n    <string name=\"credits_license_desc\">Free, open-source software. You may use, study, share and improve it.</string>\n    <string name=\"credits_discord\">Discord Server</string>\n    <string name=\"credits_telegram\">Telegram Channel</string>\n    <string name=\"credits_website\">Website</string>\n    <string name=\"credits_instagram\">Instagram</string>\n    <string name=\"credits_github\">GitHub</string>\n    <string name=\"credits_view_repo\">View Repository</string>\n    <string name=\"app_version_info\">%1$s • %2$s</string>\n    <string name=\"like_what_i_do\">Like what I do?</string>\n    <string name=\"buy_mo_a_coffee\">Buy me a coffee</string>\n    <string name=\"community_and_info\">Community &amp; Info</string>\n    <string name=\"metrolist\">METROLIST</string>\n    <string name=\"wanna_play_favorite_song\">Wanna play their favorite song?</string>\n    <string name=\"yeah\">Yeah</string>\n    <string name=\"stands_with_palestine\">This project stands with Palestine 🇵🇸</string>\n    <!-- Podcasts -->\n    <string name=\"filter_podcasts\">Podcasts</string>\n    <string name=\"view_podcast\">View podcast</string>\n    <string name=\"podcast_channels\">Podcast Channels</string>\n    <string name=\"latest_episodes\">Latest Episodes</string>\n    <string name=\"your_shows\">Your Shows</string>\n    <string name=\"new_episodes\">New Episodes</string>\n    <string name=\"episodes_for_later\">Episodes for Later</string>\n    <string name=\"save_episode_for_later\">Save for later</string>\n    <string name=\"save_episode_for_later_desc\">Add to your Episodes for Later playlist</string>\n    <string name=\"remove_episode_from_saved\">Remove from saved</string>\n    <string name=\"subscribe_to_podcast\">Save podcast to library</string>\n    <plurals name=\"n_episode\">\n        <item quantity=\"one\">%d episode</item>\n        <item quantity=\"other\">%d episodes</item>\n    </plurals>\n    <string name=\"filter_episodes\">Episodes</string>\n    <string name=\"filter_profiles\">Profiles</string>\n    <string name=\"filter_channels\">Channels</string>\n    <string name=\"auto_playlist\">Auto playlist</string>\n    <string name=\"downloaded_episodes\">Downloaded episodes</string>\n    <string name=\"no_subscribed_channels\">No subscribed channels</string>\n    <string name=\"no_downloaded_episodes\">No downloaded episodes</string>\n    <plurals name=\"n_channel\">\n        <item quantity=\"one\">%d channel</item>\n        <item quantity=\"other\">%d channels</item>\n    </plurals>\n\n    <!-- Restore confirmation dialog -->\n    <string name=\"restore_confirm_title\">Restore backup?</string>\n    <string name=\"restore_confirm_message\">This will restore your app data from the backup.</string>\n    <string name=\"restore_account_warning\">You will need to log in again after restore. The following account will be signed out:</string>\n    <string name=\"restore\">Restore</string>\n    <string name=\"checking_previous_account\">Checking for previous account…</string>\n    <string name=\"no_account_found\">No account found</string>\n\n    <!-- Cache settings -->\n\n    <!-- Upload songs -->\n    <string name=\"upload_songs\">Upload songs</string>\n    <string name=\"uploading\">Uploading…</string>\n    <string name=\"upload_progress\">%1$d of %2$d</string>\n    <string name=\"upload_complete\">Upload complete</string>\n    <string name=\"upload_failed\">Upload failed</string>\n    <string name=\"upload_file_too_large\">File too large (max 300MB)</string>\n    <string name=\"upload_unsupported_format\">Unsupported format. Use mp3, m4a, wma, flac, or ogg</string>\n    <string name=\"delete_uploaded_song\">Delete uploaded song</string>\n    <string name=\"delete_uploaded_song_confirm\">Are you sure you want to delete this uploaded song? This cannot be undone.</string>\n    <string name=\"delete_uploaded_song_success\">Uploaded song deleted</string>\n    <string name=\"delete_uploaded_song_failed\">Failed to delete uploaded song</string>\n    <string name=\"delete_uploaded_songs\">Delete uploaded songs</string>\n    <string name=\"delete_uploaded_songs_confirm\">Are you sure you want to delete %1$d uploaded songs? This cannot be undone.</string>\n    <string name=\"deleted_n_songs\">Deleted %1$d songs</string>\n    <string name=\"deleting\">Deleting…</string>\n\n    <!-- Android Auto settings -->\n    <string name=\"android_auto\">Android Auto</string>\n    <string name=\"android_auto_visible_sections\">Visible sections</string>\n    <string name=\"android_auto_reorder_hint\">Tap a section to enable or disable it.\\nLong press the drag handle to reorder. The first section will be shown by default.</string>\n    <string name=\"android_auto_youtube_playlists\">Show YouTube suggested playlists</string>\n    <string name=\"android_auto_youtube_playlists_desc\">Display YouTube Music recommended playlists inside the Playlists section when browsing with Android Auto</string>    \n    <string name=\"android_auto_target_playlist\">Quick-add destination</string>\n    <string name=\"android_auto_target_playlist_desc\">Select the playlist where songs will be saved when using the quick-add button in Android Auto</string>\n    <string name=\"android_auto_target_playlist_auto\">Not set</string>\n    <string name=\"added_to_playlist\">Added to playlist</string>\n    <string name=\"android_auto_target_playlist_not_set\">No playlist selected. Choose one in Android Auto settings.</string>\n    \n    <!-- Stats time transfer -->\n    <string name=\"time_transfer_title\">Time Transfer</string>\n    <string name=\"time_transfer_warning\">WARNING: It is not possible to revert this action once it is completed. A backup file should be created before proceeding. You can only target songs with at least one registered event/play.</string>\n    <string name=\"time_transfer_source_song\">Source song</string>\n    <string name=\"time_transfer_target_song\">Target song</string>\n    <string name=\"time_transfer_listen_time_label\">Listen time: </string>\n    <string name=\"time_transfer_convert\">Convert</string>\n    <string name=\"song_dropdown_more_results\">Type more to narrow results (%d more)</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<!-- Upstream InnerTune strings -->\n<!-- Do not add new features here -->\n<!-- Try to re-use strings here, but be careful on its usage e.g. verb vs noun -->\n<!-- \"InnerTune\" is replaced with \"Metrolist\" -->\n<!-- Translators: Do not modify this file for translations, use InnerTune's Weblate instead. -->\n\n<resources>\n    <!-- Bottom navigation -->\n    <string name=\"home\">Home</string>\n    <string name=\"songs\">Songs</string>\n    <string name=\"artists\">Artists</string>\n    <string name=\"albums\">Albums</string>\n    <string name=\"playlists\">Playlists</string>\n\n    <!-- Top bar -->\n    <plurals name=\"n_selected\">\n        <item quantity=\"other\">%d selected</item>\n    </plurals>\n\n    <!-- Home -->\n    <string name=\"history\">History</string>\n    <string name=\"stats\">Stats</string>\n    <string name=\"mood_and_genres\">Mood and Genres</string>\n    <string name=\"account\">Account</string>\n    <string name=\"quick_picks\">Quick picks</string>\n    <string name=\"quick_picks_empty\">Listen to songs to generate your quick picks</string>\n    <string name=\"forgotten_favorites\">Forgotten favorites</string>\n    <string name=\"keep_listening\">Keep listening</string>\n    <string name=\"your_youtube_playlists\">Your YouTube playlists</string>\n    <string name=\"similar_to\">Similar to</string>\n    <string name=\"new_release_albums\">New release albums</string>\n\n    <!-- History -->\n    <string name=\"today\">Today</string>\n    <string name=\"yesterday\">Yesterday</string>\n    <string name=\"this_week\">This week</string>\n    <string name=\"last_week\">Last week</string>\n\n    <!-- Stats -->\n    <string name=\"most_played_songs\">Most played songs</string>\n    <string name=\"most_played_artists\">Most played artists</string>\n    <string name=\"most_played_albums\">Most played albums</string>\n\n    <!-- Search -->\n    <string name=\"search\">Search</string>\n    <string name=\"search_yt_music\">Search YouTube Music…</string>\n    <string name=\"search_library\">Search library…</string>\n    <string name=\"filter_library\">Library</string>\n    <string name=\"filter_liked\">Liked</string>\n    <string name=\"filter_downloaded\">Downloaded</string>\n    <string name=\"filter_all\">All</string>\n    <string name=\"filter_songs\">Songs</string>\n    <string name=\"filter_videos\">Videos</string>\n    <string name=\"filter_albums\">Albums</string>\n    <string name=\"filter_artists\">Artists</string>\n    <string name=\"filter_playlists\">Playlists</string>\n    <string name=\"filter_community_playlists\">Community playlists</string>\n    <string name=\"filter_featured_playlists\">Featured playlists</string>\n    <string name=\"filter_bookmarked\">Bookmarked</string>\n    <string name=\"no_results_found\">No results found</string>\n\n    <!-- Library -->\n    <string name=\"library_song_empty\">Library songs will show up here</string>\n    <string name=\"library_artist_empty\">Library artists will show up here</string>\n    <string name=\"library_album_empty\">Library albums will show up here</string>\n    <string name=\"library_playlist_empty\">Your playlists will show up here</string>\n\n    <!-- Artist screen -->\n    <string name=\"from_your_library\">From your library</string>\n\n    <!-- Album screen -->\n    <string name=\"other_versions\">Other versions</string>\n\n    <!-- Playlist -->\n    <string name=\"liked_songs\">Liked songs</string>\n    <string name=\"downloaded_songs\">Downloaded songs</string>\n    <string name=\"playlist_is_empty\">The playlist is empty</string>\n    <string name=\"remove_download_playlist_confirm\">Do you really want to remove all \\\"%s\\\" playlist songs from the Downloaded Songs storage?</string>\n    <string name=\"delete_playlist_confirm\">Do you really want to delete the playlist \\\"%s\\\"?</string>\n\n    <!-- Button -->\n    <string name=\"retry\">Retry</string>\n    <string name=\"radio\">Radio</string>\n    <string name=\"shuffle\">Shuffle</string>\n    <string name=\"reset\">Reset</string>\n\n    <!-- Menu -->\n    <string name=\"details\">Details</string>\n    <string name=\"edit\">Edit</string>\n    <string name=\"start_radio\">Start radio</string>\n    <string name=\"play\">Play</string>\n    <string name=\"pause\">Pause</string>\n    <string name=\"play_next\">Play next</string>\n    <string name=\"add_to_queue\">Add to queue</string>\n    <string name=\"add_to_library\">Add to library</string>\n    <string name=\"add_all_to_library\">Add all to library</string>\n    <string name=\"remove_from_library\">Remove from library</string>\n    <string name=\"remove_all_from_library\">Remove all from library</string>\n    <string name=\"action_download\">Download</string>\n    <string name=\"downloading\">Downloading</string>\n    <string name=\"remove_download\">Remove download</string>\n    <string name=\"import_playlist\">Import playlist</string>\n    <string name=\"add_to_playlist\">Add to playlist</string>\n    <string name=\"view_artist\">View artist</string>\n    <string name=\"view_album\">View album</string>\n    <string name=\"refetch\">Refetch</string>\n    <string name=\"share\">Share</string>\n    <string name=\"delete\">Delete</string>\n    <string name=\"remove_from_history\">Remove from history</string>\n    <string name=\"remove_from_playlist\">Remove from playlist</string>\n    <string name=\"remove_from_queue\">Remove from queue</string>\n    <string name=\"search_online\">Search online</string>\n    <string name=\"action_sync\">Sync</string>\n    <string name=\"advanced\">Advanced</string>\n    <string name=\"tempo_and_pitch\">Tempo and Pitch</string>\n\n    <!-- Sort menu -->\n    <string name=\"sort_by_create_date\">Date added</string>\n    <string name=\"sort_by_name\">Name</string>\n    <string name=\"sort_by_artist\">Artist</string>\n    <string name=\"sort_by_year\">Year</string>\n    <string name=\"sort_by_song_count\">Song count</string>\n    <string name=\"sort_by_length\">Length</string>\n    <string name=\"sort_by_play_time\">Play time</string>\n    <string name=\"sort_by_custom\">Custom order</string>\n\n    <!-- Dialog -->\n    <string name=\"media_id\">Media id</string>\n    <string name=\"mime_type\">MIME type</string>\n    <string name=\"codecs\">Codecs</string>\n    <string name=\"bitrate\">Bitrate</string>\n    <string name=\"sample_rate\">Sample rate</string>\n    <string name=\"loudness\">Loudness</string>\n    <string name=\"volume\">Volume</string>\n    <string name=\"file_size\">File size</string>\n    <string name=\"unknown\">Unknown</string>\n    <string name=\"copied\">Copied to clipboard</string>\n\n    <string name=\"edit_lyrics\">Edit lyrics</string>\n    <string name=\"search_lyrics\">Search lyrics</string>\n\n    <string name=\"edit_song\">Edit song</string>\n    <string name=\"song_title\">Song title</string>\n    <string name=\"song_artists\">Song artists</string>\n    <string name=\"error_song_title_empty\">Song title cannot be empty.</string>\n    <string name=\"error_song_artist_empty\">Song artist cannot be empty.</string>\n    <string name=\"save\">Save</string>\n\n    <string name=\"choose_playlist\">Choose playlist</string>\n    <string name=\"edit_playlist\">Edit playlist</string>\n    <string name=\"create_playlist\">Create playlist</string>\n    <string name=\"playlist_name\">Playlist name</string>\n    <string name=\"error_playlist_name_empty\">Playlist name cannot be empty.</string>\n\n    <string name=\"edit_artist\">Edit artist</string>\n    <string name=\"artist_name\">Artist name</string>\n    <string name=\"error_artist_name_empty\">Artist name cannot be empty.</string>\n\n    <string name=\"duplicates\">Duplicates</string>\n    <string name=\"skip_duplicates\">Skip duplicates</string>\n    <string name=\"add_anyway\">Add anyway</string>\n    <string name=\"duplicates_description_single\">The song is already in your playlist</string>\n    <string name=\"duplicates_description_multiple\">%d songs are already in your playlist</string>\n\n    <!-- Noun -->\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d song</item>\n        <item quantity=\"other\">%d songs</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d artist</item>\n        <item quantity=\"other\">%d artists</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d album</item>\n        <item quantity=\"other\">%d albums</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d playlist</item>\n        <item quantity=\"other\">%d playlists</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d week</item>\n        <item quantity=\"other\">%d weeks</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d month</item>\n        <item quantity=\"other\">%d months</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d year</item>\n        <item quantity=\"other\">%d years</item>\n    </plurals>\n\n    <!-- Snackbar -->\n    <string name=\"playlist_imported\">Playlist imported</string>\n    <string name=\"removed_song_from_playlist\">Removed \\\"%s\\\" from playlist</string>\n    <string name=\"playlist_synced\">Playlist synced</string>\n    <string name=\"undo\">Undo</string>\n\n    <!-- Player -->\n    <string name=\"lyrics_not_found\">Lyrics not found</string>\n    <string name=\"sleep_timer\">Sleep timer</string>\n    <string name=\"end_of_song\">End of song</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">1 minute</item>\n        <item quantity=\"other\">%d minutes</item>\n    </plurals>\n    <string name=\"error_no_stream\">No stream available</string>\n    <string name=\"error_no_internet\">No network connection</string>\n    <string name=\"error_timeout\">Timeout</string>\n    <string name=\"error_unknown\">Unknown error</string>\n\n    <!-- Player action -->\n    <string name=\"action_like\">Like</string>\n    <string name=\"action_like_all\">Like all</string>\n    <string name=\"action_remove_like\">Remove like</string>\n    <string name=\"action_remove_like_all\">Remove all likes</string>\n    <string name=\"action_shuffle_on\">Shuffle on</string>\n    <string name=\"action_shuffle_off\">Shuffle off</string>\n    <string name=\"repeat_mode_off\">Repeat mode off</string>\n    <string name=\"repeat_mode_one\">Repeat current song</string>\n    <string name=\"repeat_mode_all\">Repeat queue</string>\n\n    <!-- Queue Title -->\n    <string name=\"queue_all_songs\">All songs</string>\n    <string name=\"queue_searched_songs\">Searched songs</string>\n\n    <!-- Notification name -->\n    <string name=\"music_player\">Music Player</string>\n\n    <!-- Settings -->\n    <string name=\"settings\">Settings</string>\n    <string name=\"appearance\">Appearance</string>\n    <string name=\"theme\">Theme</string>\n    <string name=\"theme_desc\">Customize your app theme</string>\n    <string name=\"theme_colors\">Theme &amp; Colors</string>\n    <string name=\"theme_mode\">Theme Mode</string>\n    <string name=\"color_palette\">Color Palette</string>\n    <string name=\"use_system_colors\">Use system colors</string>\n    <string name=\"enable_dynamic_theme\">Enable dynamic theme</string>\n    <string name=\"dark_theme\">Dark theme</string>\n    <string name=\"dark_theme_on\">On</string>\n    <string name=\"dark_theme_off\">Off</string>\n    <string name=\"dark_theme_follow_system\">Follow system</string>\n    <string name=\"pure_black\">Pure black</string>\n    <string name=\"customize_navigation_tabs\">Customize navigation tabs</string>\n    <string name=\"player\">Player</string>\n    <string name=\"player_text_alignment\">Player text alignment</string>\n    <string name=\"lyrics_text_position\">Lyrics text position</string>\n    <string name=\"sided\">Sided</string>\n    <string name=\"left\">Left</string>\n    <string name=\"center\">Center</string>\n    <string name=\"right\">Right</string>\n    <string name=\"player_slider_style\">Player slider style</string>\n    <string name=\"default_\">Default</string>\n    <string name=\"squiggly\">Squiggly</string>\n    <string name=\"misc\">Misc</string>\n    <string name=\"default_open_tab\">Default open tab</string>\n    <string name=\"grid_cell_size\">Grid cell size</string>\n    <string name=\"small\">Small</string>\n    <string name=\"big\">Big</string>\n\n    <string name=\"content\">Content</string>\n    <string name=\"action_logout\">Log out</string>\n    <string name=\"action_login\">Log in</string>\n    <string name=\"login\">Login</string>\n    <string name=\"not_logged_in\">Not logged in</string>\n    <string name=\"login_failed\">Login failed</string>\n\n    <string name=\"content_language\">Default content language</string>\n    <string name=\"content_country\">Default content country</string>\n    <string name=\"system_default\">System default</string>\n    <string name=\"enable_proxy\">Enable proxy</string>\n    <string name=\"proxy_type\">Proxy type</string>\n    <string name=\"proxy_url\">Proxy URL</string>\n    <string name=\"restart_to_take_effect\">Restart to take effect</string>\n\n    <string name=\"player_and_audio\">Player and audio</string>\n    <string name=\"audio_quality\">Audio quality</string>\n    <string name=\"audio_quality_auto\">Auto</string>\n    <string name=\"audio_quality_high\">High</string>\n    <string name=\"audio_quality_low\">Low</string>\n    <string name=\"queue\">Queue</string>\n    <string name=\"persistent_queue\">Persistent queue</string>\n    <string name=\"persistent_queue_desc\">Restore your last queue when the app starts</string>\n    <string name=\"auto_load_more\">Auto load more songs</string>\n    <string name=\"auto_load_more_desc\">Automatically add more songs when the end of the queue is reached, if possible</string>\n    <string name=\"skip_silence\">Skip silence</string>\n    <string name=\"audio_normalization\">Audio normalization</string>\n    <string name=\"auto_skip_next_on_error\">Auto skip to next song when error occurs</string>\n    <string name=\"auto_skip_next_on_error_desc\">Ensure your continuous playback experience</string>\n    <string name=\"stop_music_on_task_clear\">Stop music on task clear</string>\n    <string name=\"equalizer\">Equalizer</string>\n\n    <string name=\"storage\">Storage</string>\n    <string name=\"cache\">Cache</string>\n    <string name=\"image_cache\">Image Cache</string>\n    <string name=\"song_cache\">Song Cache</string>\n    <string name=\"max_cache_size\">Max cache size</string>\n    <string name=\"unlimited\">Unlimited</string>\n    <string name=\"clear_all_downloads\">Clear all downloads</string>\n    <string name=\"max_image_cache_size\">Max image cache size</string>\n    <string name=\"clear_image_cache\">Clear image cache</string>\n    <string name=\"max_song_cache_size\">Max song cache size</string>\n    <string name=\"clear_song_cache\">Clear song cache</string>\n    <string name=\"size_used\">%s used</string>\n\n    <string name=\"privacy\">Privacy</string>\n    <string name=\"listen_history\">Listen history</string>\n    <string name=\"pause_listen_history\">Pause listen history</string>\n    <string name=\"clear_listen_history\">Clear listen history</string>\n    <string name=\"clear_listen_history_confirm\">Are you sure you want to clear all listen history?</string>\n    <string name=\"search_history\">Search history</string>\n    <string name=\"pause_search_history\">Pause search history</string>\n    <string name=\"clear_search_history\">Clear search history</string>\n    <string name=\"clear_search_history_confirm\">Are you sure you want to clear all search history?</string>\n    <string name=\"use_login_for_browse\">Use login for browsing content</string>\n    <string name=\"use_login_for_browse_desc\">This can influence what content you see and for example shows premium-only albums if you are logged in with a Premium account</string>\n    <string name=\"disable_screenshot\">Disable screenshot</string>\n    <string name=\"disable_screenshot_desc\">When this option is on, screenshots and the app\\'s view in Recents are disabled.</string>\n    <string name=\"enable_lrclib\">Enable LrcLib lyrics provider</string>\n    <string name=\"enable_kugou\">Enable KuGou lyrics provider</string>\n    <string name=\"hide_explicit\">Hide explicit content</string>\n\n    <string name=\"backup_restore\">Backup and restore</string>\n    <string name=\"action_backup\">Backup</string>\n    <string name=\"action_restore\">Restore</string>\n    <string name=\"imported_playlist\">Imported playlist</string>\n    <string name=\"backup_create_success\">Backup created successfully</string>\n    <string name=\"backup_create_failed\">Couldn\\'t create backup</string>\n    <string name=\"restore_failed\">Failed to restore backup</string>\n\n    <string name=\"discord_integration\">Discord Integration</string>\n    <string name=\"discord_information\">Metrolist uses the KizzyRPC library to set your Discord account\\'s status. This involves using the Discord Gateway connection, which may be considered a violation of Discord\\'s TOS. However, there are no known cases of user accounts being suspended for this reason. Use at your own risk.\\n\\Metrolist will only extract your token, and everything else is stored locally.</string>\n    <string name=\"dismiss\">Dismiss</string>\n    <string name=\"options\">Options</string>\n    <string name=\"preview\">Preview</string>\n    <string name=\"enable_discord_rpc\">Enable Rich Presence</string>\n\n    <string name=\"about\">About</string>\n    <string name=\"app_version\">App version</string>\n\n    <string name=\"new_version_available\">New version available</string>\n    <string name=\"translation_models\">Translation Models</string>\n    <string name=\"clear_translation_models\">Clear translation models</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <style name=\"Theme.Metrolist\" parent=\"android:Theme.Material.Light.NoActionBar\">\n        <item name=\"android:windowContentOverlay\">@null</item>\n        <item name=\"android:windowDrawsSystemBarBackgrounds\">true</item>\n        <item name=\"android:windowMinWidthMinor\">100%</item>\n        <item name=\"android:windowMinWidthMajor\">100%</item>\n        <item name=\"android:windowNoTitle\">true</item>\n        <item name=\"android:windowBackground\">#ffffff</item>\n        <item name=\"android:enforceNavigationBarContrast\" tools:ignore=\"NewApi\">false</item>\n        <item name=\"android:statusBarColor\">@android:color/transparent</item>\n        <item name=\"android:navigationBarColor\">@android:color/transparent</item>\n    </style>\n\n    <!-- Widget Theme with Material 3 colors fallback for API < 31 -->\n    <style name=\"Theme.Widget.Metrolist\" parent=\"@android:style/Theme.DeviceDefault.DayNight\">\n        <item name=\"android:colorBackground\">#E6E1E5</item>\n    </style>\n\n    <!-- Transparent theme for Music Recognizer Tile Service -->\n    <style name=\"Theme.Metrolist.Transparent\" parent=\"@android:style/Theme.Translucent.NoTitleBar\">\n        <item name=\"android:windowBackground\">@android:color/transparent</item>\n        <item name=\"android:windowIsTranslucent\">true</item>\n        <item name=\"android:windowIsFloating\">false</item>\n        <item name=\"android:windowNoTitle\">true</item>\n        <item name=\"android:windowDisablePreview\">true</item>\n        <item name=\"android:windowAnimationStyle\">@null</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/values.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Override default media3 notification icons -->\n</resources>"
  },
  {
    "path": "app/src/main/res/values/widget_colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Widget colors fallback for Android < 12 (API 31) -->\n    <!-- Material 3 Primary Container (Light) -->\n    <color name=\"widget_primary_container\">#EADDFF</color>\n    <color name=\"widget_on_primary_container\">#21005D</color>\n\n    <!-- Material 3 Tertiary Container (Light) - For like button -->\n    <color name=\"widget_tertiary_container\">#FFD8E4</color>\n    <color name=\"widget_on_tertiary_container\">#31111D</color>\n\n    <!-- Play button colors (Low style) -->\n    <color name=\"widget_play_button_low_bg\">@color/widget_primary_container</color>\n    <color name=\"widget_play_button_low_icon\">@color/widget_on_primary_container</color>\n\n    <!-- Music Recognizer Widget colors (Light) -->\n    <!-- Active mic button background – deeper accent -->\n    <color name=\"widget_mic_active_bg\">#7965AF</color>\n    <!-- Pulse ring colors (opacity variants for outward ripple animation) -->\n    <color name=\"widget_mic_pulse_ring\">#994F378B</color>\n    <color name=\"widget_mic_pulse_ring_mid\">#664F378B</color>\n    <color name=\"widget_mic_pulse_ring_low\">#334F378B</color>\n    <color name=\"widget_mic_pulse_ring_fade\">#1A4F378B</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-ar/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"back_button_desc\">للخلف</string>\n    <string name=\"album_cover_desc\">غلاف الألبوم</string>\n    <string name=\"top_music_videos\">أفضل مقاطع الفيديو الموسيقيه</string>\n    <string name=\"trending\">الأكثر رواجًا</string>\n    <string name=\"weeks\">أسابيع</string>\n    <string name=\"months\">شهور</string>\n    <string name=\"years\">سنين</string>\n    <string name=\"continuous\">مستمر</string>\n    <string name=\"liked\">الأغاني المفضلة</string>\n    <string name=\"offline\">التحميلات</string>\n    <string name=\"my_top\">الأعلي استماع</string>\n    <string name=\"cached_playlist\">الأغاني المخزنة مؤقتًا</string>\n    <string name=\"sync_playlist\">مزامنة قائمة التشغيل</string>\n    <string name=\"sync_disabled\">المزامنة غير مفعلة</string>\n    <string name=\"allows_for_sync_witch_youtube\">ملاحظة: يسمح هذا بالمزامنة مع YouTube Music. لا يمكن تغيير هذا لاحقًا.</string>\n    <string name=\"remove_from_cache\">إزالة من التخزين المؤقت</string>\n    <string name=\"copy_link\">نسخ الرابط</string>\n    <string name=\"select\">تحديد الكل</string>\n    <string name=\"like_all\">الإعجاب بالكل</string>\n    <string name=\"dislike_all\">إلغاء الإعجاب بالكل</string>\n    <string name=\"sort_by_last_updated\">تاريخ التحديث</string>\n    <string name=\"already_in_playlist\">موجود بالفعل في قائمة التشغيل:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"zero\">لا مرات</item>\n        <item quantity=\"one\">%d مرة</item>\n        <item quantity=\"two\">%d مرتان</item>\n        <item quantity=\"few\">%d مرات</item>\n        <item quantity=\"many\">%d مرة</item>\n        <item quantity=\"other\">%d مرة</item>\n    </plurals>\n    <plurals name=\"seconds\">\n        <item quantity=\"zero\">لا ثوانٍ</item>\n        <item quantity=\"one\">%d ثانية</item>\n        <item quantity=\"two\">%d ثانيتان</item>\n        <item quantity=\"few\">%d ثوانٍ</item>\n        <item quantity=\"many\">%d ثانية</item>\n        <item quantity=\"other\">%d ثانية</item>\n    </plurals>\n    <string name=\"player_background_style\">نمط خلفية المشغل</string>\n    <string name=\"gradient\">ألوان متدرجة</string>\n    <string name=\"player_background_blur\">ضبابي</string>\n    <string name=\"follow_theme\">اتباع السيم</string>\n    <string name=\"player_buttons_style\">ألوان أزرار المشغل</string>\n    <string name=\"default_style\">افتراضي</string>\n    <string name=\"enable_swipe_thumbnail\">تمكين التمرير لتغيير الأغنية</string>\n    <string name=\"swipe_song_to_add\">مرر الأغنية إلى اليسار لإضافتها إلى قائمة الانتظار أو إلى اليمين لتشغيلها بعد الأغنية الحالية</string>\n    <string name=\"lyrics_click_change\">تغيير كلمات الأغنية عند النقر عليها</string>\n    <string name=\"slim\">نحيل</string>\n    <string name=\"slim_navbar\">شريط تنقل سفلي رفيع</string>\n    <string name=\"auto_playlists\">قوائم التشغيل التلقائيه</string>\n    <string name=\"show_liked_playlist\">إظهار قائمة \\\"الاعجابات\\\"</string>\n    <string name=\"show_downloaded_playlist\">إظهار قائمة \\\"التحميلات\\\"</string>\n    <string name=\"show_top_playlist\">إظهار قائمة \\\"الأعلى أستماع\\\"</string>\n    <string name=\"show_cached_playlist\">إظهار قائمة \\\"الأغاني المخزنة مؤقتًا\\'</string>\n    <string name=\"generating_image\">جاري إنشاء الصورة</string>\n    <string name=\"please_wait\">انتظر من فضلك</string>\n    <string name=\"cancel\">إلغاء</string>\n    <string name=\"share_lyrics\">شارك كلمات الأغاني</string>\n    <string name=\"share_as_text\">المشاركة كنص</string>\n    <string name=\"share_as_image\">المشاركة كصورة</string>\n    <string name=\"max_selection_limit\">الحد الأقصى للاختيار</string>\n    <string name=\"share_selected\">شارك العناصر المحدده</string>\n    <string name=\"customize_colors\">تخصيص الألوان</string>\n    <string name=\"text_color\">لون النص</string>\n    <string name=\"secondary_text_color\">لون النص الثانوي</string>\n    <string name=\"background_color\">لون الخلفية</string>\n    <string name=\"advanced_login\">تسجيل الدخول باستخدام token</string>\n    <string name=\"token_hidden\">اضغط لعرض ال Token</string>\n    <string name=\"token_shown\">اضغط مرة أخرى للنسخ أو التعديل</string>\n    <string name=\"token_adv_login_description\">هذه طريقة تسجيل دخول متقدمة. كبديل للبوابة الإلكترونية، يمكنك إدخال أو تحديث رمز تسجيل الدخول الخاص بك هنا مباشرة. على سبيل المثال، يمكن أن يسرع ذلك عملية تسجيل الدخول على أجهزة متعددة. يرجى ملاحظة أن أي صيغة رمز غير صالحة لا يستطيع التطبيق تحليلها لن يتم قبولها</string>\n    <string name=\"general\">عام</string>\n    <string name=\"proxy\">الوكيل</string>\n    <string name=\"default_lib_chips\">تغيير الشريحة الافتراضية للمكتبة</string>\n    <string name=\"set_quick_picks\">ضبط الاختيارات السريعة</string>\n    <string name=\"last_song_listened\">بناءً على آخر أغنية تم الاستماع إليها</string>\n    <string name=\"app_language\">لغة التطبيق</string>\n    <string name=\"enable_similar_content\">تفعيل المحتوى المشابه</string>\n    <string name=\"similar_content_desc\">إضافة المزيد من الأغاني المشابهة تلقائيًا عند الوصول إلى نهاية قائمة الانتظار</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"auto_download_on_like\">التنزيل أوتوماتيكي عند الإعجاب</string>\n    <string name=\"auto_download_on_like_desc\">تنزيل الأغاني تلقائيًا عند الاعجاب بها</string>\n    <string name=\"clear_song_cache_dialog\">هل أنت متأكد أنك تريد مسح كافة الأغاني المخزنة مؤقتًا؟</string>\n    <string name=\"clear_downloads_dialog\">هل أنت متأكد أنك تريد مسح كافة التنزيلات؟</string>\n    <string name=\"lyrics\">الكلمات</string>\n    <string name=\"not_logged_in_youtube\">لم يتم تسجيل الدخول إلى يوتيوب</string>\n    <string name=\"default_links\">فتح الروابط المدعومة افتراضيًا</string>\n    <string name=\"open_app_settings_error\">لم يتمكن التطبيق من فتح إعدادات التطبيق</string>\n    <string name=\"all_time\">طوال الوقت</string>\n    <string name=\"past_24_hours\">آخر 24 ساعة</string>\n    <string name=\"past_week\">الأسبوع الماضي</string>\n    <string name=\"past_month\">الشهر الماضي</string>\n    <string name=\"past_year\">السنة الماضية</string>\n    <string name=\"top_length\">طول قائمة الأغاني المفضلة الخاصة بي</string>\n    <string name=\"history_duration\">مدة التاريخ</string>\n    <string name=\"information\">المعلومات</string>\n    <string name=\"description\">الوصف</string>\n    <string name=\"views\">المشاهدات</string>\n    <string name=\"likes\">الإعجابات</string>\n    <string name=\"dislikes\">الغير محبوب</string>\n    <string name=\"local_history\">محلي</string>\n    <string name=\"remote_history\">مزامن</string>\n    <string name=\"link_copied\">تم نسخ الرابط</string>\n    <string name=\"similar_content\">محتوى مشابه</string>\n    <string name=\"charts\">المخططات</string>\n    <string name=\"release_notes\">ملاحظات الاصدار الجديد</string>\n    <string name=\"lyrics_auto_scroll\">تمرير كلمات الأغاني تلقائياً</string>\n    <string name=\"playlist_add_local_to_synced_note\">ملاحظة: إضافة الأغاني المحلية إلى قوائم التشغيل المتزامنة / قوائم التشغيل عن بعد غير مدعومة. أي نوع آخر منها صالح</string>\n    <string name=\"import_csv\">استيراد قوائم التشغيل من نوع \\\"csv\\\"</string>\n    <string name=\"import_online\">استيراد قوائم التشغيل من نوع \\\"m3u\\\"</string>\n    <string name=\"lyrics_romanize_japanese\">كلمات الأغاني اليابانيه الرومانيه</string>\n    <string name=\"lyrics_romanize_korean\">كلمات الأغاني الكوريه الرومانيه</string>\n    <string name=\"yt_sync\">المزامنة التلقائية مع الحساب</string>\n    <string name=\"more_content\">المزيد من المحتوي</string>\n    <string name=\"new_player_design\">تصميم المشغل الجديد</string>\n    <string name=\"swipe_sensitivity\">حساسية السحب الخاص بالمشغل الصغير</string>\n    <string name=\"clear_image_cache_dialog\">هل أنت متأكد أنك تريد مسح كافة الصور المخزنة مؤقتًا؟</string>\n    <string name=\"disable\">تعطيل</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"subscribe\">اشتراك</string>\n    <string name=\"subscribed\">مشترك</string>\n    <string name=\"new_mini_player_design\">تصميم جديد للمشغل المصغر</string>\n    <string name=\"now_playing\">المُشغل الأن</string>\n    <string name=\"seek_forward_dynamic\">+%1$d ثواني للأمام</string>\n    <string name=\"seek_backward_dynamic\">- %1$d ثواني للخلف</string>\n    <string name=\"seek_seconds_addup\">التقدم التدريجي</string>\n    <string name=\"seek_seconds_addup_description\">إذا مُكنت أضف 5 ثوان إضافية بشكل تدريجي على كل مطاردة</string>\n    <string name=\"disable_load_more_when_repeat_all\">تعطيل تحميل المزيد عند تفعيل تكرار الكل</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">لا يتم تحميل المزيد من الأغاني والمحتوى المماثل تلقائيًا عند تمكين وضع تكرار الكل</string>\n    <string name=\"close\">إغلاق</string>\n    <string name=\"hide_player_thumbnail\">إخفاء صورة الأغنيه</string>\n    <string name=\"hide_player_thumbnail_desc\">استبدال غلاف الألبوم بشعار التطبيق في المشغل</string>\n    <string name=\"settings_section_ui\">الواجهة</string>\n    <string name=\"settings_section_privacy\">الخصوصية والأمان</string>\n    <string name=\"settings_section_player_content\">المشغل و المحتوي</string>\n    <string name=\"settings_section_storage\">التخزين و البيانات</string>\n    <string name=\"settings_section_system\">‫النظام والمعلومات</string>\n    <string name=\"starting_radio\">بدء تشغيل الراديو</string>\n    <string name=\"config_proxy\">تكوين الوكيل</string>\n    <string name=\"proxy_username\">اسم مستخدم الوكيل</string>\n    <string name=\"proxy_password\">كلمة سر الوكيل</string>\n    <string name=\"enable_authentication\">تمكين المصادقة</string>\n    <string name=\"lyrics_romanization_cyrillic\">إعدادات الرومنة</string>\n    <string name=\"lyrics_romanize_title\">الكتابة بالحروف اللاتينية</string>\n    <string name=\"lyrics_romanization\">رومنة كلمات الأغاني</string>\n    <string name=\"lyrics_romanize_russian\">كلمات الأغاني الروسية الرومانية</string>\n    <string name=\"lyrics_romanize_ukrainian\">كلمات الأغاني الأوكرانية الرومانية</string>\n    <string name=\"lyrics_romanize_belarusian\">كلمات الأغاني البيلاروسية بالرومانية</string>\n    <string name=\"lyrics_romanize_kyrgyz\">كلمات أغنية Romanize Kyrgyz</string>\n    <string name=\"lyrics_romanize_serbian\">كلمات الأغاني الصربية الرومانية</string>\n    <string name=\"lyrics_romanize_bulgarian\">كلمات الأغاني البلغارية الرومانية</string>\n    <string name=\"line_by_line_option_title\">تجريبي: اكتشاف اللغة سطرًا بسطر</string>\n    <string name=\"line_by_line_option_desc\">سيتم اكتشاف اللغة السيريلية سطرًا بسطر بدلاً من الأغنية بأكملها.</string>\n    <string name=\"line_by_line_dialog_title\">هل أنت متأكد؟</string>\n    <string name=\"line_by_line_dialog_desc\">بالنسبة لميزة التجربة هذه، إما أن تنجح أو تفشل.\\n\\nبشكل افتراضي، يتم تحديد اللغة من خلال الأغنية بأكملها، ولكن عند تفعيل هذا الخيار، سيتم تحديد اللغة سطراً تلو الآخر. وهذا سيسمح للأغاني متعددة اللغات بالعمل، ولكن قد لا تكون اللغة صحيحة دائماً (على سبيل المثال، إذا كانت هناك كلمات أوكرانية لا تحتوي على أي أحرف خاصة بالأوكرانية، فقد يتم تحويلها إلى الروسية بدلاً من ذلك).\\n\\nإذا لم تواجه أي مشاكل، يُوصى بإبقاء هذا الخيار معطلاً.</string>\n    <string name=\"romanize_current_track\">كتابة المسار الحالي بالرومانية</string>\n    <string name=\"edit_playlist_cover\">تبديل غلاف القائمة</string>\n    <string name=\"edit_playlist_cover_note\">ملاحظة: يجب ربط حسابك برقم هاتف والتحقق منه على YouTube Music لتغيير غلاف القائمة.</string>\n    <string name=\"edit_playlist_cover_note_wait\">بعد تحديد الصورة، يرجى الانتظار قليلاً حتى يظهر الغلاف الجديد في قائمتك.</string>\n    <string name=\"choose_from_library\">اختر من المكتبة</string>\n    <string name=\"remove_custom_image\">إزالة الصورة المخصصة</string>\n    <string name=\"audio_offload\">تمكين التفريغ</string>\n    <string name=\"audio_offload_description\">استخدم مسار الصوت غير المحمّل لتشغيل الصوت. قد يؤدي تعطيل هذا إلى زيادة استخدام الطاقة، ولكن قد يكون مفيدًا إذا واجهت مشكلات في تشغيل الصوت أو المعالجة اللاحقة</string>\n    <string name=\"uploaded_playlist\">المرفوعه</string>\n    <string name=\"filter_uploaded\">المرفوعه</string>\n    <string name=\"show_uploaded_playlist\">إظهار قائمة التشغيل \\\"المرفوعه\\\"</string>\n    <string name=\"discord_use_details\">استخدم التفاصيل بدلاً من الحالة</string>\n    <string name=\"discord_use_details_description\">إظهار عنوان الأغنية بشكل بارز بدلاً من أسماء الفنانين</string>\n    <string name=\"updater\">التحديثات</string>\n    <string name=\"check_for_updates\">التحقق تلقائيًا من التحديثات</string>\n    <string name=\"update_notifications\">تمكين إشعارات التحديث</string>\n    <string name=\"update_available_title\">التحديث متاح</string>\n    <string name=\"update_channel_name\">تحديثات التطبيق</string>\n    <string name=\"update_channel_desc\">إشعارات حول الإصدارات الجديدة</string>\n    <string name=\"lyrics_romanize_macedonian\">كلمات أغنية رومانية مقدونية</string>\n    <string name=\"integrations\">التكاملات</string>\n    <string name=\"username\">اسم المستخدم</string>\n    <string name=\"password\">كلمة المرور</string>\n    <string name=\"lastfm_integration\">Last.fm</string>\n    <string name=\"enable_scrobbling\">تمكين سكروبلينج</string>\n    <string name=\"lastfm_now_playing\">إرسال المُشغله حاليا</string>\n    <string name=\"scrobbling_configuration\">تكوين سكروبلينج</string>\n    <string name=\"scrobble_min_track_duration\">أغاني سكرابل أطول من</string>\n    <string name=\"scrobble_delay_percent\">نسبة تأخير سكرابل</string>\n    <string name=\"scrobble_delay_minutes\">دقائق تأخير سكرابل</string>\n    <string name=\"swipe_song_to_remove\">مرر الأغنية لإزالتها من قائمة التشغيل</string>\n    <string name=\"last_fm_send_likes_description\">عندما تعجبك أغنية أو تلغي إعجابك بها في Metrolist سيتم تلقائيًا وضع علامة (أحببت) أو (غير محبوب) على نفس الأغنية في Last.fm</string>\n    <string name=\"last_fm_send_likes\">إرسال الإعجابات/إلغاء الإعجابات</string>\n    <string name=\"lyrics_romanize_chinese\">كلمات الأغاني الصينية بالحروف اللاتينية</string>\n    <string name=\"hide_video_songs\">إخفاء أغاني الفيديو</string>\n    <string name=\"google_cast\">جوجل كاست</string>\n    <string name=\"google_cast_description\">تمكين بث الصوت إلى كروم كاست والأجهزة الأخرى التي تدعم البث</string>\n    <string name=\"primary_color_style\">اللون الأساسي</string>\n    <string name=\"auto_scroll\">إعادة المزامنة</string>\n    <string name=\"details_desc\">اطلع علي معلومات الأغنية</string>\n    <string name=\"edit_desc\">غير العنوان أو الفنان</string>\n    <string name=\"start_radio_desc\">انشئ محطة بناء علي هذا العنصر</string>\n    <string name=\"play_next_desc\">أضف إلي أعلي قائمة الانتظار الخاصة بك</string>\n    <string name=\"add_to_queue_desc\">أضف إلي أسفل قائمة الانتظار الخاصة بك</string>\n    <string name=\"add_to_library_desc\">احفظ في مكتبتك</string>\n    <string name=\"download_desc\">اجعلها متاحة للتشغيل دون اتصال بالإنترنت</string>\n    <string name=\"add_to_playlist_desc\">أضفها إلي إحدي قوائم التشغيل الخاصة بك ‍</string>\n    <string name=\"refetch_desc\">استخراج أحداث البيانات الوصفية من يوتيوب ميزوك</string>\n    <string name=\"share_desc\">شارك رابط هذا العنصر</string>\n    <string name=\"delete_desc\">قم بإزالة هذا العنصر نهائيا</string>\n    <string name=\"advanced_desc\">غير ايقاع الأغنية ودرجة صوتها</string>\n    <string name=\"equalizer_desc\">اضبط معادل الصوت</string>\n    <string name=\"enable_dynamic_icon\">تفعيل الأيقونات الديناميكية</string>\n    <string name=\"mini_player\">مشغل مصغر</string>\n    <string name=\"pure_black_mini_player\">مشغل صغير أسود نقي</string>\n    <string name=\"cache_size_warning_title\">انتظر!</string>\n    <string name=\"cache_size_warning_message\">لقد اخترت حدًا لحجم ذاكرة التخزين المؤقت أصغر مما يستخدمه التطبيق حاليًا ( %1$s ). إذا تابعت، فقد يقوم التطبيق بإزالة بعض البيانات المخزنة مؤقتًا (%2$s) لتتوافق مع الحد الجديد. هل تريد المتابعة على أي حال؟</string>\n    <string name=\"cache_size_warning_confirm\">متابعة</string>\n    <string name=\"tertiary_color_style\">اللون الثانوي</string>\n    <string name=\"logging_in\">جارٍ تسجيل الدخول…</string>\n    <string name=\"download_playlist_desc\">وصف القائمة</string>\n    <string name=\"remove_download_playlist_desc\">قم بإزالة جميع الأغاني التي تم تنزيلها من قائمة التشغيل هذه</string>\n    <string name=\"download_in_progress_desc\">عملية التنزيل جارية</string>\n    <string name=\"share_playlist_desc\">شارك قائمة التشغيل هذه مع الآخرين</string>\n    <string name=\"delete_playlist_desc\">قم بإزالة قائمة التشغيل هذه نهائياً</string>\n    <string name=\"sync_playlist_desc\">مزامنة قائمة التشغيل مع YT Music</string>\n    <string name=\"enable_better_lyrics\">تفعيل كلمات الأغاني الأفضل</string>\n    <string name=\"enable_better_lyrics_desc\">كلمات متزامنة مقطعيًا لأي أغنية للكاريوكي</string>\n    <string name=\"lyrics_animation_style\">أسلوب الرسوم المتحركة للكلمات كلمة بكلمة</string>\n    <string name=\"none\">لا تأثير</string>\n    <string name=\"fade\">تلاشي</string>\n    <string name=\"glow\">الاشعاع</string>\n    <string name=\"slide\">انزلاق</string>\n    <string name=\"karaoke\">كاريوكي</string>\n    <string name=\"apple_music_style\">أبل ميوزك</string>\n    <string name=\"lyrics_text_size\">حجم نص كلمات الأغنية</string>\n    <string name=\"lyrics_line_spacing\">تباعد أسطر الكلمات</string>\n    <string name=\"shuffle_playlist_first\">قم بتشغيل قائمة التشغيل/الألبوم عشوائياً أولاً</string>\n    <string name=\"shuffle_playlist_first_desc\">عند تشغيل الأغاني عشوائياً، قم بتشغيل جميع الأغاني من قائمة التشغيل/الألبوم الأصلي أولاً، ثم المحتوى المشابه</string>\n    <string name=\"show_wrapped_card\">عرض البطاقة المغلفة</string>\n    <string name=\"album_art_for\">غلاف ألبوم لـ %s</string>\n    <string name=\"wrapped_total_albums_title\">استمعت إلى</string>\n    <string name=\"wrapped_total_albums_subtitle\">ألبومات فريدة</string>\n    <string name=\"wrapped_top_album_title\">ألبومك المفضل هو</string>\n    <string name=\"wrapped_playlist_ready\">قائمة التشغيل الشخصية الخاصة بك جاهزة</string>\n    <string name=\"wrapped_top_5_albums_title\">أفضل 5 ألبومات لديك</string>\n    <string name=\"wrapped_album_listening_time\">لقد استمعت إلى هذا الألبوم لمدة %d دقيقة</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d دقيقة</string>\n    <string name=\"wrapped_no_data\">لا توجد بيانات</string>\n    <string name=\"wrapped_top_5_artists_title\">أفضل فناني العام</string>\n    <string name=\"wrapped_artist_listening_time\">%d دقيقة</string>\n    <string name=\"wrapped_top_5_songs_title\">أفضل أغاني السنة</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">صورة الألبوم</string>\n    <string name=\"wrapped_top_artist_title\">فنانك المفضل لهذا العام هو</string>\n    <string name=\"wrapped_total_artists_title\">انت استمعت إلى</string>\n    <string name=\"wrapped_total_artists_subtitle\">فنانون فريدون</string>\n    <string name=\"wrapped_total_songs_title\">انت استمعت إلى</string>\n    <string name=\"wrapped_total_songs_subtitle\">أغاني فريدة</string>\n    <string name=\"wrapped_intro_subtitle\">حان الوقت لمعرفة ما كنت تستمع إليه</string>\n    <string name=\"wrapped_intro_button\">هيا بنا!</string>\n    <string name=\"wrapped_logo_content_description\">شعار متروليست</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"wrapped_ready_title\">ملخصك السنوي جاهز!</string>\n    <string name=\"wrapped_ready_subtitle\">حان الوقت لمعرفة ما أحببته في هذا السنة.</string>\n    <string name=\"wrapped_thank_you\">شكرًا لاستماعكم</string>\n    <string name=\"wrapped_special_thanks\">شكر خاص لمصطفي العجمي لإنشاء متروليست</string>\n    <string name=\"wrapped_close\">اغلاق ملخص السنة</string>\n    <string name=\"wrapped_top_artist_image_content_description\">صورة الفنان الرئيسي</string>\n    <string name=\"wrapped_top_artist_listening_time\">لقد استمعت إليهم لمدة %d دقيقة</string>\n    <string name=\"wrapped_top_song_title\">أغنيتك الأكثر استماعاً هي</string>\n    <string name=\"wrapped_top_song_listening_time\">لقد استمعت لمدة %d دقيقة</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_playlist_title\">ملخصك السنوي لـ %s</string>\n    <string name=\"wrapped_create_playlist\">إنشاء قائمة تشغيل</string>\n    <string name=\"wrapped_playlist_saved\">تم حفظ قائمة التشغيل</string>\n    <string name=\"casting_to\">التحويل إلى %s</string>\n    <string name=\"progress_percent\">التقدم %s%%</string>\n    <string name=\"listening_to_metrolist\">الاستماع إلى متروليست</string>\n    <string name=\"open\">فتح</string>\n    <string name=\"failed_to_create_image\">فشل إنشاء الصورة: %s</string>\n    <string name=\"failed_to_parse_proxy\">فشل تحليل عنوان URL للوكيل.</string>\n    <string name=\"copied_title\">العنوان منسوخ</string>\n    <string name=\"copied_artist\">الفنان منسوخ</string>\n    <string name=\"error_playing\">حدث خطأ أثناء التشغيل</string>\n    <string name=\"lyrics_glow_effect\">تفعيل تأثير الكلمات المتوهجة</string>\n    <string name=\"lyrics_glow_effect_desc\">أضف تأثيرات متحركة متوهجة وارتدادية إلى كلمات الأغاني المتحركه</string>\n    <string name=\"wavy\">متموج</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"zero\">%d ملف تعريف</item>\n        <item quantity=\"one\">%d ملف تعريف</item>\n        <item quantity=\"two\">%d ملفا تعريف</item>\n        <item quantity=\"few\">%d ملفات تعريف</item>\n        <item quantity=\"many\">%d ملف تعريف</item>\n        <item quantity=\"other\">%d ملف تعريف</item>\n    </plurals>\n    <string name=\"equalizer_header\">مُعادل</string>\n    <string name=\"no_profiles\">لا توجد ملفات تعريف للمعادل</string>\n    <string name=\"import_profile\">ملف تعريف الاستيراد</string>\n    <string name=\"eq_disabled\">معطّل</string>\n    <plurals name=\"band_count\">\n        <item quantity=\"zero\">%d نطاق</item>\n        <item quantity=\"one\">%d نطاق</item>\n        <item quantity=\"two\">%d نطاقين</item>\n        <item quantity=\"few\">%d نطاقات</item>\n        <item quantity=\"many\">%d نطاقًا</item>\n        <item quantity=\"other\">%d نطاق</item>\n    </plurals>\n    <string name=\"delete_profile_desc\">حذف الملف الشخصي</string>\n    <string name=\"delete_profile_confirmation\">هل أنت متأكد من رغبتك في حذف %1$s؟ لا يمكن التراجع عن هذا الإجراء.</string>\n    <string name=\"error_file_read\">تعذر قراءة الملف</string>\n    <string name=\"error_file_open\">فشل فتح الملف: %1$s</string>\n    <string name=\"import_error_title\">خطأ في الاستيراد</string>\n    <string name=\"pause_music_when_media_is_muted\">أوقف تشغيل الموسيقى مؤقتًا عند كتم صوت الوسائط</string>\n    <string name=\"enable_simpmusic\">تفعيل كلمات أغاني SimpMusic</string>\n    <string name=\"enable_simpmusic_desc\">كلمات الأغاني المتزامنة تلقائيًا\\nمن Musixmatch ونص YouTube</string>\n    <string name=\"about_artist\">حول</string>\n    <string name=\"show_more\">عرض المزيد</string>\n    <string name=\"show_less\">عرض القليل</string>\n    <string name=\"artist_page_settings\">صفحة الفنان</string>\n    <string name=\"show_artist_description\">عرض وصف الفنان</string>\n    <string name=\"show_artist_subscriber_count\">عرض عدد المشتركين</string>\n    <string name=\"show_artist_monthly_listeners\">عدد المستمعين الشهريين</string>\n    <string name=\"skip_silence_desc\">قم بتخطي الأجزاء الصامتة من الأغاني</string>\n    <string name=\"skip_silence_instant\">تجاوز الصمت فوراً</string>\n    <string name=\"skip_silence_instant_desc\">قم بتخطي اللحظات الصامتة بدلاً من تسريع التشغيل</string>\n    <string name=\"remember_shuffle_and_repeat\">تذكر خلط الاغاني و تكرار الأغنية</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">تذكر وضع خلط الاغاني و تكرار الاغنية بعد إعادة تشغيل التطبيق</string>\n    <string name=\"lyrics_offset\">ازاحة كلمات الأغنية</string>\n    <string name=\"system_equalizer\">معادل النظام</string>\n    <string name=\"album_art\">صورة الالبوم</string>\n    <string name=\"no_song_playing\">لا اغنية حاليا</string>\n    <string name=\"tap_to_open\">اضغط لفتح تطبيق متروليست</string>\n    <string name=\"previous\">السابق</string>\n    <string name=\"play_pause\">تشغيل\\\\ايقاف مؤقت</string>\n    <string name=\"next\">التالي</string>\n    <string name=\"like\">اعجاب</string>\n    <string name=\"widget_description\">أداة تشغيل الموسيقى مع عناصر تحكم التشغيل</string>\n    <string name=\"turntable_widget_description\">أداة موسيقى دائرية تحتوي على أزرار التشغيل والإعجاب</string>\n    <string name=\"persistent_shuffle_title\">التشغيل العشوائي المستمر</string>\n    <string name=\"persistent_shuffle_desc\">الإبقاء على التشغيل العشوائي مفعّلًا عند بدء أغانٍي أو قوائم تشغيل جديدة</string>\n    <string name=\"error_title\">خطأ</string>\n    <string name=\"error_eq_apply_failed\">فشل تطبيق ملف تعريف المعادل الصوتي : EQ %1$s</string>\n    <string name=\"error_playback_failed\">فشل التشغيل</string>\n    <string name=\"crop_album_art\">اقتصاص صورة غلاف الألبوم</string>\n    <string name=\"crop_album_art_desc\">فرض نسبة عرض إلى ارتفاع مربعة عن طريق اقتصاص صور معاينة الفيديو</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">إبقاء الشاشة مفتوحة عند توسيع مشغل الفيديو</string>\n    <string name=\"listen_together\">استمعوا معًا</string>\n    <string name=\"listen_together_server_url\">عنوان URL للخادم</string>\n    <string name=\"listen_together_username\">اسم المستخدم</string>\n    <string name=\"listen_together_connected\">متصل</string>\n    <string name=\"listen_together_reconnecting\">إعادة الاتصال…</string>\n    <string name=\"listen_together_disconnected\">غير متصل</string>\n    <string name=\"listen_together_connecting\">جارٍ الاتصال…</string>\n    <string name=\"listen_together_error\">خطأ في الاتصال</string>\n    <string name=\"listen_together_create_room\">إنشاء غرفة</string>\n    <string name=\"listen_together_create_room_desc\">أنشئ غرفة وشارك الكود مع الأصدقاء</string>\n    <string name=\"listen_together_join_room\">الانضمام إلى الغرفة</string>\n    <string name=\"listen_together_room_code\">رمز الغرفة</string>\n    <string name=\"listen_together_you_are_host\">أنت المضيف</string>\n    <string name=\"listen_together_you_are_guest\">أنت ضيف</string>\n    <string name=\"mute\">كتم الصوت</string>\n    <string name=\"unmute\">إلغاء كتم الصوت</string>\n    <string name=\"listen_together_join_requests\">طلبات الانضمام</string>\n    <string name=\"listen_together_view_logs\">عرض السجلات</string>\n    <string name=\"listen_together_view_logs_desc\">تصحيح اتصال و\\nرسائل</string>\n    <string name=\"listen_together_logs\">سجلات الاتصال</string>\n    <string name=\"listen_together_no_logs\">لا توجد سجلات حتى الآن</string>\n    <string name=\"listen_together_description\">استمع إلى الموسيقى مع أصدقائك في الوقت الفعلي. أنشئ غرفة لتكون المضيف أو انضم إلى غرفة موجودة باستخدام رمز</string>\n    <string name=\"listen_together_background_disconnect_note\">ملاحظة: قد يتم قطع اتصالك إذا أنشأت غرفة أثناء عدم تشغيل أي موسيقى ثم انتقلت إلى تطبيق آخر.</string>\n    <string name=\"listen_together_not_configured\">لم يتم تكوين ميزة \\\"الاستماع معًا\\\". يُرجى إعداد عنوان URL للخادم في الإعدادات ← التكاملات ← الاستماع معًا.</string>\n    <string name=\"listen_together_suggestion_received\">%1$s طلب %2$s</string>\n    <string name=\"listen_together_suggestion_sent\">تم إرسال اقتراح إلى المضيف!</string>\n    <string name=\"listen_together_join_request_notification\">يريد %1$s الانضمام إلى الغرفة</string>\n    <string name=\"listen_together_notification_channel_name\">استمعوا معًا</string>\n    <string name=\"listen_together_notification_channel_desc\">إشعارات الاستماع أحداث معًا</string>\n    <string name=\"listen_together_room_created\">تم إنشاء الغرفة: %s</string>\n    <string name=\"listen_together_cannot_edit_username_in_room\">لا يمكن تعديل اسم المستخدم أثناء التواجد في غرفة</string>\n    <string name=\"waiting_for_approval\">في انتظار موافقة المضيف</string>\n    <string name=\"invalid_room_code\">رمز غرفة غير صالح</string>\n    <string name=\"join_request_denied\">تم رفض طلب الانضمام</string>\n    <string name=\"join_existing_room\">الانضمام إلى غرفة موجودة</string>\n    <string name=\"room_code\">رمز الغرفة</string>\n    <string name=\"leave_room\">مغادرة الغرفة</string>\n    <string name=\"join_room\">انضم</string>\n    <string name=\"create_room\">إنشاء</string>\n    <string name=\"joining_room\">الانضمام إلى الغرفة %s…</string>\n    <string name=\"creating_room\">إنشاء غرفة…</string>\n    <string name=\"connect\">اتصال</string>\n    <string name=\"disconnect\">قطع الاتصال</string>\n    <string name=\"create\">إنشاء</string>\n    <string name=\"join\">انضم</string>\n    <string name=\"approve\">أوافق</string>\n    <string name=\"reject\">رفض</string>\n    <string name=\"clear\">واضح</string>\n    <string name=\"copy\">نسخ</string>\n    <string name=\"copied_to_clipboard\">نُسخ إلى الحافظة</string>\n    <string name=\"not_set\">غير محدد</string>\n    <string name=\"hosting_room\">غرفة الاستضافة</string>\n    <string name=\"in_room\">في الغرفة</string>\n    <string name=\"pending_requests\">الطلبات المعلقة</string>\n    <string name=\"pending_suggestions\">الاقتراحات المعلقة</string>\n    <string name=\"suggest_to_host\">اقتراح الاستضافة</string>\n    <string name=\"kick_user\">طرد</string>\n    <string name=\"host_label\">مضيف</string>\n    <string name=\"you_label\">انت</string>\n    <string name=\"connected_users\">المستخدمون المتصلون</string>\n    <string name=\"enter_username\">أدخل اسم المستخدم</string>\n    <string name=\"error_username_empty\">اسم المستخدم مطلوب.</string>\n    <string name=\"resync\">إعادة مزامنة</string>\n    <string name=\"crash_title\">تعطل التطبيق</string>\n    <string name=\"crash_description\">حدث خطأ غير متوقع يرجى مشاركة تقرير العطل لمساعدتنا في حل المشكلة.</string>\n    <string name=\"crash_share_logs\">مشاركة السجلات</string>\n    <string name=\"crash_share_title\">مشاركة تقرير التعطل</string>\n    <string name=\"crash_report_subject\">تقرير تعطل تطبيق Metrolist</string>\n    <string name=\"crash_close\">إغلاق</string>\n    <string name=\"crash_no_log\">لا يوجد سجل أعطال متاح</string>\n    <string name=\"palette_dynamic\">ديناميكي</string>\n    <string name=\"palette_crimson\">قرمزي</string>\n    <string name=\"palette_rose\">روز</string>\n    <string name=\"palette_purple\">بنفسجي</string>\n    <string name=\"palette_deep_purple\">بنفسجي داكن</string>\n    <string name=\"palette_indigo\">نيلي</string>\n    <string name=\"palette_blue\">أزرق</string>\n    <string name=\"palette_sky_blue\">أزرق سماوي</string>\n    <string name=\"palette_cyan\">سماوي</string>\n    <string name=\"palette_teal\">أزرق مخضر</string>\n    <string name=\"palette_green\">أخضر</string>\n    <string name=\"palette_light_green\">أخضر فاتح</string>\n    <string name=\"palette_lime\">ليموني</string>\n    <string name=\"palette_yellow\">أصفر</string>\n    <string name=\"palette_amber\">كهرماني</string>\n    <string name=\"palette_orange\">برتقالي</string>\n    <string name=\"palette_deep_orange\">برتقالي داكن</string>\n    <string name=\"palette_brown\">بني</string>\n    <string name=\"palette_grey\">رمادي</string>\n    <string name=\"palette_blue_grey\">أزرق رمادي</string>\n    <string name=\"cd_back\">رجوع</string>\n    <string name=\"cd_pure_black_mode\">وضع الأسود النقي</string>\n    <string name=\"cd_light_mode\">الوضع الفاتح</string>\n    <string name=\"cd_dark_mode\">الوضع الداكن</string>\n    <string name=\"cd_system_mode\">وضع النظام</string>\n    <string name=\"cd_palette_item\">لوحة ألوان %1$s</string>\n    <string name=\"not_playing\">لا يتم تشغيل أي أغنية</string>\n    <string name=\"tap_to_play\">اضغط لفتح Metrolist</string>\n    <string name=\"widget_music_player\">مشغل موسيقى</string>\n    <string name=\"widget_turntable\">القرص الدوار</string>\n    <string name=\"listen_together_choose_server\">اختر خادمًا</string>\n    <string name=\"listen_together_custom_server\">خادم مخصص</string>\n    <string name=\"listen_together_use_custom_server\">استخدام خادم مخصص</string>\n    <string name=\"listen_together_auto_approval_joins\">الموافقة التلقائية على طلبات الانضمام</string>\n    <string name=\"listen_together_auto_approval_joins_desc\">الموافقة التلقائية على طلبات الانضمام بدلاً من مراجعتها يدويًا</string>\n    <string name=\"listen_together_sync_volume\">مزامنة وحدة تخزين المضيف</string>\n    <string name=\"listen_together_sync_volume_desc\">مستوى صوت الضيوف مطابق لمستوى صوت المضيف</string>\n    <string name=\"copy_code\">نسخ الكود</string>\n    <string name=\"kick_user_desc\">إزالة هذا الشخص من الجلسة</string>\n    <string name=\"permanently_kick_user\">حظر دائم</string>\n    <string name=\"permanently_kick_user_desc\">حظر طلبات انضمام هذا الشخص وإخفاء اقتراحاته</string>\n    <string name=\"transfer_ownership\">نقل الملكية</string>\n    <string name=\"transfer_ownership_desc\">اجعل هذا الشخص مضيف الغرفة</string>\n    <string name=\"manage_user\">إدارة المستخدم</string>\n    <string name=\"listen_together_blocked_users\">المستخدمون المحظورون</string>\n    <string name=\"listen_together_blocked_users_count\">تم حظر %d مستخدم (مستخدمين)</string>\n    <string name=\"listen_together_no_blocked_users\">لا يوجد مستخدمون محظورون</string>\n    <string name=\"unblock\">إلغاء الحظر</string>\n    <string name=\"user_blocked_by_host\">تم حظر المستخدم من قبل المضيف</string>\n    <string name=\"recognize_music\">التعرف على الموسيقى</string>\n    <string name=\"youtube_url_column\">خانة رابط يوتيوب (اختياري)</string>\n    <string name=\"ai_translation_mode\">وضع الترجمة</string>\n    <string name=\"ai_model\">نموذج</string>\n    <string name=\"re_listen\">إعادة الاستماع</string>\n    <string name=\"ai_translating_lyrics\">ترجمة كلمات الأغاني...</string>\n    <string name=\"ai_api_key\">مفتاح API</string>\n    <string name=\"clear_recognition_history_confirm\">هل أنت متأكد من رغبتك في مسح جميع سجلات التعرف؟</string>\n    <string name=\"no_match_found\">لم يتم العثور على تطابق</string>\n    <string name=\"ai_provider\">الموفر</string>\n    <string name=\"delete_from_history\">حذف من السجل</string>\n    <string name=\"artist_name_column\">خانة اسم الفنان</string>\n    <string name=\"processing\">جارٍ المعالجة…</string>\n    <string name=\"ai_translation_transcribed\">النص المكتوب</string>\n    <string name=\"ai_target_language\">اللغة المستهدفة</string>\n    <string name=\"clear_recognition_history\">مسح سجل التعرف</string>\n    <string name=\"ai_translation_literal\">ترجمة</string>\n    <string name=\"ai_setup_guide\">بيانات اعتماد واجهة برمجة التطبيقات</string>\n    <string name=\"ai_lyrics_translation\">ترجمة كلمات الأغاني باستخدام الذكاء الاصطناعي</string>\n    <string name=\"ai_error_translation_failed\">فشلت الترجمة</string>\n    <string name=\"map_csv_columns\">تعيين خانات CSV</string>\n    <string name=\"together\">معًا</string>\n    <string name=\"ai_error_no_lyrics\">لا توجد كلمات للترجمة</string>\n    <string name=\"ai_lyrics_translated\">تمت ترجمة كلمات الأغنية</string>\n    <string name=\"column_label\">خانة %d</string>\n    <string name=\"ai_error_lyrics_empty\">كلمات الأغنية فارغة</string>\n    <string name=\"recognition_error\">خطأ في التعرف</string>\n    <string name=\"ai_error_api_key_required\">مفتاح API مطلوب</string>\n    <string name=\"ai_error_unknown\">حدث خطأ غير معروف</string>\n    <string name=\"enable_high_refresh_rate_desc\">فرض تشغيل الشاشة بأعلى معدل تحديث مدعوم (مثل 120 هرتز)</string>\n    <string name=\"ai_error_language_required\">اللغة المستهدفة مطلوبة</string>\n    <string name=\"first_row_is_header\">الصف الأول هو عنوان</string>\n    <string name=\"try_again\">حاول مرة أخرى</string>\n    <string name=\"tap_to_recognize\">انقر للتعرف</string>\n    <string name=\"recognition_history\">سجل التعرف</string>\n    <string name=\"listen_together_settings_desc\">تكوين الخادم واسم المستخدم والمزيد</string>\n    <string name=\"enable_high_refresh_rate\">تفعيل معدل التحديث العالي</string>\n    <string name=\"song_title_column\">خانة عنوان الأغنية</string>\n    <string name=\"recently_converted\">تم تحويله مؤخرًا</string>\n    <string name=\"importing_csv\">استيراد ملف CSV</string>\n    <string name=\"play_on_app\">تشغيل على Metrolist</string>\n    <string name=\"listening\">الاستماع…</string>\n    <string name=\"ai_api_key_required\">مفتاح API مطلوب</string>\n    <string name=\"ai_error_unexpected\">نتيجة ترجمة غير متوقعة</string>\n    <string name=\"enter_room_code\">أدخل رمز الغرفة</string>\n    <string name=\"continue_action\">متابعة</string>\n    <string name=\"ai_base_url\">عنوان URL الأساسي</string>\n    <string name=\"play_all\">تشغيل الكل</string>\n    <string name=\"enable\">تفعيل</string>\n    <string name=\"crossfade\">التلاشي المتقاطع</string>\n    <string name=\"crossfade_desc\">انتقال تدريجي بين الأغاني</string>\n    <string name=\"crossfade_duration\">مدة التلاشي التدريجي</string>\n    <string name=\"crossfade_gapless\">تعطيل للألبومات بدون فواصل</string>\n    <string name=\"crossfade_gapless_desc\">لا تقم بالتلاشي التدريجي إذا كان الألبوم بدون فواصل</string>\n    <string name=\"crossfade_beta_title\">ميزة تجريبية</string>\n    <string name=\"crossfade_beta_message\">التلاشي التدريجي ميزة جديدة وقد تحتوي على أخطاء إذا واجهت أي مشاكل يُرجى الإبلاغ عنها هذه الميزة تُعطّل إخراج الصوت بسبب قيود تقنية</string>\n    <string name=\"audio_offload_disabled_by_crossfade\">معطل لأن التلاشي المتقاطع نشط</string>\n    <string name=\"hide_youtube_shorts\">إخفاء مقاطع يوتيوب القصيرة</string>\n    <string name=\"listen_together_in_top_bar\">استمع معًا من شريط الأعلى</string>\n    <string name=\"listen_together_in_top_bar_desc\">عرض استمعوا معًا في شريط التطبيق العلوي بدلاً من شريط التنقل</string>\n    <string name=\"player_background_solid\">ثابت</string>\n    <string name=\"prevent_duplicate_tracks_in_queue\">منع المسارات المكررة في قائمة الانتظار</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">عند إضافة مسار إلى قائمة الانتظار قم بإزالته من موضعه السابق إذا كان موجودًا بالفعل</string>\n    <string name=\"resume_on_bluetooth_connect\">استئناف الاتصال عبر البلوتوث</string>\n    <string name=\"lyrics_romanize_hindi\">كلمات الأغاني الهندية بالرومانية</string>\n    <string name=\"lyrics_romanize_punjabi\">كلمات الأغاني البنجابية بالرومنة</string>\n    <string name=\"lyrics_romanize_as_main\">عرض كلمات الأغاني بالحروف اللاتينية كنص رئيسي</string>\n    <string name=\"ai_translation_literal_desc\">ترجمة المعنى إلى اللغة المستهدفة</string>\n    <string name=\"ai_translation_transcribed_desc\">تحويل النطق إلى النص المستهدف</string>\n    <string name=\"ai_provider_help\">الحصول على مفاتيح API</string>\n    <string name=\"ai_provider_openrouter_help\">تفضل بزيارة https://openrouter.ai للحصول على نماذج مجانية ومدفوعة</string>\n    <string name=\"ai_provider_openai_help\">زيارة\\nhttps://platform.openai.com/api-keys</string>\n    <string name=\"ai_provider_claude_help\">زيارة\\nhttps://console.anthropic.com/setti ngs/keys</string>\n    <string name=\"ai_provider_gemini_help\">زيارة\\nhttps://aistudio.google.com/apikey</string>\n    <string name=\"ai_provider_perplexity_help\">زيارة\\nhttps://perplexity.ai/settings/api</string>\n    <string name=\"ai_provider_xai_help\">تفضل بزيارة https://console.x.ai</string>\n    <string name=\"ai_provider_deepl_help\">تفضل بزيارة https://deepl.com/pro-api للحصول على مفاتيح مجانية ومدفوعة</string>\n    <string name=\"ai_deepl_formality\">رسمية</string>\n    <string name=\"ai_deepl_formality_default\">افتراضي</string>\n    <string name=\"ai_deepl_formality_more\">أكثر رسمية</string>\n    <string name=\"ai_deepl_formality_less\">أقل رسمية</string>\n    <string name=\"discord_status\">الحالة</string>\n    <string name=\"discord_status_online\">أونلاين</string>\n    <string name=\"discord_status_idle\">خامل</string>\n    <string name=\"discord_status_dnd\">ممنوع الإزعاج</string>\n    <string name=\"discord_buttons\">أزرار</string>\n    <string name=\"discord_button_1\">الزر 1</string>\n    <string name=\"discord_button_2\">الزر 2</string>\n    <string name=\"login_successful\">تم تسجيل الدخول بنجاح!</string>\n    <string name=\"discord_information_warning\">تستخدم هذه الميزة مكتبة KizzyRPC للاتصال ببوابة ديسكورد وتعيين حالة حضورك الغني. على الرغم من عدم وجود حالات تعليق حسابات معروفة ناتجة عن استخدام مماثل، إلا أن هذه الطريقة غير مدعومة رسميًا من قبل ديسكورد وقد تُعتبر انتهاكًا لشروط الخدمة. يتم استخراج رمزك محليًا ولا يتم إرساله أبدًا إلى خوادم جهات خارجية. تابع على مسؤوليتك الخاصة.</string>\n    <string name=\"discord_activity_type\">نوع النشاط</string>\n    <string name=\"discord_activity_playing\">جارٍ التشغيل</string>\n    <string name=\"discord_activity_listening\">الاستماع</string>\n    <string name=\"discord_activity_watching\">مشاهدة</string>\n    <string name=\"discord_activity_competing\">التنافس</string>\n    <string name=\"discord_button_text_variables\">{song_name} → اسم الأغنية\\n{artist_name} → اسم الفنان\\n{album_name} → اسم الألبوم</string>\n    <string name=\"discord_rpc_preview\">معاينة الحالة الغنية</string>\n    <string name=\"discord_presence\">التواجد</string>\n    <string name=\"discord_connect_description\">سجّل الدخول باستخدام Discord للمشاركة ما تستمع إليه</string>\n    <string name=\"discord_playing_metrolist\">تشغيل Metrolist</string>\n    <string name=\"discord_watching_metrolist\">مشاهدة Metrolist</string>\n    <string name=\"discord_competing_metrolist\">التنافس في Metrolist</string>\n    <string name=\"discord_activity_name\">اسم النشاط</string>\n    <string name=\"discord_activity_name_description\">اسم مخصص للنشاط (اتركه فارغًا للافتراضي)</string>\n    <string name=\"discord_advanced_mode\">الوضع المتقدم</string>\n    <string name=\"discord_advanced_mode_description\">عرض خيارات تخصيص إضافية لـ Rich Presence</string>\n    <string name=\"display_density\">كثافة الشاشة</string>\n    <string name=\"restart\">إعادة التشغيل</string>\n    <string name=\"restart_required\">إعادة التشغيل مطلوبة</string>\n    <string name=\"density_restart_message\">سيسري تغيير كثافة الشاشة بعد إعادة تشغيل التطبيق.\\nهل تريد إعادة التشغيل الآن؟</string>\n    <string name=\"found_in_settings_content\">موجود في الإعدادات &gt; المحتوى</string>\n    <string name=\"plays\">مرات التشغيل</string>\n    <string name=\"speed_dial\">الاتصال السريع</string>\n    <string name=\"pin_to_speed_dial\">رقم التعريف الشخصي للاتصال السريع</string>\n    <string name=\"unpin_from_speed_dial\">إلغاء التثبيت من الاتصال السريع</string>\n    <string name=\"randomize_home_order\">ترتيب الشاشة الرئيسية عشوائيًا</string>\n    <string name=\"randomize_home_order_desc\">إعادة ترتيب أقسام الشاشة الرئيسية عشوائيًا حسب الأهمية</string>\n    <string name=\"daily_discover_sounds_like\">صوته مثل %1$s</string>\n    <string name=\"daily_discover_because_you_listen_to\">لأنك تستمع إلى %1$s</string>\n    <string name=\"daily_discover_similar_to\">مشابه لـ %1$s</string>\n    <string name=\"daily_discover_based_on\">بناءً على %1$s</string>\n    <string name=\"daily_discover_for_fans_of\">لمحبي %1$s</string>\n    <string name=\"from_the_community\">‌من قِبل المجتمع</string>\n    <string name=\"enable_lrclib_desc\">قاعدة بيانات لكلمات الأغاني المتزامنة تعتمد على مساهمات المجتمع</string>\n    <string name=\"enable_kugou_desc\">يأخذ كلمات الأغاني من KuGou، وهي منصة موسيقى صينية شهيرة</string>\n    <string name=\"youtube_music_lyrics_note\">ملاحظة: ستظهر كلمات الأغاني من يوتيوب ميوزك تلقائيًا عندما لا تتوفر كلمات أخرى.\\nعادةً لا تتم مزامنة كلمات الأغاني من يوتيوب ميوزك.</string>\n    <string name=\"enable_lyricsplus\">تفعيل LyricsPlus</string>\n    <string name=\"enable_lyricsplus_desc\">كلمات متزامنة من مصادر متعددة</string>\n    <string name=\"lyrics_provider_selection\">اختيار الموفر</string>\n    <string name=\"lyrics_provider_selection_desc\">اختر مزودي كلمات الأغاني المفعلين</string>\n    <string name=\"lyrics_provider_priority\">أولوية مزوّد كلمات الأغاني</string>\n    <string name=\"lyrics_provider_priority_desc\">اسحب لإعادة ترتيب المزوّدين حسب التفضيل. كلما كان المزوّد في موضع أعلى كانت له أولوية أعلى.</string>\n    <string name=\"changelog\">سجل التغييرات</string>\n    <string name=\"changelog_empty\">لا توجد سجلات تغييرات متاحة</string>\n    <string name=\"github_releases_url\">https://github.com/MetrolistGroup/Metrolist/releases</string>\n    <string name=\"list_bullet\">‌•</string>\n    <string name=\"view_on_github\">عرض على GitHub</string>\n    <string name=\"current_version\">الإصدار الحالي</string>\n    <string name=\"version_format\">الإصدار: %s</string>\n    <string name=\"update_settings\">تحديث الإعدادات</string>\n    <string name=\"check_for_updates_title\">تحقق من وجود تحديثات</string>\n    <string name=\"checking_for_updates\">جارٍ التحقق من وجود تحديثات…</string>\n    <string name=\"latest_version_format\">الأحدث: %s</string>\n    <string name=\"check_for_updates_button\">التحقق من وجود تحديثات</string>\n    <string name=\"hide_changelog\">إخفاء سجل التغييرات</string>\n    <string name=\"view_changelog\">عرض سجل التغييرات</string>\n    <string name=\"failed_to_check_updates\">فشل التحقق من وجود تحديثات: %s</string>\n    <string name=\"set_as_default\">تعيين كافتراضي</string>\n    <string name=\"sleep_timer_default_set\">تم ضبط مؤقت النوم افتراضيًا على %d دقيقة</string>\n    <string name=\"error_episode_save\">فشل حفظ الحلقة</string>\n    <string name=\"error_episode_remove\">فشل حذف الحلقة</string>\n    <string name=\"error_podcast_subscribe\">فشل الاشتراك في البودكاست</string>\n    <string name=\"error_podcast_unsubscribe\">فشل إلغاء الاشتراك في البودكاست</string>\n    <string name=\"listen_together_auto_approval_suggestions\">الموافقة التلقائية على اقتراحات الأغاني</string>\n    <string name=\"listen_together_auto_approval_suggestions_desc\">الموافقة التلقائية على اقتراحات الأغاني من الضيوف ووضعها في قائمة الانتظار</string>\n    <string name=\"logout_dialog_title\">الاحتفاظ ببيانات المكتبة؟</string>\n    <string name=\"logout_dialog_message\">هل تريد الاحتفاظ بقوائم التشغيل وبيانات المكتبة؟\\nسيتم الاحتفاظ بالأغاني التي تم تنزيلها بغض النظر.</string>\n    <string name=\"logout_keep\">احتفظ</string>\n    <string name=\"logout_clear\">مسح</string>\n    <string name=\"credits_lead_developer\">مطور رئيسي</string>\n    <string name=\"importing_playlist\">استيراد قائمة التشغيل</string>\n    <string name=\"credits_collaborator\">متعاون</string>\n    <string name=\"credits_collaborators_section\">المتعاونون</string>\n    <string name=\"credits_license_name\">رخصة جنو العمومية الإصدار 3.0</string>\n    <string name=\"credits_license_desc\">برنامج مجاني ومفتوح المصدر يمكنك استخدامه ودراسته ومشاركته وتحسينه.</string>\n    <string name=\"credits_discord\">خادم ديسكورد</string>\n    <string name=\"credits_telegram\">قناة تيليجرام</string>\n    <string name=\"credits_website\">موقع ويب</string>\n    <string name=\"credits_instagram\">انستغرام</string>\n    <string name=\"credits_github\">جيت هاب</string>\n    <string name=\"credits_view_repo\">عرض المستودع</string>\n    <string name=\"app_version_info\">%1$s • %2$s</string>\n    <string name=\"like_what_i_do\">هل يعجبك ما أفعله؟</string>\n    <string name=\"buy_mo_a_coffee\">اشترِ لي قهوة</string>\n    <string name=\"community_and_info\">المجتمع والمعلومات</string>\n    <string name=\"metrolist\">METROLIST</string>\n    <string name=\"wanna_play_favorite_song\">هل تريد تشغيل أغنيتهم المفضلة؟</string>\n    <string name=\"yeah\">نعم</string>\n    <string name=\"stands_with_palestine\">هذا المشروع يقف مع فلسطين 🇵🇸</string>\n    <string name=\"filter_podcasts\">بودكاست</string>\n    <string name=\"view_podcast\">عرض البودكاست</string>\n    <string name=\"podcast_channels\">قنوات البودكاست</string>\n    <string name=\"latest_episodes\">أحدث الحلقات</string>\n    <string name=\"your_shows\">عروضك</string>\n    <string name=\"new_episodes\">حلقات جديدة</string>\n    <string name=\"episodes_for_later\">الحلقات لاحقًا</string>\n    <string name=\"save_episode_for_later\">حفظ لوقت لاحق</string>\n    <string name=\"save_episode_for_later_desc\">أضف إلى حلقاتك للمشاهدة لاحقًا قائمة التشغيل</string>\n    <string name=\"remove_episode_from_saved\">إزالة من المحفوظات</string>\n    <string name=\"subscribe_to_podcast\">حفظ البودكاست في المكتبة</string>\n    <plurals name=\"n_episode\">\n        <item quantity=\"zero\">%d حلقة</item>\n        <item quantity=\"one\">%d حلقة</item>\n        <item quantity=\"two\">%d حلقتان</item>\n        <item quantity=\"few\">%d حلقات</item>\n        <item quantity=\"many\">%d حلقة</item>\n        <item quantity=\"other\">%d حلقة</item>\n    </plurals>\n    <string name=\"restore_confirm_title\">استعادة النسخة الاحتياطية؟</string>\n    <string name=\"restore_confirm_message\">سيؤدي هذا إلى استعادة بيانات تطبيقك من النسخة الاحتياطية.</string>\n    <string name=\"restore_account_warning\">ستحتاج إلى تسجيل الدخول مرة أخرى بعد الاستعادة. سيتم تسجيل خروج الحساب التالي:</string>\n    <string name=\"restore\">استعادة</string>\n    <string name=\"checking_previous_account\">جارٍ التحقق من الحساب السابق…</string>\n    <string name=\"no_account_found\">لم يتم العثور على حساب</string>\n    <string name=\"widget_recognizer_name\">التعرف على الموسيقى</string>\n    <string name=\"widget_recognizer_description\">حدد الأغاني التي يتم تشغيلها من حولك مباشرة من شاشتك الرئيسية</string>\n    <string name=\"widget_recognizer_tap_to_search\">انقر لتحديد الأغنية</string>\n    <string name=\"widget_recognizer_listening\">جارٍ الاستماع…</string>\n    <string name=\"widget_recognizer_processing\">جارٍ تحديد…</string>\n    <string name=\"widget_recognizer_no_match\">لم يتم العثور على تطابق. حاول مرة أخرى</string>\n    <string name=\"widget_recognizer_error\">فشل التعرّف</string>\n    <string name=\"widget_recognizer_error_generic\">مرة أخرى حدث خطأ، يرجى المحاولة</string>\n    <string name=\"widget_recognizer_unknown_song\">أغنية غير معروفة</string>\n    <string name=\"widget_recognizer_unknown_artist\">فنان مجهول</string>\n    <string name=\"widget_recognizer_mic_desc\">تحديد الأغنية</string>\n    <string name=\"widget_recognizer_channel_name\">التعرف على الموسيقى</string>\n    <string name=\"widget_recognizer_channel_desc\">يعرض إشعارًا أثناء تحديد أغنية من الأداة</string>\n    <string name=\"widget_recognizer_notification_text\">تسجيل الصوت لتحديد الأغنية…</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-ar/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"artists\">فنانون</string>\n    <string name=\"albums\">ألبومات</string>\n    <string name=\"history\">السجل</string>\n    <string name=\"stats\">إحصائيات</string>\n    <string name=\"account\">حساب</string>\n    <string name=\"quick_picks\">إختيارات سريعة</string>\n    <string name=\"forgotten_favorites\">مفضلات مهملة</string>\n    <string name=\"your_youtube_playlists\">قوائم أغانيك على YouTube</string>\n    <string name=\"similar_to\">تشبه</string>\n    <string name=\"new_release_albums\">ألبومات أصدرت حديثًا</string>\n    <string name=\"today\">اليوم</string>\n    <string name=\"yesterday\">أمس</string>\n    <string name=\"last_week\">الأسبوع الماضي</string>\n    <string name=\"most_played_artists\">الفنانون الأكثر استماعًا</string>\n    <string name=\"most_played_albums\">الألبومات الأكثر استماعًا</string>\n    <string name=\"search_yt_music\">ابحث في YouTube Music…</string>\n    <string name=\"search_library\">ابحث في المكتبة…</string>\n    <string name=\"filter_liked\">أعجبك</string>\n    <string name=\"filter_downloaded\">تم تنزيلها</string>\n    <string name=\"filter_all\">جميع</string>\n    <string name=\"filter_songs\">اغاني</string>\n    <string name=\"filter_artists\">فنانون</string>\n    <string name=\"filter_playlists\">قوائم أغاني</string>\n    <string name=\"filter_community_playlists\">قوائم أغاني من مستخدمين</string>\n    <string name=\"filter_featured_playlists\">قوائم أغاني مميزة</string>\n    <string name=\"filter_bookmarked\">محفوظ</string>\n    <string name=\"no_results_found\">لم يتم العثور على نتائج</string>\n    <string name=\"library_artist_empty\">سيظهر الفنانون هنا</string>\n    <string name=\"library_album_empty\">ستظهر الألبومات هنا</string>\n    <string name=\"library_playlist_empty\">قوائم أغانيك ستظهر هنا</string>\n    <string name=\"other_versions\">نسخ اخرى</string>\n    <string name=\"songs\">أغاني</string>\n    <string name=\"playlists\">قوائم الأغاني</string>\n    <string name=\"mood_and_genres\">الأوضاع والتصنيف</string>\n    <string name=\"quick_picks_empty\">استمع إلى الأغاني لتوليد قائمة السريعة</string>\n    <string name=\"search\">ابحث</string>\n    <string name=\"keep_listening\">تابع الاستماع</string>\n    <string name=\"this_week\">الأسبوع الحالي</string>\n    <string name=\"most_played_songs\">الأغاني الأكثر استماعًا</string>\n    <string name=\"filter_library\">مكتبة</string>\n    <string name=\"filter_albums\">ألبومات</string>\n    <string name=\"filter_videos\">فيديوهات</string>\n    <string name=\"library_song_empty\">ستظهر أغاني المكتبة هنا</string>\n    <string name=\"from_your_library\">من مكتبتك</string>\n    <string name=\"home\">الرئيسية</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"zero\">%d تم اختياره</item>\n        <item quantity=\"one\">%d تم اختياره</item>\n        <item quantity=\"two\">%d تم اختيارهم</item>\n        <item quantity=\"few\">%d تم اختيارها</item>\n        <item quantity=\"many\">%d تم اختيارها</item>\n        <item quantity=\"other\">%d تم اختيارهم</item>\n    </plurals>\n    <string name=\"liked_songs\">أغاني مفضلة</string>\n    <string name=\"downloaded_songs\">أغاني تم تنزيلها</string>\n    <string name=\"playlist_is_empty\">قائمة الأغاني فارغة</string>\n    <string name=\"remove_download_playlist_confirm\">هل تريد حقًا إزالة جميع أغاني قائمة الأغاني \\\"%s\\\" من مساحة تخزين الأغاني التي تم تنزيلها؟</string>\n    <string name=\"delete_playlist_confirm\">هل تريد حقًا إزالة قائمة الأغاني \\\"%s\\\"؟</string>\n    <string name=\"retry\">أعد المحاولة</string>\n    <string name=\"radio\">راديو</string>\n    <string name=\"shuffle\">خلط</string>\n    <string name=\"reset\">إعادة إلى الوضع الأصلي</string>\n    <string name=\"details\">تفاصيل</string>\n    <string name=\"edit\">عدل</string>\n    <string name=\"start_radio\">بدء الراديو</string>\n    <string name=\"play\">شغل</string>\n    <string name=\"play_next\">تشغيل التالي</string>\n    <string name=\"add_to_library\">ضف إلى المكتبة</string>\n    <string name=\"add_to_queue\">ضف إلى قائمة الانتظار</string>\n    <string name=\"remove_from_library\">امسح من المكتبة</string>\n    <string name=\"remove_download\">مسح التنزيل</string>\n    <string name=\"import_playlist\">استورد قائمة الأغاني</string>\n    <string name=\"add_to_playlist\">ضف إلى قائمة الأغاني</string>\n    <string name=\"view_artist\">عرض الفنان</string>\n    <string name=\"view_album\">عرض الألبوم</string>\n    <string name=\"refetch\">أعد الإحضار</string>\n    <string name=\"share\">شارك</string>\n    <string name=\"delete\">امسح</string>\n    <string name=\"remove_from_history\">امسح من التاريخ</string>\n    <string name=\"remove_from_queue\">امسح من قائمة الانتظار</string>\n    <string name=\"search_online\">ابحث على الإنترنت</string>\n    <string name=\"action_sync\">مزامنة</string>\n    <string name=\"tempo_and_pitch\">سرعة الإيقاع والحدة</string>\n    <string name=\"sort_by_name\">الاسم</string>\n    <string name=\"sort_by_artist\">الفنان</string>\n    <string name=\"media_id\">الرقم التعريفي للوسيط</string>\n    <string name=\"mime_type\">نوع MIME</string>\n    <string name=\"codecs\">حزم الترميز وفك الترميز</string>\n    <string name=\"bitrate\">معدل البتات</string>\n    <string name=\"sample_rate\">معدل أخذ العينات</string>\n    <string name=\"sort_by_song_count\">عدد الأغاني</string>\n    <string name=\"volume\">مستوى الصوت</string>\n    <string name=\"unknown\">غير معروف</string>\n    <string name=\"copied\">تم النسخ إلى الحافظة</string>\n    <string name=\"edit_lyrics\">عدل الكلمات</string>\n    <string name=\"edit_song\">عدل الأغنية</string>\n    <string name=\"song_title\">اسم الأغنية</string>\n    <string name=\"error_song_title_empty\">اسم الأغنية لا يمكن أن يكون فارغًا.</string>\n    <string name=\"save\">احتفظ</string>\n    <string name=\"choose_playlist\">اختر قائمة أغاني</string>\n    <string name=\"edit_playlist\">عدل قائمة الأغاني</string>\n    <string name=\"create_playlist\">أنشأ قائمة أغاني</string>\n    <string name=\"skip_duplicates\">تخطي النسخ المكررة</string>\n    <string name=\"add_anyway\">أضف على أي حال</string>\n    <string name=\"duplicates_description_single\">الأغنية موجودة بالفعل في قائمة أغانيك</string>\n    <string name=\"duplicates_description_multiple\">%d أغنية موجودة بالفعل في قائمة التشغيل الخاصة بك</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"zero\">%d أغاني</item>\n        <item quantity=\"one\">أغنية</item>\n        <item quantity=\"two\">أغنيتان</item>\n        <item quantity=\"few\">%d أغاني</item>\n        <item quantity=\"many\">%d أغنية</item>\n        <item quantity=\"other\">%d أغنية</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"zero\">%d فنانون</item>\n        <item quantity=\"one\">فنان</item>\n        <item quantity=\"two\">فنانان</item>\n        <item quantity=\"few\">%d فنانون</item>\n        <item quantity=\"many\">%d فنان</item>\n        <item quantity=\"other\">%d فنان</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"zero\">%d قوائم أغاني</item>\n        <item quantity=\"one\">قائمة أغاني</item>\n        <item quantity=\"two\">قائمتا أغاني</item>\n        <item quantity=\"few\">%d قوائم أغاني</item>\n        <item quantity=\"many\">%d قائمة أغاني</item>\n        <item quantity=\"other\">%d قائمة أغاني</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"zero\">%d أسابيع</item>\n        <item quantity=\"one\">أسبوع</item>\n        <item quantity=\"two\">أسبوعان</item>\n        <item quantity=\"few\">%d أسابيع</item>\n        <item quantity=\"many\">%d أسبوع</item>\n        <item quantity=\"other\">%d أسبوع</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"zero\">%d شهور</item>\n        <item quantity=\"one\">شهر</item>\n        <item quantity=\"two\">شهران</item>\n        <item quantity=\"few\">%d شهور</item>\n        <item quantity=\"many\">%d شهر</item>\n        <item quantity=\"other\">%d شهر</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"zero\">%d أعوام</item>\n        <item quantity=\"one\">عام</item>\n        <item quantity=\"two\">عامان</item>\n        <item quantity=\"few\">%d أعوام</item>\n        <item quantity=\"many\">%d عام</item>\n        <item quantity=\"other\">%d عام</item>\n    </plurals>\n    <string name=\"removed_song_from_playlist\">تم مسح \\\"%s\\\" من قائمة الأغاني</string>\n    <string name=\"undo\">تراجع</string>\n    <string name=\"lyrics_not_found\">لم يتم العثور على كلمات</string>\n    <string name=\"sleep_timer\">مؤقت النوم</string>\n    <string name=\"error_timeout\">نفذ الوقت</string>\n    <string name=\"action_shuffle_on\">الخلط مفعل</string>\n    <string name=\"action_shuffle_off\">الخلط موقف</string>\n    <string name=\"repeat_mode_off\">وضع التكرار موقف</string>\n    <string name=\"repeat_mode_one\">كرر الأغنية الحالية</string>\n    <string name=\"repeat_mode_all\">كرر قائمة الانتظار</string>\n    <string name=\"queue_all_songs\">كل الأغاني</string>\n    <string name=\"queue_searched_songs\">الأغاني التي بحثت عنها</string>\n    <string name=\"music_player\">مشغل الموسيقى</string>\n    <string name=\"settings\">إعدادات</string>\n    <string name=\"appearance\">مظهر</string>\n    <string name=\"theme\">سمة</string>\n    <string name=\"enable_dynamic_theme\">فعل المظهر الديناميكي</string>\n    <string name=\"dark_theme\">الوضع المظلم</string>\n    <string name=\"dark_theme_on\">مفعل</string>\n    <string name=\"dark_theme_off\">متوقف</string>\n    <string name=\"dark_theme_follow_system\">اتبع النظام</string>\n    <string name=\"pure_black\">أسود نقي</string>\n    <string name=\"customize_navigation_tabs\">تخصيص نوافذ التنقل</string>\n    <string name=\"player\">مشغل</string>\n    <string name=\"player_text_alignment\">محاذاة نص المشغل</string>\n    <string name=\"lyrics_text_position\">موضع الكلمات</string>\n    <string name=\"sided\">جانبية</string>\n    <string name=\"left\">يسار</string>\n    <string name=\"center\">المنتصف</string>\n    <string name=\"right\">يمين</string>\n    <string name=\"player_slider_style\">طراز شريط تمرير المشغل</string>\n    <string name=\"default_\">الافتراضي</string>\n    <plurals name=\"minute\">\n        <item quantity=\"zero\">%d دقيقة</item>\n        <item quantity=\"one\">%d دقيقة</item>\n        <item quantity=\"two\">%d دقيقة</item>\n        <item quantity=\"few\">%d دقائق</item>\n        <item quantity=\"many\">%d دقيقة</item>\n        <item quantity=\"other\">%d دقيقة</item>\n    </plurals>\n    <string name=\"misc\">متنوعات</string>\n    <string name=\"default_open_tab\">النافذة الافتراضية عند الفتح</string>\n    <string name=\"small\">صغير</string>\n    <string name=\"grid_cell_size\">حجم خلايا الشبكة</string>\n    <string name=\"not_logged_in\">غير مسجل الدخول</string>\n    <string name=\"content_language\">لغة المحتوى الافتراضية</string>\n    <string name=\"enable_proxy\">فعل الوكيل</string>\n    <string name=\"proxy_type\">نوع الوكيل</string>\n    <string name=\"proxy_url\">رابط الوكيل</string>\n    <string name=\"restart_to_take_effect\">أعد التشغيل للتفعيل</string>\n    <string name=\"audio_quality_high\">عالي</string>\n    <string name=\"audio_quality_low\">منخفض</string>\n    <string name=\"queue\">قائمة الانتظار</string>\n    <string name=\"auto_load_more\">تحميل تلقائي لمزيد من الأغاني</string>\n    <string name=\"skip_silence\">تخطي الصمت</string>\n    <string name=\"audio_normalization\">تسوية الصوت</string>\n    <string name=\"auto_skip_next_on_error\">انتقل تلقائيًا إلى الأغنية التالية عند حدوث خطأ</string>\n    <string name=\"auto_skip_next_on_error_desc\">أضمن تجربة التشغيل المستمر الخاصة بك</string>\n    <string name=\"stop_music_on_task_clear\">إيقاف الموسيقى عند إيقاف تطبيقات الخلفية</string>\n    <string name=\"equalizer\">موازن الصوت</string>\n    <string name=\"max_cache_size\">أقصى مساحة لذاكرة التخزين المؤقتة</string>\n    <string name=\"unlimited\">غير محدود</string>\n    <string name=\"clear_all_downloads\">مسح كل التنزيلات</string>\n    <string name=\"max_image_cache_size\">أقصى مساحة لذاكرة تخزين الصور المؤقتة</string>\n    <string name=\"clear_image_cache\">مسح ذاكرة تخزين الصور المؤقتة</string>\n    <string name=\"max_song_cache_size\">أقصى مساحة لذاكرة تخزين الأغاني المؤقتة</string>\n    <string name=\"clear_song_cache\">مسح ذاكرة تخزين الأغاني المؤقتة</string>\n    <string name=\"size_used\">%s تم تخزينه</string>\n    <string name=\"pause_listen_history\">اوقف تأريخ الاستماع مؤقتًا</string>\n    <string name=\"clear_listen_history\">مسح تاريخ الاستماع</string>\n    <string name=\"enable_lrclib\">فعل موفر الكلمات LrcLib</string>\n    <string name=\"backup_restore\">النسخ الاحتياطي والاستعادة</string>\n    <string name=\"action_restore\">استعادة</string>\n    <string name=\"imported_playlist\">قائمة أغاني مستوردة</string>\n    <string name=\"hide_explicit\">اخفي المحتوى الصريح</string>\n    <string name=\"backup_create_failed\">تعذر إنشاء نسخة احتياطية</string>\n    <string name=\"discord_integration\">التكامل مع Discord</string>\n    <string name=\"add_all_to_library\">ضف الكل إلى المكتبة</string>\n    <string name=\"action_download\">تنزيل</string>\n    <string name=\"remove_all_from_library\">امسح الكل من المكتبة</string>\n    <string name=\"downloading\">يتم التنزيل</string>\n    <string name=\"sort_by_custom\">ترتيب مخصص</string>\n    <string name=\"sort_by_play_time\">مدة التشغيل</string>\n    <string name=\"loudness\">شدة الصوت</string>\n    <string name=\"file_size\">حجم الملف</string>\n    <string name=\"search_lyrics\">ابحث عن الكلمات</string>\n    <string name=\"song_artists\">فنانون الأغنية</string>\n    <string name=\"error_song_artist_empty\">فنان الأغنية لا يمكن أن يكون فارغًا.</string>\n    <string name=\"error_playlist_name_empty\">اسم قائمة الأغاني لا يمكن أن تكون فارغًا.</string>\n    <string name=\"playlist_name\">اسم قائمة الأغاني</string>\n    <string name=\"artist_name\">اسم الفنان</string>\n    <string name=\"duplicates\">نسخ مكررة</string>\n    <string name=\"edit_artist\">عدل الفنان</string>\n    <string name=\"error_artist_name_empty\">اسم الفنان لا يمكن أن يكون فارغًا.</string>\n    <string name=\"remove_from_playlist\">امسح من قائمة الأغاني</string>\n    <plurals name=\"n_album\">\n        <item quantity=\"zero\">%d ألبومات</item>\n        <item quantity=\"one\">ألبوم</item>\n        <item quantity=\"two\">ألبومان</item>\n        <item quantity=\"few\">%d ألبومات</item>\n        <item quantity=\"many\">%d ألبوم</item>\n        <item quantity=\"other\">%d ألبوم</item>\n    </plurals>\n    <string name=\"advanced\">متقدم</string>\n    <string name=\"sort_by_create_date\">تاريخ الإضافة</string>\n    <string name=\"sort_by_year\">السنة</string>\n    <string name=\"sort_by_length\">المدة</string>\n    <string name=\"playlist_synced\">تمت مزامنة قائمة الأغاني</string>\n    <string name=\"playlist_imported\">تم استيراد قائمة الأغاني</string>\n    <string name=\"end_of_song\">نهاية الأغنية</string>\n    <string name=\"error_no_internet\">لا يوجد اتصال بالشبكة</string>\n    <string name=\"error_no_stream\">لا يوجد تدفق متاح</string>\n    <string name=\"action_like\">أعجبتني</string>\n    <string name=\"error_unknown\">خطأ غير معروف</string>\n    <string name=\"action_like_all\">أعجبني الكل</string>\n    <string name=\"action_remove_like\">مسح الإعجاب</string>\n    <string name=\"action_remove_like_all\">مسح كل الأعجابات</string>\n    <string name=\"squiggly\">متعرج</string>\n    <string name=\"player_and_audio\">المشغل والصوت</string>\n    <string name=\"audio_quality_auto\">تلقائي</string>\n    <string name=\"login\">تسجيل الدخول</string>\n    <string name=\"big\">كبير</string>\n    <string name=\"content\">محتوى</string>\n    <string name=\"system_default\">الإعدادات الافتراضية للنظام</string>\n    <string name=\"content_country\">بلد المحتوى الافتراضية</string>\n    <string name=\"audio_quality\">جودة الصوت</string>\n    <string name=\"persistent_queue\">قائمة انتظار ثابتة</string>\n    <string name=\"persistent_queue_desc\">استعد قائمة انتظارك الأخيرة عند بدء تشغيل التطبيق</string>\n    <string name=\"auto_load_more_desc\">ضف المزيد من الأغاني تلقائيًا عند الوصول إلى نهاية قائمة الانتظار، إن أمكن</string>\n    <string name=\"storage\">مساحة التخزين</string>\n    <string name=\"cache\">ذاكرة التخزين المؤقت</string>\n    <string name=\"image_cache\">ذاكرة تخزين الصور المؤقتة</string>\n    <string name=\"song_cache\">ذاكرة تخزين الأغاني المؤقتة</string>\n    <string name=\"clear_listen_history_confirm\">هل أنت متأكد أنك تريد مسح كل سجل الاستماع؟</string>\n    <string name=\"search_history\">سجل البحث</string>\n    <string name=\"privacy\">خصوصية</string>\n    <string name=\"listen_history\">تاريخ الاستماع</string>\n    <string name=\"pause_search_history\">اوقف سحل البحث مؤقتًا</string>\n    <string name=\"clear_search_history\">مسح سجل البحث</string>\n    <string name=\"enable_kugou\">فعل موفر الكلمات KuGou</string>\n    <string name=\"clear_search_history_confirm\">هل أنت متأكد أنك تريد مسح كل سجل البحث؟</string>\n    <string name=\"disable_screenshot\">تعطيل قابلية اخذ لقطة للشاشة</string>\n    <string name=\"action_backup\">نسخة احتياطيه</string>\n    <string name=\"backup_create_success\">تم إنشاء النسخة الاحتياطية بنجاح</string>\n    <string name=\"restore_failed\">فشل في استعادة النسخة الاحتياطية</string>\n    <string name=\"options\">خيارات</string>\n    <string name=\"preview\">معاينة</string>\n    <string name=\"discord_information\">Metrolist يستخدم مكتبة KizzyRPC لتعيين حالة حساب Discord الخاص بك. هذا يتضمن استخدام اتصال Discord Gateway، والذي قد يعتبر انتهاكًا لشروط خدمة Discord. مع ذلك، لا توجد حالات معروفة لتعطيل حسابات المستخدمين لهذا السبب. استخدم على مسؤوليتك الخاصة. \\n \\nسيقوم Metrolist فقط باستخراج رمز الوصول الخاص بك، وسيتم تخزين كل شيء آخر محليًا.</string>\n    <string name=\"dismiss\">صرف</string>\n    <string name=\"enable_discord_rpc\">فعل Rich Presence</string>\n    <string name=\"new_version_available\">نسخة جديدة متوفرة</string>\n    <string name=\"login_failed\">فشل تسجيل الدخول</string>\n    <string name=\"about\">حول</string>\n    <string name=\"app_version\">نسخة التطبيق</string>\n    <string name=\"translation_models\">نماذج الترجمة</string>\n    <string name=\"action_logout\">تسجيل الخروج</string>\n    <string name=\"clear_translation_models\">مسح نماذج الترجمة</string>\n    <string name=\"disable_screenshot_desc\">عند تفعيل هذا الخيار، يتم تعطيل اخذ لقطات للشاشة وعدم اظهار محتوى التطبيق في التطبيقات الأخيرة.</string>\n    <string name=\"use_login_for_browse\">استخدم تسجيل الدخول لتصفح المحتوى</string>\n    <string name=\"use_login_for_browse_desc\">يمكن أن يؤثر ذلك على المحتوى الذي تراه، فعلى سبيل المثال، يعرض الألبومات المخصصة للمشتركين المميزين فقط إذا كنت مسجلاً الدخول باستخدام حساب مميز</string>\n    <string name=\"action_login\">تسجيل دخول</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-as/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">স্থানীয়</string>\n    <string name=\"remote_history\">দূৰৱৰ্তী</string>\n    <string name=\"charts\">চাৰ্ট</string>\n    <string name=\"back_button_desc\">পিছলৈ</string>\n    <string name=\"album_cover_desc\">এলবামৰ কভাৰ</string>\n    <string name=\"top_music_videos\">শীৰ্ষ মিউজিক ভিডিঅ\\'</string>\n    <string name=\"trending\">ট্ৰেণ্ডিং</string>\n    <string name=\"weeks\">সপ্তাহ</string>\n    <string name=\"months\">মাহ</string>\n    <string name=\"years\">বছৰ</string>\n    <string name=\"continuous\">অবিৰত</string>\n    <string name=\"liked\">ভাল লাগিল</string>\n    <string name=\"offline\">ডাউনলোড কৰা হৈছে</string>\n    <string name=\"my_top\">মোৰ টপ</string>\n    <string name=\"cached_playlist\">কেচ কৰা হৈছে</string>\n    <string name=\"uploaded_playlist\">আপলোড কৰা হৈছে</string>\n    <string name=\"filter_uploaded\">আপলোড কৰা হৈছে</string>\n    <string name=\"sync_playlist\">প্লেলিষ্ট ছিঙ্ক কৰক</string>\n    <string name=\"sync_disabled\">ছিঙ্ক নিষ্ক্ৰিয় কৰা হৈছে</string>\n    <string name=\"allows_for_sync_witch_youtube\">বি:দ্ৰ: ইয়াৰ দ্বাৰা YouTube Music ৰ সৈতে ছিংকিং কৰিব পৰা যায়। এইটো পিছত সলনি হ\\'ব নোৱাৰে।</string>\n    <string name=\"generating_image\">ছবি সৃষ্টি কৰা</string>\n    <string name=\"please_wait\">অনুগ্ৰহ কৰি অপেক্ষা কৰক</string>\n    <string name=\"cancel\">বাতিল কৰক</string>\n    <string name=\"share_lyrics\">গীতৰ শাৰী শ্বেয়াৰ কৰক</string>\n    <string name=\"share_as_text\">টেক্সট হিচাপে শ্বেয়াৰ কৰক</string>\n    <string name=\"share_as_image\">ইমেজ হিচাপে শ্বেয়াৰ কৰক</string>\n    <string name=\"max_selection_limit\">সৰ্বোচ্চ নিৰ্বাচন সীমা</string>\n    <string name=\"share_selected\">শ্বেয়াৰ নিৰ্বাচিত কৰা হৈছে</string>\n    <string name=\"customize_colors\">ৰং কাষ্টমাইজ কৰক</string>\n    <string name=\"text_color\">লিখনীৰ ৰং</string>\n    <string name=\"secondary_text_color\">দ্বিতীয় টেষ্টৰ ৰং</string>\n    <string name=\"background_color\">বেকগ্ৰাউণ্ড ৰং</string>\n    <string name=\"remove_from_cache\">কেচৰ পৰা আঁতৰাওক</string>\n    <string name=\"about_artist\">বিষয়</string>\n    <string name=\"show_more\">অধিক দেখুৱাওক</string>\n    <string name=\"show_less\">কম দেখুৱাওক</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-az/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">Daxili</string>\n    <string name=\"remote_history\">Server tərəfi</string>\n    <string name=\"charts\">Çartlar</string>\n    <string name=\"back_button_desc\">Geriyə</string>\n    <string name=\"album_cover_desc\">Albom üzlüyü</string>\n    <string name=\"top_music_videos\">Yüksək reytinqli kliplər</string>\n    <string name=\"trending\">Trenddə olanlar</string>\n    <string name=\"weeks\">Həftələr</string>\n    <string name=\"months\">Aylar</string>\n    <string name=\"years\">İllər</string>\n    <string name=\"continuous\">Davamlı</string>\n    <string name=\"liked\">Sevimli</string>\n    <string name=\"offline\">Yüklənmiş</string>\n    <string name=\"my_top\">Mənim ən sevimlilərim</string>\n    <string name=\"cached_playlist\">Keşə yüklənmiş</string>\n    <string name=\"sync_playlist\">Çalğı siyahısını sinxronizasiya et</string>\n    <string name=\"sync_disabled\">Sinxronizasiya qeyri-aktivdir</string>\n    <string name=\"allows_for_sync_witch_youtube\">Qeyd: Bu, YouTube Music ilə sinxronizasiya etməyə imkan verir. Bu sonradan dəyişdirilə bilməz.</string>\n    <string name=\"generating_image\">Şəkil yaradılır</string>\n    <string name=\"please_wait\">Zəhmət olmasa gözləyin</string>\n    <string name=\"cancel\">Ləğv et</string>\n    <string name=\"share_lyrics\">Mahnı sözlərini paylaşın</string>\n    <string name=\"share_as_text\">Mətn kimi paylaş</string>\n    <string name=\"share_as_image\">Şəkil kimi paylaş</string>\n    <string name=\"max_selection_limit\">Maksimum seçim limiti</string>\n    <string name=\"share_selected\">Seçilmişləri paylaş</string>\n    <string name=\"customize_colors\">Rəngləri fərdiləşdirin</string>\n    <string name=\"text_color\">Mətnin rəngi</string>\n    <string name=\"secondary_text_color\">İkinci dərəcəli mətn rəngi</string>\n    <string name=\"background_color\">Fon rəngi</string>\n    <string name=\"remove_from_cache\">Keşdən sil</string>\n    <string name=\"copy_link\">Linki kopyala</string>\n    <string name=\"select\">Hamısın seç</string>\n    <string name=\"like_all\">Hamısını bəyən</string>\n    <string name=\"dislike_all\">Hamısını bəyənmə</string>\n    <string name=\"sort_by_last_updated\">Yenilənmə tarixi</string>\n    <string name=\"link_copied\">Link bufetdə kopyalandı</string>\n    <string name=\"starting_radio\">Radio işə salınır</string>\n    <string name=\"now_playing\">İndi oynayır</string>\n    <string name=\"lyrics\">Mahnı sözləri</string>\n    <string name=\"close\">Bağla</string>\n    <string name=\"hide_player_thumbnail\">Tətbiqin miniatürünü gizlədin</string>\n    <string name=\"hide_player_thumbnail_desc\">Albom təsvirini pleyerdə tətbiq loqosu ilə əvəz edin</string>\n    <string name=\"already_in_playlist\">Artıq calğı siyahısında olanlar:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d dəfə</item>\n        <item quantity=\"other\">%d dəfələr</item>\n    </plurals>\n    <string name=\"seek_forward_dynamic\">%1$d saniyə irəli</string>\n    <string name=\"seek_backward_dynamic\">%1$d saniyə geriyə</string>\n    <string name=\"seek_seconds_addup\">Proqressiv axtarış</string>\n    <string name=\"seek_seconds_addup_description\">Aktivdirsə, hər axtarış atlamasında tədricən 5 əlavə saniyə əlavə edir</string>\n    <string name=\"similar_content\">Oxşar kontent</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-b+sr+Latn/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"likes\">Lajkovi</string>\n    <string name=\"local_history\">Lokalna</string>\n    <string name=\"remote_history\">Udaljena</string>\n    <string name=\"charts\">Liste</string>\n    <string name=\"back_button_desc\">Nazad</string>\n    <string name=\"album_cover_desc\">Omot albuma</string>\n    <string name=\"top_music_videos\">Najgledaniji muzički spotovi</string>\n    <string name=\"trending\">U trendu</string>\n    <string name=\"weeks\">Nedelje</string>\n    <string name=\"months\">Meseci</string>\n    <string name=\"years\">Godine</string>\n    <string name=\"continuous\">Kontinuirano</string>\n    <string name=\"liked\">Lajkovano</string>\n    <string name=\"my_top\">Moje najbolje</string>\n    <string name=\"offline\">Preuzeto</string>\n    <string name=\"cached_playlist\">Keširano</string>\n    <string name=\"sync_playlist\">Sinhronizuj plejlistu</string>\n    <string name=\"sync_disabled\">Sinhronizacija onemogućena</string>\n    <string name=\"allows_for_sync_witch_youtube\">Napomena: Ovo omogućava sinhronizaciju sa YouTube Music. Ovo se NE MOŽE promeniti kasnije.</string>\n    <string name=\"generating_image\">Generišem sliku</string>\n    <string name=\"please_wait\">Molimo sačekajte</string>\n    <string name=\"cancel\">Otkaži</string>\n    <string name=\"share_lyrics\">Podeli tekst pesme</string>\n    <string name=\"share_as_text\">Podeli kao tekst</string>\n    <string name=\"share_as_image\">Podeli kao sliku</string>\n    <string name=\"max_selection_limit\">Maksimalni broj izbora</string>\n    <string name=\"share_selected\">Podeli izabrano</string>\n    <string name=\"customize_colors\">Izmeni boje</string>\n    <string name=\"text_color\">Boja teksta</string>\n    <string name=\"secondary_text_color\">Sekundarna boja teksta</string>\n    <string name=\"background_color\">Boja pozadine</string>\n    <string name=\"remove_from_cache\">Ukloni iz keša</string>\n    <string name=\"copy_link\">Kopiraj link</string>\n    <string name=\"select\">Izaberi sve</string>\n    <string name=\"like_all\">Obeleži sve kao omiljeno</string>\n    <string name=\"dislike_all\">Poništi sve lajkove</string>\n    <string name=\"sort_by_last_updated\">Datum ažuriran</string>\n    <string name=\"link_copied\">Link je kopiran</string>\n    <string name=\"lyrics\">Tekst pesme</string>\n    <string name=\"already_in_playlist\">Već je u plejlisiti:</string>\n    <string name=\"similar_content\">Sličan sadržaj</string>\n    <string name=\"player_background_style\">Stil pozadine plejera</string>\n    <string name=\"follow_theme\">Prati temu</string>\n    <string name=\"gradient\">Gradijent</string>\n    <string name=\"player_background_blur\">Zamućenje</string>\n    <string name=\"player_buttons_style\">Boje dugmadi plejera</string>\n    <string name=\"default_style\">Podrazumevano</string>\n    <string name=\"enable_swipe_thumbnail\">Omogući prevlačenje za promenu pesme</string>\n    <string name=\"swipe_song_to_add\">Prevuci pesmu ulevo da je dodaš u red, ili udesno da je pustiš odmah posle</string>\n    <string name=\"lyrics_click_change\">Promeni tekst pesme na klik</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">%d sekunda</item>\n        <item quantity=\"few\">%d sekunde</item>\n        <item quantity=\"other\">%d sekundi</item>\n    </plurals>\n    <string name=\"starting_radio\">Pokretanje radija</string>\n    <string name=\"now_playing\">Trentuno pušteno</string>\n    <string name=\"hide_player_thumbnail\">Sakrij ikonicu plejera</string>\n    <string name=\"seek_forward_dynamic\">+%1$d sekundi unapred</string>\n    <string name=\"seek_backward_dynamic\">-%1$d sekundi unazad</string>\n    <string name=\"seek_seconds_addup\">Napredna pretraga</string>\n    <string name=\"uploaded_playlist\">Poslato</string>\n    <string name=\"filter_uploaded\">Poslato</string>\n    <string name=\"download_playlist_desc\">Preuzmi sve pesme za reprodukciju van mreže</string>\n    <string name=\"remove_download_playlist_desc\">Obriši sve preuzete pesme iz ove plejliste</string>\n    <string name=\"download_in_progress_desc\">Preuzimanje je u toku</string>\n    <string name=\"share_playlist_desc\">Podeli ovu plejlistu sa drugima</string>\n    <string name=\"sync_playlist_desc\">Sinhronizuj plejlistu sa Youtube Music</string>\n    <string name=\"close\">Zatvori</string>\n    <string name=\"seek_seconds_addup_description\">Kada je aktivirano, dodaje 5 sekundi za svako preskakanje traženja</string>\n    <string name=\"new_player_design\">Novi dizajn plejera</string>\n    <string name=\"new_mini_player_design\">Novi dizajn mini-plejera</string>\n    <string name=\"primary_color_style\">Primarna boja</string>\n    <string name=\"tertiary_color_style\">Tercijalna boja</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-b+sr+Latn/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Početna</string>\n    <string name=\"songs\">Pesme</string>\n    <string name=\"artists\">Umetnici</string>\n    <string name=\"albums\">Albumi</string>\n    <string name=\"playlists\">Plejliste</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d izabran</item>\n        <item quantity=\"few\">%d izabrana</item>\n        <item quantity=\"other\">%d izabrano</item>\n    </plurals>\n    <string name=\"history\">Istorija</string>\n    <string name=\"stats\">Statistike</string>\n    <string name=\"mood_and_genres\">Raspoloženje i Žanrovi</string>\n    <string name=\"account\">Račun</string>\n    <string name=\"quick_picks\">Brzi izbori</string>\n    <string name=\"quick_picks_empty\">Odslušaj pesme kako bi generisao tvoje brze izbore</string>\n    <string name=\"forgotten_favorites\">Zaboravljeni favoriti</string>\n    <string name=\"keep_listening\">Nastavi slušanje</string>\n    <string name=\"your_youtube_playlists\">Tvoje YouTube plejliste</string>\n    <string name=\"similar_to\">Slično ko</string>\n    <string name=\"new_release_albums\">Novi albumi</string>\n    <string name=\"today\">Danas</string>\n    <string name=\"yesterday\">Juče</string>\n    <string name=\"this_week\">Ove nedelje</string>\n    <string name=\"last_week\">Prošle nedelje</string>\n    <string name=\"most_played_songs\">Najslušanije pesme</string>\n    <string name=\"most_played_artists\">Najslušaniji izvođači</string>\n    <string name=\"most_played_albums\">Najslušaniji albumi</string>\n    <string name=\"search\">Pretraži</string>\n    <string name=\"search_yt_music\">Pretraži YouTube Music…</string>\n    <string name=\"filter_downloaded\">Preuzeto</string>\n    <string name=\"filter_all\">Sve</string>\n    <string name=\"filter_songs\">Pesme</string>\n    <string name=\"filter_videos\">Videi</string>\n    <string name=\"filter_albums\">Albumi</string>\n    <string name=\"filter_artists\">Izvođači</string>\n    <string name=\"filter_playlists\">Plejliste</string>\n    <string name=\"filter_community_playlists\">Plejliste zajednice</string>\n    <string name=\"filter_featured_playlists\">Istaknute plejliste</string>\n    <string name=\"filter_bookmarked\">Obeleženo</string>\n    <string name=\"no_results_found\">Nema rezultata</string>\n    <string name=\"library_song_empty\">Pesme biblioteke će se prikazati ovde</string>\n    <string name=\"library_playlist_empty\">Tvoje plejliste će se pojaviti ovde</string>\n    <string name=\"from_your_library\">Iz tvoje biblioteke</string>\n    <string name=\"other_versions\">Druge verzije</string>\n    <string name=\"downloaded_songs\">Preuzete pesme</string>\n    <string name=\"playlist_is_empty\">Plejlista je prazna</string>\n    <string name=\"delete_playlist_confirm\">Da li zaista želite da obrišete plejlistu \\\"%s\\\"?</string>\n    <string name=\"retry\">Probaj ponovo</string>\n    <string name=\"radio\">Radio</string>\n    <string name=\"shuffle\">Promešaj</string>\n    <string name=\"reset\">Ponovo pokreni</string>\n    <string name=\"details\">Detalji</string>\n    <string name=\"edit\">Uredi</string>\n    <string name=\"start_radio\">Pokrenite radio</string>\n    <string name=\"add_all_to_library\">Dodaj u sve biblioteke</string>\n    <string name=\"remove_from_library\">Ukloni iz biblioteke</string>\n    <string name=\"remove_all_from_library\">Ukloni iz svih biblioteka</string>\n    <string name=\"action_download\">Preuzmi</string>\n    <string name=\"downloading\">Preuzima se</string>\n    <string name=\"remove_download\">Ukloni preuzimanje</string>\n    <string name=\"import_playlist\">Uvezi plejlistu</string>\n    <string name=\"add_to_playlist\">Dodaj u plejlistu</string>\n    <string name=\"view_artist\">Pogledaj umetnika</string>\n    <string name=\"view_album\">Pogledaj album</string>\n    <string name=\"refetch\">Dodaj ponovo</string>\n    <string name=\"share\">Podeli</string>\n    <string name=\"delete\">Izbriši</string>\n    <string name=\"remove_from_playlist\">Ukloni iz plejliste</string>\n    <string name=\"remove_from_queue\">Ukloni iz reda</string>\n    <string name=\"search_online\">Pretraži preko mreže</string>\n    <string name=\"action_sync\">Sinhronizuj</string>\n    <string name=\"advanced\">Napredno</string>\n    <string name=\"tempo_and_pitch\">Tempo i Visina tona</string>\n    <string name=\"sort_by_create_date\">Datum dodavanja</string>\n    <string name=\"sort_by_name\">Ime</string>\n    <string name=\"sort_by_artist\">Umetnik</string>\n    <string name=\"sort_by_year\">Godina</string>\n    <string name=\"sort_by_song_count\">Broj pesama</string>\n    <string name=\"sort_by_custom\">Specijalan red</string>\n    <string name=\"media_id\">Id medija</string>\n    <string name=\"sample_rate\">Stopa uzorkovanja</string>\n    <string name=\"loudness\">Glasnina</string>\n    <string name=\"file_size\">Veličina datoteke</string>\n    <string name=\"unknown\">Nepoznato</string>\n    <string name=\"copied\">Kopirano u međuspremnik</string>\n    <string name=\"edit_lyrics\">Izmeni tekst</string>\n    <string name=\"search_lyrics\">Pretraži tekstove</string>\n    <string name=\"edit_song\">Izmeni pesmu</string>\n    <string name=\"song_title\">Naziv pesme</string>\n    <string name=\"song_artists\">Umetnik pesme</string>\n    <string name=\"error_song_title_empty\">Naslov pesme ne može biti prazan.</string>\n    <string name=\"error_song_artist_empty\">Umetnik pesme ne može biti prazan.</string>\n    <string name=\"error_playlist_name_empty\">Ime plejliste ne može biti prazno.</string>\n    <string name=\"edit_artist\">Izmeni umetnika</string>\n    <string name=\"artist_name\">Ime umetnika</string>\n    <string name=\"error_artist_name_empty\">Ime umetnika ne može biti prazno.</string>\n    <string name=\"duplicates\">Duplikati</string>\n    <string name=\"skip_duplicates\">Preskoči duplikate</string>\n    <string name=\"add_anyway\">Dodaj svejedno</string>\n    <string name=\"duplicates_description_single\">Pesma je već na vašoj plejlisti</string>\n    <string name=\"duplicates_description_multiple\">%d pesme su već u vašoj plejlisti</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d pesma</item>\n        <item quantity=\"few\">%d pesme</item>\n        <item quantity=\"other\">%d pesama</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d izvođač</item>\n        <item quantity=\"few\">%d izvođača</item>\n        <item quantity=\"other\">%d izvođača</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d album</item>\n        <item quantity=\"few\">%d albuma</item>\n        <item quantity=\"other\">%d albuma</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d plejlista</item>\n        <item quantity=\"few\">%d plejliste</item>\n        <item quantity=\"other\">%d plejlisti</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d nedelja</item>\n        <item quantity=\"few\">%d nedelje</item>\n        <item quantity=\"other\">%d nedelja</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d mesec</item>\n        <item quantity=\"few\">%d meseca</item>\n        <item quantity=\"other\">%d meseci</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d godina</item>\n        <item quantity=\"few\">%d godine</item>\n        <item quantity=\"other\">%d godina</item>\n    </plurals>\n    <string name=\"playlist_imported\">Plejlista uvezena</string>\n    <string name=\"removed_song_from_playlist\">Ukloni \\\"%s\\\" iz plejliste</string>\n    <string name=\"undo\">Vrati</string>\n    <string name=\"action_like_all\">Sviđa mi se sve</string>\n    <string name=\"action_remove_like\">Ukloni sviđanje</string>\n    <string name=\"action_remove_like_all\">Ukloni sva sviđanja</string>\n    <string name=\"action_shuffle_on\">Mešanje uključeno</string>\n    <string name=\"action_shuffle_off\">Mešanje isključeno</string>\n    <string name=\"repeat_mode_off\">Režim ponavljanja je isključen</string>\n    <string name=\"repeat_mode_one\">Ponavljaj trenutnu pesmu</string>\n    <string name=\"repeat_mode_all\">Ponovi red</string>\n    <string name=\"queue_all_songs\">Sve pesme</string>\n    <string name=\"queue_searched_songs\">Pretražene pesme</string>\n    <string name=\"music_player\">Muzički Plejer</string>\n    <string name=\"settings\">Podešavanja</string>\n    <string name=\"appearance\">Izgled</string>\n    <string name=\"theme\">Tema</string>\n    <string name=\"enable_dynamic_theme\">Uključi dinamičnu temu</string>\n    <string name=\"dark_theme\">Tamna tema</string>\n    <string name=\"dark_theme_on\">Uključeno</string>\n    <string name=\"dark_theme_off\">Isključeno</string>\n    <string name=\"dark_theme_follow_system\">Prati sistem</string>\n    <string name=\"pure_black\">Čisto crna</string>\n    <string name=\"customize_navigation_tabs\">Izmeni kartice za navigaciju</string>\n    <string name=\"player\">Plejer</string>\n    <string name=\"player_text_alignment\">Poravnanje teksta plejera</string>\n    <string name=\"lyrics_text_position\">Pozicija teksta pesama</string>\n    <string name=\"left\">Levo</string>\n    <string name=\"center\">Centar</string>\n    <string name=\"right\">Desno</string>\n    <string name=\"player_slider_style\">Izgled klizača plejera</string>\n    <string name=\"default_\">Podrazumevano</string>\n    <string name=\"squiggly\">Valovito</string>\n    <string name=\"sided\">Na strani</string>\n    <string name=\"grid_cell_size\">Veličina ćelije mreže</string>\n    <string name=\"small\">Malo</string>\n    <string name=\"login\">Prijavi se</string>\n    <string name=\"not_logged_in\">Niste prijavljeni</string>\n    <string name=\"content_language\">Podrazumevani jezik sadržaja</string>\n    <string name=\"system_default\">Sistemski podrazumevano</string>\n    <string name=\"enable_proxy\">Omogući proksiju</string>\n    <string name=\"proxy_type\">Tip proksija</string>\n    <string name=\"proxy_url\">URL Proksija</string>\n    <string name=\"player_and_audio\">Plejer i zvuk</string>\n    <string name=\"audio_quality\">Kvalitet zvuka</string>\n    <string name=\"audio_quality_auto\">Automatski</string>\n    <string name=\"audio_quality_high\">Visoko</string>\n    <string name=\"audio_quality_low\">Nisko</string>\n    <string name=\"persistent_queue\">Uporan red</string>\n    <string name=\"persistent_queue_desc\">Obnovite svoj zadnji red kada se aplikacija ponovo pokrene</string>\n    <string name=\"auto_load_more\">Automatski učitajte više pesama</string>\n    <string name=\"auto_load_more_desc\">Automatski dodajte više pesma kada se red završi, ako je moguće</string>\n    <string name=\"skip_silence\">Preskoči tišinu</string>\n    <string name=\"audio_normalization\">Normalizacija zvuka</string>\n    <string name=\"auto_skip_next_on_error\">Automatski preskoči do sledeće pesme kada dođe do greške</string>\n    <string name=\"auto_skip_next_on_error_desc\">Obezbedite svoje neprekidno iskustvo reprodukcije</string>\n    <string name=\"stop_music_on_task_clear\">Zaustavite muziku kada se procesi obrišu</string>\n    <string name=\"equalizer\">Ekvalizator</string>\n    <string name=\"max_song_cache_size\">Maksimalna veličina keša pesama</string>\n    <string name=\"clear_song_cache\">Obriši keš pesama</string>\n    <string name=\"size_used\">%s korišteno</string>\n    <string name=\"privacy\">Privatnost</string>\n    <string name=\"listen_history\">Istorija slušanja</string>\n    <string name=\"clear_search_history\">Obriši istoriju pretrage</string>\n    <string name=\"clear_search_history_confirm\">Da li ste sigurni da želite da obrišete svu istoriju pretrage?</string>\n    <string name=\"pause_listen_history\">Pauziraj istoriju slušanja</string>\n    <string name=\"clear_listen_history\">Obriši istoriju slušanja</string>\n    <string name=\"clear_listen_history_confirm\">Da li ste sigurni da želite da obrišete svu istoriju slušanja?</string>\n    <string name=\"search_history\">Istorija pretrage</string>\n    <string name=\"pause_search_history\">Pauziraj istoriju pretrage</string>\n    <string name=\"disable_screenshot\">Onemogući slikanje ekrana</string>\n    <string name=\"disable_screenshot_desc\">Kada je opcija omogućena, slikanje zaslona i pregled aplikacije u Nedavnim je onemogućeno.</string>\n    <string name=\"enable_lrclib\">Omogući LrcLib dobavljača teksta pesama</string>\n    <string name=\"enable_kugou\">Omogući KuGou dobavljača teksta</string>\n    <string name=\"hide_explicit\">Sakrij eksplicitan sadržaj</string>\n    <string name=\"backup_restore\">Rezervne kopije i vraćanje</string>\n    <string name=\"action_backup\">Rezervna kopija</string>\n    <string name=\"action_restore\">Vraćanje</string>\n    <string name=\"imported_playlist\">Plejlista je uvezena</string>\n    <string name=\"backup_create_success\">Rezervna kopija je uspešno sagrađena</string>\n    <string name=\"backup_create_failed\">Nije moguće napraviti rezervnu kopiju</string>\n    <string name=\"restore_failed\">Neuspešno vraćanje rezervne kopije</string>\n    <string name=\"discord_integration\">Discord integracija</string>\n    <string name=\"discord_information\">Metrolist koristi KizzyRPC biblioteku kako bi stavio tvoj Discord status. Ovo uključuje korištenje Discord Gateway spajanja, što bi se moglo smatrati prekršajem Discord-ovog TOS-a. Međutim nema poznatih slučajeva gde su korisnički nalozi bili suspendovani zbog ovog razloga. Korisite na svoj rizik.\\n\\nMetrolist će samo izvesti tvoj žeton, i sve drugo je sklađeno lokalno.</string>\n    <string name=\"dismiss\">Odbaci</string>\n    <string name=\"options\">Opcije</string>\n    <string name=\"enable_discord_rpc\">Omogući Bogatu Prisutnost</string>\n    <string name=\"about\">O Metrolist-u</string>\n    <string name=\"app_version\">Verzija aplikacije</string>\n    <string name=\"new_version_available\">Dostupna je nova verzija</string>\n    <string name=\"translation_models\">Modeli prevođenja</string>\n    <string name=\"clear_translation_models\">Obriši modele prevođenja</string>\n    <string name=\"library_artist_empty\">Izvođači biblioteke će se pojaviti ovde</string>\n    <string name=\"library_album_empty\">Albumi biblioteke će se pojaviti ovde</string>\n    <string name=\"search_library\">Pretraži biblioteku…</string>\n    <string name=\"liked_songs\">Pesme koje vam se sviđaju</string>\n    <string name=\"filter_library\">Biblioteka</string>\n    <string name=\"filter_liked\">Sviđa mi se</string>\n    <string name=\"remove_download_playlist_confirm\">Da li zaista želite da obrišete sve \\\"%s\\\" pesme sa plejliste iz skladišta Preuzetih Pesama?</string>\n    <string name=\"play\">Pusti</string>\n    <string name=\"add_to_queue\">Dodaj u red</string>\n    <string name=\"play_next\">Pusti sledeće</string>\n    <string name=\"add_to_library\">Dodaj u biblioteku</string>\n    <string name=\"remove_from_history\">Ukloni iz istorije</string>\n    <string name=\"sort_by_length\">Dužina</string>\n    <string name=\"sort_by_play_time\">Vreme puštanja</string>\n    <string name=\"mime_type\">MIME tip</string>\n    <string name=\"codecs\">Kodek</string>\n    <string name=\"bitrate\">Bitrata</string>\n    <string name=\"create_playlist\">Napravi plejlistu</string>\n    <string name=\"volume\">Zvuk</string>\n    <string name=\"edit_playlist\">Izmeni plejlistu</string>\n    <string name=\"save\">Sačuvaj</string>\n    <string name=\"choose_playlist\">Izaberi plejlistu</string>\n    <string name=\"playlist_name\">Ime plejliste</string>\n    <string name=\"error_no_internet\">Nema internet veze</string>\n    <string name=\"lyrics_not_found\">Tekst pesme nije pronađen</string>\n    <string name=\"playlist_synced\">Plejlista sinhronizovana</string>\n    <string name=\"sleep_timer\">Merač vremena za spavanje</string>\n    <string name=\"end_of_song\">Kraj pesme</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">%d minut</item>\n        <item quantity=\"few\">%d minuta</item>\n        <item quantity=\"other\">%d minuta</item>\n    </plurals>\n    <string name=\"error_no_stream\">Prenos nije dostupan</string>\n    <string name=\"error_timeout\">Istek vremena</string>\n    <string name=\"action_like\">Sviđa mi se</string>\n    <string name=\"error_unknown\">Nepoznata greška</string>\n    <string name=\"misc\">Razno</string>\n    <string name=\"default_open_tab\">Podrazumevana otvorena kartica</string>\n    <string name=\"restart_to_take_effect\">Ponovo pokrenite da biste videli promenu</string>\n    <string name=\"big\">Veliko</string>\n    <string name=\"content\">Sadržaj</string>\n    <string name=\"content_country\">Podrazumevana država sadržaja</string>\n    <string name=\"clear_all_downloads\">Obrišite sva preuzimanja</string>\n    <string name=\"queue\">Red</string>\n    <string name=\"song_cache\">Keš pesmi</string>\n    <string name=\"storage\">Skladište</string>\n    <string name=\"image_cache\">Keš slika</string>\n    <string name=\"cache\">Keš</string>\n    <string name=\"max_cache_size\">Maksimalna veličina keša</string>\n    <string name=\"unlimited\">Neograničeno</string>\n    <string name=\"max_image_cache_size\">Maksimalna veličina keša slika</string>\n    <string name=\"clear_image_cache\">Obrišite keš slika</string>\n    <string name=\"login_failed\">Neuspešna prijava</string>\n    <string name=\"preview\">Pregled</string>\n    <string name=\"action_logout\">Izloguj se</string>\n    <string name=\"use_login_for_browse\">Koristite prijavu za pregledanje sadržaja</string>\n    <string name=\"use_login_for_browse_desc\">Ovo može uticati na sadržaj koji vidite i, na primer, prikazivati albume koji su dostupni samo za Premium korisnike ako ste prijavljeni sa Premium nalogom</string>\n    <string name=\"action_login\">Uloguj se</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-be/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"album_cover_desc\">Вокладка альбома</string>\n    <string name=\"trending\">У трэндзе</string>\n    <string name=\"weeks\">Тыдні</string>\n    <string name=\"months\">Месяцы</string>\n    <string name=\"years\">Гады</string>\n    <string name=\"charts\">Чарты</string>\n    <string name=\"back_button_desc\">Назад</string>\n    <string name=\"top_music_videos\">Топ музычных відэа</string>\n    <string name=\"liked\">Упадабаныя</string>\n    <string name=\"offline\">Спампоўкі</string>\n    <string name=\"my_top\">Мой топ</string>\n    <string name=\"sync_playlist\">Сінхранізаваць плей-ліст</string>\n    <string name=\"sync_disabled\">Сінхранізацыя адключана</string>\n    <string name=\"allows_for_sync_witch_youtube\">Заўвага: Гэта дазволіць сінхранізацыю з YouTube Music. Гэта НЕЛЬГА змяніць пазней.</string>\n    <string name=\"generating_image\">Генерацыя відарыса</string>\n    <string name=\"please_wait\">Пачакайце, калі ласка</string>\n    <string name=\"cancel\">Скасаваць</string>\n    <string name=\"share_lyrics\">Падзяліцца тэкстам песні</string>\n    <string name=\"share_as_text\">Абагуліць як тэкст</string>\n    <string name=\"share_as_image\">Абагуліць як відарыс</string>\n    <string name=\"cd_dark_mode\">Цёмны рэжым</string>\n    <string name=\"cd_system_mode\">Сістэмны рэжым</string>\n    <string name=\"palette_green\">Зялёны</string>\n    <string name=\"palette_yellow\">Жоўты</string>\n    <string name=\"crash_close\">Закрыць</string>\n    <string name=\"palette_purple\">Фіялетавы</string>\n    <string name=\"palette_blue\">Сіні</string>\n    <string name=\"palette_sky_blue\">Небесны сіні</string>\n    <string name=\"palette_light_green\">Светла зялёны</string>\n    <string name=\"palette_lime\">Лайм</string>\n    <string name=\"palette_orange\">Аранжавы</string>\n    <string name=\"palette_brown\">Карычневы</string>\n    <string name=\"palette_grey\">Серы</string>\n    <string name=\"palette_blue_grey\">Сіне серы</string>\n    <string name=\"cd_pure_black_mode\">Вельмі цёмны рэжым</string>\n    <string name=\"cd_light_mode\">Светлы рэжым</string>\n    <string name=\"local_history\">Лакальны</string>\n    <string name=\"remote_history\">Дістанцыйны</string>\n    <string name=\"uploaded_playlist\">Загружаны</string>\n    <string name=\"filter_uploaded\">Загружаны</string>\n    <string name=\"max_selection_limit\">Максімальны ліміт выбара</string>\n    <string name=\"show_more\">Паказаць больш</string>\n    <string name=\"show_less\">Паказаць менш</string>\n    <string name=\"show_artist_subscriber_count\">Паказаць колькасць падпісчыкаў</string>\n    <string name=\"download_playlist_desc\">Спампаваць ўсі песні для слухання афлайн</string>\n    <string name=\"remove_download_playlist_desc\">Выдаліць усі песні з гэтага плэйлісту</string>\n    <string name=\"download_in_progress_desc\">Спампаванне ў прагрэсе</string>\n    <string name=\"share_playlist_desc\">Падзяліцца гэтым плэйлістом з другімі</string>\n    <string name=\"delete_playlist_desc\">Выдаліць гэты плэйліст назаўсёды</string>\n    <string name=\"sync_playlist_desc\">Сінхранізіраваць плэйліст з YouTube Music</string>\n    <string name=\"copy_link\">Скапіраваць спасылку</string>\n    <string name=\"select\">Выбраць усё</string>\n    <string name=\"link_copied\">Спасылка скапіравана ў буфер абмену</string>\n    <string name=\"now_playing\">Зараз грае</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-be/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <string name=\"home\">Галоўнае</string>\n    <string name=\"songs\">Песні</string>\n    <string name=\"artists\">Выканаўцы</string>\n    <string name=\"albums\">Альбомы</string>\n    <string name=\"playlists\">Плэй-лісты</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d абрана</item>\n        <item quantity=\"few\">%d абраны</item>\n        <item quantity=\"many\">%d абрана</item>\n        <item quantity=\"other\">%d абрана</item>\n    </plurals>\n    <string name=\"history\">Гісторыя</string>\n    <string name=\"stats\">Статыстыка</string>\n    <string name=\"mood_and_genres\">Настроі і жанры</string>\n    <string name=\"account\">Уліковы запіс</string>\n    <string name=\"quick_picks\">Хуткі выбар</string>\n    <string name=\"quick_picks_empty\">Праслухайце якія-небудзь кампазіцыі, каб стварыць ваш хуткі выбар.</string>\n    <string name=\"new_release_albums\">Новыя рэлізы альбомаў</string>\n    <string name=\"today\">Сёння</string>\n    <string name=\"yesterday\">Учора</string>\n    <string name=\"this_week\">На гэтым тыднія</string>\n    <string name=\"last_week\">На тым тыдні</string>\n    <string name=\"most_played_songs\">Лепшыя кампазіцыі</string>\n    <string name=\"most_played_artists\">Лепшыя выканаўцы</string>\n    <string name=\"search\">Пошук</string>\n    <string name=\"search_yt_music\">Пошук у YouTube Music…</string>\n    <string name=\"search_library\">Пошук у бібліятэцы…</string>\n    <string name=\"filter_all\">Усе</string>\n    <string name=\"filter_songs\">Песні</string>\n    <string name=\"filter_videos\">Відэа</string>\n    <string name=\"filter_albums\">Альбомы</string>\n    <string name=\"filter_artists\">Выканаўцы</string>\n    <string name=\"filter_playlists\">Плэй-лісты</string>\n    <string name=\"filter_community_playlists\">Плэй-лісты aд cупольнасці</string>\n    <string name=\"filter_featured_playlists\">Вартыя ўвагі плэй-лісты</string>\n    <string name=\"no_results_found\">Вынікі не знойдзены</string>\n    <string name=\"from_your_library\">З вашай бібліятэкі</string>\n    <string name=\"liked_songs\">Упадабаныя кампазіцыi</string>\n    <string name=\"downloaded_songs\">Спампаваныя кампазіцыі</string>\n    <string name=\"playlist_is_empty\">Плай-ліст пусты</string>\n    <string name=\"retry\">Паўтарыць</string>\n    <string name=\"radio\">Радыё</string>\n    <string name=\"shuffle\">Перамяшаць</string>\n    <string name=\"details\">Падрабязнасці</string>\n    <string name=\"edit\">Змяніць</string>\n    <string name=\"start_radio\">Уключыць радыё</string>\n    <string name=\"play\">Прайграць</string>\n    <string name=\"play_next\">Прайграць наступным</string>\n    <string name=\"add_to_queue\">Дадаць у чаргу</string>\n    <string name=\"add_to_library\">Дадаць у бібліятэку</string>\n    <string name=\"remove_from_library\">Выдаліць з бібліятэкі</string>\n    <string name=\"action_download\">Спампаваць</string>\n    <string name=\"downloading\">Спампоўка</string>\n    <string name=\"remove_download\">Выдаліць са спамповак</string>\n    <string name=\"import_playlist\">Імпартаваць плей-ліст</string>\n    <string name=\"add_to_playlist\">Дадаць у плэй-ліст</string>\n    <string name=\"view_artist\">Перайсці да выканаўца</string>\n    <string name=\"view_album\">Перайсці да альбома</string>\n    <string name=\"refetch\">Абнавіць</string>\n    <string name=\"share\">Абагуліць</string>\n    <string name=\"delete\">Выдаліць</string>\n    <string name=\"remove_from_history\">Выдаліць з гісторыі</string>\n    <string name=\"search_online\">Шукаць у сетцы</string>\n    <string name=\"action_sync\">Cінхранізацыя</string>\n    <string name=\"sort_by_create_date\">Нядаўна дададзена</string>\n    <string name=\"sort_by_name\">Назва</string>\n    <string name=\"sort_by_artist\">Выканаўца</string>\n    <string name=\"sort_by_year\">Год</string>\n    <string name=\"sort_by_song_count\">Колькасць песен</string>\n    <string name=\"sort_by_length\">Працягласць</string>\n    <string name=\"sort_by_play_time\">Колькасць прайграванняў</string>\n    <string name=\"sort_by_custom\">Уласны парадак</string>\n    <string name=\"media_id\">Iдэнтыфікатар медыя</string>\n    <string name=\"mime_type\">Тып MIME</string>\n    <string name=\"codecs\">Кодэкі</string>\n    <string name=\"bitrate\">Хуткасць</string>\n    <string name=\"sample_rate\">Частата дыскрэтызацыі</string>\n    <string name=\"loudness\">Гучнасць</string>\n    <string name=\"volume\">Узровень гучнасці</string>\n    <string name=\"file_size\">Памер файла</string>\n    <string name=\"unknown\">Невядомы</string>\n    <string name=\"copied\">Скапіявана</string>\n    <string name=\"edit_lyrics\">Змяниць тэкст песні</string>\n    <string name=\"search_lyrics\">Пошук тэкста песні</string>\n    <string name=\"edit_song\">Змяніць песню</string>\n    <string name=\"song_title\">Назва песні</string>\n    <string name=\"song_artists\">Выканаўца песні</string>\n    <string name=\"error_song_title_empty\">Песня павінна мець назву.</string>\n    <string name=\"error_song_artist_empty\">Песня павінна мець выканаўца.</string>\n    <string name=\"save\">Захаваць</string>\n    <string name=\"choose_playlist\">Выбраць плэй-ліст</string>\n    <string name=\"edit_playlist\">Змяніць плей-ліст</string>\n    <string name=\"create_playlist\">Стварыць плей-ліст</string>\n    <string name=\"playlist_name\">Назва плей-ліста</string>\n    <string name=\"error_playlist_name_empty\">Плей-ліст павінен мець назву.</string>\n    <string name=\"edit_artist\">Змяніць выканайца</string>\n    <string name=\"artist_name\">Імя ваканаўца</string>\n    <string name=\"error_artist_name_empty\">Выканаўца павінен мець імя.</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d песня</item>\n        <item quantity=\"few\">%d песні</item>\n        <item quantity=\"many\">%d песен</item>\n        <item quantity=\"other\">%d песен</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d выканаўца</item>\n        <item quantity=\"few\">%d выканаўцы</item>\n        <item quantity=\"many\">%d выканаўцаў</item>\n        <item quantity=\"other\">%d выканаўцаў</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d альбом</item>\n        <item quantity=\"few\">%d альбомы</item>\n        <item quantity=\"many\">%d альбомаў</item>\n        <item quantity=\"other\">%d альбомаў</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d плей-ліст</item>\n        <item quantity=\"few\">%d плей-ліста</item>\n        <item quantity=\"many\">%d плей-лістоў</item>\n        <item quantity=\"other\">%d плей-лістоў</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d тыдзень</item>\n        <item quantity=\"few\">%d тыдні</item>\n        <item quantity=\"many\">%d тыдняў</item>\n        <item quantity=\"other\">%d тыдняў</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d месяц</item>\n        <item quantity=\"few\">%d месяцы</item>\n        <item quantity=\"many\">%d месяцаў</item>\n        <item quantity=\"other\">%d месяцаў</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d год</item>\n        <item quantity=\"few\">%d гады</item>\n        <item quantity=\"many\">%d raдоў</item>\n        <item quantity=\"other\">%d raдоў</item>\n    </plurals>\n    <string name=\"playlist_imported\">Плей-ліст імпартаваны</string>\n    <string name=\"removed_song_from_playlist\">\\\"%s\\\" выдалена з плей-ліста</string>\n    <string name=\"playlist_synced\">Плей-ліст сінхранізаваны</string>\n    <string name=\"undo\">Адрабіць</string>\n    <string name=\"lyrics_not_found\">Тэкст песні не знойдзены</string>\n    <string name=\"sleep_timer\">Таймер рэжыму сну</string>\n    <string name=\"end_of_song\">Канец песні</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">%d хвіліна</item>\n        <item quantity=\"few\">%d хвіліны</item>\n        <item quantity=\"many\">%d хвілін</item>\n        <item quantity=\"other\">%d хвілін</item>\n    </plurals>\n    <string name=\"error_no_stream\">Няма даступных плыняў</string>\n    <string name=\"error_no_internet\">Няма падлучэння да сеткі</string>\n    <string name=\"error_timeout\">Час чакання</string>\n    <string name=\"error_unknown\">Невядомая памылка</string>\n    <string name=\"action_like\">Упадабаць</string>\n    <string name=\"action_remove_like\">Выдаліць з упадабаных</string>\n    <string name=\"action_shuffle_on\">Уключыць перамешванне</string>\n    <string name=\"action_shuffle_off\">Выключыць перамешванне</string>\n    <string name=\"repeat_mode_off\">Рэжым паўтору выключаны</string>\n    <string name=\"repeat_mode_one\">Паўтарыць бягучую песню</string>\n    <string name=\"repeat_mode_all\">Паўтарыць чаргу</string>\n    <string name=\"queue_all_songs\">Усе песні</string>\n    <string name=\"queue_searched_songs\">Шуканыя кампазіцыі</string>\n    <string name=\"music_player\">Музычны прайгравальнік</string>\n    <string name=\"settings\">Налады</string>\n    <string name=\"appearance\">Выгляд</string>\n    <string name=\"enable_dynamic_theme\">Уключыць дынамічную каляровую тэму</string>\n    <string name=\"dark_theme\">Цёмная каляровая тэма</string>\n    <string name=\"dark_theme_on\">Укл.</string>\n    <string name=\"dark_theme_off\">Выкл.</string>\n    <string name=\"dark_theme_follow_system\">Прытрымлівацца сістэмнай каляровай тэмы</string>\n    <string name=\"pure_black\">Рэжым чыстага чорнага колеру</string>\n    <string name=\"default_open_tab\">Прадвызначаная укладка</string>\n    <string name=\"customize_navigation_tabs\">Дапасаваць укладак</string>\n    <string name=\"lyrics_text_position\">Пазіцыя тэкста песні</string>\n    <string name=\"left\">Па левым краі</string>\n    <string name=\"center\">Па цэнтры</string>\n    <string name=\"right\">Па правым краі</string>\n    <string name=\"content\">Змесціва</string>\n    <string name=\"login\">Лагін</string>\n    <string name=\"content_language\">Мова змесціва</string>\n    <string name=\"content_country\">Краіна змесціва</string>\n    <string name=\"system_default\">Прытрымлівацца сістэмнай</string>\n    <string name=\"enable_proxy\">Уключыць проксі</string>\n    <string name=\"proxy_type\">Тып проксі</string>\n    <string name=\"proxy_url\">URL проксі</string>\n    <string name=\"restart_to_take_effect\">Перазапусціць каб ужыць змяненні</string>\n    <string name=\"player_and_audio\">Прайгравальнік ды аўдыя</string>\n    <string name=\"audio_quality\">Якасць аўдыя</string>\n    <string name=\"audio_quality_auto\">Аўта</string>\n    <string name=\"audio_quality_high\">Высокая</string>\n    <string name=\"audio_quality_low\">Нізкая</string>\n    <string name=\"persistent_queue\">Сталая чарга</string>\n    <string name=\"skip_silence\">Прапусціць цішыню</string>\n    <string name=\"audio_normalization\">Нармалізацыя аўдыя</string>\n    <string name=\"equalizer\">Эквалайзер</string>\n    <string name=\"storage\">Сховішча</string>\n    <string name=\"cache\">Кеш</string>\n    <string name=\"image_cache\">Кеш выяў</string>\n    <string name=\"song_cache\">Кеш песен</string>\n    <string name=\"max_cache_size\">Максімальны памер кэшу</string>\n    <string name=\"unlimited\">Безліміт</string>\n    <string name=\"clear_all_downloads\">Ачысціць усе спампоўкі</string>\n    <string name=\"max_image_cache_size\">Максімальны памер кэшу выяў</string>\n    <string name=\"clear_image_cache\">Ачысціць кэш выяў</string>\n    <string name=\"max_song_cache_size\">Максімальны памер кэшу песен</string>\n    <string name=\"clear_song_cache\">Ачысціць кэш песен</string>\n    <string name=\"size_used\">%s выкарыстоўваецца</string>\n    <string name=\"privacy\">Прыватнасць</string>\n    <string name=\"pause_listen_history\">Прыпыніць гісторыю праслухоўвання</string>\n    <string name=\"clear_listen_history\">Ачысціць гісторыю праслухоўвання</string>\n    <string name=\"clear_listen_history_confirm\">Сапраўды ачысціць гісторыю праслухоўвання?</string>\n    <string name=\"pause_search_history\">Прыпыніць гісторыю пошуку</string>\n    <string name=\"clear_search_history\">Ачысціць гісторыю пошуку</string>\n    <string name=\"clear_search_history_confirm\">Сапраўды ачысціць гісторыю пошуку?</string>\n    <string name=\"enable_kugou\">Шукаць тэксты песен у KuGou</string>\n    <string name=\"backup_restore\">Рэзервовае капіраванне ды аднеўленне</string>\n    <string name=\"action_backup\">Рэзервовае капіраванне</string>\n    <string name=\"action_restore\">Аднаўленне з рэзервовай копіі</string>\n    <string name=\"imported_playlist\">Імпартаваны плей-ліст</string>\n    <string name=\"backup_create_success\">Рэзервовная копія паспяхова створана</string>\n    <string name=\"backup_create_failed\">Немагчыма стапрыць рэзервовую копію</string>\n    <string name=\"restore_failed\">Немагчыма ужывіць рэзервовую копію</string>\n    <string name=\"about\">Аб праграме</string>\n    <string name=\"app_version\">Версія праграмы</string>\n    <string name=\"new_version_available\">Даступная новая версія</string>\n    <string name=\"translation_models\">Мадэлі перакладу</string>\n    <string name=\"clear_translation_models\">Ачысціць мадэлі перакладу</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-bg/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"charts\">Класации</string>\n    <string name=\"back_button_desc\">Назад</string>\n    <string name=\"album_cover_desc\">Корица на албум</string>\n    <string name=\"top_music_videos\">Топ музикални клипове</string>\n    <string name=\"trending\">Популярни в момента</string>\n    <string name=\"weeks\">Седмици</string>\n    <string name=\"months\">Месеци</string>\n    <string name=\"years\">Години</string>\n    <string name=\"liked\">Харесани</string>\n    <string name=\"offline\">Изтеглени</string>\n    <string name=\"cached_playlist\">Кеширани</string>\n    <string name=\"sync_playlist\">Синхронизирай плейлист</string>\n    <string name=\"allows_for_sync_witch_youtube\">Бележка: Това позволява сихронизирането с YouTube Music. НЕ МОЖЕ да промените това по-късно.</string>\n    <string name=\"generating_image\">Генериране на изображение</string>\n    <string name=\"please_wait\">Моля изчакайте</string>\n    <string name=\"cancel\">Отказ</string>\n    <string name=\"share_lyrics\">Сподели текст</string>\n    <string name=\"share_as_text\">Сподели като текст</string>\n    <string name=\"share_as_image\">Сподели като изображение</string>\n    <string name=\"share_selected\">Сподели избраното</string>\n    <string name=\"customize_colors\">Персонализирай цветовете</string>\n    <string name=\"text_color\">Цвят на текста</string>\n    <string name=\"secondary_text_color\">Вторичен цвят на текста</string>\n    <string name=\"background_color\">Цвят на фона</string>\n    <string name=\"remove_from_cache\">Изтрий от кеша</string>\n    <string name=\"copy_link\">Копирай линка</string>\n    <string name=\"select\">Избери всички</string>\n    <string name=\"like_all\">Харесай всички</string>\n    <string name=\"dislike_all\">Отхаресай всичко</string>\n    <string name=\"sort_by_last_updated\">Дата на обновяване</string>\n    <string name=\"link_copied\">Линкът е копиран в клипборда</string>\n    <string name=\"lyrics\">Текст</string>\n    <string name=\"already_in_playlist\">Вече в плейлиста:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d време</item>\n        <item quantity=\"other\">%d времена</item>\n    </plurals>\n    <string name=\"similar_content\">Подобно съдържание</string>\n    <string name=\"player_background_style\">Настройки на фона на плеъра</string>\n    <string name=\"follow_theme\">Следвай темата</string>\n    <string name=\"gradient\">Градиент</string>\n    <string name=\"player_background_blur\">Замъглено</string>\n    <string name=\"player_buttons_style\">Цветове на бутоните на плеъра</string>\n    <string name=\"default_style\">По подразбиране</string>\n    <string name=\"lyrics_click_change\">Смени текста с клик</string>\n    <string name=\"lyrics_auto_scroll\">Автоматично превъртане на текст</string>\n    <string name=\"lyrics_romanize_japanese\">Романизирай японски текстове</string>\n    <string name=\"lyrics_romanize_korean\">Романизирай корейски текстове</string>\n    <string name=\"slim_navbar\">Тънка долна навигационна лента</string>\n    <string name=\"auto_playlists\">Автоматични плейлисти</string>\n    <string name=\"show_liked_playlist\">Покажи плейлист \\\"Харесани\\\"</string>\n    <string name=\"show_downloaded_playlist\">Покажи плейлист \\\"Изтеглени\\\"</string>\n    <string name=\"show_top_playlist\">Покажи плейлист \\\"Топ\\\"</string>\n    <string name=\"show_cached_playlist\">Покажи плейлист \\\"Кеширани\\\"</string>\n    <string name=\"advanced_login\">Вход с токен</string>\n    <string name=\"token_hidden\">Натисни за показване на токен</string>\n    <string name=\"token_shown\">Натисни отново за копиране или редакция</string>\n    <string name=\"token_adv_login_description\">Това е метод за влизане за ОПИТНИ потребители. Като алтернатива на уеб портала можете директно да въведете или актуализирате своя идентификационен код тук. Така например може да се ускори влизането в системата от няколко устройства. Моля, имайте предвид, че всички невалидни формати на токени, които приложението не успява да анализира, няма да бъдат приети</string>\n    <string name=\"yt_sync\">Автоматично синхронизиране с акаунт</string>\n    <string name=\"more_content\">Повече съдържание</string>\n    <string name=\"general\">Общи</string>\n    <string name=\"proxy\">Прокси</string>\n    <string name=\"set_quick_picks\">Задайте бързи избори</string>\n    <string name=\"last_song_listened\">На база последно слушана песен</string>\n    <string name=\"app_language\">Език на приложението</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"past_week\">Последната седмица</string>\n    <string name=\"past_month\">Последния месец</string>\n    <string name=\"past_year\">Последната година</string>\n    <string name=\"information\">Информация</string>\n    <string name=\"description\">Описание</string>\n    <string name=\"views\">Гледания</string>\n    <string name=\"likes\">Харесвания</string>\n    <string name=\"dislikes\">Нехаресвания</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">1 секунда</item>\n        <item quantity=\"other\">%d секунди</item>\n    </plurals>\n    <string name=\"local_history\">Локални</string>\n    <string name=\"remote_history\">Дистанционно</string>\n    <string name=\"continuous\">Продължаващо</string>\n    <string name=\"my_top\">Мойте топ</string>\n    <string name=\"sync_disabled\">Синхронизирането е изключено</string>\n    <string name=\"max_selection_limit\">Максимален лимит за избиране</string>\n    <string name=\"enable_swipe_thumbnail\">Разреши плъзгането за смяна на песента</string>\n    <string name=\"swipe_song_to_add\">Плъзнете песента наляво, за да я добавите към опашката, или надясно, за да я пуснете следваща</string>\n    <string name=\"slim\">Тесен</string>\n    <string name=\"new_player_design\">Нов дизайн на плеъра</string>\n    <string name=\"enable_similar_content\">Показвай подобно съдържание</string>\n    <string name=\"similar_content_desc\">Автоматично добавяне на подобни песни при достигане на края на опашката</string>\n    <string name=\"import_online\">Импортиране на M3U плейлисти</string>\n    <string name=\"import_csv\">Импортиране на CSV плейлисти</string>\n    <string name=\"playlist_add_local_to_synced_note\">Бележка: Добавянето на локални песни към синхронизирани/отдалечени плейлисти не се поддържа. Всички други комбинации са валидни</string>\n    <string name=\"auto_download_on_like\">Автоматично изтегляне при харесване</string>\n    <string name=\"auto_download_on_like_desc\">Автоматично сваляй песни при харесване</string>\n    <string name=\"swipe_sensitivity\">Чувствителност на жестовете в мини плеъра</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"clear_song_cache_dialog\">Сигурни ли сте, че искате да изчистите всички кеширани песни?</string>\n    <string name=\"clear_image_cache_dialog\">Сигурни ли сте, че искате да изчистите всички кеширани изображения?</string>\n    <string name=\"clear_downloads_dialog\">Наистина ли искате да изчистите всички изтеглени файлове?</string>\n    <string name=\"disable\">Деактивирай</string>\n    <string name=\"not_logged_in_youtube\">Не сте влезли в YouTube</string>\n    <string name=\"default_links\">Отвори поддържани връзки</string>\n    <string name=\"open_app_settings_error\">Неуспешно отваряне на настройките на приложението</string>\n    <string name=\"release_notes\">Информация за новата версия</string>\n    <string name=\"all_time\">Общо</string>\n    <string name=\"past_24_hours\">Последните 24 часа</string>\n    <string name=\"top_length\">Дължина на моя топ лист</string>\n    <string name=\"history_duration\">Продължителност на историята</string>\n    <string name=\"subscribe\">Абонирай се</string>\n    <string name=\"subscribed\">Абониран</string>\n    <string name=\"new_mini_player_design\">Нов дизайн на мини плеър</string>\n    <string name=\"now_playing\">Сега слушате</string>\n    <string name=\"close\">Затвори</string>\n    <string name=\"hide_player_thumbnail_desc\">Заменете корицата на албума с логото на приложението в плеъра</string>\n    <string name=\"seek_forward_dynamic\">+%1$d секунди напред</string>\n    <string name=\"seek_backward_dynamic\">-%1$d секунди назад</string>\n    <string name=\"uploaded_playlist\">Качено</string>\n    <string name=\"filter_uploaded\">Качени</string>\n    <string name=\"starting_radio\">Стартиране на радио</string>\n    <string name=\"hide_player_thumbnail\">Скриване на миниатюрата на плейъра</string>\n    <string name=\"seek_seconds_addup_description\">Ако е активирано, добавя допълнителни 5 секунди всеки път, когато прескочите превъртането</string>\n    <string name=\"show_uploaded_playlist\">Показване на плейлиста „Качени“</string>\n    <string name=\"edit_playlist_cover\">Редактиране на обложката на плейлиста</string>\n    <string name=\"edit_playlist_cover_note\">Забележка: За да променяте обложката на плейлиста, акаунтът ви трябва да е свързан с телефонен номер и потвърден в YouTube Music.</string>\n    <string name=\"edit_playlist_cover_note_wait\">След като изберете изображение, моля, изчакайте малко, докато новата обложка се появи във Вашия плейлист.</string>\n    <string name=\"choose_from_library\">Изберете от библиотеката</string>\n    <string name=\"remove_custom_image\">Премахване на персонализирано изображение</string>\n    <string name=\"config_proxy\">Конфигуриране на прокси</string>\n    <string name=\"proxy_username\">Потребителско име на прокси</string>\n    <string name=\"proxy_password\">Парола на прокси</string>\n    <string name=\"enable_authentication\">Активиране на удостоверяване</string>\n    <string name=\"discord_use_details\">Използвайте подробности вместо статус</string>\n    <string name=\"discord_use_details_description\">Показване заглавията на песните вместо имената на изпълнителите</string>\n    <string name=\"disable_load_more_when_repeat_all\">Деактивиране на автоматичното изтегляне при повтаряне на всички песни</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Не изтегляйте автоматично още песни и подобно съдържание, когато е активирано „Повтаряне на всички“</string>\n    <string name=\"lyrics_romanization_cyrillic\">Кирилица</string>\n    <string name=\"lyrics_romanize_title\">Романизация</string>\n    <string name=\"lyrics_romanization\">Романизация на текста на песните</string>\n    <string name=\"lyrics_romanize_russian\">Романизиране на руски текстове</string>\n    <string name=\"lyrics_romanize_ukrainian\">Романизиране на украински текстове</string>\n    <string name=\"lyrics_romanize_belarusian\">Романизиране на беларуски текстове</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Романизиране на киргизки текстове</string>\n    <string name=\"lyrics_romanize_serbian\">Романизиране на сръбски текстове</string>\n    <string name=\"lyrics_romanize_bulgarian\">Романизиране на български текстове</string>\n    <string name=\"line_by_line_option_title\">ЕКСПЕРИМЕНТАЛНО: Откриване на език ред по ред</string>\n    <string name=\"line_by_line_option_desc\">Текстът на кирилица ще се разпознава ред по ред, вместо за цялата песен.</string>\n    <string name=\"line_by_line_dialog_title\">Сигурен ли сте?</string>\n    <string name=\"line_by_line_dialog_desc\">Това е експериментална функция и може да не работи винаги.\\n\\nПо подразбиране езикът се определя от цялата песен, но когато тази опция е включена, той ще се определя ред по ред. Това ще позволи многоезичните песни да работят, НО езикът може да не е правилен винаги (например, ако има украински текст, който не съдържа специфични за украинския език букви, той може да бъде романизиран като руски).\\n\\nАко нямате проблеми, препоръчително е да оставите тази опция изключена.</string>\n    <string name=\"romanize_current_track\">Романизиране на текущата песен</string>\n    <string name=\"settings_section_ui\">Интерфейс</string>\n    <string name=\"settings_section_privacy\">Поверителност и сигурност</string>\n    <string name=\"settings_section_player_content\">Плейър и съдържание</string>\n    <string name=\"settings_section_storage\">Съхранение и \\'БекЪп\\'</string>\n    <string name=\"settings_section_system\">Системни &amp; относно приложението</string>\n    <string name=\"updater\">Актуализатор</string>\n    <string name=\"check_for_updates\">Автоматична проверка за актуализации</string>\n    <string name=\"update_notifications\">Активиране на известия за актуализации</string>\n    <string name=\"update_available_title\">Налична е актуализация</string>\n    <string name=\"update_channel_name\">Актуализации на приложения</string>\n    <string name=\"update_channel_desc\">Известия за нови версии</string>\n    <string name=\"audio_offload\">Активиране на хардуерно аудио ускорение</string>\n    <string name=\"audio_offload_description\">Използвайте хардуерно възпроизвеждане на аудио. Деактивирането на тази функция може да увеличи консумацията на енергия, но може да бъде полезно, ако имате проблеми с възпроизвеждането на аудио или последващата обработка</string>\n    <string name=\"lyrics_romanize_macedonian\">Романизиране на македонските текстове</string>\n    <string name=\"integrations\">Интеграции</string>\n    <string name=\"username\">Потребителско име</string>\n    <string name=\"password\">Парола</string>\n    <string name=\"lastfm_integration\">Интеграция с Last.fm</string>\n    <string name=\"enable_scrobbling\">Активиране на скробълинг</string>\n    <string name=\"lastfm_now_playing\">Изпращане на данни за текущата песен</string>\n    <string name=\"scrobbling_configuration\">Конфигурация на скробълинга</string>\n    <string name=\"scrobble_delay_percent\">Процент на забавяне при Scrobble</string>\n    <string name=\"scrobble_delay_minutes\">Закъснение на Scrobble в минути</string>\n    <string name=\"seek_seconds_addup\">Кумулативно превъртане</string>\n    <string name=\"swipe_song_to_remove\">Плъзнете върху песента, за да я премахнете от плейлиста</string>\n    <string name=\"default_lib_chips\">Промяна на \\'дефолтния\\' чип на библиотеката</string>\n    <string name=\"scrobble_min_track_duration\">Песни от Scrobble, по-дълги от</string>\n    <string name=\"primary_color_style\">Основен цвят</string>\n    <string name=\"tertiary_color_style\">Третичен цвят</string>\n    <string name=\"auto_scroll\">Повторно синхронизиране</string>\n    <string name=\"lyrics_romanize_chinese\">Романизиране на китайски текстове</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"google_cast_description\">Активиране на предаване на звук към Chromecast и други устройства поддържащи Cast</string>\n    <string name=\"last_fm_send_likes\">Изпращане на харесвания/нехаресвания</string>\n    <string name=\"last_fm_send_likes_description\">Добавяне/премахване на любимо в Last.fm, когато харесате/премахнете песен в Metrolist</string>\n    <string name=\"logging_in\">Влизане…</string>\n    <string name=\"hide_video_songs\">Скриване на видео</string>\n    <string name=\"details_desc\">Преглед на информацията за песента</string>\n    <string name=\"edit_desc\">Промяна на заглавието или изпълнителя</string>\n    <string name=\"download_playlist_desc\">Изтеглете всички песни за възпроизвеждане офлайн</string>\n    <string name=\"remove_download_playlist_desc\">Премахване на всички изтеглени песни от този плейлист</string>\n    <string name=\"download_in_progress_desc\">Изтеглянето е в ход</string>\n    <string name=\"share_playlist_desc\">Споделете плейлистата с други</string>\n    <string name=\"delete_playlist_desc\">Изтрий за постоянно този плейлист</string>\n    <string name=\"enable_better_lyrics\">Включване на Better Lyrics</string>\n    <string name=\"add_to_library_desc\">Запазване в библиотеката</string>\n    <string name=\"download_desc\">Направете достъпно офлайн</string>\n    <string name=\"share_desc\">Споделете линк на този елемент</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-bg/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"artists\">Изпълнители</string>\n    <string name=\"albums\">Албуми</string>\n    <string name=\"playlists\">Плейлисти</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d избран</item>\n        <item quantity=\"other\">%d избрани</item>\n    </plurals>\n    <string name=\"similar_to\">Подобно на</string>\n    <string name=\"new_release_albums\">Нови албуми</string>\n    <string name=\"today\">Днес</string>\n    <string name=\"yesterday\">Вчера</string>\n    <string name=\"this_week\">Тази седмица</string>\n    <string name=\"last_week\">Миналата седмица</string>\n    <string name=\"songs\">Песни</string>\n    <string name=\"home\">Начало</string>\n    <string name=\"history\">История</string>\n    <string name=\"mood_and_genres\">Настроения и жанрове</string>\n    <string name=\"stats\">Статистика</string>\n    <string name=\"quick_picks\">Бърз избор</string>\n    <string name=\"keep_listening\">Продължи да слушаш</string>\n    <string name=\"account\">Профил</string>\n    <string name=\"quick_picks_empty\">Слушай песни, за да генерираш своя бърз избор</string>\n    <string name=\"forgotten_favorites\">Забравени любими</string>\n    <string name=\"your_youtube_playlists\">Вашите плейлисти в YouTube</string>\n    <string name=\"most_played_songs\">Най-пускани песни</string>\n    <string name=\"other_versions\">Други версии</string>\n    <string name=\"radio\">Радио</string>\n    <string name=\"start_radio\">Стартирай радио</string>\n    <string name=\"most_played_albums\">Най-пускани албуми</string>\n    <string name=\"filter_library\">Библиотека</string>\n    <string name=\"filter_albums\">Албуми</string>\n    <string name=\"search\">Търсене</string>\n    <string name=\"downloaded_songs\">Изтеглени песни</string>\n    <string name=\"playlist_is_empty\">Плейлистът е празен</string>\n    <string name=\"from_your_library\">От вашата библиотека</string>\n    <string name=\"play\">Изпълни</string>\n    <string name=\"remove_download_playlist_confirm\">Наистина ли искате да премахнете всички \\\"%s\\\" плейлист песни от хранилището за изтеглени песни?</string>\n    <string name=\"library_song_empty\">Песните от библиотеката ще се показват тук</string>\n    <string name=\"retry\">Опитай отново</string>\n    <string name=\"search_library\">Търсене в библиотека…</string>\n    <string name=\"filter_artists\">Изпълнители</string>\n    <string name=\"most_played_artists\">Най-пускани изпълнители</string>\n    <string name=\"search_yt_music\">Търсене в YouTube Music…</string>\n    <string name=\"filter_liked\">Харесвани</string>\n    <string name=\"filter_downloaded\">Изтеглени</string>\n    <string name=\"filter_all\">Всички</string>\n    <string name=\"filter_songs\">Песни</string>\n    <string name=\"no_results_found\">Няма намерени резултати</string>\n    <string name=\"filter_playlists\">Плейлисти</string>\n    <string name=\"filter_videos\">Видеоклипове</string>\n    <string name=\"filter_community_playlists\">Плейлисти на общността</string>\n    <string name=\"filter_bookmarked\">Маркирано</string>\n    <string name=\"filter_featured_playlists\">Представени плейлисти</string>\n    <string name=\"library_artist_empty\">Изпълнителите от библиотеката ще се показват тук</string>\n    <string name=\"library_album_empty\">Албумите от библиотеката ще се показват тук</string>\n    <string name=\"liked_songs\">Харесвани песни</string>\n    <string name=\"library_playlist_empty\">Вашите плейлисти ще се показват тук</string>\n    <string name=\"delete_playlist_confirm\">Наистина ли искате да изтриете плейлиста \\\"%s\\\"?</string>\n    <string name=\"add_to_library\">Добави към библиотеката</string>\n    <string name=\"shuffle\">Разбъркай</string>\n    <string name=\"reset\">Нулиране</string>\n    <string name=\"play_next\">Изпълни следващ</string>\n    <string name=\"details\">Подробности</string>\n    <string name=\"edit\">Редактирай</string>\n    <string name=\"add_to_queue\">Добави към опашката</string>\n    <string name=\"tempo_and_pitch\">Темпо и височина</string>\n    <string name=\"sort_by_create_date\">Дата на добавяне</string>\n    <string name=\"remove_from_playlist\">Премахни от плейлиста</string>\n    <string name=\"remove_from_history\">Премахни от историята</string>\n    <string name=\"copied\">Копирано в клипборда</string>\n    <string name=\"view_album\">Вижте албум</string>\n    <string name=\"action_sync\">Синхронизиране</string>\n    <string name=\"add_all_to_library\">Добави всички към библиотеката</string>\n    <string name=\"media_id\">ID на медия</string>\n    <string name=\"search_lyrics\">Търси текстове</string>\n    <string name=\"unknown\">Неизвестен</string>\n    <string name=\"mime_type\">Тип MIME</string>\n    <string name=\"codecs\">Кодеци</string>\n    <string name=\"add_to_playlist\">Добави към плейлист</string>\n    <string name=\"sort_by_custom\">Потребителско</string>\n    <string name=\"volume\">Увеличаване</string>\n    <string name=\"remove_from_queue\">Премахни от опашката</string>\n    <string name=\"sort_by_song_count\">Брой песни</string>\n    <string name=\"loudness\">Сила на звука</string>\n    <string name=\"remove_all_from_library\">Премахни всички от библиотеката</string>\n    <string name=\"sample_rate\">Честота на дискретизация</string>\n    <string name=\"file_size\">Размер на файла</string>\n    <string name=\"share\">Сподели</string>\n    <string name=\"remove_download\">Премахни изтегляне</string>\n    <string name=\"view_artist\">Вижте изпълнител</string>\n    <string name=\"remove_from_library\">Премахни от библиотеката</string>\n    <string name=\"action_download\">Изтегляне</string>\n    <string name=\"downloading\">Изтегля се</string>\n    <string name=\"import_playlist\">Внасяне на плейлист</string>\n    <string name=\"refetch\">Повторно извличане</string>\n    <string name=\"delete\">Изтрий</string>\n    <string name=\"search_online\">Търси онлайн</string>\n    <string name=\"advanced\">Разширени</string>\n    <string name=\"sort_by_name\">Име</string>\n    <string name=\"sort_by_artist\">Изпълнител</string>\n    <string name=\"sort_by_year\">Година</string>\n    <string name=\"sort_by_length\">Дължина</string>\n    <string name=\"sort_by_play_time\">Време на изпълнение</string>\n    <string name=\"bitrate\">Скорост на предаване</string>\n    <string name=\"edit_lyrics\">Редактирай текстове</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d песен</item>\n        <item quantity=\"other\">%d песни</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d месец</item>\n        <item quantity=\"other\">%d месеци</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d година</item>\n        <item quantity=\"other\">%d години</item>\n    </plurals>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">1 минута</item>\n        <item quantity=\"other\">%d минути</item>\n    </plurals>\n    <string name=\"error_playlist_name_empty\">Името на плейлиста не може да бъде празно.</string>\n    <string name=\"playlist_name\">Име на плейлист</string>\n    <string name=\"song_title\">Заглавие на песен</string>\n    <string name=\"error_timeout\">Време за изчакване</string>\n    <string name=\"choose_playlist\">Избери плейлист</string>\n    <string name=\"create_playlist\">Създаване на плейлист</string>\n    <string name=\"action_like_all\">Всички харесвания</string>\n    <string name=\"duplicates_description_single\">Песента вече е във вашия плейлист</string>\n    <string name=\"error_no_internet\">Няма мрежова връзка</string>\n    <string name=\"playlist_synced\">Плейлистът е синхронизиран</string>\n    <string name=\"error_song_title_empty\">Заглавието на песента не може да бъде празно.</string>\n    <string name=\"action_shuffle_on\">Разбъркване включено</string>\n    <string name=\"edit_song\">Редактирай песен</string>\n    <string name=\"song_artists\">Изпълнители на песен</string>\n    <string name=\"error_song_artist_empty\">Изпълнителят на песента не може да бъде празен.</string>\n    <string name=\"save\">Запази</string>\n    <string name=\"edit_playlist\">Редактирай плейлист</string>\n    <string name=\"edit_artist\">Редактирай изпълнител</string>\n    <string name=\"artist_name\">Име на изпълнител</string>\n    <string name=\"error_artist_name_empty\">Името на изпълнителя не може да бъде празно.</string>\n    <string name=\"duplicates\">Дубликати</string>\n    <string name=\"skip_duplicates\">Пропусни дубликати</string>\n    <string name=\"add_anyway\">Добави все пак</string>\n    <string name=\"duplicates_description_multiple\">%d песни вече са във вашия плейлист</string>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d изпълнител</item>\n        <item quantity=\"other\">%d изпълнители</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d албум</item>\n        <item quantity=\"other\">%d албуми</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d плейлист</item>\n        <item quantity=\"other\">%d плейлисти</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d седмица</item>\n        <item quantity=\"other\">%d седмици</item>\n    </plurals>\n    <string name=\"playlist_imported\">Плейлистът е внесен</string>\n    <string name=\"removed_song_from_playlist\">Премахнат „%s“ от плейлиста</string>\n    <string name=\"undo\">Отмяна</string>\n    <string name=\"lyrics_not_found\">Текстът не е намерен</string>\n    <string name=\"sleep_timer\">Таймер за заспиване</string>\n    <string name=\"end_of_song\">Край на песента</string>\n    <string name=\"error_no_stream\">Няма наличен поток</string>\n    <string name=\"error_unknown\">Неизвестна грешка</string>\n    <string name=\"action_remove_like\">Премахни харесвания</string>\n    <string name=\"action_like\">Харесвания</string>\n    <string name=\"action_remove_like_all\">Премахни всички харесвания</string>\n    <string name=\"action_shuffle_off\">Разбъркване изключено</string>\n    <string name=\"repeat_mode_off\">Режим на повторение е изключен</string>\n    <string name=\"repeat_mode_one\">Повтори текущата песен</string>\n    <string name=\"repeat_mode_all\">Повтори опашката</string>\n    <string name=\"discord_information\">Metrolist използва библиотеката KizzyRPC, за да зададе състоянието на профила ви в Discord. Това включва използването на връзката Discord Gateway, което може да се счита за нарушение на TOS на Discord. Въпреки това няма известни случаи на спиране на потребителски профили поради тази причина. Използвайте на свой собствен риск. \\n \\nMetrolist ще извлече само вашия токен, а всичко останало се съхранява локално.</string>\n    <string name=\"action_backup\">Архивиране</string>\n    <string name=\"translation_models\">Модели за превод</string>\n    <string name=\"clear_image_cache\">Изчисти кеша на изображение</string>\n    <string name=\"backup_create_failed\">Неуспешно създаване на резервно копие</string>\n    <string name=\"enable_dynamic_theme\">Активирай динамична тема</string>\n    <string name=\"clear_song_cache\">Изчисти кеша на песен</string>\n    <string name=\"login_failed\">Неуспешно влизане</string>\n    <string name=\"imported_playlist\">Внесен плейлист</string>\n    <string name=\"queue\">Опашка</string>\n    <string name=\"audio_quality_auto\">Автоматично</string>\n    <string name=\"login\">Вход</string>\n    <string name=\"pure_black\">Чисто черно</string>\n    <string name=\"skip_silence\">Пропусни тишината</string>\n    <string name=\"audio_quality_low\">Ниско</string>\n    <string name=\"right\">Надясно</string>\n    <string name=\"lyrics_text_position\">Позиция на текста на песен</string>\n    <string name=\"queue_searched_songs\">Търсени песни</string>\n    <string name=\"player_slider_style\">Плъзгащ стил на плейъра</string>\n    <string name=\"content_country\">Държава на съдържание по подразбиране</string>\n    <string name=\"image_cache\">Кеш на изображение</string>\n    <string name=\"enable_proxy\">Активирай прокси</string>\n    <string name=\"dark_theme\">Тъмна тема</string>\n    <string name=\"storage\">Съхранение</string>\n    <string name=\"clear_translation_models\">Ясни модели за превод</string>\n    <string name=\"proxy_url\">Прокси URL</string>\n    <string name=\"search_history\">История на търсене</string>\n    <string name=\"auto_skip_next_on_error\">Автоматично премини към следваща песен, когато възникне грешка</string>\n    <string name=\"settings\">Настройки</string>\n    <string name=\"action_restore\">Възстановяване</string>\n    <string name=\"player\">Плейър</string>\n    <string name=\"player_text_alignment\">Подравняване текста на плейъра</string>\n    <string name=\"clear_search_history_confirm\">Сигурни ли сте, че искате да изчистите цялата история на търсене?</string>\n    <string name=\"song_cache\">Кеш на песен</string>\n    <string name=\"not_logged_in\">Не сте влезли в</string>\n    <string name=\"queue_all_songs\">Всички песни</string>\n    <string name=\"app_version\">Версия на приложението</string>\n    <string name=\"about\">Относно</string>\n    <string name=\"theme\">Тема</string>\n    <string name=\"backup_restore\">Архивиране и възстановяване</string>\n    <string name=\"center\">Център</string>\n    <string name=\"content\">Съдържание</string>\n    <string name=\"auto_skip_next_on_error_desc\">Осигурете си непрекъснато изживяване при възпроизвеждане</string>\n    <string name=\"pause_listen_history\">Пауза на историята на слушане</string>\n    <string name=\"enable_kugou\">Активирай доставчик на текстове KuGou</string>\n    <string name=\"squiggly\">Криволичещо</string>\n    <string name=\"system_default\">Система по подразбиране</string>\n    <string name=\"customize_navigation_tabs\">Персонализирай разделите за навигация</string>\n    <string name=\"music_player\">Музикален плейър</string>\n    <string name=\"appearance\">Външен вид</string>\n    <string name=\"dark_theme_on\">Включено</string>\n    <string name=\"dark_theme_off\">Изключено</string>\n    <string name=\"dark_theme_follow_system\">Следвай системата</string>\n    <string name=\"left\">Наляво</string>\n    <string name=\"sided\">Странично</string>\n    <string name=\"default_\">По подразбиране</string>\n    <string name=\"misc\">Разни</string>\n    <string name=\"default_open_tab\">Отвори раздел по подразбиране</string>\n    <string name=\"grid_cell_size\">Размер на мрежовата клетка</string>\n    <string name=\"small\">Малък</string>\n    <string name=\"big\">Голям</string>\n    <string name=\"content_language\">Език на съдържание по подразбиране</string>\n    <string name=\"proxy_type\">Тип прокси</string>\n    <string name=\"restart_to_take_effect\">Рестартирай за да влезе в сила</string>\n    <string name=\"player_and_audio\">Плейър и аудио</string>\n    <string name=\"audio_quality\">Качество на звука</string>\n    <string name=\"audio_quality_high\">Високо</string>\n    <string name=\"persistent_queue\">Постоянна опашка</string>\n    <string name=\"persistent_queue_desc\">Възстанови последната си опашка при стартиране на приложението</string>\n    <string name=\"auto_load_more\">Автоматично зареди още песни</string>\n    <string name=\"auto_load_more_desc\">Автоматично добави още песни при достигане края на опашката, ако е възможно</string>\n    <string name=\"audio_normalization\">Нормализация на звука</string>\n    <string name=\"stop_music_on_task_clear\">Спри музиката при изчистване на задачата</string>\n    <string name=\"equalizer\">Еквалайзер</string>\n    <string name=\"cache\">Кеш</string>\n    <string name=\"max_cache_size\">Максимален размер на кеша</string>\n    <string name=\"unlimited\">Неограничен</string>\n    <string name=\"clear_all_downloads\">Изчисти всички изтеглени</string>\n    <string name=\"max_image_cache_size\">Максимален размер на кеша за изображение</string>\n    <string name=\"max_song_cache_size\">Максимален размер на кеша на песен</string>\n    <string name=\"size_used\">%s използвани</string>\n    <string name=\"privacy\">Поверителност</string>\n    <string name=\"listen_history\">История на слушане</string>\n    <string name=\"clear_listen_history\">Изчисти историята на слушане</string>\n    <string name=\"clear_listen_history_confirm\">Сигурни ли сте, че искате да изчистите цялата история на слушане?</string>\n    <string name=\"pause_search_history\">Пауза на историята на търсене</string>\n    <string name=\"clear_search_history\">Изчисти историята на търсене</string>\n    <string name=\"disable_screenshot\">Деактивирай снимка на екрана</string>\n    <string name=\"disable_screenshot_desc\">Когато тази опция е включена, снимка на екрана и изгледът на приложението в Скорошни са деактивирани.</string>\n    <string name=\"enable_lrclib\">Активирай доставчик на текстове LrcLib</string>\n    <string name=\"hide_explicit\">Скрий нецензурно съдържание</string>\n    <string name=\"backup_create_success\">Архивирането е създадено успешно</string>\n    <string name=\"restore_failed\">Неуспешно възстановяване на резервно копие</string>\n    <string name=\"discord_integration\">Discord интеграция</string>\n    <string name=\"dismiss\">Отхвърли</string>\n    <string name=\"options\">Опции</string>\n    <string name=\"preview\">Преглед</string>\n    <string name=\"action_logout\">Изход</string>\n    <string name=\"enable_discord_rpc\">Активирай Rich Presence</string>\n    <string name=\"new_version_available\">Налична е нова версия</string>\n    <string name=\"use_login_for_browse\">Използвай вход за разглеждане на съдържание</string>\n    <string name=\"use_login_for_browse_desc\">Това може да повлияе какво съдържание виждате и например показва само премиум албуми, ако сте влезли с Премиум профил</string>\n    <string name=\"action_login\">Вход</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-bn/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">ফোন স্টোরেজ</string>\n    <string name=\"remote_history\">দুরবর্তী</string>\n    <string name=\"charts\">শীর্ষ তালিকা</string>\n    <string name=\"back_button_desc\">পূর্ববর্তী</string>\n    <string name=\"album_cover_desc\">এ্যালবাম কভার</string>\n    <string name=\"top_music_videos\">শীর্ষ মিউজিক ভিডিও</string>\n    <string name=\"trending\">ট্রেন্ডিং</string>\n    <string name=\"weeks\">সপ্তাহ</string>\n    <string name=\"months\">মাস</string>\n    <string name=\"years\">বছর</string>\n    <string name=\"continuous\">বিরতিহীন</string>\n    <string name=\"liked\">পছন্দ</string>\n    <string name=\"offline\">ডাউনলোডেড</string>\n    <string name=\"my_top\">শীর্ষ গান</string>\n    <string name=\"cached_playlist\">জমাকৃত</string>\n    <string name=\"uploaded_playlist\">আপলোডেড</string>\n    <string name=\"filter_uploaded\">আপলোডেড</string>\n    <string name=\"sync_playlist\">প্লেলিস্ট সমন্বয়</string>\n    <string name=\"sync_disabled\">সমন্বয় বন্ধ</string>\n    <string name=\"allows_for_sync_witch_youtube\">নোট: এটা ইউটিউব মিউজিকের সাথে সমন্বয় করতে দেয়। পরবর্তীতে পরিবর্তন করা যাবেনা।</string>\n    <string name=\"generating_image\">ছবি তৈরী হচ্ছে</string>\n    <string name=\"please_wait\">অপেক্ষা করুন</string>\n    <string name=\"cancel\">বাতিল</string>\n    <string name=\"share_lyrics\">লিরিক্স শেয়ার</string>\n    <string name=\"share_as_text\">লেখা হিসেবে শেয়ার</string>\n    <string name=\"share_as_image\">ছবি হিসেবে শেয়ার</string>\n    <string name=\"max_selection_limit\">সর্বোচ্চ নির্বাচন সংখ্যা</string>\n    <string name=\"share_selected\">নির্বাচিতগুলো শেয়ার</string>\n    <string name=\"customize_colors\">রঙ পরিবর্তন</string>\n    <string name=\"text_color\">লিখার রঙ</string>\n    <string name=\"secondary_text_color\">লিখার রঙ (গৌণ)</string>\n    <string name=\"background_color\">ব্যাকগ্রাউন্ড এর রঙ</string>\n    <string name=\"remove_from_cache\">জমা থেকে মুছে ফেলা হয়েছে</string>\n    <string name=\"about_artist\">উদ্দেশ্য</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">%d সেকেন্ড</item>\n        <item quantity=\"other\">%d সেকেন্ড</item>\n    </plurals>\n    <string name=\"show_less\">কম দেখুন</string>\n    <string name=\"show_artist_description\">শিল্পীর সম্পর্কে দেখুন</string>\n    <string name=\"show_artist_subscriber_count\">সাবস্ক্রাইবার সংখ্যা দেখুন</string>\n    <string name=\"show_artist_monthly_listeners\">মাসিক শ্রোতা সংখ্যা দেখুন</string>\n    <string name=\"download_playlist_desc\">অফলাইনে শোনার জন্য সব গান ডাউনলোড করুন</string>\n    <string name=\"remove_download_playlist_desc\">এই প্লেলিস্ট থেকে সব ডাউনলোড গান সরিয়ে দিন</string>\n    <string name=\"download_in_progress_desc\">ভাউনলোড চলছে</string>\n    <string name=\"share_playlist_desc\">এই প্লেলিস্টটি অন্যদের সাথে শেয়ার করুন</string>\n    <string name=\"delete_playlist_desc\">এই প্লেলিস্টটি সম্পূর্ণভাবে মুছে ফেলুন</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-bn/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">হোম</string>\n    <string name=\"songs\">গান</string>\n    <string name=\"artists\">আর্টিস্ট</string>\n    <string name=\"albums\">অ্যালবাম</string>\n    <string name=\"playlists\">প্লেলিস্ট</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d সিলেক্টেড</item>\n        <item quantity=\"other\">%d সিলেক্টেড</item>\n    </plurals>\n    <string name=\"history\">হিস্ট্রি</string>\n    <string name=\"stats\">স্টাটস</string>\n    <string name=\"mood_and_genres\">মুড ও জনরা</string>\n    <string name=\"account\">অ্যাকাউন্ট</string>\n    <string name=\"quick_picks\">কুইক পিকস্</string>\n    <string name=\"quick_picks_empty\">কুইক পিক তৈরি করতে আগে কিছু গান শুনুন</string>\n    <string name=\"new_release_albums\">নতুন অ্যালবাম ও সিঙ্গেল</string>\n    <string name=\"today\">আজ</string>\n    <string name=\"yesterday\">গতকাল</string>\n    <string name=\"this_week\">এই সপ্তাহ</string>\n    <string name=\"last_week\">গত সপ্তাহ</string>\n    <string name=\"most_played_songs\">সর্বাধিক শোনা গানগুলি</string>\n    <string name=\"most_played_artists\">সর্বাধিক শোনা আর্টিস্ট</string>\n    <string name=\"most_played_albums\">সর্বাধিক শোনা অ্যালবাম</string>\n    <string name=\"search\">সার্চ</string>\n    <string name=\"search_yt_music\">YouTube Music এ সার্চ করুন</string>\n    <string name=\"search_library\">লাইব্রেরিতে সার্চ করুন</string>\n    <string name=\"filter_library\">লাইব্রেরি</string>\n    <string name=\"filter_liked\">লাইকড</string>\n    <string name=\"filter_downloaded\">ডাউনলোডেড</string>\n    <string name=\"filter_all\">সব</string>\n    <string name=\"filter_songs\">গান</string>\n    <string name=\"filter_videos\">ভিডিও</string>\n    <string name=\"filter_albums\">অ্যালবাম</string>\n    <string name=\"filter_artists\">আর্টিস্ট</string>\n    <string name=\"filter_playlists\">প্লেলিস্ট</string>\n    <string name=\"filter_community_playlists\">কমিউনিটি প্লেলিস্ট</string>\n    <string name=\"filter_featured_playlists\">ফিচার্ড প্লেলিস্ট</string>\n    <string name=\"filter_bookmarked\">বুকমার্কর্ড</string>\n    <string name=\"no_results_found\">কিছু পাওয়া যায়নি</string>\n    <string name=\"from_your_library\">আপনার লাইব্রেরি থেকে</string>\n    <string name=\"liked_songs\">আপনার লাইক করা গান</string>\n    <string name=\"downloaded_songs\">ডাউনলোড করা গান</string>\n    <string name=\"playlist_is_empty\">প্লেলিস্ট খালি</string>\n    <string name=\"retry\">আবার চেষ্টা করুন</string>\n    <string name=\"radio\">রেডিও</string>\n    <string name=\"shuffle\">শাফেল</string>\n    <string name=\"reset\">রিসেট</string>\n    <string name=\"details\">বিস্তারিত</string>\n    <string name=\"edit\">এডিট</string>\n    <string name=\"start_radio\">রেডিও চালু করুন</string>\n    <string name=\"play\">প্লে</string>\n    <string name=\"play_next\">পরবর্তী গান</string>\n    <string name=\"add_to_queue\">কিউতে অ্যাড করুন</string>\n    <string name=\"add_to_library\">লাইব্রেরি তে অ্যাড করুন</string>\n    <string name=\"remove_from_library\">লাইব্রেরি থেকে সরান</string>\n    <string name=\"action_download\">ডাউনলোড</string>\n    <string name=\"downloading\">ডাউনলোড হচ্ছে</string>\n    <string name=\"remove_download\">ডাউনলোড থেকে সরান</string>\n    <string name=\"import_playlist\">প্লেলিস্ট ইমপোর্ট করুন</string>\n    <string name=\"add_to_playlist\">প্লেলিস্টে অ্যাড করুন</string>\n    <string name=\"view_artist\">আর্টিস্ট দেখুন</string>\n    <string name=\"view_album\">অ্যালবাম দেখুন</string>\n    <string name=\"refetch\">রিফ্রেশ</string>\n    <string name=\"share\">শেয়ার</string>\n    <string name=\"delete\">ডিলিট</string>\n    <string name=\"remove_from_history\">হিস্ট্রি থেকে সরান</string>\n    <string name=\"search_online\">অনলাইন এ খুঁজুন</string>\n    <string name=\"action_sync\">সিঙ্ক্রোনাইজ</string>\n    <string name=\"advanced\">অ্যাডভান্স</string>\n    <string name=\"sort_by_create_date\">দিন</string>\n    <string name=\"sort_by_name\">নাম</string>\n    <string name=\"sort_by_artist\">আর্টিস্ট</string>\n    <string name=\"sort_by_year\">বছর</string>\n    <string name=\"sort_by_song_count\">মোস্ট প্লেইড</string>\n    <string name=\"sort_by_length\">গানের লেন্থ</string>\n    <string name=\"sort_by_play_time\">প্লে টাইম</string>\n    <string name=\"sort_by_custom\">কাস্টম</string>\n    <string name=\"media_id\">মিডিয়া আইডি</string>\n    <string name=\"mime_type\">মাইম টাইপ</string>\n    <string name=\"codecs\">কডেক</string>\n    <string name=\"bitrate\">বিটরেট</string>\n    <string name=\"sample_rate\">স্যাম্পল রেট</string>\n    <string name=\"loudness\">লাউডনেস</string>\n    <string name=\"volume\">ভলিউম</string>\n    <string name=\"file_size\">ফাইল সাইজ</string>\n    <string name=\"unknown\">আননোন</string>\n    <string name=\"copied\">ক্লিপবোর্ডে কপি করা হয়েছে</string>\n    <string name=\"edit_lyrics\">গানের লিরিক এডিট</string>\n    <string name=\"search_lyrics\">গানের লিরিক সার্চ</string>\n    <string name=\"edit_song\">গান এডিট করুন</string>\n    <string name=\"song_title\">টাইটেল</string>\n    <string name=\"song_artists\">আর্টিস্ট</string>\n    <string name=\"error_song_title_empty\">গানের টাইটেল খালি হতে পারে না।</string>\n    <string name=\"error_song_artist_empty\">গানের আর্টিস্ট খালি থাকতে পারে না।</string>\n    <string name=\"save\">সেভ</string>\n    <string name=\"choose_playlist\">একটি প্লেলিস্ট সিলেক্টে করুন</string>\n    <string name=\"edit_playlist\">প্লেলিস্ট এডিট করুন</string>\n    <string name=\"create_playlist\">প্লেলিস্ট তৈরি করুন</string>\n    <string name=\"playlist_name\">প্লেলিস্টের নাম</string>\n    <string name=\"error_playlist_name_empty\">প্লেলিস্টের নাম ফাকা রাখা যাবে না।</string>\n    <string name=\"edit_artist\">আর্টিস্ট এডিট করুন</string>\n    <string name=\"artist_name\">আর্টিস্টের নাম</string>\n    <string name=\"error_artist_name_empty\">আর্টিস্টের নাম খালি রাখা যাবে না।</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d গান</item>\n        <item quantity=\"other\">%d টা গান</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d আর্টিস্ট</item>\n        <item quantity=\"other\">%d টা আর্টিস্ট</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d অ্যালবাম</item>\n        <item quantity=\"other\">%d টা অ্যালবাম</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d প্লেলিস্ট</item>\n        <item quantity=\"other\">%d টা প্লেলিস্ট</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d সপ্তাহ</item>\n        <item quantity=\"other\">%d সপ্তাহ</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d মাস</item>\n        <item quantity=\"other\">%d মাস</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d বছর</item>\n        <item quantity=\"other\">%d বছর</item>\n    </plurals>\n    <string name=\"playlist_imported\">ইমপোর্ট করা প্লেলিস্ট</string>\n    <string name=\"removed_song_from_playlist\">প্লেলিস্ট থেকে \\\"%s\\\" সরানো হয়েছে</string>\n    <string name=\"playlist_synced\">সিঙ্ক্রোনাইজ করা প্লেলিস্ট</string>\n    <string name=\"undo\">বাতিল করুন</string>\n    <string name=\"lyrics_not_found\">গানের লিরিক পাওয়া যায়নি</string>\n    <string name=\"sleep_timer\">স্লীপ টাইমার</string>\n    <string name=\"end_of_song\">গান শেষে</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">%d মিনিট</item>\n        <item quantity=\"other\">%d মিনিট</item>\n    </plurals>\n    <string name=\"error_no_stream\">স্ট্রিম পাওয়া যায়নি</string>\n    <string name=\"error_no_internet\">নেটওয়ার্ক কানেকশন নেই</string>\n    <string name=\"error_timeout\">ইরর টাইমআউট</string>\n    <string name=\"error_unknown\">আননোন ইরর</string>\n    <string name=\"action_like\">লাইক</string>\n    <string name=\"action_remove_like\">অলাইক</string>\n    <string name=\"action_shuffle_on\">শাফেল অন</string>\n    <string name=\"action_shuffle_off\">শাফেল অফ</string>\n    <string name=\"repeat_mode_off\">রিপিট অফ</string>\n    <string name=\"repeat_mode_one\">একটা গান রিপিট</string>\n    <string name=\"repeat_mode_all\">রিপিট কিউ</string>\n    <string name=\"queue_all_songs\">সব গান কিউ</string>\n    <string name=\"queue_searched_songs\">সার্চ করা গান কিউ</string>\n    <string name=\"music_player\">প্লেয়ার</string>\n    <string name=\"settings\">সেটিংস</string>\n    <string name=\"appearance\">স্টাইল</string>\n    <string name=\"enable_dynamic_theme\">ডাইনামিক থিম চালু করুন</string>\n    <string name=\"dark_theme\">ডার্ক থিম</string>\n    <string name=\"dark_theme_on\">অন</string>\n    <string name=\"dark_theme_off\">অফ</string>\n    <string name=\"dark_theme_follow_system\">সিস্টেম থিম</string>\n    <string name=\"pure_black\">ব্ল্যাক</string>\n    <string name=\"default_open_tab\">ডিফল্ট ট্যাব</string>\n    <string name=\"customize_navigation_tabs\">নেভিগেশন ট্যাব কাস্টমাইজ করুন</string>\n    <string name=\"lyrics_text_position\">লিরিকের পজিশন</string>\n    <string name=\"left\">বামে</string>\n    <string name=\"center\">মাঝে</string>\n    <string name=\"right\">ডানে</string>\n    <string name=\"content\">কনটেন্ট</string>\n    <string name=\"login\">লগ ইন</string>\n    <string name=\"content_language\">ডিফল্ট কন্টেন্টের ভাষা</string>\n    <string name=\"content_country\">ডিফল্ট কন্টেন্টের দেশ</string>\n    <string name=\"system_default\">সিস্টেমের ডিফল্ট</string>\n    <string name=\"enable_proxy\">প্রক্সি চালু করুন</string>\n    <string name=\"proxy_type\">প্রক্সির ধরণ</string>\n    <string name=\"proxy_url\">প্রক্সি URL</string>\n    <string name=\"restart_to_take_effect\">পরিবর্তনগুলি আপ্লাই করতে অ্যাপটি আবার চালু করুন</string>\n    <string name=\"player_and_audio\">প্লেয়ার ও অডিও</string>\n    <string name=\"audio_quality\">অডিও কোয়ালিটি</string>\n    <string name=\"audio_quality_auto\">অটো</string>\n    <string name=\"audio_quality_high\">হাই</string>\n    <string name=\"audio_quality_low\">লো</string>\n    <string name=\"persistent_queue\">অনবরত কিউ</string>\n    <string name=\"skip_silence\">নীরব সময়টুকু স্কিপ করুন</string>\n    <string name=\"audio_normalization\">অডিও নরমালাইজেশন</string>\n    <string name=\"equalizer\">অডিও টিউনার</string>\n    <string name=\"storage\">স্টোরেজ</string>\n    <string name=\"cache\">ক্যাশ</string>\n    <string name=\"image_cache\">ইমেজ ক্যাশ</string>\n    <string name=\"song_cache\">অডিও ক্যাশ</string>\n    <string name=\"max_cache_size\">ক্যাশ সাইজ</string>\n    <string name=\"unlimited\">আনলিমিটেড</string>\n    <string name=\"clear_all_downloads\">সব ডাউনলোড মুছুন</string>\n    <string name=\"max_image_cache_size\">ইমেজ ক্যাশ সাইজ</string>\n    <string name=\"clear_image_cache\">ইমেজ ক্যাশে ক্লিয়ার করুন</string>\n    <string name=\"max_song_cache_size\">অডিও ক্যাশ সাইজ</string>\n    <string name=\"clear_song_cache\">অডিও ক্যাশে ক্লিয়ার করুন</string>\n    <string name=\"size_used\">%s ব্যবহৃত</string>\n    <string name=\"privacy\">গোপনীয়তা</string>\n    <string name=\"pause_listen_history\">প্লেব্যাক হিস্ট্রি বন্ধ করুন</string>\n    <string name=\"clear_listen_history\">প্লেব্যাক হিস্ট্রি ক্লিয়ার করুন</string>\n    <string name=\"clear_listen_history_confirm\">প্লেব্যাক হিস্ট্রি মুছে ফেলবেন?</string>\n    <string name=\"pause_search_history\">সার্চ হিস্ট্রি বন্ধ করুন</string>\n    <string name=\"clear_search_history\">সার্চ হিস্ট্রি ক্লিয়ার করুন</string>\n    <string name=\"clear_search_history_confirm\">সার্চ হিস্ট্রি মুছে ফেলবেন?</string>\n    <string name=\"enable_kugou\">KuGou থেকে লিরিক নিন</string>\n    <string name=\"backup_restore\">ব্যাকআপ এবং রিস্টোর</string>\n    <string name=\"action_backup\">ব্যাকআপ</string>\n    <string name=\"action_restore\">রিস্টোর</string>\n    <string name=\"imported_playlist\">ইমপোর্টেড করা প্লেলিস্ট</string>\n    <string name=\"backup_create_success\">ব্যাকআপ তৈরি করা হয়েছে</string>\n    <string name=\"backup_create_failed\">ব্যাকআপ করতে বার্থ</string>\n    <string name=\"restore_failed\">ব্যাকআপ থেকে রিস্টোর করতে অক্ষম</string>\n    <string name=\"about\">অ্যাপ সম্পর্কে</string>\n    <string name=\"app_version\">অ্যাপ ভার্সন</string>\n    <string name=\"new_version_available\">নতুন ভার্সন এসেছে</string>\n    <string name=\"translation_models\">ট্রান্সলেশন মডেল</string>\n    <string name=\"clear_translation_models\">ট্রান্সলেশন মডেল ক্লিয়ার করুন</string>\n    <string name=\"forgotten_favorites\">ভুলে যাওয়া প্রিয়গুলি</string>\n    <string name=\"keep_listening\">শুনতে থাকুন</string>\n    <string name=\"your_youtube_playlists\">আপনার ইউটিউব প্লেলিস্টগুলি</string>\n    <string name=\"similar_to\">সদৃশ</string>\n    <string name=\"library_song_empty\">লাইব্রেরির গানগুলি এখানে প্রদর্শিত হবে</string>\n    <string name=\"library_artist_empty\">লাইব্রেরির শিল্পীরা এখানে উপস্থিত হবেন</string>\n    <string name=\"library_album_empty\">লাইব্রেরি অ্যালবামগুলি এখানে প্রদর্শিত হবে</string>\n    <string name=\"library_playlist_empty\">আপনার প্লেলিস্টগুলি এখানে প্রদর্শিত হবে</string>\n    <string name=\"other_versions\">অন্যান্য সংস্করণসমূহ</string>\n    <string name=\"remove_download_playlist_confirm\">আপনি কি সত্যিই \\\"%s\\\" প্লেলিস্টের সব গান ডাউনলোড করা গানের স্টোরেজ থেকে মুছে ফেলতে চান?</string>\n    <string name=\"delete_playlist_confirm\">আপনি কি সত্যিই প্লেলিস্ট \\\"%s\\\" মুছে ফেলতে চান?</string>\n    <string name=\"add_all_to_library\">সবগুলো লাইব্রেরিতে যোগ করুন</string>\n    <string name=\"remove_all_from_library\">লাইব্রেরি থেকে সব মুছে ফেলুন</string>\n    <string name=\"remove_from_playlist\">প্লেলিস্ট থেকে সরান</string>\n    <string name=\"remove_from_queue\">কিউ থেকে সরান</string>\n    <string name=\"tempo_and_pitch\">টেম্পো এবং পিচ</string>\n    <string name=\"duplicates\">নকলগুলি</string>\n    <string name=\"skip_duplicates\">ডুপ্লিকেটগুলি এড়িয়ে যান</string>\n    <string name=\"add_anyway\">যাই হোক যোগ করুন</string>\n    <string name=\"duplicates_description_single\">গানটি ইতিমধ্যেই আপনার প্লেলিস্টে রয়েছে</string>\n    <string name=\"duplicates_description_multiple\">%d গান ইতিমধ্যেই আপনার প্লেলিস্টে রয়েছে</string>\n    <string name=\"action_like_all\">সবাইয়ের মতো</string>\n    <string name=\"action_remove_like_all\">সব লাইক মুছে ফেলুন</string>\n    <string name=\"theme\">থিম</string>\n    <string name=\"player\">প্লয়ার</string>\n    <string name=\"player_text_alignment\">প্লেয়ার টেক্সট সারিবদ্ধকরণ</string>\n    <string name=\"sided\">পাশযুক্ত</string>\n    <string name=\"player_slider_style\">প্লেয়ার স্লাইডার স্টাইল</string>\n    <string name=\"default_\">ডিফল্ট</string>\n    <string name=\"squiggly\">স্কুইগলি</string>\n    <string name=\"misc\">বিবিধ</string>\n    <string name=\"grid_cell_size\">গ্রিড সেল সাইজ</string>\n    <string name=\"small\">ছোট</string>\n    <string name=\"big\">বড়</string>\n    <string name=\"action_logout\">লগ আউট করুন</string>\n    <string name=\"action_login\">লগ ইন করুন</string>\n    <string name=\"not_logged_in\">লগ ইন করা হয়নি</string>\n    <string name=\"login_failed\">লগইন ব্যর্থ হয়েছে</string>\n    <string name=\"queue\">কিউ</string>\n    <string name=\"persistent_queue_desc\">অ্যাপ শুরু হলে আপনার শেষ কিউ পুনরুদ্ধার করুন</string>\n    <string name=\"auto_load_more\">অটো আরও গান লোড করুন</string>\n    <string name=\"auto_load_more_desc\">যদি সম্ভব হয়, কিউর শেষ হলে স্বয়ংক্রিয়ভাবে আরও গান যোগ করুন</string>\n    <string name=\"auto_skip_next_on_error\">ত্রুটি ঘটলে স্বয়ংক্রিয়ভাবে পরবর্তী গানে চলে যাওয়া</string>\n    <string name=\"auto_skip_next_on_error_desc\">আপনার অবিচ্ছিন্ন প্লেব্যাক অভিজ্ঞতা নিশ্চিত করুন</string>\n    <string name=\"stop_music_on_task_clear\">টাস্ক ক্লিয়ার হলে মিউজিক বন্ধ করুন</string>\n    <string name=\"listen_history\">ইতিহাস শুনুন</string>\n    <string name=\"search_history\">অনুসন্ধান ইতিহাস</string>\n    <string name=\"use_login_for_browse\">ব্রাউজিং কন্টেন্টের জন্য লগইন ব্যবহার করুন</string>\n    <string name=\"use_login_for_browse_desc\">এটি আপনার দেখা কন্টেন্টকে প্রভাবিত করতে পারে এবং উদাহরণস্বরূপ, যদি আপনি একটি প্রিমিয়াম অ্যাকাউন্ট দিয়ে লগইন করেন তবে শুধুমাত্র প্রিমিয়াম অ্যালবামগুলি দেখায়</string>\n    <string name=\"disable_screenshot\">স্ক্রিনশট নিষ্ক্রিয় করুন</string>\n    <string name=\"disable_screenshot_desc\">যখন এই বিকল্পটি চালু থাকে, তখন স্ক্রিনশট এবং রিসেন্টস-এ অ্যাপের ভিউ নিষ্ক্রিয় থাকে।</string>\n    <string name=\"enable_lrclib\">LrcLib গানের লিরিক্স প্রদানকারী সক্রিয় করুন</string>\n    <string name=\"hide_explicit\">স্পষ্ট বিষয়বস্তু লুকান</string>\n    <string name=\"discord_integration\">ডিসকর্ড ইন্টিগ্রেশন</string>\n    <string name=\"discord_information\">Metrolist আপনার Discord অ্যাকাউন্টের স্ট্যাটাস সেট করার জন্য KizzyRPC লাইব্রেরি ব্যবহার করে। এর মধ্যে Discord Gateway সংযোগ ব্যবহার করা হয়, যা Discord-এর পরিষেবার শর্তাবলীর (TOS) লঙ্ঘন হিসেবে বিবেচিত হতে পারে। তবে, এই কারণে ব্যবহারকারীর অ্যাকাউন্ট সাসপেন্ড হওয়ার কোনো পরিচিত ঘটনা নেই। ব্যবহার আপনার নিজের ঝুঁকিতে করুন।\\n\\nMetrolist শুধুমাত্র আপনার টোকেন সংগ্রহ করবে, এবং বাকী সবকিছু স্থানীয়ভাবে সংরক্ষিত থাকবে।</string>\n    <string name=\"dismiss\">বাতিল করুন</string>\n    <string name=\"options\">বিকল্পসমূহ</string>\n    <string name=\"preview\">প্রিভিউ</string>\n    <string name=\"enable_discord_rpc\">রিচ প্রেজেন্স সক্ষম করুন</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-bn-rIN/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">হোম</string>\n    <string name=\"songs\">গান</string>\n    <string name=\"artists\">শিল্পী</string>\n    <string name=\"albums\">অ্যালবাম</string>\n    <string name=\"playlists\">প্লেলিস্ট</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d নির্বাচিত</item>\n        <item quantity=\"other\">%d নির্বাচিত</item>\n    </plurals>\n    <string name=\"history\">ইতিহাস</string>\n    <string name=\"stats\">পরিসংখ্যান</string>\n    <string name=\"mood_and_genres\">মেজাজ এবং শৈলী</string>\n    <string name=\"account\">অ্যাকাউন্ট</string>\n    <string name=\"quick_picks\">দ্রুত পছন্দ</string>\n    <string name=\"quick_picks_empty\">আপনার নিজের শর্টকাট তৈরি করতে কিছু টিউন শুনুন</string>\n    <string name=\"new_release_albums\">নতুন অ্যালবাম ও সিঙ্গেল</string>\n    <string name=\"today\">আজ</string>\n    <string name=\"yesterday\">গতকাল</string>\n    <string name=\"this_week\">এই সপ্তাহ</string>\n    <string name=\"last_week\">গত সপ্তাহ</string>\n    <string name=\"most_played_songs\">সর্বাধিক শোনা গানগুলি</string>\n    <string name=\"most_played_artists\">সর্বাধিক শোনা শিল্পী</string>\n    <string name=\"most_played_albums\">সর্বাধিক শোনা অ্যালবাম</string>\n    <string name=\"search\">খুঁজুন</string>\n    <string name=\"search_yt_music\">খুঁজুন YouTube Music এ</string>\n    <string name=\"search_library\">খুঁজুন লাইব্রেরি তে</string>\n    <string name=\"filter_all\">সব</string>\n    <string name=\"filter_songs\">সংগীত</string>\n    <string name=\"filter_videos\">ভিডিও</string>\n    <string name=\"filter_albums\">অ্যালবাম</string>\n    <string name=\"filter_artists\">শিল্পী</string>\n    <string name=\"filter_playlists\">প্লেলিস্ট</string>\n    <string name=\"filter_community_playlists\">প্লেলিস্ট ক্রম</string>\n    <string name=\"filter_featured_playlists\">বিশিষ্ট প্লেলিস্ট</string>\n    <string name=\"no_results_found\">কিছু পাওয়া যায়নি</string>\n    <string name=\"from_your_library\">আপনার লাইব্রেরি থেকে</string>\n    <string name=\"liked_songs\">আপনার পছন্দের গান</string>\n    <string name=\"downloaded_songs\">ডাউনলোড করা গান</string>\n    <string name=\"playlist_is_empty\">প্লেলিস্ট খালি</string>\n    <string name=\"retry\">পুনরায় চেষ্টা করুন</string>\n    <string name=\"radio\">বেতার</string>\n    <string name=\"shuffle\">এলোমেলো</string>\n    <string name=\"details\">বিস্তারিত</string>\n    <string name=\"edit\">সম্পাদনা</string>\n    <string name=\"start_radio\">বেতার খুলুন</string>\n    <string name=\"play\">গান বাজান</string>\n    <string name=\"play_next\">পরবর্তী গান বাজান</string>\n    <string name=\"add_to_queue\">পরবর্তী গান যোগ করুন</string>\n    <string name=\"add_to_library\">লাইব্রেরি তে যোগ করুন</string>\n    <string name=\"remove_from_library\">লাইব্রেরি থেকে সরান</string>\n    <string name=\"action_download\">ডাউনলোড</string>\n    <string name=\"downloading\">ডাউনলোড হচ্ছে</string>\n    <string name=\"remove_download\">ডাউনলোড মুছে ফেলুন</string>\n    <string name=\"import_playlist\">প্লেলিস্ট আমদানি করুন</string>\n    <string name=\"add_to_playlist\">প্লেলিস্টে যোগ করুন</string>\n    <string name=\"view_artist\">শিল্পী দেখুন</string>\n    <string name=\"view_album\">অ্যালবাম দেখুন</string>\n    <string name=\"refetch\">আবার আনুন</string>\n    <string name=\"share\">শেয়ার</string>\n    <string name=\"delete\">মুছুন</string>\n    <string name=\"remove_from_history\">ইতিহাস থেকে অপসারণ</string>\n    <string name=\"search_online\">অনলাইন এ খুঁজুন</string>\n    <string name=\"action_sync\">সুসংগত</string>\n    <string name=\"sort_by_create_date\">সময় সম্পাদনা করা হয়েছে</string>\n    <string name=\"sort_by_name\">নাম</string>\n    <string name=\"sort_by_artist\">শিল্পী</string>\n    <string name=\"sort_by_year\">বছর</string>\n    <string name=\"sort_by_song_count\">প্লে সংখ্যান</string>\n    <string name=\"sort_by_length\">দৈর্ঘ্য</string>\n    <string name=\"sort_by_play_time\">বাজানো হয়েছে</string>\n    <string name=\"sort_by_custom\">অনুকুলিত</string>\n    <string name=\"loudness\">শব্দের মাত্রা</string>\n    <string name=\"volume\">ধ্বনির মাত্রা</string>\n    <string name=\"file_size\">ফাইলের আকার</string>\n    <string name=\"unknown\">অজানা</string>\n    <string name=\"copied\">ক্লিপবোর্ডে কপি করা হয়েছে</string>\n    <string name=\"edit_lyrics\">গানের কথা সম্পাদনা</string>\n    <string name=\"search_lyrics\">গানের কথা অনুসন্ধান</string>\n    <string name=\"edit_song\">গান সম্পাদনা করুন</string>\n    <string name=\"song_title\">শিরোনাম</string>\n    <string name=\"song_artists\">শিল্পী</string>\n    <string name=\"error_song_title_empty\">গানের শিরোনাম খালি হতে পারে না।</string>\n    <string name=\"error_song_artist_empty\">গানের শিল্পী খালি থাকতে পারে না।</string>\n    <string name=\"save\">সংরক্ষণ</string>\n    <string name=\"choose_playlist\">একটি প্লেলিস্ট চয়ন করুন</string>\n    <string name=\"edit_playlist\">প্লেলিস্ট সম্পাদনা করুন</string>\n    <string name=\"create_playlist\">প্লেলিস্ট তৈরি করুন</string>\n    <string name=\"playlist_name\">প্লেলিস্টের নাম</string>\n    <string name=\"error_playlist_name_empty\">প্লেলিস্টের নাম খালি রাখা যাবে না।</string>\n    <string name=\"edit_artist\">শিল্পী সম্পাদনা করুন</string>\n    <string name=\"artist_name\">শিল্পীর নাম</string>\n    <string name=\"error_artist_name_empty\">শিল্পীর নাম খালি রাখা যাবে না।</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d গান</item>\n        <item quantity=\"other\">%d গান</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d শিল্পী</item>\n        <item quantity=\"other\">%d শিল্পী</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d অ্যালবাম</item>\n        <item quantity=\"other\">%d অ্যালবাম</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d প্লেলিস্ট</item>\n        <item quantity=\"other\">%d প্লেলিস্ট</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d সপ্তাহ</item>\n        <item quantity=\"other\">%d সপ্তাহ</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d মাস</item>\n        <item quantity=\"other\">%d মাস</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d বছর</item>\n        <item quantity=\"other\">%d বছর</item>\n    </plurals>\n    <string name=\"playlist_imported\">আমদানি করা প্লেলিস্ট</string>\n    <string name=\"removed_song_from_playlist\">প্লেলিস্ট থেকে \\\"%s\\\" সরানো হয়েছে</string>\n    <string name=\"playlist_synced\">সিঙ্ক্রোনাইজ করা প্লেলিস্ট</string>\n    <string name=\"undo\">বাতিল করুন</string>\n    <string name=\"lyrics_not_found\">গানের কথা পাওয়া যায়নি</string>\n    <string name=\"sleep_timer\">ঘুমের টাইমার</string>\n    <string name=\"end_of_song\">গানের শেষ</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">%d মিনিট</item>\n        <item quantity=\"other\">%d মিনিট</item>\n    </plurals>\n    <string name=\"error_no_stream\">স্ট্রিম উপলব্ধ নয়</string>\n    <string name=\"error_no_internet\">নেটওয়ার্ক সংযোগ নেই</string>\n    <string name=\"error_timeout\">ত্রুটি সময়সীমা শেষ</string>\n    <string name=\"error_unknown\">অজানা ত্রুটি</string>\n    <string name=\"action_like\">পছন্দ</string>\n    <string name=\"action_remove_like\">অপছন্দ</string>\n    <string name=\"queue_all_songs\">সব গান</string>\n    <string name=\"queue_searched_songs\">অনুসন্ধান করা গান</string>\n    <string name=\"music_player\">প্লেয়ার</string>\n    <string name=\"settings\">সেটিংস</string>\n    <string name=\"appearance\">দৃষ্টিগোচরতা</string>\n    <string name=\"enable_dynamic_theme\">গতিশীল থিম সক্ষম করুন</string>\n    <string name=\"dark_theme\">অন্ধকার থিম</string>\n    <string name=\"dark_theme_on\">সক্রিয়</string>\n    <string name=\"dark_theme_off\">নিষ্ক্রিয়</string>\n    <string name=\"dark_theme_follow_system\">সিস্টেম অনুসরণ করুন</string>\n    <string name=\"pure_black\">কালো</string>\n    <string name=\"default_open_tab\">ডিফল্ট প্রধান ট্যাব</string>\n    <string name=\"customize_navigation_tabs\">নেভিগেশন ট্যাব কাস্টমাইজ করুন</string>\n    <string name=\"lyrics_text_position\">গানের কথার অবস্থান</string>\n    <string name=\"left\">বাম</string>\n    <string name=\"center\">কেন্দ্র</string>\n    <string name=\"right\">ডান</string>\n    <string name=\"content\">কনটেন্ট</string>\n    <string name=\"login\">সাইন ইন</string>\n    <string name=\"content_language\">ডিফল্ট কন্টেন্টের ভাষা</string>\n    <string name=\"content_country\">ডিফল্ট কনটেন্ট দেশ</string>\n    <string name=\"system_default\">সিস্টেমের ডিফল্ট</string>\n    <string name=\"enable_proxy\">প্রক্সি সক্রিয় করুন</string>\n    <string name=\"proxy_type\">প্রক্সি ধরণ</string>\n    <string name=\"proxy_url\">প্রক্সি URL</string>\n    <string name=\"restart_to_take_effect\">পরিবর্তনগুলি প্রয়োগ করতে অ্যাপটি পুনরায় চালু করুন</string>\n    <string name=\"player_and_audio\">প্লেয়ার এবং অডিও</string>\n    <string name=\"audio_quality\">অডিওর মান</string>\n    <string name=\"audio_quality_auto\">স্বয়ংক্রিয়</string>\n    <string name=\"audio_quality_high\">উচ্চ</string>\n    <string name=\"audio_quality_low\">নিম্ন</string>\n    <string name=\"persistent_queue\">অবিরাম সারি</string>\n    <string name=\"skip_silence\">নীরবতা এড়িয়ে যান</string>\n    <string name=\"audio_normalization\">অডিও স্বাভাবিকীকরণ</string>\n    <string name=\"equalizer\">অডিও টিউনার</string>\n    <string name=\"storage\">স্টোরেজ</string>\n    <string name=\"cache\">ক্যাশে</string>\n    <string name=\"image_cache\">ইমেজ ক্যাশে</string>\n    <string name=\"song_cache\">অডিও ক্যাশে</string>\n    <string name=\"max_cache_size\">সর্বাধিক ক্যাশে আকার</string>\n    <string name=\"unlimited\">সীমাহীন</string>\n    <string name=\"clear_all_downloads\">সমস্ত ডাউনলোড মুছুন</string>\n    <string name=\"max_image_cache_size\">ইমেজ ক্যাশে সর্বাধিক আকার</string>\n    <string name=\"clear_image_cache\">ইমেজ ক্যাশে সাফ করুন</string>\n    <string name=\"max_song_cache_size\">অডিও ক্যাশে সর্বোচ্চ আকার</string>\n    <string name=\"clear_song_cache\">অডিও ক্যাশে সাফ করুন</string>\n    <string name=\"size_used\">%s ব্যবহৃত</string>\n    <string name=\"privacy\">গোপনীয়তা</string>\n    <string name=\"pause_listen_history\">আপনার শোনার ইতিহাস থামান</string>\n    <string name=\"clear_listen_history\">আপনার শোনার ইতিহাস সাফ করুন</string>\n    <string name=\"clear_listen_history_confirm\">আপনি কি আপনার শোনার ইতিহাস মুছে ফেলার বিষয়ে নিশ্চিত?</string>\n    <string name=\"pause_search_history\">আপনার অনুসন্ধান ইতিহাস স্থগিত</string>\n    <string name=\"clear_search_history\">আপনার অনুসন্ধান ইতিহাস সাফ করুন</string>\n    <string name=\"clear_search_history_confirm\">আপনি কি আপনার অনুসন্ধান ইতিহাস মুছে ফেলার বিষয়ে নিশ্চিত?</string>\n    <string name=\"enable_kugou\">KuGou দ্বারা প্রদত্ত গানের কথা সক্রিয় করুন</string>\n    <string name=\"backup_restore\">ব্যাকআপ এবং পুনঃস্থাপন</string>\n    <string name=\"action_backup\">ব্যাকআপ</string>\n    <string name=\"action_restore\">পুনঃস্থাপন</string>\n    <string name=\"imported_playlist\">আমদানি করা প্লেলিস্ট</string>\n    <string name=\"backup_create_success\">ব্যাকআপ সফলভাবে তৈরি করা হয়েছে</string>\n    <string name=\"backup_create_failed\">ব্যাকআপ করতে অক্ষম</string>\n    <string name=\"restore_failed\">ব্যাকআপ থেকে পুনরুদ্ধার করতে অক্ষম</string>\n    <string name=\"about\">সম্পর্কিত</string>\n    <string name=\"app_version\">অ্যাপ সংস্করণ</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-bs/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"lyrics_auto_scroll\">Automatsko pomjeranje stihova</string>\n    <string name=\"show_cached_playlist\">Prikaži \\\"Keširanu\\\" plejlistu</string>\n    <string name=\"remote_history\">Udaljen</string>\n    <string name=\"back_button_desc\">Nazad</string>\n    <string name=\"trending\">U trendu</string>\n    <string name=\"weeks\">Sedmice</string>\n    <string name=\"years\">Godina</string>\n    <string name=\"continuous\">Kontinuirano</string>\n    <string name=\"offline\">Skinuto</string>\n    <string name=\"sync_playlist\">Sinhronizovana plejlista</string>\n    <string name=\"please_wait\">Molimo vas sačekajte</string>\n    <string name=\"select\">Izaberi sve</string>\n    <string name=\"similar_content\">#Sličan #sadržaj</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d put</item>\n        <item quantity=\"few\">%d puta</item>\n        <item quantity=\"other\">%d puta</item>\n    </plurals>\n    <string name=\"follow_theme\">Zaprati temu</string>\n    <string name=\"gradient\">Gradijent</string>\n    <string name=\"player_background_blur\">Zamućenje</string>\n    <string name=\"player_buttons_style\">Boje dugmadi igrača</string>\n    <string name=\"player_background_style\">#Igrač #pozadina #stil</string>\n    <string name=\"default_style\">Podrazumevani stil</string>\n    <string name=\"enable_swipe_thumbnail\">Omogući promjenu pjesme prevlačenjem</string>\n    <string name=\"swipe_song_to_add\">Prevucite pjesmu ulijevo da biste je dodali u red čekanja ili udesno da biste je pustili sljedeću</string>\n    <string name=\"lyrics_click_change\">Promijenite stihove klikom</string>\n    <string name=\"slim_navbar\">Sakrij oznake donje navigacijske trake</string>\n    <string name=\"auto_playlists\">Automatske plejliste</string>\n    <string name=\"slim\">Usko</string>\n    <string name=\"show_liked_playlist\">Prikaži plejliste \\\"Sviđa mi se\\\"</string>\n    <string name=\"show_downloaded_playlist\">Prikaži plejlistu “Preuzeto”</string>\n    <string name=\"show_top_playlist\">Prikaži \\\"Najbolju\\\" plejlistu</string>\n    <string name=\"local_history\">Lokalno</string>\n    <string name=\"months\">Mjeseci</string>\n    <string name=\"advanced_login\">Napredni login(token)</string>\n    <string name=\"token_hidden\">Pritisni da pokažeš token</string>\n    <string name=\"token_shown\">Pritisni opet da kopiraš ili urediš</string>\n    <string name=\"general\">Generalno</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"default_lib_chips\">Promijenite zadani bibliotečki čip</string>\n    <string name=\"set_quick_picks\">Postavite brze odabire</string>\n    <string name=\"last_song_listened\">Bazirano na zadnju pjesmu koju ste slušali</string>\n    <string name=\"app_language\">Jezik aplikacije</string>\n    <string name=\"enable_similar_content\">Omogućite sličan sadržaj</string>\n    <string name=\"similar_content_desc\">Automatski dodaj još sličnih pjesama kada se dođe do kraja reda čekanja</string>\n    <string name=\"release_notes\">Bilješke o izdanju</string>\n    <string name=\"default_links\">Otvorite podržane veze</string>\n    <string name=\"open_app_settings_error\">Nije moguće otvoriti postavke aplikacije</string>\n    <string name=\"dislikes\">Dislajkova</string>\n    <string name=\"import_online\">Uvezite \\\"m3u\\\" plejliste</string>\n    <string name=\"import_csv\">Uvoz playlista iz \\\"csv\\\" formata</string>\n    <string name=\"auto_download_on_like_desc\">Automatski preuzmite pjesme kada vam se sviđaju</string>\n    <string name=\"clear_song_cache_dialog\">Jeste li sigurni da želite obrisati sve keširane pjesme?</string>\n    <string name=\"clear_downloads_dialog\">Jeste li sigurni da želite obrisati sva preuzimanja?</string>\n    <string name=\"all_time\">Sve vrijeme</string>\n    <string name=\"past_24_hours\">Zadnjih 24 sata</string>\n    <string name=\"past_week\">Prošle sedmice</string>\n    <string name=\"past_month\">Prošli mjesec</string>\n    <string name=\"top_length\">Dužina moje top liste</string>\n    <string name=\"information\">Informacije</string>\n    <string name=\"description\">Opis</string>\n    <string name=\"views\">Pregleda</string>\n    <string name=\"likes\">Lajkova</string>\n    <string name=\"charts\">Grafikoni</string>\n    <string name=\"album_cover_desc\">Naslovnica albuma</string>\n    <string name=\"top_music_videos\">Najbolji muzički spotovi</string>\n    <string name=\"liked\">Lajkovano</string>\n    <string name=\"my_top\">Moj top</string>\n    <string name=\"cached_playlist\">Keširano</string>\n    <string name=\"sync_disabled\">Sinhronizacoja onemogućena</string>\n    <string name=\"allows_for_sync_witch_youtube\">Napomena: Ovo omogućava sinhronizaciju s YouTube Music. Ovo se NE može kasnije promijeniti.</string>\n    <string name=\"generating_image\">Generisanje slike</string>\n    <string name=\"cancel\">Otkaži</string>\n    <string name=\"share_lyrics\">Podjeli stihove</string>\n    <string name=\"share_as_text\">Podjeli kao tekst</string>\n    <string name=\"share_as_image\">Podjeli kao sliku</string>\n    <string name=\"max_selection_limit\">Maksimalna granica izbora</string>\n    <string name=\"share_selected\">Dijeljenje odabrano</string>\n    <string name=\"customize_colors\">Prilagodite boje</string>\n    <string name=\"text_color\">Boja teksta</string>\n    <string name=\"secondary_text_color\">Sekundarna boja teksta</string>\n    <string name=\"background_color\">Boja pozadine</string>\n    <string name=\"remove_from_cache\">Ukloni iz keša</string>\n    <string name=\"copy_link\">Kopiraj vezu</string>\n    <string name=\"like_all\">Lajkuj sve</string>\n    <string name=\"dislike_all\">Dislajkuj sve</string>\n    <string name=\"sort_by_last_updated\">Datum ažuriran</string>\n    <string name=\"link_copied\">Link je kopiran u međuspremnik</string>\n    <string name=\"lyrics\">Stihovi</string>\n    <string name=\"already_in_playlist\">Već je u plejlisti:</string>\n    <string name=\"auto_download_on_like\">Automatsko preuzimanje na lajk</string>\n    <string name=\"past_year\">Prošle godine</string>\n    <string name=\"playlist_add_local_to_synced_note\">Napomena: Dodavanje lokalnih pjesama na sinhronizovane/udaljene plejliste nije podržano. Bilo koja druga kombinacija je važeća</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"not_logged_in_youtube\">Niste prijavljeni na YouTube</string>\n    <string name=\"history_duration\">Trajanje istorije</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">%dsekund</item>\n        <item quantity=\"few\">%dsekunde</item>\n        <item quantity=\"other\">%dsekundi</item>\n    </plurals>\n    <string name=\"token_adv_login_description\">Ovo je NAPREDNA metoda prijave. Kao alternativa web portalu, ovdje možete direktno unijeti ili ažurirati svoj token za prijavu. Na primjer, ovo može ubrzati prijavu na više uređaja. Imajte na umu da neće biti prihvaćeni nevažeći formati tokena koje aplikacija ne uspije analizirati</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-bs/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"albums\">Albumi</string>\n    <string name=\"songs\">Pjesme</string>\n    <string name=\"similar_to\">Slično ko</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d izabran</item>\n        <item quantity=\"few\">%d izabrana</item>\n        <item quantity=\"other\">%d izabrano</item>\n    </plurals>\n    <string name=\"history\">Historija</string>\n    <string name=\"forgotten_favorites\">Zaboravljeni favoriti</string>\n    <string name=\"this_week\">Ove sedmice</string>\n    <string name=\"search\">Pretraži</string>\n    <string name=\"filter_featured_playlists\">Istaknuti spisci</string>\n    <string name=\"library_artist_empty\">Umjetnici biblioteke će se ovdje pojaviti</string>\n    <string name=\"downloaded_songs\">Preuzete pjesme</string>\n    <string name=\"delete_playlist_confirm\">Da li zaista želite da izbrišete spisku \\\"%s\\\"?</string>\n    <string name=\"shuffle\">Izmješaj</string>\n    <string name=\"reset\">Ponovo pokreni</string>\n    <string name=\"view_artist\">Pokaži umjetnika</string>\n    <string name=\"sort_by_create_date\">Datum dodavanja</string>\n    <string name=\"media_id\">Id medija</string>\n    <string name=\"mime_type\">MIME tip</string>\n    <string name=\"codecs\">Kodek</string>\n    <string name=\"bitrate\">Bitrata</string>\n    <string name=\"song_title\">Naziv pjesme</string>\n    <string name=\"create_playlist\">Napravi spisku</string>\n    <string name=\"add_anyway\">Dodaj svejedno</string>\n    <string name=\"duplicates_description_multiple\">%d pjesme su već u vašoj spisci</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d pjesma</item>\n        <item quantity=\"few\">%d pjesme</item>\n        <item quantity=\"other\">%d pjesama</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d umjetnik</item>\n        <item quantity=\"few\">%d umjetnika</item>\n        <item quantity=\"other\">%d umjetnika</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d album</item>\n        <item quantity=\"few\">%d albuma</item>\n        <item quantity=\"other\">%d albuma</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d spisak</item>\n        <item quantity=\"few\">%d spiske</item>\n        <item quantity=\"other\">%d spiskih</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d sedmica</item>\n        <item quantity=\"few\">%d sedmice</item>\n        <item quantity=\"other\">%d sedmica</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d mjesec</item>\n        <item quantity=\"few\">%d mjeseca</item>\n        <item quantity=\"other\">%d mjeseci</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d godina</item>\n        <item quantity=\"few\">%d godine</item>\n        <item quantity=\"other\">%d godina</item>\n    </plurals>\n    <string name=\"lyrics_not_found\">Tekst nije pronađen</string>\n    <string name=\"error_no_stream\">Prijenos nije dostupan</string>\n    <string name=\"action_shuffle_on\">Miješanje uključeno</string>\n    <string name=\"grid_cell_size\">Veličina ćelije mreže</string>\n    <string name=\"skip_silence\">Preskoči tišinu</string>\n    <string name=\"audio_normalization\">Normalizacija zvuka</string>\n    <string name=\"storage\">Skladište</string>\n    <string name=\"cache\">Keš</string>\n    <string name=\"privacy\">Privatnost</string>\n    <string name=\"search_history\">Historija pretrage</string>\n    <string name=\"pause_search_history\">Pauziraj historiju pretrage</string>\n    <string name=\"disable_screenshot\">Onemogući slikanje zaslona</string>\n    <string name=\"enable_kugou\">Omogući KuGou dobavljača teksta</string>\n    <string name=\"backup_create_success\">Rezervna kopija je uspješno sagrađena</string>\n    <string name=\"backup_create_failed\">Nije moguće napraviti rezervnu kopiju</string>\n    <string name=\"restore_failed\">Neuspješan povratak rezervne kopije</string>\n    <string name=\"discord_integration\">Integracija sa Discord-om</string>\n    <string name=\"dismiss\">Odbaci</string>\n    <string name=\"home\">Početna</string>\n    <string name=\"undo\">Vrati</string>\n    <string name=\"artists\">Umjetnici</string>\n    <string name=\"playlists\">Spisci za reprodukciju</string>\n    <string name=\"search_yt_music\">Pretraži YouTube Music…</string>\n    <string name=\"from_your_library\">Iz vaše biblioteke</string>\n    <string name=\"stats\">Statistike</string>\n    <string name=\"mood_and_genres\">Raspoloženje i Žanrovi</string>\n    <string name=\"quick_picks\">Brzi izbori</string>\n    <string name=\"account\">Račun</string>\n    <string name=\"quick_picks_empty\">Odslušaj pjesme kako bi generisali vaše brze odabire</string>\n    <string name=\"keep_listening\">Nastavite da slušate</string>\n    <string name=\"your_youtube_playlists\">Vaši YouTube spisci za reprodukciju</string>\n    <string name=\"most_played_albums\">Najslušaniji albumi</string>\n    <string name=\"filter_albums\">Albumi</string>\n    <string name=\"new_release_albums\">Novi albumi</string>\n    <string name=\"filter_songs\">Pjesme</string>\n    <string name=\"today\">Danas</string>\n    <string name=\"most_played_artists\">Najslušaniji umjetnici</string>\n    <string name=\"yesterday\">Jučer</string>\n    <string name=\"last_week\">Prošle sedmice</string>\n    <string name=\"most_played_songs\">Najslušanije pjesme</string>\n    <string name=\"search_library\">Pretraži biblioteku…</string>\n    <string name=\"filter_library\">Biblioteka</string>\n    <string name=\"filter_bookmarked\">Obilježeno</string>\n    <string name=\"library_song_empty\">Pjesme biblioteke će se ovdje pokazati</string>\n    <string name=\"filter_liked\">Sviđa mi se</string>\n    <string name=\"filter_downloaded\">Preuzeto</string>\n    <string name=\"filter_all\">Sve</string>\n    <string name=\"tempo_and_pitch\">Tempo i Visina tona</string>\n    <string name=\"filter_videos\">Video-nadzori</string>\n    <string name=\"library_playlist_empty\">Vaši spisci će se ovdje pojaviti</string>\n    <string name=\"filter_artists\">Umjetnici</string>\n    <string name=\"filter_playlists\">Spisci za reprodukciju</string>\n    <string name=\"filter_community_playlists\">Zajednički spisci</string>\n    <string name=\"no_results_found\">Nema rezultata</string>\n    <string name=\"library_album_empty\">Albumi biblioteke će se ovdje pojaviti</string>\n    <string name=\"liked_songs\">Pjesme koje vam se sviđaju</string>\n    <string name=\"other_versions\">Druge verzije</string>\n    <string name=\"playlist_is_empty\">Popis je prazan</string>\n    <string name=\"remove_download_playlist_confirm\">Da li zaista želite da obrišete sve \\\"%s\\\" pjesme iz spiska iz skladišta Preuzetih Pjesama?</string>\n    <string name=\"retry\">Probaj ponovo</string>\n    <string name=\"radio\">Radio</string>\n    <string name=\"start_radio\">Pokrenite radio</string>\n    <string name=\"remove_all_from_library\">Ukloni iz svih biblioteka</string>\n    <string name=\"details\">Detalji</string>\n    <string name=\"add_to_queue\">Dodaj u red</string>\n    <string name=\"edit\">Uredi</string>\n    <string name=\"play\">Pokreni</string>\n    <string name=\"play_next\">Pokreni sljedeće</string>\n    <string name=\"add_to_library\">Dodaj u biblioteku</string>\n    <string name=\"add_all_to_library\">Dodaj u sve biblioteke</string>\n    <string name=\"remove_from_playlist\">Ukloni iz spiska</string>\n    <string name=\"remove_from_library\">Ukloni iz biblioteke</string>\n    <string name=\"downloading\">Preuzima se</string>\n    <string name=\"action_download\">Preuzmi</string>\n    <string name=\"remove_download\">Ukloni preuzimanje</string>\n    <string name=\"search_lyrics\">Pretraži tekstove</string>\n    <string name=\"import_playlist\">Uvezi spisku</string>\n    <string name=\"view_album\">Pokaži album</string>\n    <string name=\"refetch\">Dodaj ponovo</string>\n    <string name=\"search_online\">Pretraži preko mreže</string>\n    <string name=\"sort_by_song_count\">Broj pjesama</string>\n    <string name=\"volume\">Zvuk</string>\n    <string name=\"file_size\">Veličina datoteke</string>\n    <string name=\"error_song_artist_empty\">Umjetnik pjesme ne može biti prazan.</string>\n    <string name=\"add_to_playlist\">Dodaj u spisak</string>\n    <string name=\"share\">Podijeli</string>\n    <string name=\"delete\">Izbriši</string>\n    <string name=\"remove_from_queue\">Ukloni iz reda</string>\n    <string name=\"advanced\">Napredno</string>\n    <string name=\"sort_by_year\">Godina</string>\n    <string name=\"remove_from_history\">Ukloni iz historije</string>\n    <string name=\"action_sync\">Sinkronizuj</string>\n    <string name=\"sort_by_name\">Ime</string>\n    <string name=\"sort_by_custom\">Posebni red</string>\n    <string name=\"loudness\">Glasnoća</string>\n    <string name=\"unknown\">Nepoznato</string>\n    <string name=\"save\">Sačuvaj</string>\n    <string name=\"sort_by_artist\">Umjetnik</string>\n    <string name=\"sort_by_length\">Dužina</string>\n    <string name=\"sort_by_play_time\">Vrijeme puštanja</string>\n    <string name=\"sample_rate\">Stopa uzorkovanja</string>\n    <string name=\"copied\">Kopirano u međuspremnik</string>\n    <string name=\"choose_playlist\">Odaberi spisak</string>\n    <string name=\"edit_lyrics\">Izmjeni tekst</string>\n    <string name=\"edit_song\">Izmjeni pjesmu</string>\n    <string name=\"song_artists\">Umjetnik pjesme</string>\n    <string name=\"playlist_name\">Ime spiske</string>\n    <string name=\"skip_duplicates\">Preskoči duplikate</string>\n    <string name=\"error_song_title_empty\">Naslov pjesme ne može biti prazan.</string>\n    <string name=\"edit_playlist\">Izmjeni spisku</string>\n    <string name=\"edit_artist\">Izmjeni umjetnika</string>\n    <string name=\"error_playlist_name_empty\">Ime spiske ne može biti prazno.</string>\n    <string name=\"artist_name\">Ime umjetnika</string>\n    <string name=\"error_artist_name_empty\">Ime umjetnika ne može biti prazno.</string>\n    <string name=\"duplicates_description_single\">Pjesma je već u vašoj spisci</string>\n    <string name=\"duplicates\">Duplikati</string>\n    <string name=\"playlist_imported\">Spiska uvezena</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">%d minut</item>\n        <item quantity=\"few\">%d minute</item>\n        <item quantity=\"other\">%d minuta</item>\n    </plurals>\n    <string name=\"action_like_all\">Sviđa mi se sve</string>\n    <string name=\"action_remove_like_all\">Ukloni sva sviđanja</string>\n    <string name=\"queue_searched_songs\">Pretražene pjesme</string>\n    <string name=\"proxy_type\">Tip proksija</string>\n    <string name=\"removed_song_from_playlist\">Ukloni \\\"%s\\\" iz spiske</string>\n    <string name=\"playlist_synced\">Spiska sinkronizovana</string>\n    <string name=\"error_unknown\">Nepoznata greška</string>\n    <string name=\"action_like\">Sviđa mi se</string>\n    <string name=\"sleep_timer\">Mjerač vremena za spavanje</string>\n    <string name=\"end_of_song\">Kraj pjesme</string>\n    <string name=\"error_no_internet\">Nema internet veze</string>\n    <string name=\"error_timeout\">Istek vremena</string>\n    <string name=\"repeat_mode_off\">Režim ponavljanja je isključen</string>\n    <string name=\"repeat_mode_one\">Ponavljaj trenutnu pjesmu</string>\n    <string name=\"repeat_mode_all\">Ponovi red</string>\n    <string name=\"action_remove_like\">Ukloni sviđanje</string>\n    <string name=\"action_shuffle_off\">Miješanje isključeno</string>\n    <string name=\"music_player\">Muzički Plejer</string>\n    <string name=\"queue_all_songs\">Sve pjesme</string>\n    <string name=\"theme\">Tema</string>\n    <string name=\"enable_dynamic_theme\">Uključi dinamičnu temu</string>\n    <string name=\"left\">Lijevo</string>\n    <string name=\"default_\">Zadano</string>\n    <string name=\"dark_theme\">Mračna tema</string>\n    <string name=\"dark_theme_on\">Uključeno</string>\n    <string name=\"center\">Centar</string>\n    <string name=\"squiggly\">Valovito</string>\n    <string name=\"small\">Malo</string>\n    <string name=\"login\">Prijavi se</string>\n    <string name=\"settings\">Podešavanja</string>\n    <string name=\"appearance\">Izgled</string>\n    <string name=\"big\">Veliko</string>\n    <string name=\"restart_to_take_effect\">Ponovo pokrenite da biste vidjeli promjenu</string>\n    <string name=\"customize_navigation_tabs\">Izmjeni kartice za navigaciju</string>\n    <string name=\"lyrics_text_position\">Pozicija liričnog teksta</string>\n    <string name=\"dark_theme_off\">Isključeno</string>\n    <string name=\"pure_black\">Čisto crna</string>\n    <string name=\"player\">Plejer</string>\n    <string name=\"player_text_alignment\">Poravnanje teksta plejera</string>\n    <string name=\"right\">Desno</string>\n    <string name=\"default_open_tab\">Zadana otvorena kartica</string>\n    <string name=\"dark_theme_follow_system\">Prati sistem</string>\n    <string name=\"sided\">Na strani</string>\n    <string name=\"player_slider_style\">Izgled klizača plejera</string>\n    <string name=\"misc\">Ostalo</string>\n    <string name=\"content\">Sadržaj</string>\n    <string name=\"system_default\">Sistemski zadano</string>\n    <string name=\"not_logged_in\">Niste prijavljeni</string>\n    <string name=\"content_language\">Zadani jezik sadržaja</string>\n    <string name=\"content_country\">Zadana zemlja sadržaja</string>\n    <string name=\"enable_proxy\">Omogući proksiju</string>\n    <string name=\"proxy_url\">URL Proksija</string>\n    <string name=\"player_and_audio\">Plejer i zvuk</string>\n    <string name=\"audio_quality_auto\">Automatski</string>\n    <string name=\"audio_quality\">Kvalitet zvuka</string>\n    <string name=\"audio_quality_high\">Visoko</string>\n    <string name=\"clear_search_history\">Izbriši historiju pretrage</string>\n    <string name=\"audio_quality_low\">Nisko</string>\n    <string name=\"auto_load_more\">Automatski učitajte više pjesama</string>\n    <string name=\"queue\">Red</string>\n    <string name=\"persistent_queue_desc\">Obnovite svoj posljednji red kada se aplikacija ponovo pokrene</string>\n    <string name=\"auto_load_more_desc\">Automatski dodajte više pjesmih kada se red završi, ako je moguće</string>\n    <string name=\"equalizer\">Ekvalizator</string>\n    <string name=\"clear_song_cache\">Obriši keš pjesama</string>\n    <string name=\"persistent_queue\">Uporan red</string>\n    <string name=\"auto_skip_next_on_error\">Automatski preskoči do sljedeće pjesme kada dođe do greške</string>\n    <string name=\"auto_skip_next_on_error_desc\">Obezbjedite svoje neprekidno iskustvo reprodukcije</string>\n    <string name=\"stop_music_on_task_clear\">Zaustavite muziku kada se procesi obrišu</string>\n    <string name=\"clear_all_downloads\">Obrišite sva preuzimanja</string>\n    <string name=\"max_image_cache_size\">Maksimalna veličina keša slika</string>\n    <string name=\"clear_image_cache\">Obrišite keš slika</string>\n    <string name=\"max_song_cache_size\">Maksimalna veličina keša pjesama</string>\n    <string name=\"image_cache\">Keš slika</string>\n    <string name=\"listen_history\">Historija slušanja</string>\n    <string name=\"song_cache\">Keš pjesama</string>\n    <string name=\"unlimited\">Neograničeno</string>\n    <string name=\"size_used\">%s korišteno</string>\n    <string name=\"max_cache_size\">Maksimalna veličina keša</string>\n    <string name=\"pause_listen_history\">Pauziraj historiju slušanja</string>\n    <string name=\"clear_listen_history_confirm\">Da li ste sigurni da želite da izbrišete svu historiju slušanja?</string>\n    <string name=\"clear_listen_history\">Izbriši historiju slušanja</string>\n    <string name=\"clear_search_history_confirm\">Da li ste sigurni da želite da izbrišete svu historiju pretrage?</string>\n    <string name=\"disable_screenshot_desc\">Kada je opcija omogućena, slikanje zaslona i pregled aplikacije u Nedavnim je onemogućeno.</string>\n    <string name=\"enable_lrclib\">Omogući LrcLib dobavljača teksta</string>\n    <string name=\"hide_explicit\">Sakrij eksplicitan sadržaj</string>\n    <string name=\"backup_restore\">Rezervne kopije i povratak</string>\n    <string name=\"action_backup\">Rezervna kopija</string>\n    <string name=\"action_restore\">Povratak</string>\n    <string name=\"imported_playlist\">Spiska je uvezena</string>\n    <string name=\"discord_information\">Metrolist koristi KizzyRPC biblioteku kako bi stavio tvoj Discord status. Ovo uključuje korištenje Discord Gateway spajanja, što bi se moglo smatrati prekršajem Discord-ovog TOS-a. Međutim nema poznatih slučajeva gdje su korisnički nalozi bili suspendovani zbog ovoga razloga. Korisite na svoju štetu.\\n\\nMetrolist će samo izvesti vaš žeton, i sve drugo je lokalno sklađeno.</string>\n    <string name=\"login_failed\">Neuspješno upisivanje</string>\n    <string name=\"options\">Opcije</string>\n    <string name=\"preview\">Pregled</string>\n    <string name=\"enable_discord_rpc\">Omogući Bogatu Prisutnost</string>\n    <string name=\"app_version\">Verzija aplikacije</string>\n    <string name=\"action_logout\">Izpiši se</string>\n    <string name=\"about\">O Metrolist-u</string>\n    <string name=\"clear_translation_models\">Rasčisti modele prevođenja</string>\n    <string name=\"new_version_available\">Dostupna je nova verzija</string>\n    <string name=\"translation_models\">Modeli prevođenja</string>\n    <string name=\"use_login_for_browse_desc\">Ovo može utjecati na sadržaj koji vidite i, na primjer, prikazivati albume koji su dostupni samo za Premium korisnike ako ste prijavljeni sa Premium računom ili nalogom</string>\n    <string name=\"use_login_for_browse\">Koristite prijavu za pregledavanje sadržaja</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-ca/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">Local</string>\n    <string name=\"remote_history\">Remot</string>\n    <string name=\"charts\">Llistes</string>\n    <string name=\"back_button_desc\">Enrere</string>\n    <string name=\"album_cover_desc\">Portada de l\\'àlbum</string>\n    <string name=\"top_music_videos\">Top vídeos musicals</string>\n    <string name=\"trending\">Tendència</string>\n    <string name=\"weeks\">Setmana</string>\n    <string name=\"months\">Mesos</string>\n    <string name=\"years\">Anys</string>\n    <string name=\"continuous\">Continu</string>\n    <string name=\"liked\">Agradat</string>\n    <string name=\"offline\">Descarregat</string>\n    <string name=\"my_top\">El meu top</string>\n    <string name=\"cached_playlist\">En memòria cau</string>\n    <string name=\"sync_playlist\">Sincronitza la llista</string>\n    <string name=\"sync_disabled\">Sincronització deshabitada</string>\n    <string name=\"allows_for_sync_witch_youtube\">Nota: Això permet sincronitzar amb Youtube Music. Un cop fet, això NO es podrà canviar.</string>\n    <string name=\"generating_image\">Generant imatge</string>\n    <string name=\"please_wait\">Sisplau espera</string>\n    <string name=\"cancel\">Cancel·la</string>\n    <string name=\"share_lyrics\">Compartir lletres</string>\n    <string name=\"share_as_text\">Compartir com a text</string>\n    <string name=\"share_as_image\">Compartir com a imatge</string>\n    <string name=\"max_selection_limit\">Límit màxim de selecció</string>\n    <string name=\"share_selected\">Compartir seleccionat</string>\n    <string name=\"customize_colors\">Personalitzar colors</string>\n    <string name=\"text_color\">Color del text</string>\n    <string name=\"secondary_text_color\">Color secundari del text</string>\n    <string name=\"background_color\">Color de fons</string>\n    <string name=\"remove_from_cache\">Elimina de la memòria cau</string>\n    <string name=\"copy_link\">Còpia l\\'enllaç</string>\n    <string name=\"select\">Selecciona tot</string>\n    <string name=\"like_all\">Agrada tot</string>\n    <string name=\"dislike_all\">Desagrada a tot</string>\n    <string name=\"sort_by_last_updated\">Data d\\'actualització</string>\n    <string name=\"link_copied\">Enllaç copiat</string>\n    <string name=\"starting_radio\">Començant ràdio</string>\n    <string name=\"now_playing\">Reproduïnt</string>\n    <string name=\"lyrics\">Lletres</string>\n    <string name=\"close\">Tanca</string>\n    <string name=\"hide_player_thumbnail\">Amaga la Imatge del Reproductor</string>\n    <string name=\"hide_player_thumbnail_desc\">Substitueix l\\'art de l\\'àlbum pel logotip de la app en el reproductor</string>\n    <string name=\"already_in_playlist\">Ja a la llista:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d vegada</item>\n        <item quantity=\"many\">%d de vegades</item>\n        <item quantity=\"other\">%d vegades</item>\n    </plurals>\n    <string name=\"seek_forward_dynamic\">+%1$d segons endavant</string>\n    <string name=\"seek_backward_dynamic\">-%1$d segons enrere</string>\n    <string name=\"seek_seconds_addup\">Cerca progressiva</string>\n    <string name=\"seek_seconds_addup_description\">Si està activat, afegeix fins a 5 segons extra per cada salt de cerca</string>\n    <string name=\"similar_content\">Contingut similar</string>\n    <string name=\"player_background_style\">Estil de fons del reproductor</string>\n    <string name=\"follow_theme\">Segueix el tema</string>\n    <string name=\"gradient\">Gradient</string>\n    <string name=\"new_player_design\">Disseny nou del reproductor</string>\n    <string name=\"new_mini_player_design\">Disseny nou del mini reproductor</string>\n    <string name=\"player_background_blur\">Difumina</string>\n    <string name=\"player_buttons_style\">Colors dels botons del reproductor</string>\n    <string name=\"default_style\">Per defecte</string>\n    <string name=\"enable_swipe_thumbnail\">Habilita llisca per canviar de cançó</string>\n    <string name=\"uploaded_playlist\">Pujades</string>\n    <string name=\"filter_uploaded\">Pujat</string>\n    <string name=\"download_playlist_desc\">Descarrega les cançons per a reproduir-les sense xarxa</string>\n    <string name=\"remove_download_playlist_desc\">Elimina les cançons descarregades d\\'aquesta llista</string>\n    <string name=\"download_in_progress_desc\">Descàrrega en procés</string>\n    <string name=\"share_playlist_desc\">Comparteix la llista amb altres</string>\n    <string name=\"delete_playlist_desc\">Elimina aquesta llista de manera permanent</string>\n    <string name=\"sync_playlist_desc\">Sincronitza una llista amb YouTube Music</string>\n    <string name=\"primary_color_style\">Color primari</string>\n    <string name=\"tertiary_color_style\">Color terciari</string>\n    <string name=\"swipe_song_to_add\">Llisca la cançó cap a la esquerra per a afegir-la a la cua o a la dreta per a reproduir-la a continuació</string>\n    <string name=\"swipe_song_to_remove\">Llisca la cançó per a eliminar-la de la llista</string>\n    <string name=\"lyrics_click_change\">Canvia la lletra polsant</string>\n    <string name=\"lyrics_auto_scroll\">Lletra amb desplaçament automàtic</string>\n    <string name=\"lyrics_glow_effect\">Habilita lletra amb efecte lluent</string>\n    <string name=\"lyrics_glow_effect_desc\">Afig animació lluent i efecte de rebot per a la lletra activa</string>\n    <string name=\"enable_better_lyrics\">Habilita Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">Lletra sincronitzada per síl·labes per a qualsevol cançó, per a karaoke</string>\n    <string name=\"auto_scroll\">Torna a sincronitzar</string>\n    <string name=\"slim\">Prim</string>\n    <string name=\"slim_navbar\">Barra de navegació inferior prima</string>\n    <string name=\"auto_playlists\">Llista de reproducció automàtica</string>\n    <string name=\"show_liked_playlist\">Mostrar llista \\\"Agradat\\\"</string>\n    <string name=\"show_downloaded_playlist\">Mostrar llista \\\"Descarregat\\\"</string>\n    <string name=\"show_top_playlist\">Mostrar llista \\\"Top\\\"</string>\n    <string name=\"show_cached_playlist\">Mostrar llista \\\"En memòria cau\\\"</string>\n    <string name=\"show_uploaded_playlist\">Mostrar llista \\\"Pujades\\\"</string>\n    <string name=\"advanced_login\">Inicia sessió amb un token</string>\n    <string name=\"show_wrapped_card\">Mostrar targeta Recopilació</string>\n    <string name=\"shuffle_playlist_first\">Mescla llista/àlbum primer</string>\n    <string name=\"shuffle_playlist_first_desc\">Quan mescles, reprodueix totes les cançons de la llista/àlbum primer, després contingut semblant</string>\n    <string name=\"token_hidden\">Toca per mostrar el token</string>\n    <string name=\"token_shown\">Toca de nou per a copiar o editar</string>\n    <string name=\"token_adv_login_description\">Aquest és una forma d\\'inici de sessió AVANÇADA. Com alternativa al portal web, vostè ha d\\'introduir o actualitzat el seu token ací directament. Per exemple, aquesta acció pot accelerar l\\'inici de sessió en múltiples dispositius. Tinga en compte que qualsevol format de token que la app no puga processar no serà acceptat</string>\n    <string name=\"yt_sync\">Sincronitza automàticament amb el compte</string>\n    <string name=\"more_content\">Més contingut</string>\n    <string name=\"edit_playlist_cover\">Edita la portada de la llista</string>\n    <string name=\"edit_playlist_cover_note\">Nota: Cal que el seu compte estiga enllaçat a un número de telèfon i a un compte verificat de YouTube Music per poder canviar la portada de la llista de reproducció.</string>\n    <string name=\"edit_playlist_cover_note_wait\">Després de seleccionar una imatge, per favor espere un moment per a que la nova portada es mostre en la llista de reproducció.</string>\n    <string name=\"choose_from_library\">Tria de la biblioteca</string>\n    <string name=\"remove_custom_image\">Elimina imatge personalitzada</string>\n    <string name=\"general\">General</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"default_lib_chips\">Canviar el xip de biblioteca predeterminat</string>\n    <string name=\"set_quick_picks\">Establir selecció ràpida</string>\n    <string name=\"last_song_listened\">Basat en l\\'última cançó escoltada</string>\n    <string name=\"app_language\">Llengua de l\\'aplicació</string>\n    <string name=\"config_proxy\">Configurar proxy</string>\n    <string name=\"proxy_username\">Usuari de proxy</string>\n    <string name=\"proxy_password\">Contrasenya de proxy</string>\n    <string name=\"enable_authentication\">Habilitar autentificació</string>\n    <string name=\"discord_use_details\">Utilitza els detalls en lloc de l\\'estat</string>\n    <string name=\"discord_use_details_description\">Mostra el nom de la cançó de manera destacada en lloc dels noms dels artistes</string>\n    <string name=\"enable_similar_content\">Habilitar contingut similar</string>\n    <string name=\"similar_content_desc\">Afegir automàticament més cançons similars quan s\\'aplegue a la fi de la cua</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"import_online\">Importa una llista de reproducció \\\"m3u\\\"</string>\n    <string name=\"import_csv\">Importa una llista de reproducció \\\"csv\\\"</string>\n    <string name=\"auto_download_on_like\">Descarregar automàticament en prémer \\\"m\\'agrada\\\"</string>\n    <string name=\"auto_download_on_like_desc\">Descarrega automàticament les cançons quan prems el botó de \\\"m\\'agrada\\\"</string>\n    <string name=\"swipe_sensitivity\">Sensibilitat al lliscar el mini reproductor</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"clear_song_cache_dialog\">Està segur de que desitja eliminar les cançons de la memòria cau?</string>\n    <string name=\"clear_image_cache_dialog\">Està segur de que desitja eliminar les imatges de la memòria cau?</string>\n    <string name=\"clear_downloads_dialog\">Està segur de que desitja eliminar les descàrregues?</string>\n    <string name=\"disable\">Deshabilitar</string>\n    <string name=\"not_logged_in_youtube\">No ha iniciat sessió a YouTube</string>\n    <string name=\"default_links\">Obrir enllaços compatibles</string>\n    <string name=\"open_app_settings_error\">No s\\'ha pogut obrir la configuració de l\\'aplicació</string>\n    <string name=\"release_notes\">Notes de la versió</string>\n    <string name=\"all_time\">De tots els temps</string>\n    <string name=\"past_24_hours\">Últimes 24 hores</string>\n    <string name=\"past_week\">Setmana passada</string>\n    <string name=\"past_month\">Mes passat</string>\n    <string name=\"past_year\">Any passat</string>\n    <string name=\"top_length\">Llargària de la meua llista Top</string>\n    <string name=\"history_duration\">Durada de l\\'historial</string>\n    <string name=\"information\">Informació</string>\n    <string name=\"description\">Descripció</string>\n    <string name=\"views\">Visites</string>\n    <string name=\"likes\">M\\'agrada</string>\n    <string name=\"dislikes\">No m\\'agrada</string>\n    <string name=\"subscribe\">Subscriure\\'m</string>\n    <string name=\"subscribed\">Subscrit</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">1 segon</item>\n        <item quantity=\"many\">%d de segons</item>\n        <item quantity=\"other\">%d segons</item>\n    </plurals>\n    <string name=\"disable_load_more_when_repeat_all\">Deshabilitar carregar més en estar repetint tot</string>\n    <string name=\"lyrics_romanization_cyrillic\">Ciríl·lic</string>\n    <string name=\"lyrics_romanize_title\">Romanització</string>\n    <string name=\"lyrics_romanization\">Romanització de lletres</string>\n    <string name=\"lyrics_romanize_japanese\">Romanitzar lletra en japonés</string>\n    <string name=\"lyrics_romanize_korean\">Romanitzar lletres en coreà</string>\n    <string name=\"lyrics_romanize_chinese\">Romanitzar lletres en xinés</string>\n    <string name=\"lyrics_romanize_russian\">Romanitzar lletres en rus</string>\n    <string name=\"lyrics_romanize_ukrainian\">Romanitzar lletres en ucraïnés</string>\n    <string name=\"lyrics_romanize_belarusian\">Romanitzar lletres en bielorús</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Romanitzar lletres en kirguís</string>\n    <string name=\"lyrics_romanize_serbian\">Romanitzar lletres en serbi</string>\n    <string name=\"lyrics_romanize_bulgarian\">Romanitzar lletres en búlgar</string>\n    <string name=\"line_by_line_option_title\">EXPERIMENTAL: Detectar llengua línia per línia</string>\n    <string name=\"line_by_line_option_desc\">La llengua ciríl·lic serà detectada línia per línia en lloc de per a tota la cançó.</string>\n    <string name=\"line_by_line_dialog_title\">Està segur?</string>\n    <string name=\"romanize_current_track\">Romanitzar la pista actual</string>\n    <string name=\"settings_section_ui\">Interfície</string>\n    <string name=\"settings_section_privacy\">Privacitat i Seguretat</string>\n    <string name=\"settings_section_player_content\">Reproductor i Contingut</string>\n    <string name=\"settings_section_storage\">Emmagatzematge i Dades</string>\n    <string name=\"settings_section_system\">Sistema i Sobre</string>\n    <string name=\"updater\">Actualitzador</string>\n    <string name=\"check_for_updates\">Buscar actualitzacions automàticament</string>\n    <string name=\"update_notifications\">Habilitar notificacions d\\'actualització</string>\n    <string name=\"update_available_title\">Actualització disponible</string>\n    <string name=\"update_channel_name\">Actualitzacions de l\\'aplicació</string>\n    <string name=\"update_channel_desc\">Notificacions sobre les noves versions</string>\n    <string name=\"audio_offload\">Habilitar descàrrega (offload)</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"google_cast_description\">Habilitar la transmissió d\\'àudio a Chromecast i altres dispositius compatibles amb Cast</string>\n    <string name=\"lyrics_romanize_macedonian\">Romanitzar lletra en macedoni</string>\n    <string name=\"integrations\">Integracions</string>\n    <string name=\"username\">Nom d\\'usuari</string>\n    <string name=\"password\">Contrasenya</string>\n    <string name=\"lastfm_integration\">Integració amb Last.fm</string>\n    <string name=\"enable_scrobbling\">Habilitar scrobbling</string>\n    <string name=\"lastfm_now_playing\">Enviar Reproducció Actual</string>\n    <string name=\"last_fm_send_likes\">Enviar Agrada/No m\\'agrada</string>\n    <string name=\"last_fm_send_likes_description\">Indicar que M\\'agrada/No m\\'agrada a Last.fm en indicar M\\'agrada/No m\\'agrada a Metrolist</string>\n    <string name=\"logging_in\">Iniciant sessió…</string>\n    <string name=\"scrobbling_configuration\">Configuració de scrobbling</string>\n    <string name=\"scrobble_min_track_duration\">Fer scrobble a cançons més llargues de</string>\n    <string name=\"scrobble_delay_percent\">Percentatge del retard de scrobble</string>\n    <string name=\"scrobble_delay_minutes\">Minuts de retard de scrobble</string>\n    <string name=\"hide_video_songs\">Amaga vídeos de les cançons</string>\n    <string name=\"details_desc\">Veure la informació de la cançó</string>\n    <string name=\"edit_desc\">Canviar títol o artista</string>\n    <string name=\"start_radio_desc\">Crear una estació basada en aquest element</string>\n    <string name=\"play_next_desc\">Afegir al començament de la cua</string>\n    <string name=\"add_to_queue_desc\">Afegir al final de la cua</string>\n    <string name=\"add_to_library_desc\">Guarda a la teua biblioteca</string>\n    <string name=\"download_desc\">Habilita per a la reproducció sense xarxa</string>\n    <string name=\"add_to_playlist_desc\">Afig a una de les teues llistes de reproducció</string>\n    <string name=\"refetch_desc\">Obtindre les últimes metadades de YouTube Music</string>\n    <string name=\"share_desc\">Compartir un enllaç a aquest element</string>\n    <string name=\"delete_desc\">Eliminar element de manera permanent</string>\n    <string name=\"advanced_desc\">Canviar tempo i to de la cançó</string>\n    <string name=\"equalizer_desc\">Ajustar l\\'equalitzador d\\'àudio</string>\n    <string name=\"enable_dynamic_icon\">Habilitar icona dinàmica</string>\n    <string name=\"mini_player\">Mini Reproductor</string>\n    <string name=\"pure_black_mini_player\">Mini reproductor negre pur</string>\n    <string name=\"cache_size_warning_title\">Espera!</string>\n    <string name=\"cache_size_warning_confirm\">Continuar</string>\n    <string name=\"lyrics_animation_style\">Animació d\\'estil paraula per paraula</string>\n    <string name=\"none\">Cap</string>\n    <string name=\"fade\">Esvair</string>\n    <string name=\"glow\">Lluir</string>\n    <string name=\"slide\">Lliscar</string>\n    <string name=\"karaoke\">Karaoke</string>\n    <string name=\"apple_music_style\">Apple Music</string>\n    <string name=\"lyrics_text_size\">Mida del text de la lletra</string>\n    <string name=\"lyrics_line_spacing\">Espai entre línies de la lletra</string>\n    <string name=\"album_art_for\">Portada d\\'àlbum de %s</string>\n    <string name=\"wrapped_total_albums_title\">Has escoltat</string>\n    <string name=\"wrapped_total_albums_subtitle\">àlbums únics</string>\n    <string name=\"wrapped_top_album_title\">El teu àlbum preferit és</string>\n    <string name=\"wrapped_playlist_ready\">La teua llista de reproducció personal està llesta</string>\n    <string name=\"wrapped_top_5_albums_title\">Els teus 5 àlbums preferits</string>\n    <string name=\"wrapped_album_listening_time\">Has escoltat aquest àlbum durant %d minuts</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d minuts</string>\n    <string name=\"wrapped_no_data\">No hi ha dades</string>\n    <string name=\"wrapped_top_5_artists_title\">El teu artista preferit d\\'enguany</string>\n    <string name=\"wrapped_artist_listening_time\">%d minuts</string>\n    <string name=\"wrapped_top_5_songs_title\">Les teues cançons preferides d\\'enguany</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">Portada de l\\'àlbum</string>\n    <string name=\"wrapped_top_artist_title\">El teu artista preferit d\\'enguany és</string>\n    <string name=\"wrapped_top_artist_image_content_description\">Imatge de l\\'artista preferit</string>\n    <string name=\"wrapped_top_artist_listening_time\">Has escoltat %d minuts seus</string>\n    <string name=\"wrapped_top_song_title\">La cançó que més has reproduït és</string>\n    <string name=\"wrapped_top_song_listening_time\">L\\'has escoltada durant %d minuts</string>\n    <string name=\"wrapped_total_artists_title\">Has escoltat a</string>\n    <string name=\"wrapped_total_artists_subtitle\">artistes únics</string>\n    <string name=\"wrapped_total_songs_title\">Has escoltat</string>\n    <string name=\"wrapped_total_songs_subtitle\">cançons diferents</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_intro_subtitle\">És hora de veure què has estat escoltant</string>\n    <string name=\"wrapped_intro_button\">anem!</string>\n    <string name=\"wrapped_logo_content_description\">Logo de Metrolist</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"wrapped_ready_title\">EL TEU RECOPILATORI ESTÀ PREPARAT!</string>\n    <string name=\"wrapped_ready_subtitle\">És hora de veure què t\\'ha encantat enguany.</string>\n    <string name=\"wrapped_thank_you\">Gràcies per escoltar</string>\n    <string name=\"wrapped_special_thanks\">Un especial agraïment a MO Agamy per crear Metrolist</string>\n    <string name=\"wrapped_close\">Tancar Recopilatori</string>\n    <string name=\"wrapped_playlist_title\">El teu Recopilatori %s</string>\n    <string name=\"wrapped_create_playlist\">Crear llista de reproducció</string>\n    <string name=\"wrapped_playlist_saved\">Llista de reproducció desada</string>\n    <string name=\"casting_to\">Transmetent a %s</string>\n    <string name=\"progress_percent\">Progrés %s%%</string>\n    <string name=\"listening_to_metrolist\">Escoltant Metrolist</string>\n    <string name=\"open\">Obrir</string>\n    <string name=\"failed_to_create_image\">No s\\'ha pogut generar la imatge: %s</string>\n    <string name=\"copied_title\">Títol copiat</string>\n    <string name=\"copied_artist\">Artista copiat</string>\n    <string name=\"error_playing\">Error de reproducció</string>\n    <string name=\"failed_to_parse_proxy\">Error al processar la url del proxy.</string>\n    <string name=\"playlist_add_local_to_synced_note\">Nota: No es poden afegir cançons locals a llistes sincronitzades/remotes. Qualsevol altra combinació és acceptable</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">No carregar automàticament més cançons similars mentre està actiu el mode repetir tot</string>\n    <string name=\"line_by_line_dialog_desc\">Es tracta d\\'una característica experimental propensa a errades.\\n\\nPer defecte, la llengua es determina per a tota la cançó, però amb aquesta opció, en el seu lloc, es determinarà la línia per línia. Açò permetrà que cançons multi-llengua funcionen correctament PERÒ la llengua detectada pot no ser sempre correcta (per exemple, si hi ha una lletra en ucraïnés que no conté cap caràcter específic de l\\'idioma pot ser romanitzada com a russa).\\n\\nSi no té cap problema, es recomana mantenir aquesta opció desactivada.</string>\n    <string name=\"audio_offload_description\">Utilitzar la direcció de descarrega (offload) per a la reproducció de àudio. Desactivar aquesta funció podria incrementar l\\'ús d\\'energia però podria ser beneficiós si està tenint problemes en la reproducció d\\'àudio o en pos processat</string>\n    <string name=\"cache_size_warning_message\">Ha triat un límit de grandària de memòria cau inferior al que l\\'aplicació està utilitzant actualment (%1$s). Si continua, l\\'aplicació eliminarà alguns %2$s emmagatzemats per ajustar-se al nou límit. Continuar de tota manera?</string>\n    <string name=\"wavy\">Ondulat</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"one\">%d Perfil</item>\n        <item quantity=\"many\">%d de Perfils</item>\n        <item quantity=\"other\">%d Perfils</item>\n    </plurals>\n    <string name=\"equalizer_header\">Equalitzador</string>\n    <string name=\"no_profiles\">Sense perfils d\\'equalitzador</string>\n    <string name=\"import_profile\">Importar Perfil</string>\n    <string name=\"eq_disabled\">Deshabilitat</string>\n    <plurals name=\"band_count\">\n        <item quantity=\"one\">%d grup</item>\n        <item quantity=\"many\">%d de grups</item>\n        <item quantity=\"other\">%d grups</item>\n    </plurals>\n    <string name=\"delete_profile_desc\">Eliminar Perfil</string>\n    <string name=\"delete_profile_confirmation\">Està segur de que vol eliminar %1$s? Aquesta acció no és reversible.</string>\n    <string name=\"error_file_read\">No s\\'ha pogut llegir l\\'arxiu</string>\n    <string name=\"error_file_open\">No s\\'ha pogut obrir l\\'arxiu: %1$s</string>\n    <string name=\"import_error_title\">Error d\\'Importació</string>\n    <string name=\"previous\">Anterior</string>\n    <string name=\"play_pause\">Reprodueix/Pausa</string>\n    <string name=\"next\">Següent</string>\n    <string name=\"like\">M\\'agrada</string>\n    <string name=\"about_artist\">Quant a</string>\n    <string name=\"error_title\">Error</string>\n    <string name=\"album_art\">Coberta</string>\n    <string name=\"system_equalizer\">Equalitzador del sistema</string>\n    <string name=\"lyrics_offset\">Desplaçament de la lletra</string>\n    <string name=\"show_more\">Mostra\\'n més</string>\n    <string name=\"show_less\">Mostra\\'n menys</string>\n    <string name=\"artist_page_settings\">Pàgina de l\\'artista</string>\n    <string name=\"persistent_shuffle_title\">Aleatori persistent</string>\n    <string name=\"error_playback_failed\">Ha fallat la reproducció</string>\n    <string name=\"enable_simpmusic\">Activa les lletres de SimpMusic</string>\n    <string name=\"no_song_playing\">No s\\'està reproduint res</string>\n    <string name=\"skip_silence_instant\">Omet el silenci immediatament</string>\n    <string name=\"show_artist_description\">Mostra la descripció de l\\'artista</string>\n    <string name=\"show_artist_subscriber_count\">Mostra el nombre de subscriptors</string>\n    <string name=\"show_artist_monthly_listeners\">Mostra els oients mensuals</string>\n    <string name=\"crop_album_art\">Retalla la coberta</string>\n    <string name=\"tap_to_open\">Toqueu per obrir el Metrolist</string>\n    <string name=\"remember_shuffle_and_repeat\">Recorda l\\'aleatori i la repetició</string>\n    <string name=\"pause_music_when_media_is_muted\">Pausa la música en silenciar la multimèdia</string>\n    <string name=\"widget_description\">Giny reproductor de música amb controls integrats</string>\n    <string name=\"error_eq_apply_failed\">Error en aplicar el perfil d\\'equalització: %1$s</string>\n    <string name=\"enable_simpmusic_desc\">Lletres obtingudes automàticament de Musixmatch i YouTube Transcript</string>\n    <string name=\"skip_silence_desc\">Avança ràpidament les parts silencioses de les cançons</string>\n    <string name=\"turntable_widget_description\">Dispositiu musical circular amb controls de reproducció i m\\'agrada</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Recorda l\\'aleatori i la repetició en reiniciar l\\'aplicació</string>\n    <string name=\"persistent_shuffle_desc\">Mantén l\\'aleatori activat en començar noves cançons o llistes</string>\n    <string name=\"crop_album_art_desc\">Retalla les miniatures dels vídeos per forçar una relació d\\'aspecte quadrada</string>\n    <string name=\"skip_silence_instant_desc\">Omet les parts silencioses de les cançons en comptes d\\'avançar-les ràpidament</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">Amb reproductor expandit, mantén la pantalla encesa</string>\n    <string name=\"listen_together\">Escoltar Junts</string>\n    <string name=\"listen_together_server_url\">URL del servidor</string>\n    <string name=\"listen_together_username\">Nom d\\'usuari</string>\n    <string name=\"listen_together_connected\">Connectat</string>\n    <string name=\"listen_together_reconnecting\">Tornant a connectar…</string>\n    <string name=\"listen_together_disconnected\">Desconnectat</string>\n    <string name=\"listen_together_connecting\">Connectant…</string>\n    <string name=\"listen_together_error\">Error de connexió</string>\n    <string name=\"listen_together_create_room\">Crear sala</string>\n    <string name=\"listen_together_create_room_desc\">Crea una sala i comparteix el codi amb els teus amics</string>\n    <string name=\"listen_together_join_room\">Unir-se a una sala</string>\n    <string name=\"listen_together_room_code\">Codi de la sala</string>\n    <string name=\"listen_together_you_are_host\">Ets l\\'amfitrió</string>\n    <string name=\"listen_together_you_are_guest\">Ets un convidat</string>\n    <string name=\"mute\">Silenciar</string>\n    <string name=\"unmute\">Deixar de silenciar</string>\n    <string name=\"listen_together_join_requests\">Sol·licitud per a unir-se</string>\n    <string name=\"listen_together_view_logs\">Veure registres</string>\n    <string name=\"listen_together_view_logs_desc\">Depurar la connexió i els missatges</string>\n    <string name=\"listen_together_logs\">Registres de connexió</string>\n    <string name=\"listen_together_no_logs\">Encara no hi ha registres</string>\n    <string name=\"listen_together_description\">Escolta música alhora amb els teus amics. Crea una sala per a ser l\\'amfitrió o uneix-te a una sala existent amb un codi.</string>\n    <string name=\"listen_together_background_disconnect_note\">Nota: Potser es desconnecte si crea una sala en que no s\\'està reproduint música i canvia a altra aplicació.</string>\n    <string name=\"listen_together_not_configured\">Escoltar Junts no està configurat. Per favor configura la URL del servidor en Configuració → Integracions → Escoltar Junts.</string>\n    <string name=\"listen_together_suggestion_received\">%1$s ha demanat %2$s</string>\n    <string name=\"listen_together_suggestion_sent\">Petició enviada a l\\'amfitrió!</string>\n    <string name=\"listen_together_join_request_notification\">%1$s es vol unir a la sala</string>\n    <string name=\"listen_together_notification_channel_name\">Escoltar Junts</string>\n    <string name=\"listen_together_notification_channel_desc\">Notificacions per a esdeveniments d\\'Escoltar Junts</string>\n    <string name=\"listen_together_room_created\">Sala creada: %s</string>\n    <string name=\"listen_together_cannot_edit_username_in_room\">No pot editar el nom d\\'usuari estant a una sala</string>\n    <string name=\"waiting_for_approval\">Esperant l\\'aprovació de l\\'amfitrió</string>\n    <string name=\"invalid_room_code\">Codi de sala invàlid</string>\n    <string name=\"join_request_denied\">Sol·licitud d\\'unió rebutjada</string>\n    <string name=\"join_existing_room\">Unir-se a una sala existent</string>\n    <string name=\"room_code\">Codi de la sala</string>\n    <string name=\"leave_room\">Abandonar la sala</string>\n    <string name=\"join_room\">Unir-se</string>\n    <string name=\"create_room\">Crear</string>\n    <string name=\"joining_room\">Unint-se a la sala %s…</string>\n    <string name=\"creating_room\">Creant la sala…</string>\n    <string name=\"connect\">Connectar</string>\n    <string name=\"disconnect\">Desconnectar</string>\n    <string name=\"create\">Crear</string>\n    <string name=\"join\">Unir-se</string>\n    <string name=\"approve\">Aprovar</string>\n    <string name=\"reject\">Rebutjar</string>\n    <string name=\"clear\">Buidar</string>\n    <string name=\"copy\">Copiar</string>\n    <string name=\"copied_to_clipboard\">Copiat al porta-retalls</string>\n    <string name=\"not_set\">No configurat</string>\n    <string name=\"hosting_room\">Recepció</string>\n    <string name=\"in_room\">A la sala</string>\n    <string name=\"pending_requests\">Petició pendent</string>\n    <string name=\"pending_suggestions\">Suggeriments pendents</string>\n    <string name=\"suggest_to_host\">Fer una suggeriment a l\\'amfitrió</string>\n    <string name=\"kick_user\">Fer fora</string>\n    <string name=\"host_label\">Amfitrió</string>\n    <string name=\"you_label\">Vostè</string>\n    <string name=\"connected_users\">Usuaris connectats</string>\n    <string name=\"enter_username\">Introduïsca el nom d\\'usuari</string>\n    <string name=\"error_username_empty\">Cal el nom d\\'usuari.</string>\n    <string name=\"resync\">Tornar a sincronitzar</string>\n    <string name=\"crash_title\">L\\'aplicació ha fallat</string>\n    <string name=\"crash_description\">S\\'ha produït un error inesperat. Per favor, enviï\\'ns l\\'informe de fallida per a ajudar-nos a solucionar el problema.</string>\n    <string name=\"crash_share_logs\">Compartir Registres</string>\n    <string name=\"crash_share_title\">Enviar l\\'informe de fallida</string>\n    <string name=\"crash_report_subject\">Informe de Fallida de Metrolist</string>\n    <string name=\"crash_close\">Tancar</string>\n    <string name=\"crash_no_log\">No hi ha registres de fallida disponibles</string>\n    <string name=\"not_playing\">Cap cançó sonant</string>\n    <string name=\"tap_to_play\">Prem per obrir Metrolist</string>\n    <string name=\"widget_music_player\">Reproductor de música</string>\n    <string name=\"widget_turntable\">Tocadiscs</string>\n    <string name=\"together\">Junts</string>\n    <string name=\"listen_together_choose_server\">Escollir servidor</string>\n    <string name=\"listen_together_custom_server\">Servidor personalitzat</string>\n    <string name=\"listen_together_use_custom_server\">Utilitzar un servidor personalitzat</string>\n    <string name=\"listen_together_auto_approval_joins\">Aprovar automàticament les sol·licituds d\\'ingrés</string>\n    <string name=\"listen_together_auto_approval_joins_desc\">Aprova automàticament les sol·licituds d\\'ingrés a la sala en lloc de revisar-les manualment</string>\n    <string name=\"listen_together_sync_volume\">Sincronitzar volum de l\\'amfitrió</string>\n    <string name=\"listen_together_sync_volume_desc\">Els convidats copien el nivell del volum de l\\'amfitrió</string>\n    <string name=\"enter_room_code\">Introduir codi de la sala</string>\n    <string name=\"listen_together_settings_desc\">Configurar servidor, nom d\\'usuari i altres</string>\n    <string name=\"copy_code\">Copiar codi</string>\n    <string name=\"kick_user_desc\">Expulsar a aquesta persona de la sala</string>\n    <string name=\"permanently_kick_user\">Bloquejar de manera permanent</string>\n    <string name=\"permanently_kick_user_desc\">Bloca a aquesta persona de sol·licitar unir-se i oculta els seus suggeriments</string>\n    <string name=\"transfer_ownership\">Transferir Propietat</string>\n    <string name=\"transfer_ownership_desc\">Fer a aquesta persona l\\'amfitrió de la sala</string>\n    <string name=\"manage_user\">Administrar Usuari</string>\n    <string name=\"listen_together_blocked_users\">Usuaris Blocats</string>\n    <string name=\"listen_together_blocked_users_count\">%d usuari(s) blocats</string>\n    <string name=\"listen_together_no_blocked_users\">No hi ha cap usuari blocat</string>\n    <string name=\"unblock\">Desblocar</string>\n    <string name=\"user_blocked_by_host\">Usuari blocat per l\\'amfitrió</string>\n    <string name=\"ai_lyrics_translation\">Lletra traduïda per IA</string>\n    <string name=\"ai_translating_lyrics\">Traduint lletra...</string>\n    <string name=\"ai_lyrics_translated\">Lletra traduïda</string>\n    <string name=\"ai_provider\">Proveïdor</string>\n    <string name=\"ai_base_url\">URL Base</string>\n    <string name=\"ai_api_key\">Clau de API</string>\n    <string name=\"ai_model\">Model</string>\n    <string name=\"ai_translation_mode\">Mode Traducció</string>\n    <string name=\"ai_target_language\">Llengua Destí</string>\n    <string name=\"ai_setup_guide\">Credencials de l\\'API</string>\n    <string name=\"ai_translation_literal\">Traducció</string>\n    <string name=\"ai_translation_transcribed\">Transcripció</string>\n    <string name=\"ai_error_no_lyrics\">No hi ha lletra que traduir</string>\n    <string name=\"ai_error_lyrics_empty\">No hi ha lletra</string>\n    <string name=\"ai_error_language_required\">És requereix una llengua objectiu</string>\n    <string name=\"ai_error_unexpected\">Resultat de la traducció inesperat</string>\n    <string name=\"ai_error_unknown\">S\\'ha produït un error desconegut</string>\n    <string name=\"ai_error_translation_failed\">La traducció ha fallat</string>\n    <string name=\"palette_dynamic\">Dinàmica</string>\n    <string name=\"palette_crimson\">Carmesí</string>\n    <string name=\"palette_rose\">Rosa</string>\n    <string name=\"palette_purple\">Morat</string>\n    <string name=\"palette_deep_purple\">Morat Fosc</string>\n    <string name=\"palette_indigo\">Indi</string>\n    <string name=\"palette_blue\">Blau</string>\n    <string name=\"palette_sky_blue\">Blau Clar</string>\n    <string name=\"palette_cyan\">Cian</string>\n    <string name=\"palette_teal\">Blau Ànec</string>\n    <string name=\"palette_green\">Verd</string>\n    <string name=\"palette_light_green\">Verd Clar</string>\n    <string name=\"palette_lime\">Llima</string>\n    <string name=\"palette_yellow\">Groc</string>\n    <string name=\"palette_amber\">Ambre</string>\n    <string name=\"palette_orange\">Taronja</string>\n    <string name=\"palette_deep_orange\">Taronja Fosc</string>\n    <string name=\"palette_brown\">Marró</string>\n    <string name=\"palette_grey\">Gris</string>\n    <string name=\"palette_blue_grey\">Gris Blavós</string>\n    <string name=\"cd_back\">Fons</string>\n    <string name=\"cd_pure_black_mode\">Mode Morat Pur</string>\n    <string name=\"cd_light_mode\">Mode Clar</string>\n    <string name=\"cd_dark_mode\">Mode Fosc</string>\n    <string name=\"cd_system_mode\">Mode del sistema</string>\n    <string name=\"cd_palette_item\">Paleta %1$s</string>\n    <string name=\"play_all\">Reproduir tot</string>\n    <string name=\"ai_api_key_required\">Cal una Clau de API</string>\n    <string name=\"ai_error_api_key_required\">Cal una clau de API</string>\n    <string name=\"enable\">Habilita</string>\n    <string name=\"prevent_duplicate_tracks_in_queue\">Evita cançons repetides a la cua</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">Quan s\\'afig una cançó a la cua, elimina-la de la seua posició anterior si ja hi estava</string>\n    <string name=\"crossfade\">Transició suau</string>\n    <string name=\"crossfade_desc\">Transició suau entre cançons</string>\n    <string name=\"crossfade_duration\">Durada de la Transició suau</string>\n    <string name=\"crossfade_gapless\">Deshabilita els àlbums sense pauses</string>\n    <string name=\"crossfade_gapless_desc\">No aplicar el transaccionat suau si hi ha pauses a l\\'album</string>\n    <string name=\"crossfade_beta_title\">Funcionalitat Beta</string>\n    <string name=\"crossfade_beta_message\">El transicionat suau es una nova funcionalitat que pot presentar errors. Si trobes cap problema, per favor, informa\\'ns.\\n\\nAquesta funcionalitat deshabilita la descarrega d\\'àudio a causa de limitacions tècniques.</string>\n    <string name=\"audio_offload_disabled_by_crossfade\">Deshabilitat per que el transicionat suau està actiu</string>\n    <string name=\"hide_youtube_shorts\">Amaga Youtube Shorts</string>\n    <string name=\"listen_together_in_top_bar\">Escoltar Junts a la barra superior</string>\n    <string name=\"listen_together_in_top_bar_desc\">Mostra l\\'opció d\\'Escoltar Junts a la barra superior de l\\'aplicació en lloc de a la barra de navegació</string>\n    <string name=\"ai_translation_literal_desc\">Tradueix el significat a l\\'idioma objectiu</string>\n    <string name=\"ai_translation_transcribed_desc\">Canvia la pronuncia a la llengua objectiu</string>\n    <string name=\"ai_provider_help\">Obtín Claus d\\'API</string>\n    <string name=\"ai_provider_openrouter_help\">Visita https://openrouter.ai per obtenir models gratuïts i de pagament</string>\n    <string name=\"ai_provider_openai_help\">Visita https://platform.openai.com/api-keys</string>\n    <string name=\"ai_provider_claude_help\">Visita https://console.anthropic.com/settings/keys</string>\n    <string name=\"ai_provider_gemini_help\">Visita https://aistudio.google.com/apikey</string>\n    <string name=\"ai_provider_perplexity_help\">Visita https://perplexity.ai/settings/api</string>\n    <string name=\"ai_provider_xai_help\">Visita https://console.x.ai</string>\n    <string name=\"ai_provider_deepl_help\">Visit https://deepl.com/pro-api per obtenir claus gratuïtes i de pagament</string>\n    <string name=\"ai_deepl_formality\">Formalitat</string>\n    <string name=\"ai_deepl_formality_default\">Predeterminat</string>\n    <string name=\"ai_deepl_formality_more\">Més Formal</string>\n    <string name=\"ai_deepl_formality_less\">Menys Formal</string>\n    <string name=\"enable_high_refresh_rate\">Habilita una alta freqüència d\\'actualització</string>\n    <string name=\"enable_high_refresh_rate_desc\">Força la pantalla a treballar a la màxima freqüència d\\'actualització (p.e. 120Hz)</string>\n    <string name=\"recognize_music\">Identificar Música</string>\n    <string name=\"tap_to_recognize\">Toca per identificar</string>\n    <string name=\"listening\">Escoltant…</string>\n    <string name=\"processing\">Processant…</string>\n    <string name=\"no_match_found\">No s\\'ha trobat cap coincidència</string>\n    <string name=\"recognition_error\">S\\'ha produït un error en identificar</string>\n    <string name=\"try_again\">Prova de nou</string>\n    <string name=\"recognition_history\">Historial de reconeixements</string>\n    <string name=\"clear_recognition_history\">Buidar l\\'historial de reconeixements</string>\n    <string name=\"clear_recognition_history_confirm\">Està segur de que vol buidar tot l\\'historial de reconeixements?</string>\n    <string name=\"delete_from_history\">Elimina de l\\'historial</string>\n    <string name=\"re_listen\">Escoltar de nou</string>\n    <string name=\"play_on_app\">Reprodueix amb Metrolist</string>\n    <string name=\"map_csv_columns\">Mapatge de Columnes CSV</string>\n    <string name=\"first_row_is_header\">La primera fila son les capçaleres</string>\n    <string name=\"artist_name_column\">Columna del nom de l\\'Artista</string>\n    <string name=\"song_title_column\">Columna del nom de la Cançó</string>\n    <string name=\"youtube_url_column\">Columna de l\\'enllaç de Youtube (Opcional)</string>\n    <string name=\"continue_action\">Continuar</string>\n    <string name=\"importing_csv\">Important CSV</string>\n    <string name=\"recently_converted\">Convertit recentment</string>\n    <string name=\"column_label\">Col %d</string>\n    <string name=\"discord_status\">Estat</string>\n    <string name=\"discord_status_online\">En línia</string>\n    <string name=\"discord_status_idle\">Inactiu</string>\n    <string name=\"discord_status_dnd\">No molestar</string>\n    <string name=\"discord_buttons\">Botons</string>\n    <string name=\"discord_button_1\">Botó 1</string>\n    <string name=\"discord_button_2\">Botó 2</string>\n    <string name=\"login_successful\">Sessió iniciada amb èxit!</string>\n    <string name=\"discord_activity_type\">Tipus d\\'activitat</string>\n    <string name=\"discord_activity_playing\">Reproduint</string>\n    <string name=\"discord_activity_listening\">Escoltant</string>\n    <string name=\"discord_activity_watching\">Veient</string>\n    <string name=\"discord_activity_competing\">Competint</string>\n    <string name=\"discord_button_text_variables\">Variables: {nom_cançó}, {nom_artista}, {nom_album}</string>\n    <string name=\"discord_rpc_preview\">Previsualització de la Rich Presence</string>\n    <string name=\"discord_presence\">Presència</string>\n    <string name=\"discord_connect_description\">Inicia sessió a Discord per compartir el que estàs escoltant</string>\n    <string name=\"discord_playing_metrolist\">Reproduint Metrolist</string>\n    <string name=\"discord_watching_metrolist\">Veient Metrolist</string>\n    <string name=\"discord_competing_metrolist\">Competint a Metrolist</string>\n    <string name=\"discord_activity_name\">Nom de l\\'activitat</string>\n    <string name=\"discord_activity_name_description\">Nom personalitzat per a l\\'activitat (buit de manera predeterminada)</string>\n    <string name=\"discord_advanced_mode\">Mode avançat</string>\n    <string name=\"discord_advanced_mode_description\">Mostra opcions de personalització avançades per a Rich Presence</string>\n    <string name=\"player_background_solid\">Sòlid</string>\n    <string name=\"resume_on_bluetooth_connect\">Reprèn la connexió Bluetooth</string>\n    <string name=\"lyrics_romanize_hindi\">Romanitza lletra en Hindi</string>\n    <string name=\"lyrics_romanize_punjabi\">Romanitza lletra en Panjabi</string>\n    <string name=\"lyrics_romanize_as_main\">Mostra lletres romanitzades principalment</string>\n    <string name=\"discord_information_warning\">Aquesta funcionalitat utilitza la llibreria KizzyRPC per a connectar-se als ports de Discord i configurar el vostre estatus de Rich Presence. Encara que no s\\'ha informat de cap suspensió de comptes per aquest tipus d\\'ús, aquest mètode no està oficialment acceptat per Discord i es podria considerar una violació dels seus Termes d\\'ús. El vostre token s\\'extrau localment i mai s\\'envia a servidors de terceres parts. Actue sota la seua pròpia consideració.</string>\n    <string name=\"display_density\">Densitat de pantalla</string>\n    <string name=\"restart\">Reinicia</string>\n    <string name=\"restart_required\">Cal reiniciar</string>\n    <string name=\"density_restart_message\">En canvi de densitat de pantalla es farà efectiu després de reiniciar l\\'aplicació. La vol reiniciar ara?</string>\n    <string name=\"found_in_settings_content\">Es troba a Configuració &gt; Contingut</string>\n    <string name=\"plays\">reproduccions</string>\n    <string name=\"speed_dial\">Accés ràpid</string>\n    <string name=\"pin_to_speed_dial\">Fixa a l\\'accés ràpid</string>\n    <string name=\"unpin_from_speed_dial\">Lleva l\\'accés ràpid</string>\n    <string name=\"randomize_home_order\">Aleatoritza l\\'orde de la pantalla d\\'inici</string>\n    <string name=\"randomize_home_order_desc\">Reordena aleatòriament les seccions de la pantalla d\\'inici amb pesos de prioritat</string>\n    <string name=\"daily_discover_sounds_like\">Sona com %1$s</string>\n    <string name=\"daily_discover_because_you_listen_to\">Perquè has escoltat %1$s</string>\n    <string name=\"daily_discover_similar_to\">Semblant a %1$s</string>\n    <string name=\"daily_discover_based_on\">Basat en %1$s</string>\n    <string name=\"daily_discover_for_fans_of\">Per als seguidors de %1$s</string>\n    <string name=\"from_the_community\">De la comunitat</string>\n    <string name=\"enable_lrclib_desc\">Base de dades de lletres sincronitzada impulsat per la comunitat</string>\n    <string name=\"enable_kugou_desc\">Pren les lletres de KuGou, una coneguda plataforma de música Xinesa</string>\n    <string name=\"youtube_music_lyrics_note\">NOTA: Les lletres provinents de YouTube Music es mostraran automàticament quan no haja cap altra lletra disponible. Les lletres de YTM no solen estar sincronitzades.</string>\n    <string name=\"enable_lyricsplus\">Habilita LyricsPlus</string>\n    <string name=\"enable_lyricsplus_desc\">Lletra sincronitzada de diverses fonts</string>\n    <string name=\"lyrics_provider_selection\">Selecció de proveïdor</string>\n    <string name=\"lyrics_provider_selection_desc\">Tria quins proveïdors de lletres estan permesos</string>\n    <string name=\"lyrics_provider_priority\">Prioritat de proveïdors de lletres</string>\n    <string name=\"lyrics_provider_priority_desc\">Arrastra per ordenar els proveïdors per preferència. Posició elevada -&gt; proritat elevada.</string>\n    <string name=\"changelog\">Registre de canvis</string>\n    <string name=\"changelog_empty\">Registre de canvis no disponible</string>\n    <string name=\"github_releases_url\">https://github.com/MetrolistGroup/Metrolist/releases</string>\n    <string name=\"list_bullet\">•</string>\n    <string name=\"view_on_github\">Mostra a GitHub</string>\n    <string name=\"current_version\">Versió actual</string>\n    <string name=\"version_format\">Versió: %s</string>\n    <string name=\"update_settings\">Configuració d\\'actualitzacions</string>\n    <string name=\"check_for_updates_title\">Cerca actualitzacions</string>\n    <string name=\"checking_for_updates\">Cercant actualitzacions…</string>\n    <string name=\"latest_version_format\">Última: %s</string>\n    <string name=\"check_for_updates_button\">Cerca actualitzacions</string>\n    <string name=\"hide_changelog\">Amagar registre de canvis</string>\n    <string name=\"view_changelog\">Veure registre de canvis</string>\n    <string name=\"failed_to_check_updates\">No s\\'ha pogut cercar actualitzacions: %s</string>\n    <string name=\"set_as_default\">Establir com predeterminat</string>\n    <string name=\"sleep_timer_default_set\">Temporitzador de suspensió establert per defecte en %d min</string>\n    <string name=\"enable_automatic_sleeptimer\">Habilitar temporitzador de suspensió automàtica</string>\n    <string name=\"sleeptimer_description\">Habilita el temporitzador de suspensió automàtica amb el valor predeterminat per un temps personalitzat</string>\n    <string name=\"sleep_timer_repeat_description\">Estableix un dia i hora en que el temporitzador de suspensió s\\'ha d\\'activar automàticament</string>\n    <string name=\"sleep_timer_repeat\">Repeteix</string>\n    <string name=\"sleep_timer_daily\">Diari</string>\n    <string name=\"sleep_timer_weekdays\">De dilluns a divendres</string>\n    <string name=\"sleep_timer_weekdays_weekends\">Entre setmana / Caps de setmana</string>\n    <string name=\"sleep_timer_weekends\">Caps de setmana (Dissabte-Diumenge)</string>\n    <string name=\"sleep_timer_custom\">Personalitzat</string>\n    <string name=\"sleep_timer_start_time\">Hora d\\'inici</string>\n    <string name=\"sleep_timer_end_time\">Hora final</string>\n    <string name=\"sleep_timer_monday\">Dilluns</string>\n    <string name=\"sleep_timer_tuesday\">Dimarts</string>\n    <string name=\"sleep_timer_wednesday\">Dimecres</string>\n    <string name=\"sleep_timer_thursday\">Dijous</string>\n    <string name=\"sleep_timer_friday\">Divendres</string>\n    <string name=\"sleep_timer_saturday\">Dissabte</string>\n    <string name=\"sleep_timer_sunday\">Diumenge</string>\n    <string name=\"sleep_timer_stop_after_current_song\">Para en acabar la cançó actual quan el temporitzador finalitze</string>\n    <string name=\"sleep_timer_fade_out\">Esvaïment durant el minut final</string>\n    <string name=\"error_episode_save\">No s\\'ha pogut desar l\\'episodi</string>\n    <string name=\"error_episode_remove\">No s\\'ha pogut eliminar l\\'episodi</string>\n    <string name=\"error_podcast_subscribe\">No s\\'ha pogut subscriure\\'s al podcast</string>\n    <string name=\"error_podcast_unsubscribe\">No s\\'ha pogut llevar la subscripció al podcast</string>\n    <string name=\"view_channel\">Visualitzar Canal</string>\n    <string name=\"widget_recognizer_name\">Reconeixedor de música</string>\n    <string name=\"widget_recognizer_description\">Reconeix les cançons que sonen al teu voltant directament des de la pantalla d\\'inici</string>\n    <string name=\"widget_recognizer_tap_to_search\">Prem per reconèixer la cançó</string>\n    <string name=\"widget_recognizer_listening\">Escoltant…</string>\n    <string name=\"widget_recognizer_processing\">Identificant…</string>\n    <string name=\"widget_recognizer_no_match\">No s\\'ha trobat cap coincidència. Prove de nou</string>\n    <string name=\"widget_recognizer_error\">Ha fallat el reconeixement</string>\n    <string name=\"widget_recognizer_error_generic\">S\\'ha produït un error. Per favor, prove de nou</string>\n    <string name=\"widget_recognizer_unknown_song\">Cançó desconeguda</string>\n    <string name=\"widget_recognizer_unknown_artist\">Artista desconegut</string>\n    <string name=\"widget_recognizer_mic_desc\">Identifica la cançó</string>\n    <string name=\"widget_recognizer_channel_name\">Reconeixement de música</string>\n    <string name=\"widget_recognizer_channel_desc\">Mostra una notificació en reconèixer una cançó amb el widget</string>\n    <string name=\"widget_recognizer_notification_text\">Enregistrant so per identificar la cançó…</string>\n    <string name=\"listen_together_auto_approval_suggestions\">Aprovar automàticament els suggeriments de cançons</string>\n    <string name=\"listen_together_auto_approval_suggestions_desc\">Aprova automàticament i afig a la cua les cançons que suggereixen els convidats</string>\n    <string name=\"importing_playlist\">Important llista de reproduccions</string>\n    <string name=\"logout_dialog_title\">Conservar les dades de la llibreria?</string>\n    <string name=\"logout_dialog_message\">Vols conservar les dades de la llista de reproducció i de la llibreria. Les cançons descarregades es mantindran de tota manera.</string>\n    <string name=\"logout_keep\">Mantindre</string>\n    <string name=\"logout_clear\">Eliminar</string>\n    <string name=\"credits_lead_developer\">Desenvolupador principal</string>\n    <string name=\"credits_collaborator\">Col·laborador</string>\n    <string name=\"credits_collaborators_section\">Col·laboradors</string>\n    <string name=\"credits_license_name\">GNU General Public License v3.0</string>\n    <string name=\"credits_license_desc\">Software gratuït i de codi lliure. Vostè el pot fer servir, estudiar, compartir i millorar.</string>\n    <string name=\"credits_discord\">Servidor de Discord</string>\n    <string name=\"credits_telegram\">Canal de Telegram</string>\n    <string name=\"credits_website\">Lloc web</string>\n    <string name=\"credits_instagram\">Instagram</string>\n    <string name=\"credits_github\">GitHub</string>\n    <string name=\"credits_view_repo\">Veure Repositori</string>\n    <string name=\"app_version_info\">%1$s • %2$s</string>\n    <string name=\"like_what_i_do\">T\\'agrada el que faig?</string>\n    <string name=\"buy_mo_a_coffee\">Compra\\'m un cafè</string>\n    <string name=\"community_and_info\">Comunitat &amp; informació</string>\n    <string name=\"metrolist\">METROLIST</string>\n    <string name=\"wanna_play_favorite_song\">Vols escoltar la teua cançó preferida?</string>\n    <string name=\"yeah\">Sí</string>\n    <string name=\"stands_with_palestine\">Aquest projecte dona suport a Palestina 🇵🇸</string>\n    <string name=\"filter_podcasts\">Podcasts</string>\n    <string name=\"view_podcast\">Veure el podcast</string>\n    <string name=\"podcast_channels\">Canals de Podcast</string>\n    <string name=\"latest_episodes\">Últims episodis</string>\n    <string name=\"your_shows\">Els teus programes</string>\n    <string name=\"new_episodes\">Nou episodi</string>\n    <string name=\"episodes_for_later\">Episodis per a més tard</string>\n    <string name=\"save_episode_for_later\">Guardar per a més tard</string>\n    <string name=\"save_episode_for_later_desc\">Afig als teus Episodis per a més tard</string>\n    <string name=\"remove_episode_from_saved\">Elimina de desats</string>\n    <string name=\"subscribe_to_podcast\">Desa podcast a la llibreria</string>\n    <plurals name=\"n_episode\">\n        <item quantity=\"one\">%d episodi</item>\n        <item quantity=\"many\">%d d\\'episodis</item>\n        <item quantity=\"other\">%d episodis</item>\n    </plurals>\n    <string name=\"filter_episodes\">Episodis</string>\n    <string name=\"filter_profiles\">Perfils</string>\n    <string name=\"filter_channels\">Canals</string>\n    <string name=\"auto_playlist\">Llista de reproducció automàtica</string>\n    <string name=\"downloaded_episodes\">Episodis baixats</string>\n    <string name=\"no_subscribed_channels\">No t\\'has subscrit a cap canal</string>\n    <string name=\"no_downloaded_episodes\">No hi ha episodis baixats</string>\n    <plurals name=\"n_channel\">\n        <item quantity=\"one\">%d canal</item>\n        <item quantity=\"many\">%d de canals</item>\n        <item quantity=\"other\">%d canals</item>\n    </plurals>\n    <string name=\"restore_confirm_title\">Restablir còpia de seguretat?</string>\n    <string name=\"restore_confirm_message\">Aquesta acció restablirà les teues dades de la aplicació de la còpia de seguretat.</string>\n    <string name=\"restore_account_warning\">Hauràs de tornar a iniciar sessió destrés del restabliment. Es tancarà la sessió del següent compte:</string>\n    <string name=\"restore\">Restablir</string>\n    <string name=\"checking_previous_account\">Cercant comptes anteriors…</string>\n    <string name=\"no_account_found\">No s\\'ha trobat cap compte</string>\n    <string name=\"upload_songs\">Puja cançons</string>\n    <string name=\"uploading\">Pujant…</string>\n    <string name=\"upload_progress\">%1$d de %2$d</string>\n    <string name=\"upload_complete\">Pujada completada</string>\n    <string name=\"upload_failed\">La pujada ha fallat</string>\n    <string name=\"upload_file_too_large\">Arxiu massa gran (màxim 300MB)</string>\n    <string name=\"upload_unsupported_format\">Format no acceptat. Empra mp3, m4a, flac o ogg</string>\n    <string name=\"delete_uploaded_song\">Elimina cançó pujada</string>\n    <string name=\"delete_uploaded_song_confirm\">Està segur de que vol eliminar aquesta cançó? Aquesta acció no és reversible.</string>\n    <string name=\"delete_uploaded_song_success\">S\\'ha eliminat la cançó pujada</string>\n    <string name=\"delete_uploaded_song_failed\">No s\\'ha pogut eliminar la cançó pujada</string>\n    <string name=\"delete_uploaded_songs\">Eliminar cançons pujades</string>\n    <string name=\"delete_uploaded_songs_confirm\">Està segur de que vol eliminar %1$d cançons pujades. Aquesta acció no és reversible.</string>\n    <string name=\"deleted_n_songs\">S\\'ha eliminat %1$d cançons</string>\n    <string name=\"deleting\">Eliminant…</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-ca/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"filter_library\">Col·lecció</string>\n    <string name=\"history\">Historial</string>\n    <string name=\"songs\">Cançons</string>\n    <string name=\"albums\">Àlbums</string>\n    <string name=\"stats\">Estadístiques</string>\n    <string name=\"forgotten_favorites\">Preferits oblidats</string>\n    <string name=\"other_versions\">Altres versions</string>\n    <string name=\"filter_artists\">Artistes</string>\n    <string name=\"artists\">Artistes</string>\n    <string name=\"playlists\">Llistes de reproducció</string>\n    <string name=\"mood_and_genres\">Estats d’ànim i gèneres</string>\n    <string name=\"account\">Compte</string>\n    <string name=\"quick_picks\">Selecció ràpida</string>\n    <string name=\"quick_picks_empty\">Escolta cançons per generar la teva selecció ràpida</string>\n    <string name=\"keep_listening\">Continua escoltant</string>\n    <string name=\"your_youtube_playlists\">Llistes de reproducció de YouTube</string>\n    <string name=\"similar_to\">Similar a</string>\n    <string name=\"new_release_albums\">Nous llançaments d’àlbums</string>\n    <string name=\"yesterday\">Ahir</string>\n    <string name=\"this_week\">Aquesta setmana</string>\n    <string name=\"last_week\">Última setmana</string>\n    <string name=\"most_played_songs\">Cançons més reproduïdes</string>\n    <string name=\"most_played_artists\">Artistes més reproduïts</string>\n    <string name=\"most_played_albums\">Àlbums més reproduïts</string>\n    <string name=\"search_yt_music\">Cerca a YouTube Music…</string>\n    <string name=\"search_library\">Cerca a la col·lecció…</string>\n    <string name=\"filter_liked\">M’ha agradat</string>\n    <string name=\"filter_downloaded\">Descarregat</string>\n    <string name=\"filter_songs\">Cançons</string>\n    <string name=\"filter_videos\">Vídeos</string>\n    <string name=\"filter_albums\">Àlbums</string>\n    <string name=\"filter_community_playlists\">Llistes de la comunitat</string>\n    <string name=\"filter_featured_playlists\">Llistes destacades</string>\n    <string name=\"filter_bookmarked\">Preferits</string>\n    <string name=\"no_results_found\">No s’ha trobat cap resultat</string>\n    <string name=\"library_song_empty\">Les cançons de la col·lecció apareixeran aquí</string>\n    <string name=\"library_artist_empty\">Els artistes de la col·lecció apareixeran aquí</string>\n    <string name=\"library_playlist_empty\">Les teves llistes de reproducció apareixeran aquí</string>\n    <string name=\"from_your_library\">De la vostra col·lecció</string>\n    <string name=\"liked_songs\">Cançons que m’han agradat</string>\n    <string name=\"downloaded_songs\">Cançons descarregades</string>\n    <string name=\"playlist_is_empty\">Llista de reproducció buida</string>\n    <string name=\"retry\">Reintenta</string>\n    <string name=\"today\">Avui</string>\n    <string name=\"filter_playlists\">Llistes de reproducció</string>\n    <string name=\"library_album_empty\">Els àlbums de la col·lecció apareixeran aquí</string>\n    <string name=\"filter_all\">Tot</string>\n    <string name=\"home\">Inici</string>\n    <string name=\"search\">Cerca</string>\n    <string name=\"delete_playlist_confirm\">De debò voleu suprimir la llista de reproducció «%s»?</string>\n    <string name=\"radio\">Ràdio</string>\n    <string name=\"reset\">Reinicialitza</string>\n    <string name=\"details\">Detalls</string>\n    <string name=\"edit\">Edita</string>\n    <string name=\"start_radio\">Inicia la ràdio</string>\n    <string name=\"play\">Reprodueix</string>\n    <string name=\"shuffle\">Mescla</string>\n    <string name=\"add_all_to_library\">Afegeix-ho tot a la col·lecció</string>\n    <string name=\"play_next\">Reprodueix a continuació</string>\n    <string name=\"add_to_queue\">Afegeix a la cua</string>\n    <string name=\"remove_from_library\">Elimina de la col·lecció</string>\n    <string name=\"remove_all_from_library\">Elimina-ho tot de la col·lecció</string>\n    <string name=\"remove_download\">Elimina la baixada</string>\n    <string name=\"import_playlist\">Importa una llista</string>\n    <string name=\"add_to_playlist\">Afegeix a una llista</string>\n    <string name=\"view_artist\">Visualitza l’artista</string>\n    <string name=\"view_album\">Visualitza l’àlbum</string>\n    <string name=\"share\">Comparteix</string>\n    <string name=\"delete\">Suprimeix</string>\n    <string name=\"refetch\">Torna a recollir</string>\n    <string name=\"remove_from_history\">Elimina de l’historial</string>\n    <string name=\"search_online\">Cerca en línia</string>\n    <string name=\"advanced\">Avançat</string>\n    <string name=\"sort_by_create_date\">Data d’addició</string>\n    <string name=\"sort_by_name\">Nom</string>\n    <string name=\"sort_by_song_count\">Recompte de cançons</string>\n    <string name=\"sort_by_length\">Durada</string>\n    <string name=\"sort_by_play_time\">Temps de reproducció</string>\n    <string name=\"sort_by_custom\">Ordre personalitzat</string>\n    <string name=\"media_id\">Id. multimèdia</string>\n    <string name=\"mime_type\">Tipus MIME</string>\n    <string name=\"codecs\">Còdecs</string>\n    <string name=\"volume\">Volum</string>\n    <string name=\"file_size\">Mida del fitxer</string>\n    <string name=\"unknown\">Desconegut</string>\n    <string name=\"copied\">S’ha copiat al porta-retalls</string>\n    <string name=\"edit_lyrics\">Edita la lletra</string>\n    <string name=\"bitrate\">Velocitat de bits</string>\n    <string name=\"sample_rate\">Velocitat de mostratge</string>\n    <string name=\"loudness\">Sonoritat</string>\n    <string name=\"search_lyrics\">Cerca la lletra</string>\n    <string name=\"edit_song\">Edita la cançó</string>\n    <string name=\"song_title\">Títol de la cançó</string>\n    <string name=\"song_artists\">Artistes de la cançó</string>\n    <string name=\"error_song_artist_empty\">L’artista de la cançó no pot estar buit.</string>\n    <string name=\"error_song_title_empty\">El títol de la cançó no pot estar buit.</string>\n    <string name=\"choose_playlist\">Tria una llista</string>\n    <string name=\"edit_playlist\">Edita la llista</string>\n    <string name=\"create_playlist\">Crea una llista</string>\n    <string name=\"playlist_name\">Nom de la llista</string>\n    <string name=\"save\">Desa</string>\n    <string name=\"duplicates\">Duplicades</string>\n    <string name=\"edit_artist\">Edita l’artista</string>\n    <string name=\"artist_name\">Nom de l’artista</string>\n    <string name=\"error_artist_name_empty\">El nom de l’artista no pot estar buit.</string>\n    <string name=\"error_playlist_name_empty\">El nom de la llista no pot estar buit.</string>\n    <string name=\"duplicates_description_multiple\">Ja hi ha %d cançons a la vostra llista</string>\n    <string name=\"add_anyway\">Afegeix igualment</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d cançó</item>\n        <item quantity=\"many\">%d de cançons</item>\n        <item quantity=\"other\">%d cançons</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d artista</item>\n        <item quantity=\"many\">%d d’artistes</item>\n        <item quantity=\"other\">%d artistes</item>\n    </plurals>\n    <string name=\"skip_duplicates\">Omet les duplicacions</string>\n    <string name=\"duplicates_description_single\">La cançó ja és a la vostra llista</string>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d mes</item>\n        <item quantity=\"many\">%d de mesos</item>\n        <item quantity=\"other\">%d mesos</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d àlbum</item>\n        <item quantity=\"many\">%d d’àlbums</item>\n        <item quantity=\"other\">%d àlbums</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d llista</item>\n        <item quantity=\"many\">%d de llistes</item>\n        <item quantity=\"other\">%d llistes</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d setmana</item>\n        <item quantity=\"many\">%d de setmanes</item>\n        <item quantity=\"other\">%d setmanes</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d any</item>\n        <item quantity=\"many\">%d d’anys</item>\n        <item quantity=\"other\">%d anys</item>\n    </plurals>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">1 minut</item>\n        <item quantity=\"many\">%d minuts</item>\n        <item quantity=\"other\">%d minuts</item>\n    </plurals>\n    <string name=\"undo\">Desfés</string>\n    <string name=\"removed_song_from_playlist\">S’ha tret «%s» de la llista</string>\n    <string name=\"lyrics_not_found\">No s’ha trobat la lletra</string>\n    <string name=\"action_like\">M’agrada</string>\n    <string name=\"error_no_internet\">No hi ha cap connexió de xarxa</string>\n    <string name=\"repeat_mode_off\">Mode de repetició desactivat</string>\n    <string name=\"queue_all_songs\">Totes les cançons</string>\n    <string name=\"queue_searched_songs\">Cançons cercades</string>\n    <string name=\"music_player\">Reproductor de música</string>\n    <string name=\"repeat_mode_one\">Repeteix la cançó actual</string>\n    <string name=\"settings\">Paràmetres</string>\n    <string name=\"appearance\">Aspecte</string>\n    <string name=\"theme\">Tema</string>\n    <string name=\"enable_dynamic_theme\">Activa el tema dinàmic</string>\n    <string name=\"dark_theme\">Tema fosc</string>\n    <string name=\"dark_theme_on\">Activat</string>\n    <string name=\"dark_theme_off\">Desactivat</string>\n    <string name=\"dark_theme_follow_system\">Segueix el sistema</string>\n    <string name=\"player_text_alignment\">Alineació del text del reproductor</string>\n    <string name=\"lyrics_text_position\">Posició del text de la lletra</string>\n    <string name=\"pure_black\">Negre pur</string>\n    <string name=\"customize_navigation_tabs\">Personalitza les pestanyes de navegació</string>\n    <string name=\"player\">Reproductor</string>\n    <string name=\"default_\">Per defecte</string>\n    <string name=\"squiggly\">Ondulat</string>\n    <string name=\"default_open_tab\">Pestanya oberta per defecte</string>\n    <string name=\"content\">Contingut</string>\n    <string name=\"action_logout\">Finalitza la sessió</string>\n    <string name=\"small\">Petita</string>\n    <string name=\"big\">Gran</string>\n    <string name=\"action_login\">Inicia la sessió</string>\n    <string name=\"login\">Compte</string>\n    <string name=\"content_language\">Llengua per defecte del contingut</string>\n    <string name=\"content_country\">País per defecte del contingut</string>\n    <string name=\"system_default\">Valor per defecte del sistema</string>\n    <string name=\"enable_proxy\">Activa el servidor intermediari</string>\n    <string name=\"proxy_type\">Tipus de servidor intermediari</string>\n    <string name=\"player_and_audio\">Reproductor i àudio</string>\n    <string name=\"audio_quality\">Qualitat de l’àudio</string>\n    <string name=\"audio_quality_auto\">Automàtica</string>\n    <string name=\"audio_quality_low\">Baixa</string>\n    <string name=\"queue\">Cua</string>\n    <string name=\"persistent_queue\">Cua persistent</string>\n    <string name=\"auto_load_more\">Carrega més cançons automàticament</string>\n    <string name=\"audio_normalization\">Normalització de l’àudio</string>\n    <string name=\"equalizer\">Equalitzador</string>\n    <string name=\"storage\">Emmagatzematge</string>\n    <string name=\"cache\">Memòria cau</string>\n    <string name=\"image_cache\">Memòria cau d’imatges</string>\n    <string name=\"song_cache\">Memòria cau de cançons</string>\n    <string name=\"unlimited\">Il·limitada</string>\n    <string name=\"clear_all_downloads\">Neteja totes les baixades</string>\n    <string name=\"privacy\">Privadesa</string>\n    <string name=\"clear_listen_history_confirm\">Segur que voleu netejar tot l’historial d’escoltes?</string>\n    <string name=\"search_history\">Historial de cerques</string>\n    <string name=\"clear_search_history\">Neteja l’historial de cerques</string>\n    <string name=\"pause_search_history\">Pausa l’historial de cerques</string>\n    <string name=\"clear_search_history_confirm\">Segur que voleu netejar tot l’historial de cerques?</string>\n    <string name=\"disable_screenshot\">Desactiva les captures de pantalla</string>\n    <string name=\"enable_lrclib\">Activa el proveïdor de lletres LrcLib</string>\n    <string name=\"enable_kugou\">Activa el proveïdor de lletres KuGou</string>\n    <string name=\"action_backup\">Fes una còpia de seguretat</string>\n    <string name=\"action_restore\">Restaura</string>\n    <string name=\"imported_playlist\">Llista importada</string>\n    <string name=\"hide_explicit\">Amaga el contingut explícit</string>\n    <string name=\"backup_restore\">Còpia de seguretat i restauració</string>\n    <string name=\"discord_integration\">Integració amb el Discord</string>\n    <string name=\"options\">Opcions</string>\n    <string name=\"backup_create_failed\">No s’ha pogut crear la còpia de seguretat</string>\n    <string name=\"restore_failed\">No s’ha pogut restaurar la còpia de seguretat</string>\n    <string name=\"backup_create_success\">S’ha creat la còpia de seguretat satisfactòriament</string>\n    <string name=\"preview\">Previsualitza</string>\n    <string name=\"about\">Quant a</string>\n    <string name=\"app_version\">Versió de l’aplicació</string>\n    <string name=\"translation_models\">Models de traducció</string>\n    <string name=\"clear_translation_models\">Neteja els models de traducció</string>\n    <string name=\"new_version_available\">Hi ha una versió nova disponible</string>\n    <string name=\"downloading\">S’està baixant</string>\n    <string name=\"add_to_library\">Afegeix a la col·lecció</string>\n    <string name=\"action_download\">Baixa</string>\n    <string name=\"action_sync\">Sincronitza</string>\n    <string name=\"sort_by_artist\">Artista</string>\n    <string name=\"remove_from_queue\">Elimina de la cua</string>\n    <string name=\"remove_from_playlist\">Elimina de la llista</string>\n    <string name=\"tempo_and_pitch\">Tempo i to</string>\n    <string name=\"sort_by_year\">Any</string>\n    <string name=\"proxy_url\">URL del servidor intermediari</string>\n    <string name=\"audio_quality_high\">Alta</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d seleccionada</item>\n        <item quantity=\"many\">%d de seleccionades</item>\n        <item quantity=\"other\">%d seleccionades</item>\n    </plurals>\n    <string name=\"remove_download_playlist_confirm\">Realment voleu eliminar totes les cançons de la llista «%s» de l’emmagatzematge de cançons baixades?</string>\n    <string name=\"playlist_imported\">S’ha importat la llista</string>\n    <string name=\"playlist_synced\">S’ha sincronitzat la llista</string>\n    <string name=\"end_of_song\">Fi de la cançó</string>\n    <string name=\"action_like_all\">M’agrada tot</string>\n    <string name=\"action_remove_like\">Ja no m’agrada</string>\n    <string name=\"action_remove_like_all\">Ja no m’agrada res</string>\n    <string name=\"sleep_timer\">Temportizador</string>\n    <string name=\"error_unknown\">Error desconegut</string>\n    <string name=\"action_shuffle_on\">Reproducció aleatoria activada</string>\n    <string name=\"action_shuffle_off\">Reproducció aleatòria desactivada</string>\n    <string name=\"left\">Esquerra</string>\n    <string name=\"center\">Centre</string>\n    <string name=\"right\">Dreta</string>\n    <string name=\"player_slider_style\">Estil del lliscador del reproductor</string>\n    <string name=\"misc\">Miscel·lània</string>\n    <string name=\"grid_cell_size\">Mida de les caselles</string>\n    <string name=\"not_logged_in\">Sessió no iniciada</string>\n    <string name=\"login_failed\">Inici de sessió fallit</string>\n    <string name=\"restart_to_take_effect\">Reincieu perquè els canvis tinguin efecte</string>\n    <string name=\"persistent_queue_desc\">Recupera la última cua de reproducció quan reinicieu l\\'aplicació</string>\n    <string name=\"auto_load_more_desc\">Afegiu més cançons automàticament quan s\\'arribi al final de la cua de reproducció, si és possible</string>\n    <string name=\"skip_silence\">Saltar el silenci</string>\n    <string name=\"auto_skip_next_on_error\">Continua automàticament amb la següent cancço si ocórre un error</string>\n    <string name=\"auto_skip_next_on_error_desc\">Assegura\\'t una expreiència de reproducció continua</string>\n    <string name=\"stop_music_on_task_clear\">Atura la música al netejar les tasques</string>\n    <string name=\"max_cache_size\">Mida màxima del cache</string>\n    <string name=\"max_image_cache_size\">Mida màxima del cache de les imatges</string>\n    <string name=\"clear_image_cache\">Neteja el cache de les imatges</string>\n    <string name=\"max_song_cache_size\">Mida màxima del cache de les cançons</string>\n    <string name=\"clear_song_cache\">Neteja el cache de les cançons</string>\n    <string name=\"size_used\">%s utilitzat</string>\n    <string name=\"listen_history\">Historial de reproducció</string>\n    <string name=\"pause_listen_history\">Atura la recopliació del historial de reproducció</string>\n    <string name=\"clear_listen_history\">Neteja l\\'historial de reproducció</string>\n    <string name=\"error_timeout\">Pausa</string>\n    <string name=\"repeat_mode_all\">Repeteix la cua</string>\n    <string name=\"use_login_for_browse\">Utilitza iniciar sessió per cercar contingut</string>\n    <string name=\"use_login_for_browse_desc\">Això pot influenciar el contingut que veus i per exemple, mostra àlbums exclusius prèmium si la sessió està iniciada en un compte prèmium</string>\n    <string name=\"disable_screenshot_desc\">Quan aquesta opció és activada, les captures de pantalla i la visualització a \\\"recents\\\" són desactivades.</string>\n    <string name=\"discord_information\">Metrolist utilitza la biblioteca KizzyRPC per establir l\\'estat del vostre compte de Discord. Això implica utilitzar la connexió Discord Gateway, la qual cosa es pot considerar una violació de les condicions del servei de Discord. No obstant això, no hi ha casos coneguts d\\'usuaris que hagin estat suspesos pel motiu. L\\'ús és sota la vostra responsabilitat.\\n\\nMetrolist només extreu el vostre \\\"token\\\", i la resta es desa localment.</string>\n    <string name=\"dismiss\">Ignora</string>\n    <string name=\"enable_discord_rpc\">Activa \\\"Rich Presence\\\"</string>\n    <string name=\"error_no_stream\">Cap transmissió disponible</string>\n    <string name=\"sided\">Amb vores</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-cs/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"liked\">Oblíbené</string>\n    <string name=\"offline\">Offline</string>\n    <string name=\"my_top\">Moje nej</string>\n    <string name=\"select\">Vybrat vše</string>\n    <string name=\"like_all\">Vše do oblíbených</string>\n    <string name=\"sort_by_last_updated\">Datum změny</string>\n    <string name=\"already_in_playlist\">Již v playlistu:</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"default_lib_chips\">Změnit výchozí stránku knihovny</string>\n    <string name=\"set_quick_picks\">Nastavit rychlý výběr</string>\n    <string name=\"last_song_listened\">Podle poslední poslouchané skladby</string>\n    <string name=\"all_time\">Za celou dobu</string>\n    <string name=\"past_24_hours\">Posledních 24 hodin</string>\n    <string name=\"past_week\">Poslední týden</string>\n    <string name=\"past_month\">Poslední měsíc</string>\n    <string name=\"past_year\">Poslední rok</string>\n    <string name=\"top_length\">Délka mého Seznamu nejlepších</string>\n    <string name=\"share_as_image\">Sdílet jako obrázek</string>\n    <string name=\"player_background_style\">Styl pozadí přehrávače</string>\n    <string name=\"generating_image\">Generování obrázku</string>\n    <string name=\"max_selection_limit\">Maximální limit výběru</string>\n    <string name=\"share_as_text\">Sdílet jako text</string>\n    <string name=\"token_adv_login_description\">Toto je POKROČILÝ způsob přihlášení. Jako alternativu k webovému portálu zde můžete přímo zadat nebo aktualizovat váš přihlašovací token. Můžete tím například urychlit přihlašování na více zařízeních. Upozorňujeme, že jakékoli neplatné formáty tokenu, které aplikace nedokáže zpracovat, nebudou přijaty</string>\n    <string name=\"share_selected\">Sdílet vybrané</string>\n    <string name=\"customize_colors\">Přizpůsobit barvy</string>\n    <string name=\"text_color\">Barva textu</string>\n    <string name=\"secondary_text_color\">Sekundární barva textu</string>\n    <string name=\"background_color\">Barva pozadí</string>\n    <string name=\"link_copied\">Odkaz zkopírován do schránky</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%dkrát</item>\n        <item quantity=\"few\">%dkrát</item>\n        <item quantity=\"many\">%dkrát</item>\n        <item quantity=\"other\">%dkrát</item>\n    </plurals>\n    <string name=\"similar_content\">Podobný obsah</string>\n    <string name=\"follow_theme\">Podle motivu</string>\n    <string name=\"gradient\">Barevný přechod</string>\n    <string name=\"player_background_blur\">Rozostřený obal</string>\n    <string name=\"player_buttons_style\">Barvy tlačítek přehrávače</string>\n    <string name=\"default_style\">Výchozí</string>\n    <string name=\"enable_swipe_thumbnail\">Povolit posunutí pro změnu skladby</string>\n    <string name=\"swipe_song_to_add\">Posuňte skladbu doleva pro přidání do fronty nebo doprava pro přehrání jako další</string>\n    <string name=\"lyrics_click_change\">Změnit texty po klepnutí</string>\n    <string name=\"slim\">Úzký</string>\n    <string name=\"slim_navbar\">Zúžit spodní navigační panel</string>\n    <string name=\"auto_playlists\">Automatické playlisty</string>\n    <string name=\"show_liked_playlist\">Zobrazit playlist „Oblíbené“</string>\n    <string name=\"show_downloaded_playlist\">Zobrazit playlist „Stažené“</string>\n    <string name=\"show_top_playlist\">Zobrazit playlist „Nejlepší“</string>\n    <string name=\"show_cached_playlist\">Zobrazit playlist „V mezipaměti“</string>\n    <string name=\"advanced_login\">Přihlásit se pomocí tokenu</string>\n    <string name=\"token_hidden\">Klepněte pro zobrazení tokenu</string>\n    <string name=\"token_shown\">Klepněte znovu pro kopírování nebo úpravu</string>\n    <string name=\"lyrics\">Texty</string>\n    <string name=\"local_history\">Místní</string>\n    <string name=\"remote_history\">Vzdálená</string>\n    <string name=\"charts\">Žebříčky</string>\n    <string name=\"back_button_desc\">Zpět</string>\n    <string name=\"album_cover_desc\">Obal alba</string>\n    <string name=\"top_music_videos\">Nejlepší hudební videa</string>\n    <string name=\"trending\">Trendy</string>\n    <string name=\"weeks\">Týdny</string>\n    <string name=\"months\">Měsíce</string>\n    <string name=\"years\">Roky</string>\n    <string name=\"continuous\">Průběžné</string>\n    <string name=\"cached_playlist\">V mezipaměti</string>\n    <string name=\"sync_playlist\">Synchronizovat playlist</string>\n    <string name=\"sync_disabled\">Synchronizace zakázána</string>\n    <string name=\"allows_for_sync_witch_youtube\">Upozornění: tato funkce umožňuje synchronizaci se službou YouTube Music. Tuto možnost NELZE později změnit.</string>\n    <string name=\"remove_from_cache\">Odstranit z mezipaměti</string>\n    <string name=\"copy_link\">Kopírovat odkaz</string>\n    <string name=\"dislike_all\">Nelíbí se u všeho</string>\n    <string name=\"open_app_settings_error\">Nepodařilo se otevřít nastavení aplikace</string>\n    <string name=\"release_notes\">Seznam změn</string>\n    <string name=\"please_wait\">Čekejte prosím</string>\n    <string name=\"cancel\">Zrušit</string>\n    <string name=\"share_lyrics\">Sdílet texty</string>\n    <string name=\"general\">Obecné</string>\n    <string name=\"app_language\">Jazyk aplikace</string>\n    <string name=\"enable_similar_content\">Povolit podobný obsah</string>\n    <string name=\"similar_content_desc\">Automaticky přidávat další podobné skladby při dosažení konce fronty</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"auto_download_on_like\">Automaticky stáhnout po oblíbení</string>\n    <string name=\"auto_download_on_like_desc\">Automaticky stáhnout skladby, když je přidáte do oblíbených</string>\n    <string name=\"clear_song_cache_dialog\">Opravdu chcete vymazat všechny skladby v mezipaměti?</string>\n    <string name=\"clear_downloads_dialog\">Opravdu chcete vymazat všechna stahování?</string>\n    <string name=\"not_logged_in_youtube\">Nepřihlášeni do YouTube</string>\n    <string name=\"default_links\">Otevírat podporované odkazy</string>\n    <string name=\"history_duration\">Délka historie</string>\n    <string name=\"information\">Informace</string>\n    <string name=\"description\">Popis</string>\n    <string name=\"views\">Zhlédnutí</string>\n    <string name=\"likes\">Líbí se</string>\n    <string name=\"dislikes\">Nelíbí se</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">1 sekunda</item>\n        <item quantity=\"few\">%d sekundy</item>\n        <item quantity=\"many\">%d sekund</item>\n        <item quantity=\"other\">%d sekund</item>\n    </plurals>\n    <string name=\"lyrics_auto_scroll\">Automaticky posouvat texty</string>\n    <string name=\"import_csv\">Importovat playlisty CSV</string>\n    <string name=\"import_online\">Importovat playlist „m3u“</string>\n    <string name=\"playlist_add_local_to_synced_note\">Upozornění: přidávání místních skladeb do synchronizovaných/vzdálených playlistů není podporováno. Jakákoli jiná kombinace je platná</string>\n    <string name=\"lyrics_romanize_japanese\">Přepsat japonské texty do latinky</string>\n    <string name=\"lyrics_romanize_korean\">Přepsat korejské texty do latinky</string>\n    <string name=\"yt_sync\">Automaticky synchronizovat s účtem</string>\n    <string name=\"more_content\">Další obsah</string>\n    <string name=\"new_player_design\">Nový vzhled přehrávače</string>\n    <string name=\"swipe_sensitivity\">Citlivost posunutí v mini přehrávači</string>\n    <string name=\"clear_image_cache_dialog\">Opravdu chcete vymazat všechny obrázky v mezipaměti?</string>\n    <string name=\"disable\">Zakázat</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"subscribe\">Odebírat</string>\n    <string name=\"subscribed\">Odebíráno</string>\n    <string name=\"new_mini_player_design\">Nový vzhled mini přehrávače</string>\n    <string name=\"now_playing\">Právě hraje</string>\n    <string name=\"seek_forward_dynamic\">+%1$d sekund vpřed</string>\n    <string name=\"seek_backward_dynamic\">-%1$d sekund zpět</string>\n    <string name=\"seek_seconds_addup\">Progresivní posun</string>\n    <string name=\"seek_seconds_addup_description\">Při povolení se při každém přeskočení bude postupně přidávat 5 sekund</string>\n    <string name=\"close\">Zavřít</string>\n    <string name=\"disable_load_more_when_repeat_all\">Zakázat načtení více skladeb při opakování všech</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Automaticky nenačítat další skladby a podobný obsah, když je zapnutý režim opakování</string>\n    <string name=\"hide_player_thumbnail\">Skrýt náhled přehrávače</string>\n    <string name=\"hide_player_thumbnail_desc\">Nahradit obal alba v přehrávači logem aplikace</string>\n    <string name=\"settings_section_ui\">Rozhraní</string>\n    <string name=\"settings_section_privacy\">Soukromí a zabezpečení</string>\n    <string name=\"settings_section_player_content\">Přehrávač a obsah</string>\n    <string name=\"settings_section_storage\">Úložiště a data</string>\n    <string name=\"settings_section_system\">Systém a informace</string>\n    <string name=\"starting_radio\">Spouštím rádio</string>\n    <string name=\"config_proxy\">Nastavit síť proxy</string>\n    <string name=\"proxy_username\">Uživatelské jméno sítě proxy</string>\n    <string name=\"proxy_password\">Heslo sítě proxy</string>\n    <string name=\"enable_authentication\">Povolit přihlašování</string>\n    <string name=\"lyrics_romanization_cyrillic\">Cyrilice</string>\n    <string name=\"lyrics_romanize_title\">Přepis do latinky</string>\n    <string name=\"lyrics_romanization\">Přepis textů do latinky</string>\n    <string name=\"lyrics_romanize_russian\">Přepsat ruské texty do latinky</string>\n    <string name=\"lyrics_romanize_ukrainian\">Přepsat ukrajinské texty do latinky</string>\n    <string name=\"lyrics_romanize_belarusian\">Přepsat běloruské texty do latinky</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Přepsat kyrgyzské texty do latinky</string>\n    <string name=\"lyrics_romanize_serbian\">Přepsat srbské texty do latinky</string>\n    <string name=\"lyrics_romanize_bulgarian\">Přepsat bulharské texty do latinky</string>\n    <string name=\"line_by_line_option_title\">EXPERIMENTÁLNÍ: Rozpoznávat jazyk po řádcích</string>\n    <string name=\"line_by_line_option_desc\">Cyrilice bude vždy rozpoznávána řádek po řádku namísto celé skladby.</string>\n    <string name=\"line_by_line_dialog_title\">Jste si jisti?</string>\n    <string name=\"line_by_line_dialog_desc\">Toto je experimentální funkce, která nemusí vždy fungovat správně.\\n\\nVe výchozím nastavení se jazyk určuje z celé písně, ale po zapnutí této možnosti se bude určovat pro každý řádek zvlášť. To umožní fungování vícejazyčných písní, ALE jazyk nemusí být vždy určen správně (například pokud ukrajinský text neobsahuje žádná písmena specifická pro ukrajinštinu, může být chybně přepsán jako ruský).\\n\\nPokud nemáte žádné problémy, doporučujeme nechat tuto možnost vypnutou.</string>\n    <string name=\"romanize_current_track\">Přepsat aktuální skladbu do latinky</string>\n    <string name=\"edit_playlist_cover\">Upravit obal playlistu</string>\n    <string name=\"edit_playlist_cover_note\">Upozornění: pro změnu obalu alba musí být k vašemu účtu připojeno telefonní číslo a účet musí být ověřen na YouTube Music.</string>\n    <string name=\"edit_playlist_cover_note_wait\">Po vybrání obrázku prosím počkejte, než se nový obal objeví ve vašem playlistu.</string>\n    <string name=\"choose_from_library\">Vybrat z knihovny</string>\n    <string name=\"remove_custom_image\">Odstranit vlastní obrázek</string>\n    <string name=\"audio_offload\">Povolit offload</string>\n    <string name=\"audio_offload_description\">Povolit offload zvukové cesty pro přehrávání zvuku. Zakázání může zvýšit spotřebu energie, ale může být užitečné, pokud máte problémy s přehráváním zvuku nebo jeho následným zpracováním</string>\n    <string name=\"uploaded_playlist\">Nahráno</string>\n    <string name=\"filter_uploaded\">Nahráno</string>\n    <string name=\"show_uploaded_playlist\">Zobrazit playlist „Nahráno“</string>\n    <string name=\"updater\">Aktualizační služba</string>\n    <string name=\"check_for_updates\">Automaticky kontrolovat aktualizace</string>\n    <string name=\"lyrics_romanize_macedonian\">Přepsat makedonské texty do latinky</string>\n    <string name=\"update_notifications\">Povolit oznámení o aktualizacích</string>\n    <string name=\"update_available_title\">Je dostupná aktualizace</string>\n    <string name=\"update_channel_name\">Aktualizace aplikací</string>\n    <string name=\"update_channel_desc\">Oznámení o nových verzích</string>\n    <string name=\"discord_use_details\">Použít podrobnosti namísto stavu</string>\n    <string name=\"discord_use_details_description\">Zobrazit jako hlavní název skladby namísto jména interpreta</string>\n    <string name=\"integrations\">Integrace</string>\n    <string name=\"username\">Uživatelské jméno</string>\n    <string name=\"password\">Heslo</string>\n    <string name=\"lastfm_integration\">Integrace Last.fm</string>\n    <string name=\"enable_scrobbling\">Povolit scrobbling</string>\n    <string name=\"lastfm_now_playing\">Odesílat právě hrající skladbu</string>\n    <string name=\"scrobbling_configuration\">Nastavení scrobblingu</string>\n    <string name=\"scrobble_min_track_duration\">Scrobblovat skladby delší než</string>\n    <string name=\"scrobble_delay_percent\">Procento zpoždění scrobblování</string>\n    <string name=\"scrobble_delay_minutes\">Minuty zpoždění scrobblování</string>\n    <string name=\"swipe_song_to_remove\">Posuňte skladbu pro odstranění z playlistu</string>\n    <string name=\"last_fm_send_likes\">Odesílat stav oblíbení</string>\n    <string name=\"last_fm_send_likes_description\">Přidat/odebrat srdíčko u skladby v Last.fm, když ji přidáte/odeberete z oblíbených v Metrolistu</string>\n    <string name=\"lyrics_romanize_chinese\">Přepsat čínské texty do latinky</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"google_cast_description\">Povolit streamování zvuku do Chromecastu a dalších zařízení kompatibilních s protokolem Cast</string>\n    <string name=\"hide_video_songs\">Skrýt videoklipy</string>\n    <string name=\"refetch_desc\">Načíst aktuální metadata z YouTube Music</string>\n    <string name=\"play_next_desc\">Přidat na začátek vaší fronty</string>\n    <string name=\"cache_size_warning_message\">Vybrali jste si limit velikosti mezipaměti menší, než kolik aplikace aktuálně používá (%1$s). Pokud budete pokračovat, může aplikace odstranit některé %2$s v mezipaměti pro splnění nového limitu. Chcete přesto pokračovat?</string>\n    <string name=\"cache_size_warning_confirm\">Pokračovat</string>\n    <string name=\"add_to_queue_desc\">Přidat na konec vaší fronty</string>\n    <string name=\"primary_color_style\">Primární barva</string>\n    <string name=\"download_desc\">Umožnit offline přehrávání</string>\n    <string name=\"auto_scroll\">Znovu synchronizovat</string>\n    <string name=\"delete_desc\">Trvale odstranit tuto položku</string>\n    <string name=\"start_radio_desc\">Vytvořit stanici založenou na této položce</string>\n    <string name=\"share_desc\">Sdílet odkaz na tuto položku</string>\n    <string name=\"edit_desc\">Změnit název nebo umělce</string>\n    <string name=\"details_desc\">Zobrazit informace o skladbě</string>\n    <string name=\"add_to_playlist_desc\">Přidat do jednoho z vašich playlistů</string>\n    <string name=\"equalizer_desc\">Upravit ekvalizér</string>\n    <string name=\"cache_size_warning_title\">Pozor!</string>\n    <string name=\"pure_black_mini_player\">Čistě černý mini přehrávač</string>\n    <string name=\"enable_dynamic_icon\">Povolit dynamickou ikonu</string>\n    <string name=\"mini_player\">Mini přehrávač</string>\n    <string name=\"advanced_desc\">Změnit tempo a výšku skladby</string>\n    <string name=\"add_to_library_desc\">Uložit do vaší knihovny</string>\n    <string name=\"tertiary_color_style\">Tetriární barva</string>\n    <string name=\"logging_in\">Přihlašování…</string>\n    <string name=\"download_playlist_desc\">Stáhnout všechny skladby pro offline přehrávání</string>\n    <string name=\"remove_download_playlist_desc\">Odstranit všechny stažené skladby z tohoto playlistu</string>\n    <string name=\"download_in_progress_desc\">Probíhá stahování</string>\n    <string name=\"share_playlist_desc\">Sdílejte tento playlist s ostatními</string>\n    <string name=\"delete_playlist_desc\">Trvale odstranit tento playlist</string>\n    <string name=\"sync_playlist_desc\">Synchronizovat playlist s YouTube Music</string>\n    <string name=\"enable_better_lyrics\">Povolit Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">Texty synchronizované slovo po slově, pro karaoke</string>\n    <string name=\"lyrics_animation_style\">Styl animace slovo po slově</string>\n    <string name=\"none\">Žádný</string>\n    <string name=\"fade\">Přechod</string>\n    <string name=\"glow\">Záře</string>\n    <string name=\"slide\">Přesun</string>\n    <string name=\"karaoke\">Karaoke</string>\n    <string name=\"apple_music_style\">Apple Music</string>\n    <string name=\"lyrics_text_size\">Velikost textů</string>\n    <string name=\"lyrics_line_spacing\">Rozestupy řádků v textech</string>\n    <string name=\"shuffle_playlist_first\">Zamíchat nejprve playlist/album</string>\n    <string name=\"lyrics_glow_effect\">Povolit efekt záře u textů</string>\n    <string name=\"lyrics_glow_effect_desc\">Přidat animaci záře a efekt zvětšení k aktivním textům</string>\n    <string name=\"shuffle_playlist_first_desc\">Při náhodném přehrávání přehrát nejprve všechny skladby z původního playlistu/alba a až poté podobný obsah</string>\n    <string name=\"show_wrapped_card\">Zobrazit kartu Wrapped</string>\n    <string name=\"album_art_for\">Obrázek alba %s</string>\n    <string name=\"wrapped_total_albums_title\">Poslouchali jste</string>\n    <string name=\"wrapped_total_albums_subtitle\">různých alb</string>\n    <string name=\"wrapped_top_album_title\">Vaše top album je</string>\n    <string name=\"wrapped_playlist_ready\">Váš osobní playlist je připraven</string>\n    <string name=\"wrapped_top_5_albums_title\">Vašich top 5 alb</string>\n    <string name=\"wrapped_album_listening_time\">Toto album jste poslouchali %d minut</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d minut</string>\n    <string name=\"wrapped_no_data\">Žádná data</string>\n    <string name=\"wrapped_top_5_artists_title\">Vaši top umělci roku</string>\n    <string name=\"wrapped_artist_listening_time\">%d minut</string>\n    <string name=\"wrapped_top_5_songs_title\">Vaše top skladby roku</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">Obal alba</string>\n    <string name=\"wrapped_top_artist_title\">Váš top umělec roku je</string>\n    <string name=\"wrapped_top_artist_image_content_description\">Obrázek top umělce</string>\n    <string name=\"wrapped_top_artist_listening_time\">Poslouchali jste ho %d minut</string>\n    <string name=\"wrapped_top_song_title\">Vaše nejpřehrávanější skladba je</string>\n    <string name=\"wrapped_top_song_listening_time\">Poslouchali jste ji %d minut</string>\n    <string name=\"wrapped_total_artists_title\">Poslouchali jste</string>\n    <string name=\"wrapped_total_artists_subtitle\">různých umělců</string>\n    <string name=\"wrapped_total_songs_title\">Poslouchali jste</string>\n    <string name=\"wrapped_total_songs_subtitle\">různých skladeb</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_intro_subtitle\">je čas podívat se, co jste poslouchali</string>\n    <string name=\"wrapped_intro_button\">jdeme na to!</string>\n    <string name=\"wrapped_logo_content_description\">Logo Metrolist</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"wrapped_ready_title\">TVŮJ WRAPPED JE READY!</string>\n    <string name=\"wrapped_ready_subtitle\">Je čas podívat se, co se ti tento rok líbilo.</string>\n    <string name=\"wrapped_thank_you\">Děkujeme za poslouchání</string>\n    <string name=\"wrapped_special_thanks\">Zvláštní poděkování patří MO Agamy za vytvoření Metrolistu</string>\n    <string name=\"wrapped_close\">Zavřít Wrapped</string>\n    <string name=\"wrapped_playlist_title\">Váš %s Wrapped</string>\n    <string name=\"wrapped_create_playlist\">Vytvořit playlist</string>\n    <string name=\"wrapped_playlist_saved\">Playlist uložen</string>\n    <string name=\"casting_to\">Přehrávání přes %s</string>\n    <string name=\"progress_percent\">Postup %s%%</string>\n    <string name=\"listening_to_metrolist\">Poslouchá Metrolist</string>\n    <string name=\"open\">Otevřít</string>\n    <string name=\"failed_to_create_image\">Nepodařilo se vytvořit obrázek: %s</string>\n    <string name=\"copied_title\">Název zkopírován</string>\n    <string name=\"copied_artist\">Umělec zkopírován</string>\n    <string name=\"error_playing\">Chyba při přehrávání</string>\n    <string name=\"failed_to_parse_proxy\">Nepodařilo se zpracovat adresu proxy.</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"one\">%d profil</item>\n        <item quantity=\"few\">%d profily</item>\n        <item quantity=\"many\">%d profilů</item>\n        <item quantity=\"other\">%d profilů</item>\n    </plurals>\n    <string name=\"equalizer_header\">Ekvalizér</string>\n    <string name=\"no_profiles\">Žádné profily ekvalizéru</string>\n    <string name=\"import_profile\">Importovat profil</string>\n    <string name=\"eq_disabled\">Zakázáno</string>\n    <plurals name=\"band_count\">\n        <item quantity=\"one\">%d pásmo</item>\n        <item quantity=\"few\">%d pásma</item>\n        <item quantity=\"many\">%d pásem</item>\n        <item quantity=\"other\">%d pásem</item>\n    </plurals>\n    <string name=\"delete_profile_desc\">Odstranit profil</string>\n    <string name=\"delete_profile_confirmation\">Opravdu chcete odstranit profil %1$s? Tato akce je nevratná.</string>\n    <string name=\"error_file_read\">Nepodařilo se přečíst soubor</string>\n    <string name=\"error_file_open\">Nepodařilo se otevřít soubor: %1$s</string>\n    <string name=\"import_error_title\">Chyba importu</string>\n    <string name=\"wavy\">Vlnitý</string>\n    <string name=\"pause_music_when_media_is_muted\">Pozastavit hudbu při ztlumení médií</string>\n    <string name=\"enable_simpmusic\">Povolit texty SimpMusic</string>\n    <string name=\"enable_simpmusic_desc\">Automaticky získávané texty ze služby Musixmatch a přepisu YouTube</string>\n    <string name=\"system_equalizer\">Systémový ekvalizér</string>\n    <string name=\"album_art\">Obal alba</string>\n    <string name=\"no_song_playing\">Nehraje žádná skladba</string>\n    <string name=\"tap_to_open\">Klepnutím otevřete Metrolist</string>\n    <string name=\"previous\">Předchozí</string>\n    <string name=\"play_pause\">Přehrát/pozastavit</string>\n    <string name=\"next\">Další</string>\n    <string name=\"like\">Oblíbení</string>\n    <string name=\"widget_description\">Widget hudebního přehrávače s ovládáním přehrávání</string>\n    <string name=\"turntable_widget_description\">Kruhový widget přehrávače s možnostmi ovládání přehrání a oblíbení</string>\n    <string name=\"remember_shuffle_and_repeat\">Zapamatovat náhodné přehrávání a opakování</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Zapamatovat náhodné přehrávání a režim opakování při restartování aplikace</string>\n    <string name=\"lyrics_offset\">Zpoždění textů</string>\n    <string name=\"about_artist\">O umělci</string>\n    <string name=\"show_more\">Zobrazit více</string>\n    <string name=\"show_less\">Zobrazit méně</string>\n    <string name=\"artist_page_settings\">Stránka umělce</string>\n    <string name=\"show_artist_description\">Zobrazit popis umělce</string>\n    <string name=\"show_artist_subscriber_count\">Zobrazit počet odběratelů</string>\n    <string name=\"show_artist_monthly_listeners\">Zobrazit počet měsíčních posluchačů</string>\n    <string name=\"skip_silence_desc\">Zrychlit tichá místa skladeb</string>\n    <string name=\"skip_silence_instant\">Okamžitě přeskočit ticho</string>\n    <string name=\"skip_silence_instant_desc\">Přeskočit vpřed během tichých momentů namísto zrychlení přehrávání</string>\n    <string name=\"persistent_shuffle_title\">Perzistentní náhodné přehrávání</string>\n    <string name=\"persistent_shuffle_desc\">Ponechat náhodné přehrávání povolené po spuštění nové skladby nebo playlistu</string>\n    <string name=\"error_playback_failed\">Přehrávání selhalo</string>\n    <string name=\"error_title\">Chyba</string>\n    <string name=\"error_eq_apply_failed\">Nepodařilo se použít profil EQ: %1$s</string>\n    <string name=\"crop_album_art\">Oříznout obal alba</string>\n    <string name=\"crop_album_art_desc\">Vynutit čtvercový poměr stran oříznutím miniatur videí</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">Ponechat zapnutou obrazovku při zvětšeném přehrávači</string>\n    <string name=\"listen_together\">Společný poslech</string>\n    <string name=\"listen_together_server_url\">URL adresa serveru</string>\n    <string name=\"listen_together_username\">Uživatelské jméno</string>\n    <string name=\"listen_together_connected\">Připojeno</string>\n    <string name=\"listen_together_disconnected\">Odpojeno</string>\n    <string name=\"listen_together_connecting\">Připojování…</string>\n    <string name=\"listen_together_error\">Chyba spojení</string>\n    <string name=\"listen_together_create_room\">Vytvořit místnost</string>\n    <string name=\"listen_together_create_room_desc\">Vytvořte místnost a sdílejte její kód s přáteli</string>\n    <string name=\"listen_together_join_room\">Připojit se k místnosti</string>\n    <string name=\"listen_together_room_code\">Kód místnosti</string>\n    <string name=\"listen_together_join_requests\">Žádosti o připojení</string>\n    <string name=\"listen_together_background_disconnect_note\">Poznámka: Je možné, že budete odpojeni, pokud vytvoříte místnost bez hrající hudby a přepnete na jinou aplikaci.</string>\n    <string name=\"listen_together_join_request_notification\">%1$s se chce připojit do místnosti</string>\n    <string name=\"listen_together_room_created\">Místnost vytvořena: %s</string>\n    <string name=\"invalid_room_code\">Neplatný kód místnosti</string>\n    <string name=\"join_request_denied\">Žádost o připojení odmítnuta</string>\n    <string name=\"join_existing_room\">Připojit se k existující místnosti</string>\n    <string name=\"room_code\">Kód místnosti</string>\n    <string name=\"leave_room\">Opustit místnost</string>\n    <string name=\"join_room\">Připojit se</string>\n    <string name=\"create_room\">Vytvořit</string>\n    <string name=\"joining_room\">Připojování k místnosti %s…</string>\n    <string name=\"creating_room\">Vytvářím místnost…</string>\n    <string name=\"connect\">Připojit se</string>\n    <string name=\"disconnect\">Odpojit se</string>\n    <string name=\"create\">Vytvořit</string>\n    <string name=\"listen_together_reconnecting\">Obnovuji spojení…</string>\n    <string name=\"listen_together_notification_channel_name\">Společný poslech</string>\n    <string name=\"listen_together_notification_channel_desc\">Oznámení pro události funkce Společný poslech</string>\n    <string name=\"listen_together_cannot_edit_username_in_room\">Uživatelské jméno nelze změnit když jste v místnosti</string>\n    <string name=\"join\">Připojit se</string>\n    <string name=\"approve\">Schválit</string>\n    <string name=\"reject\">Zamítnout</string>\n    <string name=\"clear\">Vymazat</string>\n    <string name=\"copy\">Kopírovat</string>\n    <string name=\"copied_to_clipboard\">Zkopírováno do schránky</string>\n    <string name=\"not_set\">Nenastaveno</string>\n    <string name=\"in_room\">V místnosti</string>\n    <string name=\"pending_requests\">Čekající žádosti</string>\n    <string name=\"pending_suggestions\">Čekající návrhy</string>\n    <string name=\"kick_user\">Vyhodit</string>\n    <string name=\"you_label\">Vy</string>\n    <string name=\"connected_users\">Připojení uživatelé</string>\n    <string name=\"enter_username\">Zadejte uživatelské jméno</string>\n    <string name=\"error_username_empty\">Uživatelské jméno je povinné.</string>\n    <string name=\"listen_together_suggestion_received\">%1$s navrhl %2$s</string>\n    <string name=\"listen_together_you_are_host\">Jste hostitel</string>\n    <string name=\"listen_together_you_are_guest\">Jste host</string>\n    <string name=\"listen_together_suggestion_sent\">Návrh byl odeslán hostiteli!</string>\n    <string name=\"waiting_for_approval\">Čeká se na schválení hostitelem</string>\n    <string name=\"listen_together_view_logs\">Zobrazit logy</string>\n    <string name=\"listen_together_view_logs_desc\">Diagnostika připojení a zpráv</string>\n    <string name=\"listen_together_logs\">Logy připojení</string>\n    <string name=\"listen_together_no_logs\">Zatím žádné logy</string>\n    <string name=\"listen_together_description\">Poslouchejte hudbu s přáteli v reálném čase. Vytvořte místnost jako hostitel nebo se připojte k existující místnosti pomocí kódu.</string>\n    <string name=\"listen_together_not_configured\">Společný poslech není nakonfigurován. Nastavte adresu URL serveru v Nastavení → Integrace → Společný poslech.</string>\n    <string name=\"suggest_to_host\">Navrhnout hostiteli</string>\n    <string name=\"host_label\">Hostitel</string>\n    <string name=\"resync\">Synchronizovat znovu</string>\n    <string name=\"hosting_room\">Hostování místnosti</string>\n    <string name=\"mute\">Ztlumit</string>\n    <string name=\"unmute\">Zrušit ztlumení</string>\n    <string name=\"crash_title\">Aplikace se neočekávaně ukončila</string>\n    <string name=\"crash_description\">Došlo k neočekávané chybě. Pošlete nám hlášení o chybě, abyste nám pomohli problém opravit.</string>\n    <string name=\"crash_share_logs\">Sdílet logy</string>\n    <string name=\"crash_share_title\">Sdílet hlášení o chybě</string>\n    <string name=\"crash_report_subject\">Metrolist hlášení o chybě</string>\n    <string name=\"crash_close\">Zavřít</string>\n    <string name=\"crash_no_log\">Není k dispozici žádný záznam o chybě</string>\n    <string name=\"palette_dynamic\">Dynamická</string>\n    <string name=\"palette_crimson\">Karmínová</string>\n    <string name=\"palette_rose\">Růžová</string>\n    <string name=\"palette_purple\">Fialová</string>\n    <string name=\"palette_deep_purple\">Tmavě fialová</string>\n    <string name=\"palette_indigo\">Indigová</string>\n    <string name=\"palette_blue\">Modrá</string>\n    <string name=\"palette_sky_blue\">Světle modrá</string>\n    <string name=\"palette_cyan\">Azurová</string>\n    <string name=\"palette_teal\">Modrozelená</string>\n    <string name=\"palette_green\">Zelená</string>\n    <string name=\"palette_light_green\">Světle zelená</string>\n    <string name=\"palette_lime\">Limetková</string>\n    <string name=\"palette_yellow\">Žlutá</string>\n    <string name=\"palette_amber\">Jantarová</string>\n    <string name=\"palette_orange\">Oranžová</string>\n    <string name=\"palette_deep_orange\">Tmavě oranžová</string>\n    <string name=\"palette_brown\">Hnědá</string>\n    <string name=\"palette_grey\">Šedá</string>\n    <string name=\"palette_blue_grey\">Modrošedá</string>\n    <string name=\"cd_back\">Zpět</string>\n    <string name=\"cd_pure_black_mode\">Režim opravdové černé</string>\n    <string name=\"cd_light_mode\">Světlý režim</string>\n    <string name=\"cd_dark_mode\">Tmavý režim</string>\n    <string name=\"cd_system_mode\">Systémový režim</string>\n    <string name=\"cd_palette_item\">%1$s paleta</string>\n    <string name=\"listen_together_choose_server\">Vybrat server</string>\n    <string name=\"listen_together_custom_server\">Vlastní server</string>\n    <string name=\"listen_together_use_custom_server\">Použít vlastní server</string>\n    <string name=\"listen_together_auto_approval_joins\">Automaticky schvalovat žádosti o připojení</string>\n    <string name=\"listen_together_auto_approval_joins_desc\">Automaticky schvalovat žádosti o připojení namísto ručního schvalování</string>\n    <string name=\"listen_together_sync_volume\">Synchronizovat hlasitost s hostitelem</string>\n    <string name=\"listen_together_sync_volume_desc\">Hlasitost hostů se řídí hlasitostí hostitele</string>\n    <string name=\"copy_code\">Zkopírovat kód</string>\n    <string name=\"kick_user_desc\">Odebrat tohoto uživatele z místnosti</string>\n    <string name=\"permanently_kick_user\">Trvale zablokovat</string>\n    <string name=\"permanently_kick_user_desc\">Žádosti o připojení tohoto uživatele budou zablokovány a jeho návrhy skryty</string>\n    <string name=\"transfer_ownership\">Předat vlastnictví</string>\n    <string name=\"transfer_ownership_desc\">Nastaví tohoto uživatele hostitelem místnosti</string>\n    <string name=\"manage_user\">Spravovat uživatele</string>\n    <string name=\"listen_together_blocked_users\">Zablokování uživatelé</string>\n    <string name=\"listen_together_blocked_users_count\">%d uživatel(ů) zablokováno</string>\n    <string name=\"listen_together_no_blocked_users\">Žádní zablokování uživatelé</string>\n    <string name=\"unblock\">Odblokovat</string>\n    <string name=\"user_blocked_by_host\">Uživatel zablokován hostitelem</string>\n    <string name=\"not_playing\">Nehraje žádná skladba</string>\n    <string name=\"tap_to_play\">Klepněte pro otevření Metrolistu</string>\n    <string name=\"widget_music_player\">Hudební přehrávač</string>\n    <string name=\"widget_turntable\">Gramofon</string>\n    <string name=\"enter_room_code\">Vložte kód místnosti</string>\n    <string name=\"listen_together_settings_desc\">Nastavit server, uživatelské jméno a další</string>\n    <string name=\"recognize_music\">Rozpoznat hudbu</string>\n    <string name=\"youtube_url_column\">Sloupec adresy YouTube (nepovinný)</string>\n    <string name=\"ai_translation_mode\">Režim překladu</string>\n    <string name=\"ai_model\">Model</string>\n    <string name=\"re_listen\">Znovu poslechnout</string>\n    <string name=\"ai_translating_lyrics\">Překlad textů…</string>\n    <string name=\"ai_api_key\">Klíč API</string>\n    <string name=\"clear_recognition_history_confirm\">Opravdu chcete vymazat celou historii rozpoznávání?</string>\n    <string name=\"no_match_found\">Nenalezena žádná shoda</string>\n    <string name=\"ai_provider\">Poskytovatel</string>\n    <string name=\"delete_from_history\">Odstranit z historie</string>\n    <string name=\"artist_name_column\">Sloupec jména umělce</string>\n    <string name=\"processing\">Přemýšlím…</string>\n    <string name=\"ai_translation_transcribed\">Přepis</string>\n    <string name=\"ai_target_language\">Cílový jazyk</string>\n    <string name=\"clear_recognition_history\">Vymazat historii rozpoznávání</string>\n    <string name=\"ai_translation_literal\">Překlad</string>\n    <string name=\"ai_setup_guide\">Údaje API</string>\n    <string name=\"ai_lyrics_translation\">AI překlad textů</string>\n    <string name=\"ai_error_translation_failed\">Překlad selhal</string>\n    <string name=\"map_csv_columns\">Mapovat sloupce CSV</string>\n    <string name=\"together\">Společné</string>\n    <string name=\"ai_error_no_lyrics\">Žádné texty k překladu</string>\n    <string name=\"ai_lyrics_translated\">Texty přeloženy</string>\n    <string name=\"column_label\">Sloup. %d</string>\n    <string name=\"ai_error_lyrics_empty\">Texty jsou prázdné</string>\n    <string name=\"recognition_error\">Chyba rozpoznávání</string>\n    <string name=\"ai_error_api_key_required\">Je vyžadován klíč API</string>\n    <string name=\"ai_error_unknown\">Došlo k neznámé chybě</string>\n    <string name=\"enable_high_refresh_rate_desc\">Vynutit použití nejvyšší podporované obnovovací frekvence displeje (např. 120 Hz)</string>\n    <string name=\"ai_error_language_required\">Je vyžadován cílový jazyk</string>\n    <string name=\"first_row_is_header\">První řádek je hlavička</string>\n    <string name=\"try_again\">Zkusit znovu</string>\n    <string name=\"tap_to_recognize\">Klepněte pro rozpoznání</string>\n    <string name=\"recognition_history\">Historie rozpoznávání</string>\n    <string name=\"enable_high_refresh_rate\">Povolit vysokou obnovovací frekvenci</string>\n    <string name=\"song_title_column\">Sloupec názvu skladby</string>\n    <string name=\"recently_converted\">Nedávno převedeno</string>\n    <string name=\"importing_csv\">Import CSV</string>\n    <string name=\"play_on_app\">Přehrát v Metrolistu</string>\n    <string name=\"listening\">Poslouchám…</string>\n    <string name=\"ai_api_key_required\">Vyžadován klíč API</string>\n    <string name=\"ai_error_unexpected\">Neočekávaný výsledek překladu</string>\n    <string name=\"continue_action\">Pokračovat</string>\n    <string name=\"ai_base_url\">Základní URL</string>\n    <string name=\"play_all\">Přehrát vše</string>\n    <string name=\"enable\">Povolit</string>\n    <string name=\"crossfade\">Prolnutí</string>\n    <string name=\"crossfade_desc\">Prolnutí mezi skladbami</string>\n    <string name=\"crossfade_duration\">Trvání prolnutí</string>\n    <string name=\"crossfade_gapless\">Zakázat pro alba bez mezer</string>\n    <string name=\"crossfade_gapless_desc\">Zakázat, pokud mezi skladbami alba nejsou mezery</string>\n    <string name=\"crossfade_beta_title\">Beta funkce</string>\n    <string name=\"crossfade_beta_message\">Prolnutí je nová funkce, ve které se mohou vyskytnout chyby. Pokud na nějaké narazíte, prosíme nahlaste je.\\n\\nTato funkce zakáže offload zvuku z důvodu technických omezení.</string>\n    <string name=\"audio_offload_disabled_by_crossfade\">Zakázáno z důvodu aktivního prolnutí</string>\n    <string name=\"hide_youtube_shorts\">Skrýt YouTube Shorts</string>\n    <string name=\"listen_together_in_top_bar\">Společný poslech v horní liště</string>\n    <string name=\"listen_together_in_top_bar_desc\">Zobrazit Společný poslech v horní liště namísto navigační lišty</string>\n    <string name=\"player_background_solid\">Jednolité</string>\n    <string name=\"prevent_duplicate_tracks_in_queue\">Zabránit duplikovaným skladbám ve frontě</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">Při přidání skladby do fronty ji odstranit z její předchozí pozice, pokud se ve frontě již nachází</string>\n    <string name=\"resume_on_bluetooth_connect\">Pokračovat při připojení Bluetooth zařízení</string>\n    <string name=\"lyrics_romanize_hindi\">Přepsat hindské texty do latinky</string>\n    <string name=\"lyrics_romanize_punjabi\">Přepsat pandžábské texty do latinky</string>\n    <string name=\"lyrics_romanize_as_main\">Zobrazit texty v latince jako hlavní</string>\n    <string name=\"ai_translation_literal_desc\">Přeložit význam do cílového jazyka</string>\n    <string name=\"ai_translation_transcribed_desc\">Převést výslovnost do cílového písma</string>\n    <string name=\"ai_provider_help\">Získat klíče API</string>\n    <string name=\"ai_provider_openrouter_help\">Navštivte https://openrouter.ai pro bezplatné a placené modely</string>\n    <string name=\"ai_provider_openai_help\">Navštivte https://platform.openai.com/api-keys</string>\n    <string name=\"ai_provider_claude_help\">Navštivte https://console.anthropic.com/settings/keys</string>\n    <string name=\"ai_provider_gemini_help\">Naštivte https://aistudio.google.com/apikey</string>\n    <string name=\"ai_provider_perplexity_help\">Navštivte https://perplexity.ai/settings/api</string>\n    <string name=\"ai_provider_xai_help\">Navštivte https://console.x.ai</string>\n    <string name=\"ai_provider_deepl_help\">Navštivte https://deepl.com/pro-api pro bezplatné a placené klíče</string>\n    <string name=\"ai_deepl_formality\">Formalita</string>\n    <string name=\"ai_deepl_formality_default\">Výchozí</string>\n    <string name=\"ai_deepl_formality_more\">Formálnější</string>\n    <string name=\"ai_deepl_formality_less\">Méně formální</string>\n    <string name=\"discord_status\">Stav</string>\n    <string name=\"discord_status_online\">Online</string>\n    <string name=\"discord_status_idle\">Nečinný</string>\n    <string name=\"discord_status_dnd\">Nerušit</string>\n    <string name=\"discord_buttons\">Tlačítka</string>\n    <string name=\"discord_button_1\">Tlačítko 1</string>\n    <string name=\"discord_button_2\">Tlačítko 2</string>\n    <string name=\"login_successful\">Přihlášení bylo úspěšné!</string>\n    <string name=\"discord_information_warning\">Tato funkce využívá knihovnu KizzyRPC k připojení k bráně Discordu a nastavení vašeho stavu na něm. Ačkoli nejsou známy žádné případy zrušení účtu v důsledku podobného použití, není tato metoda oficiálně podporována společností Discord a může být považována za porušení podmínek služby. Váš token je extrahován lokálně a nikdy není odesílán na servery třetích stran. Postupujte podle vlastního uvážení.</string>\n    <string name=\"discord_activity_type\">Typ aktivity</string>\n    <string name=\"discord_activity_playing\">Hraje</string>\n    <string name=\"discord_activity_listening\">Poslouchá</string>\n    <string name=\"discord_activity_watching\">Sleduje</string>\n    <string name=\"discord_activity_competing\">Soupeří</string>\n    <string name=\"discord_button_text_variables\">Proměnné: {song_name}, {artist_name}, {album_name}</string>\n    <string name=\"discord_rpc_preview\">Náhled stavu na Discordu</string>\n    <string name=\"discord_presence\">Stav</string>\n    <string name=\"discord_connect_description\">Přihlaste se k Discordu pro sdílení hudby, kterou posloucháte</string>\n    <string name=\"discord_playing_metrolist\">Hraje Metrolist</string>\n    <string name=\"discord_watching_metrolist\">Sleduje Metrolist</string>\n    <string name=\"discord_competing_metrolist\">Soupeří v Metrolist</string>\n    <string name=\"discord_activity_name\">Název aktivity</string>\n    <string name=\"discord_activity_name_description\">Vlastní název aktivity (ponechte prázdné pro výchozí)</string>\n    <string name=\"discord_advanced_mode\">Pokročilý režim</string>\n    <string name=\"discord_advanced_mode_description\">Zobrazit dodatečné možnosti přizpůsobení pro stav na Discordu</string>\n    <string name=\"display_density\">Hustota displeje</string>\n    <string name=\"restart\">Restartovat</string>\n    <string name=\"restart_required\">Je vyžadován restart</string>\n    <string name=\"density_restart_message\">Změna hustoty displeje se použije po restartu aplikace. Chcete jej nyní provést?</string>\n    <string name=\"found_in_settings_content\">Najdete to v Nastavení &gt; Obsah</string>\n    <string name=\"plays\">přehrání</string>\n    <string name=\"speed_dial\">Rychlý výběr</string>\n    <string name=\"pin_to_speed_dial\">Připnout k Rychlému výběru</string>\n    <string name=\"unpin_from_speed_dial\">Odepnout z Rychlého výběru</string>\n    <string name=\"randomize_home_order\">Náhodné pořadí položek na domovské stránce</string>\n    <string name=\"randomize_home_order_desc\">Náhodně změnit uspořádání sekcí na domovské stránce s váženými prioritami</string>\n    <string name=\"daily_discover_sounds_like\">Zní jako %1$s</string>\n    <string name=\"daily_discover_because_you_listen_to\">Protože jste poslouchali %1$s</string>\n    <string name=\"daily_discover_similar_to\">Podobné %1$s</string>\n    <string name=\"daily_discover_based_on\">Založeno na %1$s</string>\n    <string name=\"daily_discover_for_fans_of\">Pro fanoušky %1$s</string>\n    <string name=\"from_the_community\">Z komunity</string>\n    <string name=\"enable_lrclib_desc\">Komunitní databáze synchronizovaných textů</string>\n    <string name=\"enable_kugou_desc\">Získává texty z KuGou, populární čínské hudební platformy</string>\n    <string name=\"listen_together_auto_approval_suggestions\">Automaticky schvalovat návrhy skladeb</string>\n    <string name=\"listen_together_auto_approval_suggestions_desc\">Automaticky schvalovat a zařazovat do fronty návrhy skladeb od hostů</string>\n    <string name=\"changelog\">Seznam změn</string>\n    <string name=\"changelog_empty\">Seznam změn není k dispozici</string>\n    <string name=\"github_releases_url\">https://github.com/MetrolistGroup/Metrolist/releases</string>\n    <string name=\"list_bullet\">•</string>\n    <string name=\"view_on_github\">Zobrazit na GitHubu</string>\n    <string name=\"current_version\">Aktuální verze</string>\n    <string name=\"version_format\">Verze: %s</string>\n    <string name=\"update_settings\">Nastavení aktualizací</string>\n    <string name=\"check_for_updates_title\">Zkontrolovat aktualizace</string>\n    <string name=\"checking_for_updates\">Kontroluji aktualizace…</string>\n    <string name=\"latest_version_format\">Nejnovější: %s</string>\n    <string name=\"check_for_updates_button\">Zkontrolovat aktualizace</string>\n    <string name=\"hide_changelog\">Skrýt seznam změn</string>\n    <string name=\"view_changelog\">Zobrazit seznam změn</string>\n    <string name=\"failed_to_check_updates\">Nepodařilo se zkontrolovat aktualizace: %s</string>\n    <string name=\"set_as_default\">Nastavit jako výchozí</string>\n    <string name=\"sleep_timer_default_set\">Výchozí časovač spánku nastaven na %d min</string>\n    <string name=\"logout_dialog_title\">Ponechat data knihovny?</string>\n    <string name=\"logout_dialog_message\">Chcete ponechat své playlisty a data knihovny? Stažené skladby zůstanou zachovány.</string>\n    <string name=\"logout_keep\">Ponechat</string>\n    <string name=\"logout_clear\">Vymazat</string>\n    <string name=\"credits_lead_developer\">Hlavní vývojář</string>\n    <string name=\"credits_collaborator\">Přispěvatel</string>\n    <string name=\"credits_collaborators_section\">Přispěvatelé</string>\n    <string name=\"credits_license_name\">GNU General Public License verze 3.0</string>\n    <string name=\"credits_license_desc\">Volně dostupný open-source software. Můžete jej používat, studovat, sdílet a vylepšovat.</string>\n    <string name=\"credits_discord\">Discord server</string>\n    <string name=\"credits_telegram\">Telegram kanál</string>\n    <string name=\"credits_website\">Web</string>\n    <string name=\"credits_instagram\">Instagram</string>\n    <string name=\"credits_github\">GitHub</string>\n    <string name=\"credits_view_repo\">Zobrazit repozitář</string>\n    <string name=\"app_version_info\">%1$s • %2$s</string>\n    <string name=\"like_what_i_do\">Líbí se vám, co dělám?</string>\n    <string name=\"buy_mo_a_coffee\">Pošlete mi kávu</string>\n    <string name=\"community_and_info\">Komunita a informace</string>\n    <string name=\"metrolist\">METROLIST</string>\n    <string name=\"wanna_play_favorite_song\">Chcete přehrát jejich oblíbenou skladbu?</string>\n    <string name=\"yeah\">Jasně</string>\n    <string name=\"stands_with_palestine\">Tento projekt podporuje Palestinu 🇵🇸</string>\n    <string name=\"filter_podcasts\">Podcasty</string>\n    <string name=\"view_podcast\">Zobrazit podcast</string>\n    <string name=\"podcast_channels\">Podcastové kanály</string>\n    <string name=\"latest_episodes\">Nejnovější epizody</string>\n    <string name=\"your_shows\">Vaše pořady</string>\n    <string name=\"new_episodes\">Nové epizody</string>\n    <string name=\"episodes_for_later\">Epizody na později</string>\n    <string name=\"save_episode_for_later\">Uložit na později</string>\n    <string name=\"save_episode_for_later_desc\">Přidat do vašeho playlistu Epizody na později</string>\n    <string name=\"remove_episode_from_saved\">Odebrat z uložených</string>\n    <string name=\"subscribe_to_podcast\">Uložit podcast do knihovny</string>\n    <plurals name=\"n_episode\">\n        <item quantity=\"one\">%d epizoda</item>\n        <item quantity=\"few\">%d epizody</item>\n        <item quantity=\"many\">%d epizod</item>\n        <item quantity=\"other\">%d epizod</item>\n    </plurals>\n    <string name=\"filter_episodes\">Epizody</string>\n    <string name=\"filter_channels\">Kanály</string>\n    <string name=\"auto_playlist\">Automatický playlist</string>\n    <string name=\"downloaded_episodes\">Stažené epizody</string>\n    <string name=\"no_subscribed_channels\">Žádné odebírané kanály</string>\n    <string name=\"no_downloaded_episodes\">Žádné stažené epizody</string>\n    <plurals name=\"n_channel\">\n        <item quantity=\"one\">%d kanál</item>\n        <item quantity=\"few\">%d kanály</item>\n        <item quantity=\"many\">%d kanálů</item>\n        <item quantity=\"other\">%d kanálů</item>\n    </plurals>\n    <string name=\"restore_confirm_title\">Obnovit zálohu?</string>\n    <string name=\"restore_confirm_message\">Tímto se obnoví data aplikace ze zálohy.</string>\n    <string name=\"restore_account_warning\">Po obnovení se budete muset znovu přihlásit. Následující účet bude odhlášen:</string>\n    <string name=\"restore\">Obnovit</string>\n    <string name=\"checking_previous_account\">Kontroluji předchozí účet…</string>\n    <string name=\"no_account_found\">Žádný účet nenalezen</string>\n    <string name=\"importing_playlist\">Importuji playlist</string>\n    <string name=\"error_podcast_unsubscribe\">Nepodařilo se odhlásit z podcastu</string>\n    <string name=\"error_podcast_subscribe\">Nepodařilo se přihlásit k odběru podcastu</string>\n    <string name=\"error_episode_save\">Nepodařilo se uložit epizodu</string>\n    <string name=\"error_episode_remove\">Nepodařilo se odebrat epizodu</string>\n    <string name=\"youtube_music_lyrics_note\">UPOZORNĚNÍ: Texty z YouTube Music budou automaticky zobrazeny, když nejsou dostupné texty z jiných zdrojů. Texty z YTM obvykle nejsou synchronizovány.</string>\n    <string name=\"enable_lyricsplus\">Povolit LyricsPlus</string>\n    <string name=\"enable_lyricsplus_desc\">Synchronizované texty z několika zdrojů</string>\n    <string name=\"lyrics_provider_selection\">Výběr poskytovatele</string>\n    <string name=\"lyrics_provider_selection_desc\">Vyberte, kteří poskytovatelé textů jsou povoleni</string>\n    <string name=\"lyrics_provider_priority\">Priorita poskytovatelů textů</string>\n    <string name=\"lyrics_provider_priority_desc\">Přesuňte pro změnu pořadí poskytovatelů. Vyšší pozice = vyšší priorita.</string>\n    <string name=\"widget_recognizer_name\">Rozpoznávání hudby</string>\n    <string name=\"widget_recognizer_description\">Identifikuje skladby hrající kolem vás přímo z vaší domovské obrazovky</string>\n    <string name=\"widget_recognizer_tap_to_search\">Klepněte pro identifikaci skladby</string>\n    <string name=\"widget_recognizer_listening\">Poslouchám…</string>\n    <string name=\"widget_recognizer_processing\">Identifikuji…</string>\n    <string name=\"widget_recognizer_no_match\">Nenalezena žádná shoda. Zkuste to znovu</string>\n    <string name=\"widget_recognizer_error\">Rozpoznání selhalo</string>\n    <string name=\"widget_recognizer_error_generic\">Došlo k chybě. Zkuste to prosím znovu</string>\n    <string name=\"widget_recognizer_unknown_song\">Neznámá skladba</string>\n    <string name=\"widget_recognizer_unknown_artist\">Neznámý umělec</string>\n    <string name=\"widget_recognizer_mic_desc\">Identifikovat skladbu</string>\n    <string name=\"widget_recognizer_channel_name\">Rozpoznání hudby</string>\n    <string name=\"widget_recognizer_channel_desc\">Zobrazit oznámení během identifikace skladby z widgetu</string>\n    <string name=\"widget_recognizer_notification_text\">Nahrávání zvuku pro identifikaci skladby…</string>\n    <string name=\"view_channel\">Zobrazit kanál</string>\n    <string name=\"filter_profiles\">Profily</string>\n    <string name=\"enable_automatic_sleeptimer\">Zapnout automatický časovač spánku</string>\n    <string name=\"sleeptimer_description\">Zapne automaticky časovač spánku s výchozí hodnotou po nastaveném čase</string>\n    <string name=\"sleep_timer_repeat_description\">Nastavit vlastní den a čas, kdy se časovač spánku automaticky aktivuje</string>\n    <string name=\"sleep_timer_repeat\">Opakovat</string>\n    <string name=\"sleep_timer_daily\">Denně</string>\n    <string name=\"sleep_timer_weekdays\">Od pondělí do pátku</string>\n    <string name=\"sleep_timer_weekdays_weekends\">Pracovní dny / víkendy</string>\n    <string name=\"sleep_timer_weekends\">Víkendy (so–ne)</string>\n    <string name=\"sleep_timer_custom\">Vlastní</string>\n    <string name=\"sleep_timer_start_time\">Počáteční čas</string>\n    <string name=\"sleep_timer_end_time\">Koncový čas</string>\n    <string name=\"sleep_timer_monday\">Pondělí</string>\n    <string name=\"sleep_timer_tuesday\">Úterý</string>\n    <string name=\"sleep_timer_wednesday\">Středa</string>\n    <string name=\"sleep_timer_thursday\">Čtvrtek</string>\n    <string name=\"sleep_timer_friday\">Pátek</string>\n    <string name=\"sleep_timer_saturday\">Sobota</string>\n    <string name=\"sleep_timer_sunday\">Neděle</string>\n    <string name=\"sleep_timer_stop_after_current_song\">Po vypršení časovače zastavit na konci aktuální skladby</string>\n    <string name=\"sleep_timer_fade_out\">Postupné ztlumení v poslední minutě</string>\n    <string name=\"upload_songs\">Nahrát skladby</string>\n    <string name=\"uploading\">Nahrávání…</string>\n    <string name=\"upload_progress\">%1$d z %2$d</string>\n    <string name=\"upload_complete\">Nahrávání dokončeno</string>\n    <string name=\"upload_failed\">Nahrávání selhalo</string>\n    <string name=\"upload_file_too_large\">Soubor je příliš velký (max. 300 MB)</string>\n    <string name=\"upload_unsupported_format\">Nepodporovaný formát. Použijte mp3, m4a, wma, flac nebo ogg</string>\n    <string name=\"delete_uploaded_song\">Odstranit nahranou skladbu</string>\n    <string name=\"delete_uploaded_song_confirm\">Opravdu chcete odstranit tuto nahranou skladbu? Tato akce je nevratná.</string>\n    <string name=\"delete_uploaded_song_success\">Nahraná skladba odstraněna</string>\n    <string name=\"delete_uploaded_song_failed\">Nepodařilo se odstranit skladbu</string>\n    <string name=\"delete_uploaded_songs\">Odstranit nahrané skladby</string>\n    <string name=\"delete_uploaded_songs_confirm\">Opravdu chcete odstranit %1$d nahraných skladeb? Tato akce je nevratná.</string>\n    <string name=\"deleted_n_songs\">Odstraněno %1$d skladeb</string>\n    <string name=\"deleting\">Odstraňování…</string>\n    <string name=\"export_playlist\">Exportovat playlist</string>\n    <string name=\"export_as_csv\">Exportovat jako CSV</string>\n    <string name=\"export_as_m3u\">Exportovat jako M3U</string>\n    <string name=\"export_success\">Playlist úspěšně exportován</string>\n    <string name=\"export_failed\">Nepodařilo se exportovat playlist</string>\n    <string name=\"export_option_share\">Sdílet</string>\n    <string name=\"export_option_save\">Uložit do Dokumentů</string>\n    <string name=\"qs_tile_music_recognizer\">Rozpoznat hudbu</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-cs/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Domů</string>\n    <string name=\"songs\">Skladby</string>\n    <string name=\"artists\">Umělci</string>\n    <string name=\"albums\">Alba</string>\n    <string name=\"playlists\">Playlisty</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">Vybrána %d</item>\n        <item quantity=\"few\">Vybrány %d</item>\n        <item quantity=\"other\">Vybráno %d</item>\n    </plurals>\n    <string name=\"history\">Historie</string>\n    <string name=\"stats\">Statistiky</string>\n    <string name=\"mood_and_genres\">Nálada a žánry</string>\n    <string name=\"account\">Účet</string>\n    <string name=\"quick_picks\">Rychlý výběr</string>\n    <string name=\"quick_picks_empty\">Pro vytvoření rychlého výběru si nejprve poslechněte pár skladeb</string>\n    <string name=\"new_release_albums\">Nově vydaná alba</string>\n    <string name=\"today\">Dnes</string>\n    <string name=\"yesterday\">Včera</string>\n    <string name=\"this_week\">Tento týden</string>\n    <string name=\"last_week\">Minulý týden</string>\n    <string name=\"most_played_songs\">Nejčastěji přehrávané skladby</string>\n    <string name=\"most_played_artists\">Nejčastěji přehrávaní umělci</string>\n    <string name=\"most_played_albums\">Nejčastěji přehrávaná alba</string>\n    <string name=\"search\">Vyhledávání</string>\n    <string name=\"search_yt_music\">Hledat v YouTube Music…</string>\n    <string name=\"search_library\">Hledat v knihovně…</string>\n    <string name=\"filter_library\">Knihovna</string>\n    <string name=\"filter_liked\">Oblíbené</string>\n    <string name=\"filter_downloaded\">Stažené</string>\n    <string name=\"filter_all\">Vše</string>\n    <string name=\"filter_songs\">Skladby</string>\n    <string name=\"filter_videos\">Videa</string>\n    <string name=\"filter_albums\">Alba</string>\n    <string name=\"filter_artists\">Umělci</string>\n    <string name=\"filter_playlists\">Playlisty</string>\n    <string name=\"filter_community_playlists\">Komunitní playlisty</string>\n    <string name=\"filter_featured_playlists\">Doporučené playlisty</string>\n    <string name=\"filter_bookmarked\">Záložky</string>\n    <string name=\"no_results_found\">Nenalezeny žádné výsledky</string>\n    <string name=\"from_your_library\">Z vaší knihovny</string>\n    <string name=\"liked_songs\">Oblíbené skladby</string>\n    <string name=\"downloaded_songs\">Stažené skladby</string>\n    <string name=\"playlist_is_empty\">Playlist je prázdný</string>\n    <string name=\"retry\">Zkusit znovu</string>\n    <string name=\"radio\">Rádio</string>\n    <string name=\"shuffle\">Náhodně</string>\n    <string name=\"reset\">Resetovat</string>\n    <string name=\"details\">Podrobnosti</string>\n    <string name=\"edit\">Upravit</string>\n    <string name=\"start_radio\">Spustit rádio</string>\n    <string name=\"play\">Přehrát</string>\n    <string name=\"play_next\">Přehrát jako další</string>\n    <string name=\"add_to_queue\">Přidat do fronty</string>\n    <string name=\"add_to_library\">Přidat do knihovny</string>\n    <string name=\"remove_from_library\">Odebrat z knihovny</string>\n    <string name=\"action_download\">Stáhnout</string>\n    <string name=\"downloading\">Stahování</string>\n    <string name=\"remove_download\">Odstranit ze stažených</string>\n    <string name=\"import_playlist\">Importovat playlist</string>\n    <string name=\"add_to_playlist\">Přidat do playlistu</string>\n    <string name=\"view_artist\">Zobrazit umělce</string>\n    <string name=\"view_album\">Zobrazit album</string>\n    <string name=\"refetch\">Obnovit</string>\n    <string name=\"share\">Sdílet</string>\n    <string name=\"delete\">Odstranit</string>\n    <string name=\"remove_from_history\">Odstranit z historie</string>\n    <string name=\"search_online\">Hledat online</string>\n    <string name=\"action_sync\">Synchronizace</string>\n    <string name=\"advanced\">Pokročilé</string>\n    <string name=\"sort_by_create_date\">Datum přidání</string>\n    <string name=\"sort_by_name\">Název</string>\n    <string name=\"sort_by_artist\">Umělec</string>\n    <string name=\"sort_by_year\">Rok</string>\n    <string name=\"sort_by_song_count\">Počet skladeb</string>\n    <string name=\"sort_by_length\">Délka</string>\n    <string name=\"sort_by_play_time\">Doba přehrávání</string>\n    <string name=\"sort_by_custom\">Vlastní pořadí</string>\n    <string name=\"media_id\">ID média</string>\n    <string name=\"mime_type\">Typ MIME</string>\n    <string name=\"codecs\">Kodeky</string>\n    <string name=\"bitrate\">Přenosová rychlost</string>\n    <string name=\"sample_rate\">Vzorkovací frekvence</string>\n    <string name=\"loudness\">Hlučnost</string>\n    <string name=\"volume\">Hlasitost</string>\n    <string name=\"file_size\">Velikost souboru</string>\n    <string name=\"unknown\">Neznámé</string>\n    <string name=\"copied\">Zkopírováno do schránky</string>\n    <string name=\"edit_lyrics\">Upravit texty</string>\n    <string name=\"search_lyrics\">Hledat texty</string>\n    <string name=\"edit_song\">Upravit skladbu</string>\n    <string name=\"song_title\">Název skladby</string>\n    <string name=\"song_artists\">Umělci skladby</string>\n    <string name=\"error_song_title_empty\">Název skladby nemůže být prázdný.</string>\n    <string name=\"error_song_artist_empty\">Umělec skladby nemůže být prázdný.</string>\n    <string name=\"save\">Uložit</string>\n    <string name=\"choose_playlist\">Vybrat playlist</string>\n    <string name=\"edit_playlist\">Upravit playlist</string>\n    <string name=\"create_playlist\">Vytvořit playlist</string>\n    <string name=\"playlist_name\">Název playlistu</string>\n    <string name=\"error_playlist_name_empty\">Název playlistu nemůže být prázdný.</string>\n    <string name=\"edit_artist\">Upravit umělce</string>\n    <string name=\"artist_name\">Jméno umělce</string>\n    <string name=\"error_artist_name_empty\">Jméno umělce nemůže být prázdné.</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d skladba</item>\n        <item quantity=\"few\">%d skladby</item>\n        <item quantity=\"other\">%d skladeb</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d umělec</item>\n        <item quantity=\"few\">%d umělci</item>\n        <item quantity=\"other\">%d umělců</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d album</item>\n        <item quantity=\"few\">%d alba</item>\n        <item quantity=\"other\">%d alb</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d playlist</item>\n        <item quantity=\"few\">%d playlisty</item>\n        <item quantity=\"other\">%d playlistů</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d týden</item>\n        <item quantity=\"few\">%d týdny</item>\n        <item quantity=\"other\">%d týdnů</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d měsíc</item>\n        <item quantity=\"few\">%d měsíce</item>\n        <item quantity=\"other\">%d měsíců</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d rok</item>\n        <item quantity=\"few\">%d roky</item>\n        <item quantity=\"other\">%d let</item>\n    </plurals>\n    <string name=\"playlist_imported\">Playlist importován</string>\n    <string name=\"removed_song_from_playlist\">Skladba „%s“ odebrána z playlistu</string>\n    <string name=\"playlist_synced\">Playlist synchronizován</string>\n    <string name=\"undo\">Zrušit</string>\n    <string name=\"lyrics_not_found\">Texty nenalezeny</string>\n    <string name=\"sleep_timer\">Časovač spánku</string>\n    <string name=\"end_of_song\">Konec skladby</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">1 minuta</item>\n        <item quantity=\"few\">%d minuty</item>\n        <item quantity=\"other\">%d minut</item>\n    </plurals>\n    <string name=\"error_no_stream\">Není dostupný žádný stream</string>\n    <string name=\"error_no_internet\">Není dostupné připojení k internetu</string>\n    <string name=\"error_timeout\">Vypršel čas</string>\n    <string name=\"error_unknown\">Neznámá chyba</string>\n    <string name=\"action_like\">Oblíbené</string>\n    <string name=\"action_remove_like\">Odebrat z oblíbených</string>\n    <string name=\"action_shuffle_on\">Náhodně zapnuto</string>\n    <string name=\"action_shuffle_off\">Náhodně vypnuto</string>\n    <string name=\"repeat_mode_off\">Režim opakování vypnut</string>\n    <string name=\"repeat_mode_one\">Opakovat aktuální skladbu</string>\n    <string name=\"repeat_mode_all\">Opakovat frontu</string>\n    <string name=\"queue_all_songs\">Všechny skladby</string>\n    <string name=\"queue_searched_songs\">Hledané skladby</string>\n    <string name=\"music_player\">Hudební přehrávač</string>\n    <string name=\"settings\">Nastavení</string>\n    <string name=\"appearance\">Vzhled</string>\n    <string name=\"enable_dynamic_theme\">Povolit dynamický motiv</string>\n    <string name=\"dark_theme\">Tmavý motiv</string>\n    <string name=\"dark_theme_on\">Zap</string>\n    <string name=\"dark_theme_off\">Vyp</string>\n    <string name=\"dark_theme_follow_system\">Podle systému</string>\n    <string name=\"pure_black\">Čistě černá</string>\n    <string name=\"default_open_tab\">Výchozí karta</string>\n    <string name=\"customize_navigation_tabs\">Přizpůsobit karty navigace</string>\n    <string name=\"lyrics_text_position\">Pozice textů</string>\n    <string name=\"left\">Vlevo</string>\n    <string name=\"center\">Uprostřed</string>\n    <string name=\"right\">Vpravo</string>\n    <string name=\"content\">Obsah</string>\n    <string name=\"login\">Přihlásit se</string>\n    <string name=\"content_language\">Výchozí jazyk obsahu</string>\n    <string name=\"content_country\">Výchozí země obsahu</string>\n    <string name=\"system_default\">Podle systému</string>\n    <string name=\"enable_proxy\">Povolit proxy</string>\n    <string name=\"proxy_type\">Typ proxy</string>\n    <string name=\"proxy_url\">Adresa URL proxy</string>\n    <string name=\"restart_to_take_effect\">Restartujte pro uplatnění změn</string>\n    <string name=\"player_and_audio\">Přehrávač a zvuk</string>\n    <string name=\"audio_quality\">Kvalita zvuku</string>\n    <string name=\"audio_quality_auto\">Automatická</string>\n    <string name=\"audio_quality_high\">Vysoká</string>\n    <string name=\"audio_quality_low\">Nízká</string>\n    <string name=\"persistent_queue\">Ukládat frontu</string>\n    <string name=\"skip_silence\">Přeskakovat ticho</string>\n    <string name=\"audio_normalization\">Normalizace zvuku</string>\n    <string name=\"equalizer\">Ekvalizér</string>\n    <string name=\"storage\">Úložiště</string>\n    <string name=\"cache\">Mezipaměť</string>\n    <string name=\"image_cache\">Mezipaměť obrázků</string>\n    <string name=\"song_cache\">Mezipaměť skladeb</string>\n    <string name=\"max_cache_size\">Maximální velikost mezipaměti</string>\n    <string name=\"unlimited\">Neomezená</string>\n    <string name=\"clear_all_downloads\">Vymazat všechna stahování</string>\n    <string name=\"max_image_cache_size\">Maximální velikost mezipaměti obrázků</string>\n    <string name=\"clear_image_cache\">Vymazat mezipaměť obrázků</string>\n    <string name=\"max_song_cache_size\">Maximální velikost mezipaměti skladeb</string>\n    <string name=\"clear_song_cache\">Vymazat mezipaměť skladeb</string>\n    <string name=\"size_used\">Využito %s</string>\n    <string name=\"privacy\">Soukromí</string>\n    <string name=\"pause_listen_history\">Pozastavit historii poslechu</string>\n    <string name=\"clear_listen_history\">Vymazat historii poslechu</string>\n    <string name=\"clear_listen_history_confirm\">Opravdu chcete vymazat celou historii poslechu?</string>\n    <string name=\"pause_search_history\">Pozastavit historii vyhledávání</string>\n    <string name=\"clear_search_history\">Vymazat historii vyhledávání</string>\n    <string name=\"clear_search_history_confirm\">Opravdu chcete vymazat celou historii vyhledávání?</string>\n    <string name=\"enable_kugou\">Povolit poskytovatele textů KuGou</string>\n    <string name=\"backup_restore\">Záloha a obnovení</string>\n    <string name=\"action_backup\">Zálohovat</string>\n    <string name=\"action_restore\">Obnovit</string>\n    <string name=\"imported_playlist\">Playlist importován</string>\n    <string name=\"backup_create_success\">Záloha úspěšně vytvořena</string>\n    <string name=\"backup_create_failed\">Nepodařilo se vytvořit zálohu</string>\n    <string name=\"restore_failed\">Nepodařilo se obnovit zálohu</string>\n    <string name=\"about\">O aplikaci</string>\n    <string name=\"app_version\">Verze aplikace</string>\n    <string name=\"new_version_available\">Je dostupná nová verze</string>\n    <string name=\"translation_models\">Modely překladů</string>\n    <string name=\"clear_translation_models\">Vymazat modely překladů</string>\n    <string name=\"remove_from_playlist\">Odstranit z playlistu</string>\n    <string name=\"discord_integration\">Integrace Discordu</string>\n    <string name=\"squiggly\">Klikatá</string>\n    <string name=\"duplicates_description_single\">Skladba se již nachází ve vašem playlistu</string>\n    <string name=\"keep_listening\">Poslouchejte dál</string>\n    <string name=\"duplicates\">Duplikáty</string>\n    <string name=\"sided\">Na straně</string>\n    <string name=\"big\">Velké</string>\n    <string name=\"disable_screenshot_desc\">Po zapnutí budou zakázány snímky obrazovky a náhled aplikace v Nedávných.</string>\n    <string name=\"library_song_empty\">Zde se zobrazí skladby v knihovně</string>\n    <string name=\"misc\">Různé</string>\n    <string name=\"player_slider_style\">Styl lišty přehrávače</string>\n    <string name=\"grid_cell_size\">Velikost položek mřížky</string>\n    <string name=\"remove_from_queue\">Odstranit z fronty</string>\n    <string name=\"similar_to\">Podobné</string>\n    <string name=\"small\">Malé</string>\n    <string name=\"search_history\">Historie vyhledávání</string>\n    <string name=\"dismiss\">Zavřít</string>\n    <string name=\"tempo_and_pitch\">Tempo a výška</string>\n    <string name=\"player\">Přehrávač</string>\n    <string name=\"auto_load_more\">Automaticky načíst další skladby</string>\n    <string name=\"other_versions\">Další verze</string>\n    <string name=\"queue\">Fronta</string>\n    <string name=\"forgotten_favorites\">Zapomenuté oblíbené</string>\n    <string name=\"your_youtube_playlists\">Vaše YouTube playlisty</string>\n    <string name=\"library_artist_empty\">Zde se zobrazí umělci v knihovně</string>\n    <string name=\"library_album_empty\">Zde se zobrazí alba v knihovně</string>\n    <string name=\"library_playlist_empty\">Zde se zobrazí vaše playlisty</string>\n    <string name=\"remove_download_playlist_confirm\">Opravdu chcete odstranit všech „%s“ skladeb v playlistu z úložiště Stažené skladby?</string>\n    <string name=\"add_all_to_library\">Přidat vše do knihovny</string>\n    <string name=\"remove_all_from_library\">Odstranit vše z knihovny</string>\n    <string name=\"skip_duplicates\">Přeskočit duplikáty</string>\n    <string name=\"add_anyway\">Přesto přidat</string>\n    <string name=\"duplicates_description_multiple\">%d skladeb se již nachází ve vašem playlistu</string>\n    <string name=\"action_like_all\">Oblíbit vše</string>\n    <string name=\"action_remove_like_all\">Odstranit všechny oblíbené</string>\n    <string name=\"not_logged_in\">Nejste přihlášeni</string>\n    <string name=\"persistent_queue_desc\">Obnovit poslední frontu po spuštění aplikace</string>\n    <string name=\"auto_load_more_desc\">Automaticky přidat více skladeb po dosažení konce fronty, pokud je to možné</string>\n    <string name=\"auto_skip_next_on_error\">Automaticky přejít na další sklabdu při výskytu chyby</string>\n    <string name=\"auto_skip_next_on_error_desc\">Zajistěte si nepřetržité přehrávání</string>\n    <string name=\"stop_music_on_task_clear\">Zastavit hudbu po vymazání úlohy</string>\n    <string name=\"listen_history\">Historie poslechu</string>\n    <string name=\"disable_screenshot\">Zakázat snímky obrazovky</string>\n    <string name=\"enable_lrclib\">Povolit poskytovatele textů LrcLib</string>\n    <string name=\"hide_explicit\">Skrýt explicitní obsah</string>\n    <string name=\"options\">Možnosti</string>\n    <string name=\"preview\">Náhled</string>\n    <string name=\"login_failed\">Přihlášení selhalo</string>\n    <string name=\"delete_playlist_confirm\">Opravdu chcete odstranit playlist „%s“?</string>\n    <string name=\"default_\">Výchozí</string>\n    <string name=\"theme\">Motiv</string>\n    <string name=\"player_text_alignment\">Zarovnání textu přehrávače</string>\n    <string name=\"discord_information\">Metrolist používá knihovnu KizzyRPC, aby mohl nastavit váš stav na Discordu. Tato funkce zahrnuje připojení k bráně Discord, což může být považováno za porušení podmínek společnosti Discord. Neexistují nicméně žádné známé případy uzamčení účtu z tohoto důvodu. Používejte na vlastní nebezpečí. \\n \\nMetrolist pouze extrahuje váš token, vše ostatní je uloženo lokálně.</string>\n    <string name=\"enable_discord_rpc\">Povolit stav na Discordu</string>\n    <string name=\"action_logout\">Odhlásit se</string>\n    <string name=\"use_login_for_browse\">Použít účet pro procházení obsahu</string>\n    <string name=\"use_login_for_browse_desc\">Může ovlivnit obsah, který se vám zobrazí – při přihlášení například mohou být zobrazena například alba, která jsou dostupná pouze s Premium účtem</string>\n    <string name=\"action_login\">Přihlásit se</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-de/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"liked\">Mag ich</string>\n    <string name=\"offline\">Heruntergeladen</string>\n    <string name=\"my_top\">Meine Top</string>\n    <string name=\"select\">Alles auswählen</string>\n    <string name=\"like_all\">Alle \\\"Liken\\\"</string>\n    <string name=\"sort_by_last_updated\">Aktualisierungsdatum</string>\n    <string name=\"lyrics\">Songtext</string>\n    <string name=\"already_in_playlist\">Bereits in Playlist:</string>\n    <string name=\"player_background_style\">Hintergrundstil des Players</string>\n    <string name=\"follow_theme\">Design folgen</string>\n    <string name=\"gradient\">Farbverlauf</string>\n    <string name=\"player_background_blur\">Unschärfe</string>\n    <string name=\"enable_swipe_thumbnail\">Wischen zum Songwechsel aktivieren</string>\n    <string name=\"lyrics_click_change\">Mit Tippen im Songtext springen</string>\n    <string name=\"slim\">Schmal</string>\n    <string name=\"slim_navbar\">Dünne untere Navigationsleiste</string>\n    <string name=\"advanced_login\">Mit Token anmelden</string>\n    <string name=\"token_hidden\">Tippen, um Token anzuzeigen</string>\n    <string name=\"token_shown\">Tippen, um zu kopieren oder zu bearbeiten</string>\n    <string name=\"token_adv_login_description\">Dies ist eine FORTGESCHRITTENE Anmeldemethode. Als Alternative zum Webportal können Sie hier Ihr Anmeldetoken direkt eingeben oder aktualisieren. Dies kann beispielsweise die Anmeldung auf mehreren Geräten beschleunigen. Bitte beachten Sie, dass ungültige Token-Formate, die die App nicht verarbeiten kann, nicht akzeptiert werden</string>\n    <string name=\"general\">Allgemein</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"default_lib_chips\">Standard-Mediatheksauswahl ändern</string>\n    <string name=\"set_quick_picks\">Schnellauswahl festlegen</string>\n    <string name=\"last_song_listened\">Basierend auf dem zuletzt gehörten Song</string>\n    <string name=\"app_language\">App-Sprache</string>\n    <string name=\"enable_similar_content\">Ähnliche Inhalte aktivieren</string>\n    <string name=\"similar_content_desc\">Automatisch weitere ähnliche Songs hinzufügen, wenn das Ende der Warteschlange erreicht ist</string>\n    <string name=\"default_links\">Unterstützte Links öffnen</string>\n    <string name=\"open_app_settings_error\">App-Einstellungen konnten nicht geöffnet werden</string>\n    <string name=\"release_notes\">Versionshinweise</string>\n    <string name=\"all_time\">Gesamte Zeit</string>\n    <string name=\"past_24_hours\">Letzte 24 Stunden</string>\n    <string name=\"past_week\">Letzte Woche</string>\n    <string name=\"past_month\">Letzter Monat</string>\n    <string name=\"past_year\">Letztes Jahr</string>\n    <string name=\"top_length\">Meine Top-Liste Länge</string>\n    <string name=\"history_duration\">Verlauflänge</string>\n    <string name=\"information\">Informationen</string>\n    <string name=\"description\">Beschreibung</string>\n    <string name=\"views\">Aufrufe</string>\n    <string name=\"likes\">Likes</string>\n    <string name=\"dislikes\">Dislikes</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">1 Sekunde</item>\n        <item quantity=\"other\">%d Sekunden</item>\n    </plurals>\n    <string name=\"not_logged_in_youtube\">Nicht bei YouTube angemeldet</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"local_history\">Lokal</string>\n    <string name=\"dislike_all\">Alle \\\"Disliken\\\"</string>\n    <string name=\"similar_content\">Ähnlicher Inhalt</string>\n    <string name=\"clear_song_cache_dialog\">Bist du sicher dass du den Song-Cache leeren willst?</string>\n    <string name=\"copy_link\">Link kopieren</string>\n    <string name=\"allows_for_sync_witch_youtube\">Hinweis: Dies syncronisiert die Playlist mit YouTube Music. Das kann danach NICHT mehr geändert werden.</string>\n    <string name=\"default_style\">Standard</string>\n    <string name=\"clear_downloads_dialog\">Bist du sicher dass du alle Downloads löschen willst?</string>\n    <string name=\"months\">Monate</string>\n    <string name=\"link_copied\">Link in die Zwischenablage kopiert</string>\n    <string name=\"charts\">Charts</string>\n    <string name=\"years\">Jahre</string>\n    <string name=\"weeks\">Wochen</string>\n    <string name=\"remote_history\">Online</string>\n    <string name=\"sync_playlist\">Playlist synchronisieren</string>\n    <string name=\"back_button_desc\">Zurück</string>\n    <string name=\"continuous\">Durchgängig</string>\n    <string name=\"player_buttons_style\">Player-Tastenfarbe</string>\n    <string name=\"album_cover_desc\">Albumcover</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d Mal</item>\n        <item quantity=\"other\">%d Male</item>\n    </plurals>\n    <string name=\"cached_playlist\">Im Cache</string>\n    <string name=\"remove_from_cache\">Aus Cache entfernen</string>\n    <string name=\"top_music_videos\">Top Musikvideos</string>\n    <string name=\"trending\">Im Trend</string>\n    <string name=\"swipe_song_to_add\">Wische den Song nach links, um ihn zur Warteschlange hinzuzufügen, oder nach rechts, um ihn als Nächstes abzuspielen</string>\n    <string name=\"auto_playlists\">Automatische Playlists</string>\n    <string name=\"share_selected\">Auswahl teilen</string>\n    <string name=\"customize_colors\">Farben personalisieren</string>\n    <string name=\"text_color\">Textfarbe</string>\n    <string name=\"secondary_text_color\">Sekundäre Textfarbe</string>\n    <string name=\"background_color\">Hintergrundfarbe</string>\n    <string name=\"sync_disabled\">Synchronisierung deaktiviert</string>\n    <string name=\"please_wait\">Bitte warten</string>\n    <string name=\"cancel\">Abbrechen</string>\n    <string name=\"share_lyrics\">Songtext teilen</string>\n    <string name=\"share_as_text\">Als Text teilen</string>\n    <string name=\"share_as_image\">Als Bild teilen</string>\n    <string name=\"generating_image\">Generiere Bild</string>\n    <string name=\"max_selection_limit\">Höchstgrenze der Auswahl</string>\n    <string name=\"show_liked_playlist\">\\\"Favoriten\\\"-Playlist anzeigen</string>\n    <string name=\"show_downloaded_playlist\">„Heruntergeladen“-Playlist anzeigen</string>\n    <string name=\"show_top_playlist\">„Top“-Playlist anzeigen</string>\n    <string name=\"show_cached_playlist\">„Im Cache\\\"-Playlist anzeigen</string>\n    <string name=\"auto_download_on_like\">Automatisches herunterladen bei „Like“</string>\n    <string name=\"lyrics_auto_scroll\">Automatisches Scrollen des Songtextes</string>\n    <string name=\"import_online\">Importieren von „m3u“-Playlists</string>\n    <string name=\"playlist_add_local_to_synced_note\">Hinweis: Lokale Songs können nicht zu synchronisierten/Online-Playlists hinzugefügt werden. Alle anderen Kombinationen sind möglich</string>\n    <string name=\"import_csv\">Importieren von csv-Playlists</string>\n    <string name=\"auto_download_on_like_desc\">Automatisches herunterladen von Songs, wenn du sie \\\"Likst\\\"</string>\n    <string name=\"new_player_design\">Neues Design für den Player</string>\n    <string name=\"lyrics_romanize_japanese\">Japanische Liedtexte romanisieren</string>\n    <string name=\"lyrics_romanize_korean\">Koreanische Liedtexte romanisieren</string>\n    <string name=\"yt_sync\">Auto-Sync mit Konto</string>\n    <string name=\"more_content\">Mehr Inhalt</string>\n    <string name=\"swipe_sensitivity\">Mini-Player-Wischsensitivität</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"clear_image_cache_dialog\">Bist du sicher, dass du den Bild-Cache leeren willst?</string>\n    <string name=\"disable\">Deaktivieren</string>\n    <string name=\"subscribe\">Abonnieren</string>\n    <string name=\"subscribed\">Abonniert</string>\n    <string name=\"now_playing\">Wird gerade abgespielt</string>\n    <string name=\"new_mini_player_design\">Neues Design für den Mini-Player</string>\n    <string name=\"seek_forward_dynamic\">+%1$d Sekunden vorspulen</string>\n    <string name=\"seek_backward_dynamic\">-%1$d Sekunden zurückspulen</string>\n    <string name=\"seek_seconds_addup_description\">Wenn aktiviert, werden bei jedem Spulen schrittweise 5 zusätzliche Sekunden hinzugefügt</string>\n    <string name=\"seek_seconds_addup\">Progressives Spulen</string>\n    <string name=\"close\">Schließen</string>\n    <string name=\"disable_load_more_when_repeat_all\">Mehr Laden deaktivieren, wenn alles wiederholen aktiv ist</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Nicht automatisch mehr Songs und ähnliche Inhalte laden, wenn der Modus „Alles wiederholen“ aktiviert ist</string>\n    <string name=\"hide_player_thumbnail\">Albumcover im Player verbergen</string>\n    <string name=\"hide_player_thumbnail_desc\">Ersetzt das Albumcover mit dem App-Logo im Player</string>\n    <string name=\"settings_section_ui\">Benutzeroberfläche</string>\n    <string name=\"settings_section_privacy\">Privatsphäre &amp; Sicherheit</string>\n    <string name=\"settings_section_player_content\">Player &amp; Inhalt</string>\n    <string name=\"settings_section_storage\">Speicher &amp; Daten</string>\n    <string name=\"settings_section_system\">System &amp; Über</string>\n    <string name=\"starting_radio\">Radio starten</string>\n    <string name=\"edit_playlist_cover\">Playlist-Cover bearbeiten</string>\n    <string name=\"edit_playlist_cover_note\">Hinweis: Ihr Konto muss mit einer Telefonnummer verknüpft und bei YouTube Music verifiziert sein, um das Cover der Wiedergabeliste ändern zu können.</string>\n    <string name=\"edit_playlist_cover_note_wait\">Nachdem Sie ein Bild ausgewählt haben, warten Sie bitte einen Moment, bis das neue Cover in Ihrer Wiedergabeliste angezeigt wird.</string>\n    <string name=\"choose_from_library\">Von Bibliothek auswählen</string>\n    <string name=\"remove_custom_image\">Benutzerdefiniertes Bild entfernen</string>\n    <string name=\"config_proxy\">Proxy konfigurieren</string>\n    <string name=\"proxy_username\">Proxy-Benutzername</string>\n    <string name=\"proxy_password\">Proxy-Passwort</string>\n    <string name=\"enable_authentication\">Authentifizierung aktivieren</string>\n    <string name=\"lyrics_romanization_cyrillic\">Kyrillisch</string>\n    <string name=\"lyrics_romanize_title\">Romanisierung</string>\n    <string name=\"lyrics_romanization\">Romanisierung von Liedtexten</string>\n    <string name=\"lyrics_romanize_russian\">Russische Liedtexte romanisieren</string>\n    <string name=\"lyrics_romanize_ukrainian\">Ukrainische Liedtexte romanisieren</string>\n    <string name=\"lyrics_romanize_belarusian\">Belarussische Liedtexte romanisieren</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Kirgisische Liedtexte romanisieren</string>\n    <string name=\"lyrics_romanize_serbian\">Serbische Liedtexte romanisieren</string>\n    <string name=\"lyrics_romanize_bulgarian\">Bulgarische Liedtexte romanisieren</string>\n    <string name=\"line_by_line_option_title\">EXPERIMENTELL: Sprache Zeile für Zeile erkennen</string>\n    <string name=\"line_by_line_option_desc\">Die kyrillische Sprache wird Zeile für Zeile statt für das ganze Lied erkannt.</string>\n    <string name=\"line_by_line_dialog_title\">Sind Sie sicher?</string>\n    <string name=\"line_by_line_dialog_desc\">Dies ist eine experimentelle Funktion, deren Erfolg nicht garantiert ist.\\n\\nStandardmäßig wird die Sprache anhand des gesamten Liedes bestimmt, aber wenn diese Option aktiviert ist, wird sie stattdessen Zeile für Zeile bestimmt. Dadurch können mehrsprachige Lieder funktionieren, ABER die Sprache ist möglicherweise nicht immer korrekt (wenn beispielsweise ein ukrainischer Text keine ukrainischen Buchstaben enthält, wird er möglicherweise stattdessen als russisch romanisiert). \\n\\nWenn Sie keine Probleme haben, wird empfohlen, diese Option deaktiviert zu lassen.</string>\n    <string name=\"romanize_current_track\">Aktuellen Titel romanisieren</string>\n    <string name=\"audio_offload\">Auslagerung aktivieren</string>\n    <string name=\"audio_offload_description\">Verwende den ausgelagerten Audiopfad für die Audiowiedergabe. Das Deaktivieren dieser Option kann den Stromverbrauch erhöhen, kann jedoch nützlich sein, wenn Probleme mit der Audiowiedergabe oder der Nachbearbeitung auftreten</string>\n    <string name=\"uploaded_playlist\">Hochgeladen</string>\n    <string name=\"filter_uploaded\">Hochgeladen</string>\n    <string name=\"show_uploaded_playlist\">\\\"Hochgeladen\\\"-Playlist anzeigen</string>\n    <string name=\"discord_use_details\">Verwende Details anstelle von Statusangaben</string>\n    <string name=\"discord_use_details_description\">Songtitel anstelle von Künstlernamen prominent anzeigen</string>\n    <string name=\"updater\">Updater</string>\n    <string name=\"check_for_updates\">Automatisch nach Updates prüfen</string>\n    <string name=\"update_notifications\">Benachrichtigungen zu Updates erhalten</string>\n    <string name=\"update_available_title\">Update verfügbar</string>\n    <string name=\"update_channel_name\">App-Updates</string>\n    <string name=\"update_channel_desc\">Benachrichtigungen über neue Versionen</string>\n    <string name=\"lyrics_romanize_macedonian\">Mazedonische Liedtexte romanisieren</string>\n    <string name=\"integrations\">Integrationen</string>\n    <string name=\"username\">Benutzername</string>\n    <string name=\"password\">Passwort</string>\n    <string name=\"lastfm_integration\">Last.fm-Integration</string>\n    <string name=\"enable_scrobbling\">Scrobbeln aktivieren</string>\n    <string name=\"lastfm_now_playing\">Aktuelle Wiedergabe senden</string>\n    <string name=\"scrobbling_configuration\">Scrobbling Konfiguration</string>\n    <string name=\"scrobble_min_track_duration\">Lieder scrobbeln, die länger sind als</string>\n    <string name=\"scrobble_delay_percent\">Scrobble Verzögerung (%)</string>\n    <string name=\"scrobble_delay_minutes\">Scrobble Verzögerung (min)</string>\n    <string name=\"swipe_song_to_remove\">Wische den Song zur Seite, um ihn aus der Playlist zu entfernen</string>\n    <string name=\"download_playlist_desc\">Alle Songs für die Offline-Wiedergabe herunterladen</string>\n    <string name=\"remove_download_playlist_desc\">Alle heruntergeladenen Songs aus dieser Wiedergabeliste entfernen</string>\n    <string name=\"download_in_progress_desc\">Der Download läuft</string>\n    <string name=\"share_playlist_desc\">Diese Wiedergabeliste mit anderen teilen</string>\n    <string name=\"delete_playlist_desc\">Diese Wiedergabeliste dauerhaft entfernen</string>\n    <string name=\"sync_playlist_desc\">Playlist mit YouTube Music synchronisieren</string>\n    <string name=\"primary_color_style\">Primärfarbe</string>\n    <string name=\"tertiary_color_style\">Tertiärfarbe</string>\n    <string name=\"enable_better_lyrics\">Better Lyrics-Songtext-Anbieter aktivieren</string>\n    <string name=\"enable_better_lyrics_desc\">Silbensynchronisierte Songtexte für jeden Song, für Karaoke</string>\n    <string name=\"auto_scroll\">Neu synchronisieren</string>\n    <string name=\"shuffle_playlist_first\">Wiedergabeliste/Album zuerst mischen</string>\n    <string name=\"shuffle_playlist_first_desc\">Gib beim Mischen zuerst alle Songs der ursprünglichen Wiedergabeliste/des ursprünglichen Albums und dann ähnliche Inhalte wieder</string>\n    <string name=\"show_wrapped_card\">Wrapped-Tab anzeigen</string>\n    <string name=\"lyrics_romanize_chinese\">Chinesische Liedtexte romanisieren</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"google_cast_description\">Audioübertragung auf Chromecast und andere Cast-fähige Geräte aktivieren</string>\n    <string name=\"last_fm_send_likes\">Likes/Dislikes senden</string>\n    <string name=\"last_fm_send_likes_description\">Lieder bei Last.fm als „Gefällt mir“/„Gefällt mir nicht“ markieren, wenn sie in Metrolist als „Gefällt mir“/„Gefällt mir nicht“ markiert werden</string>\n    <string name=\"logging_in\">Anmelden…</string>\n    <string name=\"hide_video_songs\">Videosongs ausblenden</string>\n    <string name=\"details_desc\">Informationen zum Song anzeigen</string>\n    <string name=\"edit_desc\">Ändern Sie den Titel oder Interpreten</string>\n    <string name=\"start_radio_desc\">Ein Radio basierend auf diesem Song erstellen</string>\n    <string name=\"play_next_desc\">Als nächstes abspielen</string>\n    <string name=\"add_to_queue_desc\">Zum Ende Ihrer Warteschlange hinzufügen</string>\n    <string name=\"add_to_library_desc\">In Ihrer Bibliothek speichern</string>\n    <string name=\"download_desc\">Für die Offline-Wiedergabe verfügbar machen</string>\n    <string name=\"add_to_playlist_desc\">Zu einer deiner Playlisten hinzufügen</string>\n    <string name=\"refetch_desc\">Die aktuellen Metadaten von YouTube Music abrufen</string>\n    <string name=\"share_desc\">Link zum teilen</string>\n    <string name=\"delete_desc\">Diese Playlist dauerhaft entfernen</string>\n    <string name=\"advanced_desc\">Tempo und Tonhöhe des Lieds ändern</string>\n    <string name=\"equalizer_desc\">Den Audio-Equalizer einstellen</string>\n    <string name=\"enable_dynamic_icon\">Dynamische Symbole aktivieren</string>\n    <string name=\"mini_player\">Mini-Player</string>\n    <string name=\"pure_black_mini_player\">Rein schwarzer Mini-Player</string>\n    <string name=\"cache_size_warning_title\">Warte mal!</string>\n    <string name=\"cache_size_warning_message\">Sie haben eine Cache-Größenbeschränkung gewählt, die kleiner ist als die derzeit von der App verwendete (%1$s). Wenn Sie fortfahren, entfernt die App möglicherweise einige zwischengespeicherte %2$s, um die neue Beschränkung einzuhalten. Möchten Sie trotzdem fortfahren?</string>\n    <string name=\"cache_size_warning_confirm\">Weiter</string>\n    <string name=\"lyrics_animation_style\">Wort-für-Wort-Animationsstil</string>\n    <string name=\"none\">Keine</string>\n    <string name=\"fade\">Ausblenden</string>\n    <string name=\"glow\">Leuchten</string>\n    <string name=\"slide\">Gleiten</string>\n    <string name=\"karaoke\">Karaoke</string>\n    <string name=\"apple_music_style\">Apple Music</string>\n    <string name=\"lyrics_text_size\">Textgröße der Liedtexte</string>\n    <string name=\"lyrics_line_spacing\">Zeilenabstand im Liedtext</string>\n    <string name=\"album_art_for\">Albumcover für %s</string>\n    <string name=\"lyrics_glow_effect\">Leuchtenden Text-Effekt aktivieren</string>\n    <string name=\"lyrics_glow_effect_desc\">Leuchtanimation und Bounce-Effekt zu aktiven Textzeilen hinzufügen</string>\n    <string name=\"wrapped_top_album_title\">Dein Top-Album ist</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d Minuten</string>\n    <string name=\"wrapped_no_data\">Keine Daten</string>\n    <string name=\"wrapped_top_5_artists_title\">Deine Top-Künstler des Jahres</string>\n    <string name=\"wrapped_artist_listening_time\">%d Minuten</string>\n    <string name=\"wrapped_top_5_songs_title\">Deine Top-Songs des Jahres</string>\n    <string name=\"wrapped_total_albums_title\">Du hast dir angehört</string>\n    <string name=\"wrapped_total_albums_subtitle\">einzigartige Alben</string>\n    <string name=\"wrapped_playlist_ready\">Deine persönliche Playlist ist fertig</string>\n    <string name=\"wrapped_top_5_albums_title\">Deine Top 5 Alben</string>\n    <string name=\"wrapped_album_listening_time\">Du hast dieses Album %d Minuten lang angehört</string>\n    <string name=\"wrapped_top_artist_title\">Dein Top-Künstler des Jahres ist</string>\n    <string name=\"wrapped_top_artist_listening_time\">Du hast dir diesen Künstler %d Minuten lang angehört</string>\n    <string name=\"wrapped_top_song_title\">Dein meistgespielter Song ist</string>\n    <string name=\"wrapped_top_song_listening_time\">Du hast dir diesen Song %d Minuten lang angehört</string>\n    <string name=\"wrapped_total_artists_subtitle\">einzigartige Künstler</string>\n    <string name=\"wrapped_total_artists_title\">Du hast dir angehört</string>\n    <string name=\"wrapped_total_songs_title\">Du hast dir angehört</string>\n    <string name=\"wrapped_total_songs_subtitle\">einzigartige Lieder</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_intro_subtitle\">Es ist an der Zeit, herauszufinden, was du dir angehört hast</string>\n    <string name=\"wrapped_intro_button\">Auf geht\\'s!</string>\n    <string name=\"wrapped_ready_title\">Dein Wrapped ist bereit!</string>\n    <string name=\"wrapped_ready_subtitle\">Zeit, einen Blick auf die Highlights des Jahres zu werfen.</string>\n    <string name=\"wrapped_thank_you\">Danke fürs Zuhören</string>\n    <string name=\"wrapped_special_thanks\">Besonderer Dank gilt MO Agamy für die Erstellung von Metrolist</string>\n    <string name=\"wrapped_close\">Wrapped schließen</string>\n    <string name=\"wrapped_playlist_title\">Dein %s Wrapped</string>\n    <string name=\"wrapped_create_playlist\">Playlist erstellen</string>\n    <string name=\"wrapped_playlist_saved\">Playlist gespeichert</string>\n    <string name=\"casting_to\">Übertragung auf %s</string>\n    <string name=\"progress_percent\">Fortschritt %s%%</string>\n    <string name=\"listening_to_metrolist\">Metrolist hören</string>\n    <string name=\"open\">Öffnen</string>\n    <string name=\"failed_to_create_image\">Bild konnte nicht erstellt werden: %s</string>\n    <string name=\"copied_title\">Kopierter Titel</string>\n    <string name=\"copied_artist\">Kopierter Künstler</string>\n    <string name=\"error_playing\">Fehler beim Abspielen</string>\n    <string name=\"failed_to_parse_proxy\">Die Proxy-URL konnte nicht analysiert werden.</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">Albumcover</string>\n    <string name=\"wrapped_logo_content_description\">Metrolist Logo</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"wrapped_top_artist_image_content_description\">Bild des Top-Künstlers</string>\n    <string name=\"wavy\">Wellig</string>\n    <string name=\"pause_music_when_media_is_muted\">Musik pausieren, wenn Medien stumm geschaltet sind</string>\n    <string name=\"equalizer_header\">Equalizer</string>\n    <string name=\"no_profiles\">Keine Equalizer-Profile</string>\n    <string name=\"import_profile\">Profil importieren</string>\n    <string name=\"eq_disabled\">Deaktiviert</string>\n    <string name=\"delete_profile_desc\">Profil löschen</string>\n    <string name=\"delete_profile_confirmation\">Möchten Sie %1$s wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.</string>\n    <string name=\"error_file_read\">Datei konnte nicht gelesen werden</string>\n    <string name=\"error_file_open\">Datei konnte nicht geöffnet werden: %1$s</string>\n    <string name=\"import_error_title\">Importfehler</string>\n    <string name=\"enable_simpmusic\">SimpMusic-Songtext-Anbieter aktivieren</string>\n    <string name=\"enable_simpmusic_desc\">Automatisch von Musixmatch und YouTube Transcript bezogene Songtexte</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"one\">%d Profil</item>\n        <item quantity=\"other\">%d Profile</item>\n    </plurals>\n    <string name=\"system_equalizer\">System-Equalizer</string>\n    <string name=\"no_song_playing\">Es wird kein Song abgespielt</string>\n    <string name=\"tap_to_open\">Tippen, um Metrolist zu öffnen</string>\n    <string name=\"previous\">Vorherige</string>\n    <string name=\"play_pause\">Wiedergabe/Pause</string>\n    <string name=\"next\">Weiter</string>\n    <string name=\"widget_description\">Musikplayer-Widget mit Wiedergabesteuerung</string>\n    <plurals name=\"band_count\">\n        <item quantity=\"one\">%d Band</item>\n        <item quantity=\"other\">%d Bänder</item>\n    </plurals>\n    <string name=\"album_art\">Albumcover</string>\n    <string name=\"turntable_widget_description\">Kreisförmiges Musik-Widget mit Wiedergabe- und Like-Steuerung</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Zufallswiedergabe- und Wiederholungsmodus beim Neustart der App beibehalten</string>\n    <string name=\"remember_shuffle_and_repeat\">Behalte den Shuffle- und Wiederholungsmodus</string>\n    <string name=\"about_artist\">Über</string>\n    <string name=\"show_more\">Mehr anzeigen</string>\n    <string name=\"show_less\">Weniger anzeigen</string>\n    <string name=\"artist_page_settings\">Künstlerseite</string>\n    <string name=\"show_artist_description\">Künstlerbeschreibung anzeigen</string>\n    <string name=\"show_artist_subscriber_count\">Abonnentenzahl anzeigen</string>\n    <string name=\"show_artist_monthly_listeners\">Monatliche Hörer anzeigen</string>\n    <string name=\"skip_silence_instant\">Stille sofort überspringen</string>\n    <string name=\"skip_silence_instant_desc\">Springe in stillen Momenten vorwärts, anstatt die Wiedergabe zu beschleunigen</string>\n    <string name=\"skip_silence_desc\">Stille Teile von Songs vorspulen</string>\n    <string name=\"crop_album_art\">Albumcover zuschneiden</string>\n    <string name=\"crop_album_art_desc\">Quadratisches Seitenverhältnis durch Zuschneiden der Video-Thumnails erzwingen</string>\n    <string name=\"persistent_shuffle_desc\">Shuffle beim Starten neuer Songs oder Wiedergabelisten aktiviert lassen</string>\n    <string name=\"error_title\">Fehler</string>\n    <string name=\"error_eq_apply_failed\">EQ-Profil konnte nicht angewendet werden: %1$s</string>\n    <string name=\"error_playback_failed\">Wiedergabe fehlgeschlagen</string>\n    <string name=\"persistent_shuffle_title\">Permanentes shuffle</string>\n    <string name=\"lyrics_offset\">Songtext versatz</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">Display eingeschaltet lassen, wenn der Player erweitert ist</string>\n    <string name=\"listen_together\">Gemeinsam hören</string>\n    <string name=\"listen_together_server_url\">Server URL</string>\n    <string name=\"listen_together_choose_server\">Server auswählen</string>\n    <string name=\"listen_together_custom_server\">Benutzerdefinierter Server</string>\n    <string name=\"listen_together_use_custom_server\">Benutzerdefinierten Server verwenden</string>\n    <string name=\"listen_together_username\">Benutzername</string>\n    <string name=\"listen_together_connected\">Verbunden</string>\n    <string name=\"listen_together_reconnecting\">Wieder verbinden…</string>\n    <string name=\"listen_together_disconnected\">Getrennt</string>\n    <string name=\"listen_together_connecting\">Verbinden…</string>\n    <string name=\"listen_together_error\">Verbindungsfehler</string>\n    <string name=\"listen_together_create_room\">Raum erstellen</string>\n    <string name=\"listen_together_create_room_desc\">Erstelle einen Raum und teile den Code mit Freunden</string>\n    <string name=\"listen_together_join_room\">Raum beitreten</string>\n    <string name=\"listen_together_room_code\">Raumcode</string>\n    <string name=\"listen_together_you_are_host\">Du bist der Gastgeber</string>\n    <string name=\"listen_together_you_are_guest\">Du bist ein Gast</string>\n    <string name=\"mute\">Stummschalten</string>\n    <string name=\"unmute\">Stummschaltung aufheben</string>\n    <string name=\"listen_together_join_requests\">Beitrittsanfragen</string>\n    <string name=\"listen_together_view_logs\">Protokolle anzeigen</string>\n    <string name=\"listen_together_view_logs_desc\">Verbindungs- und Meldungsdebugs</string>\n    <string name=\"listen_together_logs\">Verbindungsprotokolle</string>\n    <string name=\"cd_system_mode\">Systemmodus</string>\n    <string name=\"cd_dark_mode\">Dunkler Modus</string>\n    <string name=\"cd_light_mode\">Hellmodus</string>\n    <string name=\"like\">Mag ich</string>\n    <string name=\"listen_together_no_logs\">Noch keine Protokolle</string>\n    <string name=\"listen_together_auto_approval_joins\">Automatische Genehmigung von Beitrittsanfragen</string>\n    <string name=\"listen_together_auto_approval_joins_desc\">Beitrittsanfragen automatisch genehmigen, anstatt sie manuell zu prüfen</string>\n    <string name=\"listen_together_sync_volume\">Lautstärke des Hosts synchronisieren</string>\n    <string name=\"listen_together_sync_volume_desc\">Gäste passen sich der Lautstärke des Hosts an</string>\n    <string name=\"listen_together_description\">Hören gemeinsam mit deinen Freunden Musik in Echtzeit. Erstelle einen Raum, um Gastgeber zu sein, oder trete mit einem Code einem bestehenden Raum bei.</string>\n    <string name=\"listen_together_background_disconnect_note\">Hinweis: Die Verbindung kann unterbrochen werden, wenn du einen Raum erstellst, während keine Musik abgespielt wird, und dann zu einer anderen App wechselst.</string>\n    <string name=\"listen_together_not_configured\">Listen Together ist nicht konfiguriert. Bitte richte die Server-URL unter „Einstellungen“ → „Integrationen“ → „Listen Together“ ein.</string>\n    <string name=\"listen_together_suggestion_received\">%1$s hat %2$s vorgeschlagen</string>\n    <string name=\"listen_together_suggestion_sent\">Vorschlag an den Host gesendet!</string>\n    <string name=\"listen_together_join_request_notification\">%1$s will den Raum beitreten</string>\n    <string name=\"listen_together_notification_channel_name\">Gemeinsam zuhören</string>\n    <string name=\"listen_together_notification_channel_desc\">Benachrichtigungen für Gemeinsam zuhören Ereignisse</string>\n    <string name=\"listen_together_room_created\">Raum erstellt: %s</string>\n    <string name=\"listen_together_cannot_edit_username_in_room\">Benutzername kann nicht bearbeitet werden, während man sich in einem Raum befindet</string>\n    <string name=\"waiting_for_approval\">Warten auf Genehmigung durch den Host</string>\n    <string name=\"invalid_room_code\">Ungültiger Raumcode</string>\n    <string name=\"join_request_denied\">Beitrittsanfrage abgelehnt</string>\n    <string name=\"join_existing_room\">Bestehendem Raum beitreten</string>\n    <string name=\"room_code\">Raumcode</string>\n    <string name=\"leave_room\">Raum verlassen</string>\n    <string name=\"join_room\">Beitreten</string>\n    <string name=\"create_room\">Erstellen</string>\n    <string name=\"joining_room\">Raum %s wird beigetreten…</string>\n    <string name=\"creating_room\">Raum wird erstellt…</string>\n    <string name=\"connect\">Verbinden</string>\n    <string name=\"disconnect\">Trennen</string>\n    <string name=\"create\">Erstellen</string>\n    <string name=\"join\">Beitreten</string>\n    <string name=\"approve\">Genehmigen</string>\n    <string name=\"reject\">Ablehnen</string>\n    <string name=\"clear\">Löschen</string>\n    <string name=\"copy\">Kopieren</string>\n    <string name=\"copied_to_clipboard\">In die Zwischenablage kopiert</string>\n    <string name=\"not_set\">Nicht festgelegt</string>\n    <string name=\"hosting_room\">Hosting-Raum</string>\n    <string name=\"in_room\">In Raum</string>\n    <string name=\"pending_requests\">Ausstehende Anfragen</string>\n    <string name=\"pending_suggestions\">Ausstehende Vorschläge</string>\n    <string name=\"suggest_to_host\">Dem Host vorschlagen</string>\n    <string name=\"kick_user\">Kicken</string>\n    <string name=\"host_label\">Host</string>\n    <string name=\"you_label\">Du</string>\n    <string name=\"connected_users\">Verbundene Benutzer</string>\n    <string name=\"enter_username\">Benutzername eingeben</string>\n    <string name=\"error_username_empty\">Benutzername ist erforderlich.</string>\n    <string name=\"resync\">Neu synchronisieren</string>\n    <string name=\"copy_code\">Code Kopieren</string>\n    <string name=\"kick_user_desc\">Diese Person aus der Sitzung entfernen</string>\n    <string name=\"permanently_kick_user\">Dauerhaft blockieren</string>\n    <string name=\"permanently_kick_user_desc\">Die Beitrittsanfragen dieser Person blockieren und ihre Vorschläge ausblenden</string>\n    <string name=\"transfer_ownership\">Eigentum übertragen</string>\n    <string name=\"transfer_ownership_desc\">Diese Person zum Gastgeber des Raums machen</string>\n    <string name=\"manage_user\">Benutzer verwalten</string>\n    <string name=\"listen_together_blocked_users\">Blockierte Benutzer</string>\n    <string name=\"listen_together_blocked_users_count\">%d Benutzer blockiert</string>\n    <string name=\"listen_together_no_blocked_users\">Keine blockierten Benutzer</string>\n    <string name=\"unblock\">Blockierung aufheben</string>\n    <string name=\"user_blocked_by_host\">Benutzer vom Host gesperrt</string>\n    <string name=\"crash_title\">App abgestürzt</string>\n    <string name=\"crash_description\">Ein unerwarteter Fehler ist aufgetreten. Bitte teile uns den Absturzbericht mit, damit wir das Problem beheben können.</string>\n    <string name=\"crash_share_logs\">Protokolle teilen</string>\n    <string name=\"crash_share_title\">Absturzbericht teilen</string>\n    <string name=\"crash_report_subject\">Metrolist Absturzbericht</string>\n    <string name=\"crash_close\">Schließen</string>\n    <string name=\"crash_no_log\">Kein Absturzprotokoll verfügbar</string>\n    <string name=\"palette_dynamic\">Dynamisch</string>\n    <string name=\"palette_crimson\">Purpurrot</string>\n    <string name=\"palette_rose\">Rosa</string>\n    <string name=\"palette_purple\">Violett</string>\n    <string name=\"palette_deep_purple\">Tiefes Violett</string>\n    <string name=\"palette_indigo\">Indigo</string>\n    <string name=\"palette_blue\">Blau</string>\n    <string name=\"palette_sky_blue\">Himmelblau</string>\n    <string name=\"palette_cyan\">Cyan</string>\n    <string name=\"palette_teal\">Blau grün</string>\n    <string name=\"palette_green\">Grün</string>\n    <string name=\"palette_light_green\">Hellgrün</string>\n    <string name=\"palette_yellow\">Gelb</string>\n    <string name=\"palette_orange\">Orange</string>\n    <string name=\"palette_deep_orange\">Tiefes Orange</string>\n    <string name=\"palette_brown\">Braun</string>\n    <string name=\"palette_grey\">Grau</string>\n    <string name=\"palette_blue_grey\">Blau Grau</string>\n    <string name=\"cd_back\">Zurück</string>\n    <string name=\"cd_pure_black_mode\">Reines Schwarz Modus</string>\n    <string name=\"cd_palette_item\">%1$s Palette</string>\n    <string name=\"not_playing\">Es wird kein Lied abgespielt</string>\n    <string name=\"tap_to_play\">Tippen Sie hier, um Metrolist zu öffnen</string>\n    <string name=\"widget_music_player\">Musik-Player</string>\n    <string name=\"widget_turntable\">Drehscheibe</string>\n    <string name=\"together\">Gemeinsam</string>\n    <string name=\"enter_room_code\">Raum-Code eingeben</string>\n    <string name=\"listen_together_settings_desc\">Server, Benutzernamen und mehr konfigurieren</string>\n    <string name=\"ai_lyrics_translation\">KI-Song-Text-Übersetzung</string>\n    <string name=\"ai_translating_lyrics\">Songtexte übersetzen...</string>\n    <string name=\"ai_lyrics_translated\">Songtext übersetzt</string>\n    <string name=\"ai_provider\">Anbieter</string>\n    <string name=\"ai_base_url\">Basis-URL</string>\n    <string name=\"ai_api_key\">API-Schlüssel</string>\n    <string name=\"ai_model\">Modell</string>\n    <string name=\"ai_translation_mode\">Übersetzungsmodus</string>\n    <string name=\"ai_target_language\">Zielsprache</string>\n    <string name=\"ai_setup_guide\">API-Anmeldeinformationen</string>\n    <string name=\"ai_translation_literal\">Übersetzung</string>\n    <string name=\"ai_translation_transcribed\">Transkription</string>\n    <string name=\"ai_api_key_required\">API-Schlüssel erforderlich</string>\n    <string name=\"ai_error_api_key_required\">Ein API-Schlüssel ist erforderlich</string>\n    <string name=\"ai_error_no_lyrics\">Keine Songtexte zum Übersetzen</string>\n    <string name=\"ai_error_lyrics_empty\">Die Songtexte sind leer</string>\n    <string name=\"ai_error_language_required\">Die Zielsprache wird benötigt</string>\n    <string name=\"ai_error_unexpected\">Unerwartetes Übersetzungsergebnis</string>\n    <string name=\"ai_error_unknown\">Es ist ein unbekannter Fehler aufgetreten</string>\n    <string name=\"ai_error_translation_failed\">Übersetzung fehlgeschlagen</string>\n    <string name=\"palette_amber\">Bernstein</string>\n    <string name=\"play_all\">Alle abspielen</string>\n    <string name=\"recognize_music\">Musik erkennen</string>\n    <string name=\"youtube_url_column\">YouTube-URL-Spalte (optional)</string>\n    <string name=\"re_listen\">Noch einmal zuhören</string>\n    <string name=\"clear_recognition_history_confirm\">Bist du sicher, dass du den gesamten Erkennungsverlauf löschen möchten?</string>\n    <string name=\"no_match_found\">Keine Übereinstimmung gefunden</string>\n    <string name=\"delete_from_history\">Aus Verlauf löschen</string>\n    <string name=\"artist_name_column\">Spalte „Künstlername“</string>\n    <string name=\"processing\">Verarbeitung…</string>\n    <string name=\"clear_recognition_history\">Lösche Erkennungs-Verlauf</string>\n    <string name=\"map_csv_columns\">CSV-Spalten zuordnen</string>\n    <string name=\"column_label\">Spalte %d</string>\n    <string name=\"palette_lime\">Limette</string>\n    <string name=\"recognition_error\">Erkennungsfehler</string>\n    <string name=\"enable_high_refresh_rate_desc\">Erzwinge, dass das Display mit der höchsten unterstützten Bildwiederholfrequenz (z. B. 120 Hz) läuft</string>\n    <string name=\"first_row_is_header\">Die erste Zeile ist die Kopfzeile</string>\n    <string name=\"try_again\">Versuche es erneut</string>\n    <string name=\"tap_to_recognize\">Zum Erkennen antippen</string>\n    <string name=\"recognition_history\">Erkennungs-Verlauf</string>\n    <string name=\"enable_high_refresh_rate\">Aktiviere eine hohe Bildwiederholrate</string>\n    <string name=\"song_title_column\">Spalte für Songtitel</string>\n    <string name=\"recently_converted\">Kürzlich konvertiert</string>\n    <string name=\"importing_csv\">CSV importieren</string>\n    <string name=\"play_on_app\">Auf Metrolist abspielen</string>\n    <string name=\"listening\">Hören…</string>\n    <string name=\"continue_action\">Weitermachen</string>\n    <string name=\"enable\">Aktivieren</string>\n    <string name=\"crossfade\">Song-Überblendung (Crossfade)</string>\n    <string name=\"crossfade_desc\">Überblendung zwischen Liedern</string>\n    <string name=\"crossfade_duration\">Überblenddauer</string>\n    <string name=\"crossfade_gapless\">Deaktivieren für lückenlose Alben</string>\n    <string name=\"crossfade_gapless_desc\">Nicht Überblenden, wenn das Album lückenlos ist</string>\n    <string name=\"crossfade_beta_title\">Beta-Funktion</string>\n    <string name=\"crossfade_beta_message\">Song-Überblendung (Crossfade) ist eine neue Funktion und kann Fehler enthalten. Sollten Sie Probleme feststellen, melden Sie diese bitte.\\n\\nDiese Funktion deaktiviert die Audioauslagerung aus technischen Gründen.</string>\n    <string name=\"audio_offload_disabled_by_crossfade\">Deaktiviert, da Song-Überblendung (Crossfade) aktiv ist</string>\n    <string name=\"hide_youtube_shorts\">YouTube Shorts ausblenden</string>\n    <string name=\"listen_together_in_top_bar\">Gemeinsam hören in der oberen Leiste anzeigen</string>\n    <string name=\"listen_together_in_top_bar_desc\">Gemeinsam hören in der oberen App-Leiste anstelle der Navigationsleiste anzeigen</string>\n    <string name=\"player_background_solid\">Einfärbig</string>\n    <string name=\"prevent_duplicate_tracks_in_queue\">Doppelte Songs in der Warteschlange vermeiden</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">Wird ein Titel zur Warteschlange hinzugefügt, ihn von seiner vorherigen Position entfernen, falls er bereits vorhanden ist</string>\n    <string name=\"resume_on_bluetooth_connect\">Bei Bluetooth-Verbindung fortsetzen</string>\n    <string name=\"lyrics_romanize_hindi\">Hindi-Texte romanisieren</string>\n    <string name=\"lyrics_romanize_punjabi\">Punjabi-Texte romanisieren</string>\n    <string name=\"lyrics_romanize_as_main\">Romanisierten Songtext als Haupttext anzeigen</string>\n    <string name=\"ai_translation_literal_desc\">Bedeutung in Zielsprache übersetzen</string>\n    <string name=\"discord_information_warning\">Diese Funktion verwendet die KizzyRPC-Bibliothek, um eine Verbindung zum Discord-Gateway herzustellen und Ihren Rich Presence-Status festzulegen. Obwohl keine Kontosperrungen aufgrund einer ähnlichen Nutzung bekannt sind, wird diese Methode von Discord nicht offiziell unterstützt und kann als Verstoß gegen die Nutzungsbedingungen angesehen werden. Ihr Token wird lokal extrahiert und niemals an Server Dritter gesendet. Gehen Sie nach eigenem Ermessen vor.</string>\n    <string name=\"discord_activity_type\">Aktivitätstyp</string>\n    <string name=\"ai_translation_transcribed_desc\">Umwandlung der Aussprache in das Zielskript</string>\n    <string name=\"ai_provider_help\">API-Schlüssel abrufen</string>\n    <string name=\"ai_provider_openrouter_help\">Besuche https://openrouter.ai für kostenlose und kostenpflichtige Modelle</string>\n    <string name=\"ai_provider_openai_help\">Besuche https://platform.openai.com/api-keys</string>\n    <string name=\"ai_provider_claude_help\">Besuche https://console.anthropic.com/settings/keys</string>\n    <string name=\"ai_provider_gemini_help\">Besuche https://aistudio.google.com/apikey</string>\n    <string name=\"ai_provider_perplexity_help\">Besuche https://perplexity.ai/settings/api</string>\n    <string name=\"ai_provider_xai_help\">Besuche https://console.x.ai</string>\n    <string name=\"ai_provider_deepl_help\">Besuche https://deepl.com/pro-api für kostenlose und kostenpflichtige Schlüssel</string>\n    <string name=\"ai_deepl_formality\">Formalität</string>\n    <string name=\"ai_deepl_formality_default\">Standard</string>\n    <string name=\"ai_deepl_formality_more\">Formeller</string>\n    <string name=\"ai_deepl_formality_less\">Weniger formell</string>\n    <string name=\"discord_status\">Status</string>\n    <string name=\"discord_status_online\">Online</string>\n    <string name=\"discord_status_idle\">Inaktiv</string>\n    <string name=\"discord_status_dnd\">Bitte nicht stören</string>\n    <string name=\"discord_buttons\">Schaltflächen</string>\n    <string name=\"discord_button_1\">Schaltfläche 1</string>\n    <string name=\"discord_button_2\">Schaltfläche 2</string>\n    <string name=\"login_successful\">Anmeldung erfolgreich!</string>\n    <string name=\"discord_button_text_variables\">Variablen: {song_name}, {artist_name}, {album_name}</string>\n    <string name=\"discord_connect_description\">Melde dich bei Discord an, um zu teilen, was du gerade hörst</string>\n    <string name=\"discord_activity_name\">Aktivitätsname</string>\n    <string name=\"discord_activity_name_description\">Benutzerdefinierter Name für die Aktivität (für Standardwert leer lassen)</string>\n    <string name=\"discord_advanced_mode\">Erweiterter Modus</string>\n    <string name=\"discord_advanced_mode_description\">Zusätzliche Personalisierungsoptionen für Rich Presence anzeigen</string>\n    <string name=\"restart\">Neustart</string>\n    <string name=\"restart_required\">Neustart erforderlich</string>\n    <string name=\"display_density\">Anzeigedichte</string>\n    <string name=\"density_restart_message\">Die Änderung der Anzeigedichte wird nach dem Neustart der App wirksam. Möchten Sie jetzt neu starten?</string>\n    <string name=\"found_in_settings_content\">Zu finden unter Einstellungen &gt; Inhalt</string>\n    <string name=\"speed_dial\">Schnellwahl</string>\n    <string name=\"pin_to_speed_dial\">An Schnellwahl anheften</string>\n    <string name=\"unpin_from_speed_dial\">Von Schnellwahl lösen</string>\n    <string name=\"randomize_home_order\">Reihenfolge des Startbildschirms zufällig anordnen</string>\n    <string name=\"randomize_home_order_desc\">Die Abschnitte des Startbildschirms nach gewichteten Prioritäten zufällig neu anordnen</string>\n    <string name=\"daily_discover_sounds_like\">Klingt wie %1$s</string>\n    <string name=\"daily_discover_because_you_listen_to\">Weil du %1$s hörst</string>\n    <string name=\"daily_discover_similar_to\">Ähnlich wie %1$s</string>\n    <string name=\"daily_discover_based_on\">Basierend auf %1$s</string>\n    <string name=\"daily_discover_for_fans_of\">Für Fans von %1$s</string>\n    <string name=\"from_the_community\">Aus der Community</string>\n    <string name=\"enable_lrclib_desc\">Von der Community betriebene Datenbank für synchronisierte Songtexte</string>\n    <string name=\"enable_kugou_desc\">Nimmt Songtexte von KuGou, einer beliebten chinesischen Musikplattform</string>\n    <string name=\"youtube_music_lyrics_note\">HINWEIS: Songtexte von YouTube Music werden automatisch angezeigt, wenn keine anderen Songtexte verfügbar sind. Songtexte von YTM sind in der Regel nicht synchronisiert.</string>\n    <string name=\"enable_lyricsplus\">LyricsPlus-Song-Text-Anbieter aktivieren</string>\n    <string name=\"enable_lyricsplus_desc\">Synchronisierte Songtexte aus mehreren Quellen</string>\n    <string name=\"lyrics_provider_selection\">Auswahl des Anbieters</string>\n    <string name=\"lyrics_provider_selection_desc\">Wählen Sie aus, welche Anbieter von Songtexten aktiviert werden sollen</string>\n    <string name=\"lyrics_provider_priority\">Liedtext-Anbieter-Priorität</string>\n    <string name=\"lyrics_provider_priority_desc\">Ziehen, um die Anbieter neu anzuordnen. Höhere Position -&gt; höhere Priorität.</string>\n    <string name=\"changelog\">Änderungsprotokoll</string>\n    <string name=\"changelog_empty\">Kein Änderungsprotokoll verfügbar</string>\n    <string name=\"github_releases_url\">https://github.com/MetrolistGroup/Metrolist/releases</string>\n    <string name=\"list_bullet\">•</string>\n    <string name=\"view_on_github\">Auf GitHub anzeigen</string>\n    <string name=\"current_version\">Version in Nutzung</string>\n    <string name=\"version_format\">Version: %s</string>\n    <string name=\"update_settings\">Einstellungen aktualisieren</string>\n    <string name=\"check_for_updates_title\">Nach Updates suchen</string>\n    <string name=\"checking_for_updates\">Nach Updates suchen …</string>\n    <string name=\"latest_version_format\">Neueste Version: %s</string>\n    <string name=\"check_for_updates_button\">Nach Updates suchen</string>\n    <string name=\"hide_changelog\">Änderungsprotokoll ausblenden</string>\n    <string name=\"view_changelog\">Änderungsprotokoll anzeigen</string>\n    <string name=\"failed_to_check_updates\">Update-Suche fehlgeschlagen: %s</string>\n    <string name=\"set_as_default\">Als Standard einstellen</string>\n    <string name=\"plays\">Spielt</string>\n    <string name=\"error_episode_save\">Speichern der Episode fehlgeschlagen</string>\n    <string name=\"error_episode_remove\">Entfernen der Episode fehlgeschlagen</string>\n    <string name=\"error_podcast_subscribe\">Abonnieren des Podcasts fehlgeschlagen</string>\n    <string name=\"widget_recognizer_description\">Finde Lieder, die um dich herum spielen, direkt von deinem Homescreen</string>\n    <string name=\"widget_recognizer_tap_to_search\">Tippen, um Lied zu erkennen</string>\n    <string name=\"widget_recognizer_listening\">Zuhören …</string>\n    <string name=\"widget_recognizer_processing\">Erkennen …</string>\n    <string name=\"widget_recognizer_no_match\">Keine Übereinstimmung gefunden. Nochmal versuchen</string>\n    <string name=\"widget_recognizer_error\">Erkennen fehlgeschlagen</string>\n    <string name=\"widget_recognizer_error_generic\">Ein Fehler ist aufgetreten. Bitte erneut versuchen</string>\n    <string name=\"widget_recognizer_unknown_song\">Unbekanntes Lied</string>\n    <string name=\"widget_recognizer_unknown_artist\">Unbekannter Künstler</string>\n    <string name=\"widget_recognizer_mic_desc\">Lied erkennen</string>\n    <string name=\"widget_recognizer_channel_name\">Musik-Erkennung</string>\n    <string name=\"widget_recognizer_name\">Musik-Erkennung</string>\n    <string name=\"widget_recognizer_channel_desc\">Zeigt eine Benachrichtigung an, während ein Lied vom Widget erkannt wird</string>\n    <string name=\"widget_recognizer_notification_text\">Audio aufnehmen, um Lied zu erkennen …</string>\n    <string name=\"listen_together_auto_approval_suggestions\">Lied-Vorschläge automatisch akzeptieren</string>\n    <string name=\"listen_together_auto_approval_suggestions_desc\">Lied-Vorschläge von Gästen automatisch akzeptieren und der Warteschlange hinzufügen</string>\n    <string name=\"importing_playlist\">Playlist importieren</string>\n    <string name=\"discord_activity_playing\">Aktuell spielt</string>\n    <string name=\"logout_dialog_title\">Bibliothek behalten?</string>\n    <string name=\"logout_dialog_message\">Willst du deine Playlists und Bibliothek-Daten behalten? Heruntergeladene Lieder werden immer behalten.</string>\n    <string name=\"logout_keep\">Behalten</string>\n    <string name=\"logout_clear\">Löschen</string>\n    <string name=\"credits_lead_developer\">Hauptentwickler</string>\n    <string name=\"credits_collaborator\">Mitwirkender</string>\n    <string name=\"credits_collaborators_section\">Mitwirkende</string>\n    <string name=\"credits_license_name\">GNU General Public License v3.0</string>\n    <string name=\"credits_license_desc\">Kostenlose Open-Source-Software. Du kannst sie nutzen, studieren, teilen und verbessern.</string>\n    <string name=\"credits_discord\">Discord-Server</string>\n    <string name=\"credits_telegram\">Telegram-Kanal</string>\n    <string name=\"credits_website\">Webseite</string>\n    <string name=\"credits_instagram\">Instagram</string>\n    <string name=\"credits_github\">GitHub</string>\n    <string name=\"credits_view_repo\">Repository anzeigen</string>\n    <string name=\"app_version_info\">%1$s • %2$s</string>\n    <string name=\"like_what_i_do\">Gefällt dir meine Arbeit?</string>\n    <string name=\"community_and_info\">Community &amp; Info</string>\n    <string name=\"metrolist\">METROLIST</string>\n    <string name=\"wanna_play_favorite_song\">Den Lieblingssong der Person abspielen?</string>\n    <string name=\"yeah\">Ja</string>\n    <string name=\"stands_with_palestine\">Dieses Projekt steht hinter Palästina 🇵🇸</string>\n    <string name=\"filter_podcasts\">Podcasts</string>\n    <string name=\"view_podcast\">Podcast anzeigen</string>\n    <string name=\"podcast_channels\">Podcast-Kanäle</string>\n    <string name=\"latest_episodes\">Neueste Episoden</string>\n    <string name=\"your_shows\">Deine Shows</string>\n    <string name=\"new_episodes\">Neue Episoden</string>\n    <string name=\"episodes_for_later\">Episoden für Später</string>\n    <string name=\"save_episode_for_later\">Für später speichern</string>\n    <string name=\"save_episode_for_later_desc\">Zu deiner Episoden für Später Playlist hinzufügen</string>\n    <string name=\"subscribe_to_podcast\">Podcast zur Bibliothek hinzufügen</string>\n    <plurals name=\"n_episode\">\n        <item quantity=\"one\">%d Episode</item>\n        <item quantity=\"other\">%d Episoden</item>\n    </plurals>\n    <string name=\"restore_confirm_title\">Sicherung wiederherstellen?</string>\n    <string name=\"restore_confirm_message\">Das wird deine App-Daten von der Sicherung wiederherstellen.</string>\n    <string name=\"restore_account_warning\">Du musst dich nach der Wiederherstellung erneut einloggen. Du wirst aus dem folgenden Account ausgeloggt:</string>\n    <string name=\"restore\">Wiederherstellen</string>\n    <string name=\"checking_previous_account\">Auf vorherigen Account prüfen …</string>\n    <string name=\"no_account_found\">Kein Account gefunden</string>\n    <string name=\"sleep_timer_default_set\">Einschlaf-Timer standardmäßig auf %d Min eingestellt</string>\n    <string name=\"error_podcast_unsubscribe\">Podcast-Abo kündigen fehlgeschlagen</string>\n    <string name=\"discord_rpc_preview\">Rich Presence Vorschau</string>\n    <string name=\"discord_playing_metrolist\">Spielt Metrolist</string>\n    <string name=\"discord_watching_metrolist\">Schaut Metrolist an</string>\n    <string name=\"discord_competing_metrolist\">Im Wettkampf in Metrolist</string>\n    <string name=\"remove_episode_from_saved\">Von gespeicherten Episoden entfernen</string>\n    <string name=\"filter_episodes\">Episoden</string>\n    <string name=\"filter_channels\">Kanäle</string>\n    <string name=\"auto_playlist\">Auto-Playlist</string>\n    <string name=\"downloaded_episodes\">Heruntergeladene Episoden</string>\n    <string name=\"no_subscribed_channels\">Keine abonnierten Kanäle</string>\n    <string name=\"no_downloaded_episodes\">Keine heruntergeladenen Episoden</string>\n    <plurals name=\"n_channel\">\n        <item quantity=\"one\">%d Kanal</item>\n        <item quantity=\"other\">%d Kanäle</item>\n    </plurals>\n    <string name=\"sleep_timer_daily\">Täglich</string>\n    <string name=\"sleep_timer_weekdays\">Montag bis Freitag</string>\n    <string name=\"sleep_timer_weekends\">Am Wochenende (Sa.–So.)</string>\n    <string name=\"sleep_timer_start_time\">Startzeit</string>\n    <string name=\"sleep_timer_end_time\">Endzeit</string>\n    <string name=\"sleep_timer_monday\">Montags</string>\n    <string name=\"sleep_timer_tuesday\">Dienstags</string>\n    <string name=\"sleep_timer_wednesday\">Mittwochs</string>\n    <string name=\"sleep_timer_thursday\">Donnerstags</string>\n    <string name=\"sleep_timer_friday\">Freitags</string>\n    <string name=\"sleep_timer_saturday\">Samstags</string>\n    <string name=\"sleep_timer_sunday\">Sontags</string>\n    <string name=\"export_playlist\">Playlist exportieren</string>\n    <string name=\"export_as_csv\">Als CSV exportieren</string>\n    <string name=\"export_as_m3u\">Als M3U exportieren</string>\n    <string name=\"export_failed\">Exportieren der Playliste fehlgeschlagen</string>\n    <string name=\"enable_automatic_sleeptimer\">Automatischen Sleep-Timer aktivieren</string>\n    <string name=\"sleeptimer_description\">Aktiviert den Sleep-Timer automatisch mit dem Standardwert für eine benutzerdefinierte Zeit.</string>\n    <string name=\"sleep_timer_repeat_description\">Legen Sie einen benutzerdefinierten Tag und eine benutzerdefinierte Uhrzeit fest, zu denen der Sleep-Timer automatisch aktiviert werden soll.</string>\n    <string name=\"sleep_timer_repeat\">Wiederholen</string>\n    <string name=\"sleep_timer_weekdays_weekends\">Wochentage / Wochenenden</string>\n    <string name=\"sleep_timer_custom\">Benutzerdefiniert</string>\n    <string name=\"sleep_timer_stop_after_current_song\">Am Ende des aktuellen Songs anhalten, wenn der Timer abgelaufen ist</string>\n    <string name=\"sleep_timer_fade_out\">Ausblenden in der letzten Minute</string>\n    <string name=\"view_channel\">Kanal anzeigen</string>\n    <string name=\"qs_tile_music_recognizer\">Musik erkennen</string>\n    <string name=\"discord_activity_listening\">Zuhören</string>\n    <string name=\"buy_mo_a_coffee\">Spendier mir einen Kaffee</string>\n    <string name=\"filter_profiles\">Profil</string>\n    <string name=\"upload_songs\">Songs hochladen</string>\n    <string name=\"uploading\">Wird hochgeladen…</string>\n    <string name=\"upload_progress\">%1$d von %2$d</string>\n    <string name=\"upload_complete\">Hochladen abgeschlossen</string>\n    <string name=\"upload_failed\">Hochladen fehlgeschlagen</string>\n    <string name=\"upload_file_too_large\">Datei zu groß (max. 300 MB)</string>\n    <string name=\"upload_unsupported_format\">Nicht unterstütztes Format. Verwenden Sie mp3, m4a, wma, flac oder ogg.</string>\n    <string name=\"delete_uploaded_song\">Hochgeladenen Song löschen</string>\n    <string name=\"delete_uploaded_song_confirm\">Möchten Sie diesen hochgeladenen Song wirklich löschen? Dieser Vorgang kann nicht rückgängig gemacht werden.</string>\n    <string name=\"export_success\">Playlist export erfolgreich</string>\n    <string name=\"export_option_share\">Teilen</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-de/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <string name=\"home\">Startseite</string>\n    <string name=\"songs\">Songs</string>\n    <string name=\"artists\">Künstler</string>\n    <string name=\"albums\">Alben</string>\n    <string name=\"playlists\">Playlists</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d ausgewählt</item>\n        <item quantity=\"other\">%d ausgewählt</item>\n    </plurals>\n    <string name=\"history\">Hörverlauf</string>\n    <string name=\"stats\">Statistiken</string>\n    <string name=\"mood_and_genres\">Stimmungen und Genres</string>\n    <string name=\"account\">Konto</string>\n    <string name=\"quick_picks\">Schnellauswahl</string>\n    <string name=\"quick_picks_empty\">Höre dir Songs an, um deine Schnellauswahl zu erstellen</string>\n    <string name=\"new_release_albums\">Neu veröffentlichte Alben</string>\n    <string name=\"today\">Heute</string>\n    <string name=\"yesterday\">Gestern</string>\n    <string name=\"this_week\">Diese Woche</string>\n    <string name=\"last_week\">Letzte Woche</string>\n    <string name=\"most_played_songs\">Meist gespielte Songs</string>\n    <string name=\"most_played_artists\">Meist gespielte Künstler</string>\n    <string name=\"most_played_albums\">Meist gespielte Alben</string>\n    <string name=\"search\">Suche</string>\n    <string name=\"search_yt_music\">YouTube Music durchsuchen…</string>\n    <string name=\"search_library\">Bibliothek durchsuchen…</string>\n    <string name=\"filter_library\">Bibliothek</string>\n    <string name=\"filter_liked\">Mag ich</string>\n    <string name=\"filter_downloaded\">Heruntergeladen</string>\n    <string name=\"filter_all\">Alles</string>\n    <string name=\"filter_songs\">Songs</string>\n    <string name=\"filter_videos\">Videos</string>\n    <string name=\"filter_albums\">Alben</string>\n    <string name=\"filter_artists\">Künstler</string>\n    <string name=\"filter_playlists\">Playlists</string>\n    <string name=\"filter_community_playlists\">Community-Playlists</string>\n    <string name=\"filter_featured_playlists\">Ausgewählte Playlists</string>\n    <string name=\"no_results_found\">Keine Ergebnisse gefunden</string>\n    <string name=\"from_your_library\">Aus der Bibliothek</string>\n    <string name=\"liked_songs\">Songs, die ich mag</string>\n    <string name=\"downloaded_songs\">Heruntergeladene Songs</string>\n    <string name=\"playlist_is_empty\">Die Playlist ist leer</string>\n    <string name=\"retry\">Erneut versuchen</string>\n    <string name=\"radio\">Radio</string>\n    <string name=\"shuffle\">Shuffle</string>\n    <string name=\"reset\">Zurücksetzen</string>\n    <string name=\"details\">Details</string>\n    <string name=\"edit\">Bearbeiten</string>\n    <string name=\"start_radio\">Radio starten</string>\n    <string name=\"play\">Wiedergabe</string>\n    <string name=\"play_next\">Als nächstes abspielen</string>\n    <string name=\"add_to_queue\">Zur Warteschlange hinzufügen</string>\n    <string name=\"add_to_library\">Zur Bibliothek hinzufügen</string>\n    <string name=\"remove_from_library\">Aus Bibliothek entfernen</string>\n    <string name=\"action_download\">Herunterladen</string>\n    <string name=\"downloading\">Wird heruntergeladen</string>\n    <string name=\"remove_download\">Download entfernen</string>\n    <string name=\"import_playlist\">Playlist importieren</string>\n    <string name=\"add_to_playlist\">Zur Playlist hinzufügen</string>\n    <string name=\"view_artist\">Künstler ansehen</string>\n    <string name=\"view_album\">Album ansehen</string>\n    <string name=\"refetch\">Neu laden</string>\n    <string name=\"share\">Teilen</string>\n    <string name=\"delete\">Löschen</string>\n    <string name=\"remove_from_history\">Aus Verlauf entfernen</string>\n    <string name=\"search_online\">Online-Suche</string>\n    <string name=\"action_sync\">Synchronisieren</string>\n    <string name=\"advanced\">Erweitert</string>\n    <string name=\"sort_by_create_date\">Hinzufügedatum</string>\n    <string name=\"sort_by_name\">Name</string>\n    <string name=\"sort_by_artist\">Künstler</string>\n    <string name=\"sort_by_year\">Jahr</string>\n    <string name=\"sort_by_song_count\">Song anzahl</string>\n    <string name=\"sort_by_length\">Länge</string>\n    <string name=\"sort_by_play_time\">Wiedergabedauer</string>\n    <string name=\"sort_by_custom\">Individuelle Reinfolge</string>\n    <string name=\"media_id\">Medien-ID</string>\n    <string name=\"mime_type\">MIME-Typ</string>\n    <string name=\"codecs\">Codecs</string>\n    <string name=\"bitrate\">Bitrate</string>\n    <string name=\"sample_rate\">Abtastrate</string>\n    <string name=\"loudness\">Lautstärke</string>\n    <string name=\"volume\">Lautstärke</string>\n    <string name=\"file_size\">Dateigröße</string>\n    <string name=\"unknown\">Unbekannt</string>\n    <string name=\"copied\">In die Zwischenablage kopiert</string>\n    <string name=\"edit_lyrics\">Songtext bearbeiten</string>\n    <string name=\"search_lyrics\">Songtext suchen</string>\n    <string name=\"edit_song\">Song bearbeiten</string>\n    <string name=\"song_title\">Songtitel</string>\n    <string name=\"song_artists\">Song-Künstler</string>\n    <string name=\"error_song_title_empty\">Der Songtitel darf nicht leer sein.</string>\n    <string name=\"error_song_artist_empty\">Song-Künstler darf nicht leer sein.</string>\n    <string name=\"save\">Speichern</string>\n    <string name=\"choose_playlist\">Playlist auswählen</string>\n    <string name=\"edit_playlist\">Playlist bearbeiten</string>\n    <string name=\"create_playlist\">Playlist erstellen</string>\n    <string name=\"playlist_name\">Name der Playlist</string>\n    <string name=\"error_playlist_name_empty\">Der Name der Playlist darf nicht leer sein.</string>\n    <string name=\"edit_artist\">Künstler bearbeiten</string>\n    <string name=\"artist_name\">Name des Künstlers</string>\n    <string name=\"error_artist_name_empty\">Der Name des Künstlers darf nicht leer sein.</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d Song</item>\n        <item quantity=\"other\">%d Songs</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d Künstler</item>\n        <item quantity=\"other\">%d Künstler</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d Album</item>\n        <item quantity=\"other\">%d Alben</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d Playlist</item>\n        <item quantity=\"other\">%d Playlists</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d Woche</item>\n        <item quantity=\"other\">%d Wochen</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d Monat</item>\n        <item quantity=\"other\">%d Monate</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d Jahr</item>\n        <item quantity=\"other\">%d Jahre</item>\n    </plurals>\n    <string name=\"playlist_imported\">Playlist importiert</string>\n    <string name=\"removed_song_from_playlist\">\\\"%s\\\" aus der Playlist entfernt</string>\n    <string name=\"playlist_synced\">Playlist synchronisiert</string>\n    <string name=\"undo\">Rückgängig machen</string>\n    <string name=\"lyrics_not_found\">Songtext nicht gefunden</string>\n    <string name=\"sleep_timer\">Schlaf-Timer</string>\n    <string name=\"end_of_song\">Ende des Songs</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">1 Minute</item>\n        <item quantity=\"other\">%d Minuten</item>\n    </plurals>\n    <string name=\"error_no_stream\">Kein Stream verfügbar</string>\n    <string name=\"error_no_internet\">Keine Netzwerkverbindung</string>\n    <string name=\"error_timeout\">Zeitüberschreitung</string>\n    <string name=\"error_unknown\">Unbekannter Fehler</string>\n    <string name=\"action_like\">Mag ich</string>\n    <string name=\"action_remove_like\">„Mag ich“ entfernen</string>\n    <string name=\"action_shuffle_on\">Shuffle an</string>\n    <string name=\"action_shuffle_off\">Shuffle aus</string>\n    <string name=\"repeat_mode_off\">Wiederholungsmodus aus</string>\n    <string name=\"repeat_mode_one\">Aktuellen Song wiederholen</string>\n    <string name=\"repeat_mode_all\">Warteschlange wiederholen</string>\n    <string name=\"queue_all_songs\">Alle Songs</string>\n    <string name=\"queue_searched_songs\">Gesuchte Songs</string>\n    <string name=\"music_player\">Musik-Player</string>\n    <string name=\"settings\">Einstellungen</string>\n    <string name=\"appearance\">Erscheinungsbild</string>\n    <string name=\"enable_dynamic_theme\">Dynamisches Thema aktivieren</string>\n    <string name=\"dark_theme\">Dunkles Thema</string>\n    <string name=\"dark_theme_on\">An</string>\n    <string name=\"dark_theme_off\">Aus</string>\n    <string name=\"dark_theme_follow_system\">System-Standard</string>\n    <string name=\"pure_black\">Reines Schwarz</string>\n    <string name=\"default_open_tab\">Standardmäßig geöffnete Registerkarte</string>\n    <string name=\"customize_navigation_tabs\">Anpassen der Navigationsleiste</string>\n    <string name=\"lyrics_text_position\">Position des Songtextes</string>\n    <string name=\"left\">Links</string>\n    <string name=\"center\">Mitte</string>\n    <string name=\"right\">Rechts</string>\n    <string name=\"content\">Inhalt</string>\n    <string name=\"login\">Anmeldung</string>\n    <string name=\"content_language\">Standard-Inhaltssprache</string>\n    <string name=\"content_country\">Standard-Inhaltsland</string>\n    <string name=\"system_default\">System-Standard</string>\n    <string name=\"enable_proxy\">Proxy einschalten</string>\n    <string name=\"proxy_type\">Proxy-Typ</string>\n    <string name=\"proxy_url\">Proxy-URL</string>\n    <string name=\"restart_to_take_effect\">Neustarten, damit Änderungen wirksam werden</string>\n    <string name=\"player_and_audio\">Player und Audio</string>\n    <string name=\"audio_quality\">Tonqualität</string>\n    <string name=\"audio_quality_auto\">Automatisch</string>\n    <string name=\"audio_quality_high\">Hoch</string>\n    <string name=\"audio_quality_low\">Niedrig</string>\n    <string name=\"persistent_queue\">Dauerhafte Warteschlange</string>\n    <string name=\"skip_silence\">Stille überspringen</string>\n    <string name=\"audio_normalization\">Audio-Normalisierung</string>\n    <string name=\"equalizer\">Equalizer</string>\n    <string name=\"storage\">Speicher</string>\n    <string name=\"cache\">Cache</string>\n    <string name=\"image_cache\">Bild-Cache</string>\n    <string name=\"song_cache\">Song-Cache</string>\n    <string name=\"max_cache_size\">Maximale Cache-Größe</string>\n    <string name=\"unlimited\">Unbegrenzt</string>\n    <string name=\"clear_all_downloads\">Alle Downloads löschen</string>\n    <string name=\"max_image_cache_size\">Maximale Größe des Bild-Caches</string>\n    <string name=\"clear_image_cache\">Bild-Cache löschen</string>\n    <string name=\"max_song_cache_size\">Maximale Größe des Song-Cache</string>\n    <string name=\"clear_song_cache\">Song-Cache löschen</string>\n    <string name=\"size_used\">%s verwendet</string>\n    <string name=\"privacy\">Privatsphäre</string>\n    <string name=\"pause_listen_history\">Pausieren des Hörverlaufs</string>\n    <string name=\"clear_listen_history\">Hörverlauf löschen</string>\n    <string name=\"clear_listen_history_confirm\">Bist du sicher, dass du den gesamten Hörverlauf löschen willst?</string>\n    <string name=\"pause_search_history\">Suchverlauf anhalten</string>\n    <string name=\"clear_search_history\">Suchverlauf löschen</string>\n    <string name=\"clear_search_history_confirm\">Bist du sicher, dass du den gesamten Suchverlauf löschen willst?</string>\n    <string name=\"enable_kugou\">KuGou-Songtext-Anbieter aktivieren</string>\n    <string name=\"backup_restore\">Sichern und Wiederherstellen</string>\n    <string name=\"action_backup\">Sichern</string>\n    <string name=\"action_restore\">Wiederherstellen</string>\n    <string name=\"imported_playlist\">Importierte Playlist</string>\n    <string name=\"backup_create_success\">Sicherung erfolgreich erstellt</string>\n    <string name=\"backup_create_failed\">Konnte keine Sicherung erstellen</string>\n    <string name=\"restore_failed\">Wiederherstellung der Sicherung fehlgeschlagen</string>\n    <string name=\"about\">Über</string>\n    <string name=\"app_version\">App-Version</string>\n    <string name=\"new_version_available\">Neue Version verfügbar</string>\n    <string name=\"translation_models\">Übersetzungs-Modelle</string>\n    <string name=\"clear_translation_models\">Lösche Übersetzungs-Modelle</string>\n    <string name=\"forgotten_favorites\">Vergessene Favoriten</string>\n    <string name=\"action_remove_like_all\">Alle „Mag ich“ entfernen</string>\n    <string name=\"default_\">Standard</string>\n    <string name=\"auto_load_more\">Automatisch mehr Songs laden</string>\n    <string name=\"action_logout\">Abmelden</string>\n    <string name=\"theme\">Thema</string>\n    <string name=\"other_versions\">Andere Versionen</string>\n    <string name=\"stop_music_on_task_clear\">Musik stoppen wenn die App aus dem Hintergrund gelöscht wird</string>\n    <string name=\"library_album_empty\">Alben aus der Bibliothek werden hier angezeigt</string>\n    <string name=\"add_all_to_library\">Alle zur Bibliothek hinzufügen</string>\n    <string name=\"similar_to\">Ähnlich wie</string>\n    <string name=\"listen_history\">Hörverlauf</string>\n    <string name=\"not_logged_in\">Nicht angemeldet</string>\n    <string name=\"remove_from_queue\">Aus Warteschlange entfernen</string>\n    <string name=\"duplicates\">Duplikate</string>\n    <string name=\"auto_skip_next_on_error\">Automatisch zum nächsten Song springen, wenn ein Fehler auftritt</string>\n    <string name=\"sided\">Seite</string>\n    <string name=\"library_artist_empty\">Künstler aus der Bibliothek werden hier angezeigt</string>\n    <string name=\"player\">Player</string>\n    <string name=\"preview\">Vorschau</string>\n    <string name=\"misc\">Sonstiges</string>\n    <string name=\"add_anyway\">Trotzdem hinzufügen</string>\n    <string name=\"player_slider_style\">Stil des Player-Schiebereglers</string>\n    <string name=\"duplicates_description_multiple\">%d Songs sind bereits in deiner Playlist</string>\n    <string name=\"enable_discord_rpc\">Statusanzeige aktivieren</string>\n    <string name=\"auto_skip_next_on_error_desc\">Sorge für ein kontinuierliches Wiedergabeerlebnis</string>\n    <string name=\"dismiss\">Ablehnen</string>\n    <string name=\"player_text_alignment\">Ausrichtung des Player-textes</string>\n    <string name=\"disable_screenshot\">Screenshots deaktivieren</string>\n    <string name=\"small\">Klein</string>\n    <string name=\"login_failed\">Anmeldung fehlgeschlagen</string>\n    <string name=\"queue\">Warteschlange</string>\n    <string name=\"skip_duplicates\">Duplikate überspringen</string>\n    <string name=\"remove_from_playlist\">Aus Playlist entfernen</string>\n    <string name=\"tempo_and_pitch\">Tempo und Tonhöhe</string>\n    <string name=\"options\">Optionen</string>\n    <string name=\"big\">Groß</string>\n    <string name=\"keep_listening\">Weiterhören</string>\n    <string name=\"your_youtube_playlists\">Deine Youtube-Playlists</string>\n    <string name=\"library_playlist_empty\">Deine Playlists werden hier angezeigt</string>\n    <string name=\"disable_screenshot_desc\">Wenn diese Option aktiviert ist, sind Screenshots und die App-Ansicht in „Zuletzt gesehen“ deaktiviert.</string>\n    <string name=\"discord_information\">Metrolist verwendet die KizzyRPC-Bibliothek, um den Status deines Discord-Kontos zu setzen. Dazu wird die Discord-Gateway-Verbindung verwendet, was als Verstoß gegen die AGB von Discord angesehen werden kann. Es sind jedoch keine Fälle bekannt, in denen Benutzerkonten aus diesem Grund gesperrt wurden. Die Verwendung erfolgt auf eigene Gefahr.\\n\\nMetrolist extrahiert nur dein Token, alles andere wird lokal gespeichert.</string>\n    <string name=\"squiggly\">Schnörkelig</string>\n    <string name=\"grid_cell_size\">Größe der Rasterzellen</string>\n    <string name=\"search_history\">Suchverlauf</string>\n    <string name=\"enable_lrclib\">LrcLib-Songtext-Anbieter aktivieren</string>\n    <string name=\"hide_explicit\">Explizite Inhalte ausblenden</string>\n    <string name=\"discord_integration\">Discord-Integration</string>\n    <string name=\"library_song_empty\">Songs aus der Bibliothek werden hier angezeigt</string>\n    <string name=\"remove_download_playlist_confirm\">Möchtest du wirklich alle Songs der Playlist „%s“ aus dem Speicher für heruntergeladene Songs entfernen?</string>\n    <string name=\"delete_playlist_confirm\">Möchtest du die Playlist „%s“ wirklich löschen?</string>\n    <string name=\"duplicates_description_single\">Der Song befindet sich bereits in deiner Playlist</string>\n    <string name=\"action_like_all\">Allen „Mag ich“ geben</string>\n    <string name=\"remove_all_from_library\">Alle aus Bibliothek entfernen</string>\n    <string name=\"persistent_queue_desc\">Wiederherstellung der letzten Warteschlange beim Starten der App</string>\n    <string name=\"auto_load_more_desc\">Automatisches Hinzufügen weiterer Songs, wenn das Ende der Warteschlange erreicht ist, sofern möglich</string>\n    <string name=\"filter_bookmarked\">Lesezeichen</string>\n    <string name=\"use_login_for_browse\">Anmeldung zum Durchsuchen von Inhalten verwenden</string>\n    <string name=\"use_login_for_browse_desc\">Dies kann Einfluss darauf haben, welche Inhalte du siehst und zeigt zum Beispiel nur Premium-Alben an, wenn Sie mit einem Premium-Konto angemeldet sind</string>\n    <string name=\"action_login\">Anmelden</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-el/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">Τοπικά</string>\n    <string name=\"remote_history\">Απομακρυσμένα</string>\n    <string name=\"charts\">Διαγράμματα</string>\n    <string name=\"back_button_desc\">Πίσω</string>\n    <string name=\"album_cover_desc\">Εξώφυλλο άλμπουμ</string>\n    <string name=\"top_music_videos\">Διάσημα Μουσικά Βίντεο</string>\n    <string name=\"trending\">Δημοφιλή</string>\n    <string name=\"weeks\">Εβδομάδες</string>\n    <string name=\"months\">Μήνες</string>\n    <string name=\"years\">Χρόνια</string>\n    <string name=\"continuous\">Συνεχής</string>\n    <string name=\"liked\">Τα liked μου</string>\n    <string name=\"offline\">Κατεβασμένα</string>\n    <string name=\"my_top\">Τα Κορυφαία Μου</string>\n    <string name=\"cached_playlist\">Στην κρυφή μνήμη</string>\n    <string name=\"sync_playlist\">Συγχρονισμός Λιστών</string>\n    <string name=\"sync_disabled\">Συγχρονισμός ανενεργός</string>\n    <string name=\"allows_for_sync_witch_youtube\">Σημείωση: Αυτό επιτρέπει το συγχρονισμό με το YouTube Music. Αυτό ΔΕΝ μπορεί να αλλάξει αργότερα.</string>\n    <string name=\"remove_from_cache\">Αφαίρεση από την κρυφή μνήμη</string>\n    <string name=\"copy_link\">Αντιγραφή συνδέσμου</string>\n    <string name=\"select\">Επιλογή όλων</string>\n    <string name=\"like_all\">Like όλα</string>\n    <string name=\"dislike_all\">Dislike όλα</string>\n    <string name=\"sort_by_last_updated\">Ημερομηνία ενημέρωσης</string>\n    <string name=\"link_copied\">Ο σύνδεσμος αντιγράφηκε στο πρόχειρο</string>\n    <string name=\"lyrics\">Στίχοι</string>\n    <string name=\"already_in_playlist\">Ήδη σε λίστα αναπαραγωγής:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d φορά</item>\n        <item quantity=\"other\">%d φορές</item>\n    </plurals>\n    <string name=\"similar_content\">Παρόμοιο περιεχόμενο</string>\n    <string name=\"player_background_style\">Στυλ φόντου αναπαραγωγέα</string>\n    <string name=\"follow_theme\">Με βάση το θέμα</string>\n    <string name=\"gradient\">Διαβάθμιση</string>\n    <string name=\"player_background_blur\">Θάμπωμα</string>\n    <string name=\"player_buttons_style\">Χρώματα κουμπιού παίχτη</string>\n    <string name=\"default_style\">Προεπιλογή</string>\n    <string name=\"enable_swipe_thumbnail\">Ενεργοποίηση σάρωσης για αλλαγή τραγουδιού</string>\n    <string name=\"swipe_song_to_add\">Σύρετε το τραγούδι προς τα δεξιά για αναπαραγωγή επόμενου ή προς τα αριστερά για προσθήκη στην ουρά</string>\n    <string name=\"lyrics_click_change\">Αλλαγή στίχων στο κλικ</string>\n    <string name=\"slim\">Λεπτό</string>\n    <string name=\"slim_navbar\">Απόκρυψη ετικετών της γραμμής πλοήγησης</string>\n    <string name=\"auto_playlists\">Αυτόματες Λίστες Αναπαραγωγής</string>\n    <string name=\"show_liked_playlist\">Εμφάνιση Αγαπημένων Λιστών</string>\n    <string name=\"show_downloaded_playlist\">Εμφάνιση Κατεβασμένων Λιστών</string>\n    <string name=\"show_top_playlist\">Εμφάνιση Κορυφαίων Λιστών</string>\n    <string name=\"show_cached_playlist\">Εμφάνιση Λιστών στην Κρυφή Μνήμη</string>\n    <string name=\"advanced_login\">Σύνθετη σύνδεση (token)</string>\n    <string name=\"token_hidden\">Πατήστε για να εμφανιστεί το token</string>\n    <string name=\"token_shown\">Πατήστε ξανά για αντιγραφή ή επεξεργασία</string>\n    <string name=\"token_adv_login_description\">Αυτή είναι μια ΣΥΝΘΕΤΗ μέθοδος σύνδεσης. Ως εναλλακτική της πύλης Ιστού, μπορείτε να εισαγάγετε ή να ενημερώσετε απευθείας το token σύνδεσής σας εδώ. Για παράδειγμα, αυτό μπορεί να επιταχύνει τη σύνδεση σε πολλές συσκευές. Λάβετε υπόψη ότι τυχόν μη έγκυρες μορφές διακριτικών που η εφαρμογή αποτυγχάνει να αναλύσει δεν θα γίνουν αποδεκτές</string>\n    <string name=\"general\">Γενικά</string>\n    <string name=\"proxy\">Διακομιστής μεσολάβησης</string>\n    <string name=\"default_lib_chips\">Αλλαγή προεπιλεγμένου τσιπ βιβλιοθήκης</string>\n    <string name=\"set_quick_picks\">Ορισμός γρήγορων επιλογών</string>\n    <string name=\"last_song_listened\">Με βάση στο τελευταίο τραγούδι που ακούστηκε</string>\n    <string name=\"app_language\">Γλώσσα εφαρμογής</string>\n    <string name=\"enable_similar_content\">Ενεργοποίηση Παρόμοιου Περιεχομένου</string>\n    <string name=\"similar_content_desc\">Αυτόματη προσθήκη περισσότερων παρόμοιων τραγουδιών όταν φτάσει στο τέλος της ουράς</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"clear_song_cache_dialog\">Είστε σίγουροι ότι θέλετε να διαγράψετε όλα τα τραγούδια που είναι στην κρυφή μνήμη;</string>\n    <string name=\"clear_downloads_dialog\">Είστε σίγουροι ότι θέλετε να διαγράψετε όλες τις λήψεις;</string>\n    <string name=\"not_logged_in_youtube\">Δεν έχετε συνδεθεί στο YouTube</string>\n    <string name=\"default_links\">Άνοιγμα υποστηριζόμενων συνδέσμων</string>\n    <string name=\"open_app_settings_error\">Αδυναμία ανοίγματος των ρυθμίσεων της εφαρμογής</string>\n    <string name=\"release_notes\">Σημειώσεις Έκδοσης</string>\n    <string name=\"all_time\">Όλη την ώρα</string>\n    <string name=\"past_24_hours\">Προηγούμενες 24 ώρες</string>\n    <string name=\"past_week\">Προηγούμενη εβδομάδα</string>\n    <string name=\"past_month\">Προηγούμενος μήνας</string>\n    <string name=\"past_year\">Προηγούμενο έτος</string>\n    <string name=\"top_length\">Μήκος της Κορυφαίας Λίστας Μου</string>\n    <string name=\"history_duration\">Διάρκεια ιστορικού</string>\n    <string name=\"information\">Πληροφορίες</string>\n    <string name=\"description\">Περιγραφή</string>\n    <string name=\"views\">Προβολές</string>\n    <string name=\"likes\">Τα likes μου</string>\n    <string name=\"dislikes\">Τα dislikes μου</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">1 δευτερόλεπτο</item>\n        <item quantity=\"other\">%d δευτερόλεπτα</item>\n    </plurals>\n    <string name=\"text_color\">Χρώμα κειμένου</string>\n    <string name=\"please_wait\">Παρακαλώ περιμένετε</string>\n    <string name=\"cancel\">Ακύρωση</string>\n    <string name=\"share_lyrics\">Κοινοποιήστε τους στοίχους</string>\n    <string name=\"share_as_text\">Κοινή χρήση ως κείμενο</string>\n    <string name=\"share_as_image\">Κοινή χρήση ως εικόνα</string>\n    <string name=\"max_selection_limit\">Μέγιστο όριο επιλογής</string>\n    <string name=\"share_selected\">Κοινή χρήση επιλεγμένου</string>\n    <string name=\"customize_colors\">Προσαρμογή χρωμάτων</string>\n    <string name=\"secondary_text_color\">Δευτερεύον χρώμα κειμένου</string>\n    <string name=\"background_color\">Χρώμα φόντου</string>\n    <string name=\"generating_image\">Δημιουργία εικόνας</string>\n    <string name=\"auto_download_on_like\">Αυτόματη λήψη στο like</string>\n    <string name=\"auto_download_on_like_desc\">Αυτόματη λήψη τραγουδιών στο like</string>\n    <string name=\"new_mini_player_design\">Νέο σχέδιο mini player</string>\n    <string name=\"lyrics_auto_scroll\">Στίχοι αυτόματης κύλισης</string>\n    <string name=\"lyrics_romanize_japanese\">Λατινοποίηση (transliteration) ιαπωνικών στίχων</string>\n    <string name=\"lyrics_romanize_korean\">Λατινοποίηση (transliteration) κορεατικών στίχων</string>\n    <string name=\"import_online\">Εισαγωγή λιστών αναπαραγωγής \\\"m3u\\\"</string>\n    <string name=\"import_csv\">Εισαγωγή λιστών αναπαραγωγής \\\"csv\\\"</string>\n    <string name=\"playlist_add_local_to_synced_note\">Σημείωση: Η προσθήκη τοπικών τραγουδιών σε συγχρονισμένες/απομακρυσμένες λίστες αναπαραγωγής δεν υποστηρίζεται. Οποιοσδήποτε άλλος συνδυασμός είναι έγκυρος</string>\n    <string name=\"swipe_sensitivity\">Ευαισθησία ολίσθησης mini player</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"clear_image_cache_dialog\">Είστε βέβαιοι ότι θέλετε να διαγράψετε όλες τις εικόνες που έχουν αποθηκευτεί στην προσωρινή μνήμη;</string>\n    <string name=\"disable\">Απενεργοποίηση</string>\n    <string name=\"subscribe\">Εγραφή</string>\n    <string name=\"subscribed\">Οι εγραφές μου</string>\n    <string name=\"more_content\">Περισσότερο περιεχόμενο</string>\n    <string name=\"yt_sync\">Αυτόματος συγχρονισμός με λογαριασμό</string>\n    <string name=\"new_player_design\">Νέος σχεδιασμός προγράμματος αναπαραγωγής</string>\n    <string name=\"uploaded_playlist\">Ανεβασμένα</string>\n    <string name=\"filter_uploaded\">Ανεβασμένο φίλτρο</string>\n    <string name=\"starting_radio\">Εκκίνηση ραδιοφώνου</string>\n    <string name=\"now_playing\">Παίζει Τώρα</string>\n    <string name=\"close\">Κλείσιμο</string>\n    <string name=\"hide_player_thumbnail\">Απόκρυψη εικονιδίου αναπαραγωγής</string>\n    <string name=\"hide_player_thumbnail_desc\">Αντικατάσταση εξώφυλλου άλμπουμ με το λογότυπο στο πρόγραμμα αναπαραγωγής</string>\n    <string name=\"seek_forward_dynamic\">+%1$d δευτερόλεπτα μπροστά</string>\n    <string name=\"seek_backward_dynamic\">-%1$d δευτερόλεπτα πίσω</string>\n    <string name=\"seek_seconds_addup\">Προοδευτική αναζήτηση</string>\n    <string name=\"seek_seconds_addup_description\">Εάν είναι ενεργοποιημένο, προσθέτει 5 επιπλέον δευτερόλεπτα σταδιακά σε κάθε παράλειψη αναζήτησης</string>\n    <string name=\"swipe_song_to_remove\">Σύρετε το τραγούδι για να το αφαιρέσετε από τη λίστα αναπαραγωγής</string>\n    <string name=\"show_uploaded_playlist\">Εμφάνιση \\\"ανεβασμένης\\\" λίστας αναπαραγωγής</string>\n    <string name=\"edit_playlist_cover\">Επεξεργασία εξώφυλλου λίστας αναπαραγωγής</string>\n    <string name=\"edit_playlist_cover_note\">Σημείωση: Ο λογαριασμός σας πρέπει να συνδέεται με έναν αριθμό τηλεφώνου και να επαληθευτεί στο YouTube Music για να αλλάξει το εξώφυλλο της λίστας αναπαραγωγής.</string>\n    <string name=\"edit_playlist_cover_note_wait\">Αφού επιλέξετε μια εικόνα, περιμένετε λίγο μέχρι να εμφανιστεί το νέο εξώφυλλο στη λίστα αναπαραγωγής σας.</string>\n    <string name=\"choose_from_library\">Επιλέξτε από τη βιβλιοθήκη</string>\n    <string name=\"remove_custom_image\">Κατάργηση προσαρμοσμένης εικόνας</string>\n    <string name=\"config_proxy\">Διαμόρφωση διακομιστή μεσολάβησης</string>\n    <string name=\"proxy_username\">Όνομα χρήστη διακομιστή μεσολάβησης</string>\n    <string name=\"proxy_password\">Κωδικός πρόσβασης διακομιστή μεσολάβησης</string>\n    <string name=\"enable_authentication\">Ενεργοποίηση ελέγχου ταυτότητας</string>\n    <string name=\"discord_use_details\">Χρήση λεπτομερειών αντί πολιτείας</string>\n    <string name=\"discord_use_details_description\">Προβολή τίτλου τραγουδιού με έμφαση αντί για ονόματα καλλιτεχνών</string>\n    <string name=\"disable_load_more_when_repeat_all\">Απενεργοποίηση φόρτωσης περισσότερων όταν επαναλαμβάνονται όλα</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Μην φορτώνετε αυτόματα περισσότερα τραγούδια και παρόμοιο περιεχόμενο όταν είναι ενεργοποιημένη η λειτουργία επανάληψης όλων</string>\n    <string name=\"lyrics_romanization_cyrillic\">Κυριλικά</string>\n    <string name=\"lyrics_romanize_title\">Greeklish</string>\n    <string name=\"lyrics_romanization\">Στίχοι σε Greeklish</string>\n    <string name=\"lyrics_romanize_russian\">Λατινοποίηση Ρωσικών στίχων</string>\n    <string name=\"lyrics_romanize_ukrainian\">Λατινοποίηση Ουκρανικών στίχων</string>\n    <string name=\"lyrics_romanize_belarusian\">Λατινοποίηση Λευκορωσικών στίχων</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Λατινοποίηση Κυργιζιανών στίχων</string>\n    <string name=\"lyrics_romanize_serbian\">Λατινοποίηση Σερβικών στίχων</string>\n    <string name=\"lyrics_romanize_bulgarian\">Λατινοποίηση Βουλγαρικών στίχων</string>\n    <string name=\"line_by_line_option_title\">ΠΕΙΡΑΜΑΤΙΚΟ: Ανίχνευση γλώσσας γραμμή προς γραμμή</string>\n    <string name=\"line_by_line_option_desc\">Η κυριλλική γλώσσα θα ανιχνεύεται γραμμή προς γραμμή αντί για ολόκληρο το τραγούδι.</string>\n    <string name=\"line_by_line_dialog_title\">Είστε βέβαιος;</string>\n    <string name=\"line_by_line_dialog_desc\">Αυτή είναι μια πειραματική λειτουργία με αβέβαιο αποτέλεσμα.\\n\\nΑπό προεπιλογή, η γλώσσα προσδιορίζεται από ολόκληρο το τραγούδι, αλλά με αυτήν την επιλογή ενεργοποιημένη, θα προσδιορίζεται γραμμή προς γραμμή. Αυτό θα επιτρέπει τη λειτουργία τραγουδιών σε πολλές γλώσσες, ΑΛΛΑ η γλώσσα μπορεί να μην είναι πάντα σωστή (για παράδειγμα, αν υπάρχουν ουκρανικοί στίχοι που δεν περιέχουν ουκρανικά γράμματα, μπορεί να μεταγραφούν ως ρωσικά). \\n\\nΕάν δεν έχετε προβλήματα, συνιστάται να διατηρήσετε αυτή την επιλογή απενεργοποιημένη.</string>\n    <string name=\"settings_section_ui\">Διεπαφή</string>\n    <string name=\"settings_section_privacy\">Απόρρητο και ασφάλεια</string>\n    <string name=\"settings_section_player_content\">Αναπαραγωγέας &amp; Περιεχόμενο</string>\n    <string name=\"settings_section_storage\">Αποθήκευση &amp; Δεδομένα</string>\n    <string name=\"settings_section_system\">Σύστημα &amp; Σχετικά</string>\n    <string name=\"updater\">Ενημερωτής</string>\n    <string name=\"check_for_updates\">Αυτόματος έλεγχος για ενημερώσεις</string>\n    <string name=\"update_notifications\">Ενεργοποίηση ειδοποιήσεων για ενημερώσεις</string>\n    <string name=\"update_available_title\">Διαθέσιμη ενημέρωση</string>\n    <string name=\"update_channel_name\">Ενημερώσεις εφαρμογής</string>\n    <string name=\"update_channel_desc\">Ειδοποιήσεις για νέες εκδόσεις</string>\n    <string name=\"audio_offload\">Ενεργοποίηση εκφόρτωσης</string>\n    <string name=\"audio_offload_description\">Χρησιμοποιήστε τη διαδρομή εκφόρτωσης ήχου για αναπαραγωγή ήχου. Η απενεργοποίηση αυτής της λειτουργίας μπορεί να αυξήσει την κατανάλωση ενέργειας, αλλά μπορεί να είναι χρήσιμη εάν αντιμετωπίζετε προβλήματα με την αναπαραγωγή ήχου ή την μετα-επεξεργασία</string>\n    <string name=\"lyrics_romanize_macedonian\">Λατινοποίηση Σλαβομακεδονικών στίχων</string>\n    <string name=\"integrations\">Ενσωματώσεις</string>\n    <string name=\"username\">Όνομα χρήστη</string>\n    <string name=\"password\">Κωδικός πρόσβασης</string>\n    <string name=\"lastfm_integration\">Ενσωμάτωση Last.fm</string>\n    <string name=\"enable_scrobbling\">Ενεργοποίηση scrobbling</string>\n    <string name=\"lastfm_now_playing\">Αποστολή Τρέχοντος τραγουδιού</string>\n    <string name=\"scrobbling_configuration\">Ρύθμιση Scrobbling</string>\n    <string name=\"scrobble_min_track_duration\">Σκρόμπλαρε τραγούδια μεγαλύτερα από</string>\n    <string name=\"scrobble_delay_percent\">Ποσοστό καθυστέρησης σκρομπλαρίσματος</string>\n    <string name=\"scrobble_delay_minutes\">Λεπτά καθυστέρησης καταγραφής ως «scrobbled»</string>\n    <string name=\"romanize_current_track\">Λατινοποίηση (transliteration) τρέχοντος κομματιού</string>\n    <string name=\"last_fm_send_likes\">Αποστολή Likes/Unlikes</string>\n    <string name=\"about_artist\">Πληροφορίες καλλιτέχνη</string>\n    <string name=\"show_more\">Επίδειξη περισσοτέρων</string>\n    <string name=\"show_less\">Επίδειξη λιγότερων</string>\n    <string name=\"artist_page_settings\">Σελίδα καλλιτέχνη</string>\n    <string name=\"show_artist_description\">Επίδειξη περιγραφής καλλιτέχνη</string>\n    <string name=\"show_artist_subscriber_count\">Επίδειξη ακολούθων</string>\n    <string name=\"show_artist_monthly_listeners\">Επίδειξη μηνιαίων ακροατών</string>\n    <string name=\"download_playlist_desc\">Λήψη όλων των τραγουδιών για ακρόαση εκτός σύνδεσης</string>\n    <string name=\"remove_download_playlist_desc\">Αφαίρεση όλων των ληφθέντων τραγουδιών από αυτήν την λίστα αναπαραγωγής</string>\n    <string name=\"download_in_progress_desc\">Η λήψη είναι σε εξέλιξη</string>\n    <string name=\"share_playlist_desc\">Κοινή χρήση αυτής της λίστας αναπαραγωγής με άλλους</string>\n    <string name=\"delete_playlist_desc\">Διαγραφή αυτής της λίστας αναπαραγωγής οριστικά</string>\n    <string name=\"sync_playlist_desc\">Συγχρονισμός λίστας αναπαραγωγής με το YouTube Music</string>\n    <string name=\"crop_album_art\">Περικοπή εξώφυλλου άλμπουμ</string>\n    <string name=\"crop_album_art_desc\">Επιβολή τετράγωνης αναλογίας διαστάσεων με περικοπή των μικρογραφιών βίντεο</string>\n    <string name=\"primary_color_style\">Κύριο χρώμα</string>\n    <string name=\"tertiary_color_style\">Τριτογενές χρώμα</string>\n    <string name=\"wavy\">Κυματοειδές</string>\n    <string name=\"lyrics_glow_effect\">Ενεργοποίηση εφέ λαμπυρίσματος στίχων</string>\n    <string name=\"lyrics_glow_effect_desc\">Προσθήκη κινούμενης εικόνας λάμψης και εφέ αναπήδησης σε ενεργούς στίχους</string>\n    <string name=\"enable_better_lyrics\">Ενεργοποίηση καλύτερων στοίχων</string>\n    <string name=\"enable_better_lyrics_desc\">Χρησιμοποιήστε την υπηρεσία καλύτερων στοίχων για συγχρονισμένους στίχους λέξη προς λέξη</string>\n    <string name=\"enable_simpmusic\">Ενεργοποίηση στοίχων SimpMusic</string>\n    <string name=\"enable_simpmusic_desc\">Χρήση του παρόχου στίχων SimpMusic για συγχρονισμένους στίχους</string>\n    <string name=\"auto_scroll\">Επανασυγχρονισμός</string>\n    <string name=\"shuffle_playlist_first\">Τυχαία λίστα αναπαραγωγής/άλμπουμ πρώτα</string>\n    <string name=\"shuffle_playlist_first_desc\">Κατά την τυχαία αναπαραγωγή, παίξτε πρώτα όλα τα τραγούδια από την αρχική λίστα αναπαραγωγής/άλμπουμ και μετά παρόμοιο περιεχόμενο</string>\n    <string name=\"show_wrapped_card\">Εμφάνιση κάρτας Wrapped</string>\n    <string name=\"skip_silence_desc\">Γρήγορη προώθηση στα σιωπηλά μέρη τραγουδιών</string>\n    <string name=\"skip_silence_instant\">Άμεση παράλειψη σιωπής</string>\n    <string name=\"skip_silence_instant_desc\">Μετάβαση μπροστά κατά τη διάρκεια των σιωπηλών στιγμών αντί να επιταχύνετε την αναπαραγωγή</string>\n    <string name=\"persistent_shuffle_title\">Διατήρηση τυχαίας αναπαραγωγής</string>\n    <string name=\"persistent_shuffle_desc\">Διατήρηση της τυχαίας αναπαραγωγής κατά την έναρξη νέων τραγουδιών ή λιστών αναπαραγωγής</string>\n    <string name=\"remember_shuffle_and_repeat\">Απομνημόνευση τυχαίας αναπαραγωγής και επανάληψης</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Απομνημόνευση τυχαίας αναπαραγωγής και επανάληψης κατά την επανεκκίνηση της εφαρμογής</string>\n    <string name=\"pause_music_when_media_is_muted\">Παύση μουσικής όταν τα πολυμέσα είναι σε σίγαση</string>\n    <string name=\"lyrics_romanize_chinese\">Λατινοποίηση Κινέζικων στίχων</string>\n    <string name=\"lyrics_offset\">Ρύθμιση Συγχρονισμού Στίχων</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"google_cast_description\">Ενεργοποίηση μετάδοσης ήχου σε Chromecast και άλλες συσκευές με δυνατότητα Casting</string>\n    <string name=\"logging_in\">Σύνδεση…</string>\n    <string name=\"hide_video_songs\">Απόκρυψη τραγουδιών με βίντεο</string>\n    <string name=\"details_desc\">Προβολή πληροφοριών τραγουδιού</string>\n    <string name=\"edit_desc\">Αλλαγή τίτλου ή καλλιτέχνη</string>\n    <string name=\"start_radio_desc\">Δημουργία σταθμού με βάση αυτό το κομμάτι</string>\n    <string name=\"add_to_library_desc\">Αποθήκευση στη βιβλιοθήκη σας</string>\n    <string name=\"download_desc\">Να γίνει διαθέσιμο για αναπαραγωγή εκτός σύνδεσης</string>\n    <string name=\"add_to_playlist_desc\">Προσθήκη σε μια από τις λίστες αναπαραγωγής σας</string>\n    <string name=\"refetch_desc\">Λήψη των πιο πρόσφατων μεταδεδομένων από το YouTube Music</string>\n    <string name=\"share_desc\">Μοιραστείτε έναν σύνδεσμο για αυτό το στοιχείο</string>\n    <string name=\"delete_desc\">Μόνιμη διαγραφή αυτού του στοιχείου</string>\n    <string name=\"advanced_desc\">Αλλαγή του τέμπο και του τόνου του τραγουδιού</string>\n    <string name=\"equalizer_desc\">Ρύθμιση του ισοσταθμιστή ήχου</string>\n    <string name=\"enable_dynamic_icon\">Ενεργοποίηση δυναμικού εικονιδίου</string>\n    <string name=\"cache_size_warning_title\">Περιμένετε!</string>\n    <string name=\"cache_size_warning_message\">Έχετε επιλέξει ένα όριο μεγέθους προσωρινής μνήμης μικρότερο από αυτό που χρησιμοποιεί επί του παρόντος η εφαρμογή (%1$s). Εάν συνεχίσετε, η εφαρμογή ενδέχεται να καταργήσει ορισμένα %2$s για να ταιριάξει με το νέο όριο. Θέλετε να συνεχίσετε;</string>\n    <string name=\"cache_size_warning_confirm\">Συνέχεια</string>\n    <string name=\"karaoke\">Καραόκε</string>\n    <string name=\"apple_music_style\">Apple Music</string>\n    <string name=\"lyrics_text_size\">Μέγεθος κειμένου στίχων</string>\n    <string name=\"wrapped_total_albums_title\">Έχετε ακούσει</string>\n    <string name=\"wrapped_total_albums_subtitle\">μοναδικά άλμπουμ</string>\n    <string name=\"wrapped_top_album_title\">Το κορυφαίο σας άλμπουμ είναι το</string>\n    <string name=\"wrapped_playlist_ready\">Η προσωπική σας λίστα αναπαραγωγής είναι έτοιμη</string>\n    <string name=\"wrapped_top_5_albums_title\">Τα κορυφαία 5 άλμπουμ σας</string>\n    <string name=\"wrapped_album_listening_time\">Έχετε ακούσει αυτό το άλμπουμ %d λεπτά</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d λεπτά</string>\n    <string name=\"wrapped_no_data\">Δεν υπάρχουν δεδομένα</string>\n    <string name=\"wrapped_top_5_artists_title\">Οι κορυφαίοι καλλιτέχνες σας της χρονιάς</string>\n    <string name=\"wrapped_artist_listening_time\">%d λεπτά</string>\n    <string name=\"wrapped_top_5_songs_title\">Τα κορυφαία τραγούδια σας της χρονιάς</string>\n    <string name=\"wrapped_top_artist_title\">Ο κορυφαίος καλλιτέχνης σας της χρονιάς είναι</string>\n    <string name=\"wrapped_top_song_title\">Το τραγούδι που παίζει πιο συχνά είναι</string>\n    <string name=\"wrapped_top_song_listening_time\">Έχετε ακούσει %d λεπτά</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_intro_subtitle\">ήρθε η ώρα να δείτε τι έχετε ακούσει</string>\n    <string name=\"wrapped_intro_button\">Πάμε!</string>\n    <string name=\"wrapped_logo_content_description\">Λογότυπο Metrolist</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"wrapped_ready_subtitle\">Ήρθε η ώρα να δείτε τι λατρέψατε φέτος.</string>\n    <string name=\"wrapped_special_thanks\">Ιδιαίτερες ευχαριστίες στον MO Agamy για τη δημιουργία του Metrolist</string>\n    <string name=\"wrapped_create_playlist\">Δημιουργία λίστας αναπαραγωγής</string>\n    <string name=\"wrapped_playlist_saved\">Η λίστα αναπαραγωγής αποθηκεύτηκε</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"one\">%d Προφίλ</item>\n        <item quantity=\"other\">%d Προφίλ</item>\n    </plurals>\n    <string name=\"equalizer_header\">Ισοσταθμιστής</string>\n    <string name=\"no_profiles\">Δεν υπάρχουν προφίλ ισοσταθμιστή</string>\n    <string name=\"import_profile\">Εισαγωγή Προφίλ</string>\n    <string name=\"system_equalizer\">Ισοσταθμιστής Συστήματος</string>\n    <string name=\"eq_disabled\">Απενεργοποιημένο</string>\n    <plurals name=\"band_count\">\n        <item quantity=\"one\">%d συγκρότημα</item>\n        <item quantity=\"other\">%d συγκροτήματα</item>\n    </plurals>\n    <string name=\"delete_profile_desc\">Διαγραφή Προφίλ</string>\n    <string name=\"delete_profile_confirmation\">Είστε σίγουροι ότι θέλετε να διαγράψετε το %1$s; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.</string>\n    <string name=\"error_file_read\">Δεν ήταν δυνατή η ανάγνωση του αρχείου</string>\n    <string name=\"error_file_open\">Αποτυχία ανοίγματος αρχείου: %1$s</string>\n    <string name=\"import_error_title\">Σφάλμα Εισαγωγής</string>\n    <string name=\"error_title\">Σφάλμα</string>\n    <string name=\"error_eq_apply_failed\">Αποτυχία εφαρμογής προφίλ ισοσταθμιστή: %1$s</string>\n    <string name=\"casting_to\">Γίνεται casting στο %s</string>\n    <string name=\"progress_percent\">Πρόοδος %s%%</string>\n    <string name=\"open\">Άνοιγμα</string>\n    <string name=\"error_playback_failed\">Η αναπαραγωγή απέτυχε</string>\n    <string name=\"no_song_playing\">Δεν παίζει κανένα τραγούδι</string>\n    <string name=\"tap_to_open\">Πατήστε για να ανοίξετε το Metrolist</string>\n    <string name=\"previous\">Προηγούμενο</string>\n    <string name=\"play_pause\">Αναπαραγωγή/Παύση</string>\n    <string name=\"next\">Επόμενο</string>\n    <string name=\"like\">Μου αρέσει</string>\n    <string name=\"widget_description\">Widget αναπαραγωγής μουσικής με χειριστήρια αναπαραγωγής</string>\n    <string name=\"turntable_widget_description\">Γρήγορη πρόσβαση στο πιο πρόσφατο κομμάτι που ακούσατε</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">Διατήρηση της οθόνης ενεργοποιημένης όταν το πρόγραμμα αναπαραγωγής έχει επεκταθεί</string>\n    <string name=\"last_fm_send_likes_description\">Να γίνονται τα τραγούδια Love/Unlove όταν γίνεται Like/Unlike στο Metrolist</string>\n    <string name=\"play_next_desc\">Προσθήκη στην κορυφή της σειράς αναπαραγωγής</string>\n    <string name=\"add_to_queue_desc\">Προσθήκη στο τέλος της σειράς αναπαραγωγής</string>\n    <string name=\"mini_player\">Μίνι πρόγραμμα αναπαραγωγής</string>\n    <string name=\"pure_black_mini_player\">Μίνι πρόγραμμα αναπαραγωγής σε εμφάνιση pure black</string>\n    <string name=\"lyrics_animation_style\">Στυλ animation λέξη προς λέξη</string>\n    <string name=\"none\">Κανένα</string>\n    <string name=\"fade\">Σβήσιμο</string>\n    <string name=\"glow\">Λάμψη</string>\n    <string name=\"slide\">Ολίσθηση</string>\n    <string name=\"lyrics_line_spacing\">Διάστημα μεταξύ των στίχων</string>\n    <string name=\"album_art_for\">Εξώφυλλο άλμπουμ για %s</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">Εξώφυλλο άλμπουμ</string>\n    <string name=\"wrapped_top_artist_image_content_description\">Εικόνα κορυφαίου καλλιτέχνη</string>\n    <string name=\"wrapped_top_artist_listening_time\">Έχετε ακούσει για %d λεπτά</string>\n    <string name=\"wrapped_total_artists_title\">Έχετε ακούσει</string>\n    <string name=\"wrapped_total_artists_subtitle\">μοναδικούς καλλιτέχνες</string>\n    <string name=\"wrapped_total_songs_title\">Έχετε ακούσει</string>\n    <string name=\"wrapped_total_songs_subtitle\">μοναδικά τραγούδια</string>\n    <string name=\"wrapped_ready_title\">Η WRAPPED ΣΑΣ ΕΙΝΑΙ ΕΤΟΙΜΗ!</string>\n    <string name=\"wrapped_thank_you\">Σας ευχαριστούμε για την ακρόαση</string>\n    <string name=\"wrapped_close\">Κλείσιμο wrapped</string>\n    <string name=\"wrapped_playlist_title\">To Wrapped σας %s</string>\n    <string name=\"listening_to_metrolist\">Ακούτε το Metrolist</string>\n    <string name=\"failed_to_create_image\">Αποτυχία δημιουργίας εικόνας: %s</string>\n    <string name=\"copied_title\">Ο Τίτλος Αντιγράφηκε</string>\n    <string name=\"copied_artist\">Ο Καλλιτέχνης Αντιγράφηκε</string>\n    <string name=\"error_playing\">Σφάλμα αναπαραγωγής</string>\n    <string name=\"failed_to_parse_proxy\">Αποτυχία ανάλυσης του url του proxy.</string>\n    <string name=\"album_art\">Εξώφυλλο άλμπουμ</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-el/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Αρχική</string>\n    <string name=\"songs\">Τραγούδια</string>\n    <string name=\"artists\">Καλλιτέχνες</string>\n    <string name=\"albums\">Άλμπουμ</string>\n    <string name=\"playlists\">Λίστες</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d επιλεγμένο</item>\n        <item quantity=\"other\">%d επιλεγμένα</item>\n    </plurals>\n    <string name=\"history\">Ιστορικό</string>\n    <string name=\"stats\">Στατιστικά</string>\n    <string name=\"mood_and_genres\">Διάθεση και Είδη</string>\n    <string name=\"account\">Λογαριασμός</string>\n    <string name=\"quick_picks\">Γρήγορες επιλογές</string>\n    <string name=\"quick_picks_empty\">Ακούστε σε τραγούδια για να δημιουργήσετε τις γρήγορες επιλογές σας</string>\n    <string name=\"forgotten_favorites\">Ξεχασμένα αγαπημένα</string>\n    <string name=\"keep_listening\">Συνέχεια ακρόασης</string>\n    <string name=\"similar_to\">Παρόμοια με</string>\n    <string name=\"new_release_albums\">Άλμπουμ νέας κυκλοφορίας</string>\n    <string name=\"today\">Σήμερα</string>\n    <string name=\"yesterday\">Εχθές</string>\n    <string name=\"this_week\">Αυτήν την εβδομάδα</string>\n    <string name=\"last_week\">Τελευταία εβδομάδα</string>\n    <string name=\"most_played_songs\">Τα πιο πολυπαιγμένα τραγούδια</string>\n    <string name=\"most_played_artists\">Οι πιο πολυπαιγμένοι καλλιτέχνες</string>\n    <string name=\"most_played_albums\">Τα πιο πολυπαιγμένα άλμπουμ</string>\n    <string name=\"search\">Αναζήτηση</string>\n    <string name=\"search_yt_music\">Αναζήτηση Μουσικής YouTube…</string>\n    <string name=\"search_library\">Αναζήτηση βιβλιοθήκης…</string>\n    <string name=\"filter_library\">Βιβλιοθήκη</string>\n    <string name=\"filter_liked\">Αγαπημένα</string>\n    <string name=\"filter_downloaded\">Κατεβασμένα</string>\n    <string name=\"filter_all\">Όλα</string>\n    <string name=\"filter_songs\">Τραγούδια</string>\n    <string name=\"filter_videos\">Βίντεο</string>\n    <string name=\"filter_albums\">Άλμπουμ</string>\n    <string name=\"filter_artists\">Καλλιτέχνες</string>\n    <string name=\"filter_playlists\">Λίστες</string>\n    <string name=\"filter_community_playlists\">Λίστες αναπαρ. κοινότητας</string>\n    <string name=\"filter_featured_playlists\">Προτεινόμενες λίστες αναπαραγωγής</string>\n    <string name=\"filter_bookmarked\">Σελιδοδείκτες</string>\n    <string name=\"no_results_found\">Δεν βρέθηκαν αποτελέσματα</string>\n    <string name=\"library_song_empty\">Τα τραγούδια βιβλιοθήκης εμφανίζονται εδώ</string>\n    <string name=\"library_artist_empty\">Οι καλλιτέχνες βιβλιοθήκης εμφανίζονται εδώ</string>\n    <string name=\"library_album_empty\">Τα άλμπουμ βιβλιοθήκης εμφανίζονται εδώ</string>\n    <string name=\"library_playlist_empty\">Οι λίστες αναπαραγωγής θα εμφανίζονται εδώ</string>\n    <string name=\"from_your_library\">Από τη βιβλιοθήκη σας</string>\n    <string name=\"liked_songs\">Αγαπημένα τραγούδια</string>\n    <string name=\"downloaded_songs\">Κατεβασμένα τραγούδια</string>\n    <string name=\"playlist_is_empty\">Η λίστα αναπαρ. είναι κενή</string>\n    <string name=\"remove_download_playlist_confirm\">Θέλετε πραγματικά να αφαιρέσετε όλα τα τραγούδια της λίστας αναπαραγωγής \\\\\\\"%s\\\\\\\" από τα Ληφθέντα Τραγούδια;</string>\n    <string name=\"delete_playlist_confirm\">Θέλετε πραγματικά να διαγράψετε τη λίστα αναπαραγωγής \\\"%s\\\";</string>\n    <string name=\"retry\">Επανάληψη</string>\n    <string name=\"radio\">Ραδιόφωνο</string>\n    <string name=\"shuffle\">Ανακάτεμα</string>\n    <string name=\"reset\">Επαναφορά</string>\n    <string name=\"details\">Λεπτομέρειες</string>\n    <string name=\"edit\">Επεξεργασία</string>\n    <string name=\"start_radio\">Έναρξη ραδιοφώνου</string>\n    <string name=\"play\">Αναπαραγωγή</string>\n    <string name=\"play_next\">Αναπαραγωγή επόμενου</string>\n    <string name=\"add_to_queue\">Προσθήκη στην ουρά</string>\n    <string name=\"add_to_library\">Προσθήκη στη βιβλιοθήκη</string>\n    <string name=\"remove_from_library\">Αφαίρεση από τη βιβλιοθήκη</string>\n    <string name=\"action_download\">Λήψη</string>\n    <string name=\"downloading\">Γίνεται λήψη</string>\n    <string name=\"remove_download\">Αφαίρεση λήψης</string>\n    <string name=\"import_playlist\">Εισαγωγή λίστας</string>\n    <string name=\"add_to_playlist\">Προσθήκη σε λίστα</string>\n    <string name=\"view_artist\">Προβολή καλλιτέχνη</string>\n    <string name=\"view_album\">Προβολή άλμπουμ</string>\n    <string name=\"refetch\">Ανάκτηση</string>\n    <string name=\"share\">Κοινή χρήση</string>\n    <string name=\"delete\">Διαγραφή</string>\n    <string name=\"remove_from_history\">Αφαίρεση από το ιστορικό</string>\n    <string name=\"remove_from_playlist\">Αφαίρεση από λίστα</string>\n    <string name=\"remove_from_queue\">Αφαίρεση από την ουρά</string>\n    <string name=\"search_online\">Διαδίκτυο</string>\n    <string name=\"action_sync\">Συγχρονισμός</string>\n    <string name=\"advanced\">Προχωρημένο</string>\n    <string name=\"tempo_and_pitch\">Τέμπο και Τάση</string>\n    <string name=\"sort_by_create_date\">Ημερομηνία προσθήκης</string>\n    <string name=\"sort_by_name\">Όνομα</string>\n    <string name=\"sort_by_artist\">Καλλιτέχνης</string>\n    <string name=\"sort_by_year\">Έτος</string>\n    <string name=\"sort_by_song_count\">Αριθμός τραγουδιών</string>\n    <string name=\"sort_by_length\">Μήκος</string>\n    <string name=\"sort_by_play_time\">Χρόνος παιχνιδιού</string>\n    <string name=\"sort_by_custom\">Προσαρμοσμένη σειρά</string>\n    <string name=\"media_id\">Id πολυμέσων</string>\n    <string name=\"mime_type\">Τύπος MIME</string>\n    <string name=\"codecs\">Κωδικοποιητές</string>\n    <string name=\"bitrate\">Ρυθμός bitrate</string>\n    <string name=\"sample_rate\">Ρυθμός δειγματοληψίας</string>\n    <string name=\"loudness\">Ηχηρότητα</string>\n    <string name=\"volume\">Τόμος</string>\n    <string name=\"file_size\">Μέγεθος αρχείου</string>\n    <string name=\"unknown\">Άγνωστο</string>\n    <string name=\"copied\">Αντιγράφηκε στο πρόχειρο</string>\n    <string name=\"edit_lyrics\">Επεξεργασία στίχων</string>\n    <string name=\"search_lyrics\">Αναζήτηση στίχων</string>\n    <string name=\"edit_song\">Επεξεργασία τραγουδιού</string>\n    <string name=\"song_title\">Τίτλος τραγουδιού</string>\n    <string name=\"song_artists\">Καλλιτέχνες τραγουδιών</string>\n    <string name=\"error_song_title_empty\">Ο τίτλος τραγουδιού δεν μπορεί να είναι άδειος.</string>\n    <string name=\"error_song_artist_empty\">Ο καλλιτέχνης του τραγουδιού δεν μπορεί να είναι άδειος.</string>\n    <string name=\"save\">Αποθήκευση</string>\n    <string name=\"choose_playlist\">Επιλογή λίστας</string>\n    <string name=\"edit_playlist\">Επεξεργασία λίστας</string>\n    <string name=\"create_playlist\">Δημιουργία λίστας</string>\n    <string name=\"playlist_name\">Όνομα λίστας</string>\n    <string name=\"error_playlist_name_empty\">Το όνομα της λίστας αναπαραγωγής δεν μπορεί να είναι κενό.</string>\n    <string name=\"edit_artist\">Επεξεργασία καλλιτέχνη</string>\n    <string name=\"artist_name\">Όνομα καλλιτέχνη</string>\n    <string name=\"error_artist_name_empty\">Το όνομα του καλλιτέχνη δεν μπορεί να είναι κενό.</string>\n    <string name=\"duplicates\">Διπλότυπα</string>\n    <string name=\"skip_duplicates\">Παράλειψη διπλοτύπων</string>\n    <string name=\"add_anyway\">Προσθήκη ούτως ή άλλως</string>\n    <string name=\"duplicates_description_single\">Το τραγούδι είναι ήδη στη λίστα αναπαρ. σας</string>\n    <string name=\"duplicates_description_multiple\">%d τραγούδια είναι ήδη στη λίστα αναπαρ. σας</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d τραγούδι</item>\n        <item quantity=\"other\">%d τραγούδια</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d καλλιτέχνης</item>\n        <item quantity=\"other\">%d καλλιτέχνες</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d άλμπουμ</item>\n        <item quantity=\"other\">%d άλμπουμς</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d λίστα</item>\n        <item quantity=\"other\">%d λίστες</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d εβδομάδα</item>\n        <item quantity=\"other\">%d εβδομάδες</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d μήνας</item>\n        <item quantity=\"other\">%d μήνες</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d έτος</item>\n        <item quantity=\"other\">%d χρόνια</item>\n    </plurals>\n    <string name=\"playlist_imported\">Λίστα αναπαραγωγής εισήχθη</string>\n    <string name=\"removed_song_from_playlist\">Αφαίρεση του \\\"%s\\\" από τη λίστα</string>\n    <string name=\"playlist_synced\">Λίστα αναπαραγωγής συγχρονισμένη</string>\n    <string name=\"undo\">Αναίρεση</string>\n    <string name=\"lyrics_not_found\">Δεν βρέθηκαν στίχοι</string>\n    <string name=\"sleep_timer\">Χρονοδιακόπτης ύπνου</string>\n    <string name=\"end_of_song\">Τέλος τραγουδιού</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">1 λεπτό</item>\n        <item quantity=\"other\">%d λεπτά</item>\n    </plurals>\n    <string name=\"error_no_stream\">Δεν υπάρχει διαθέσιμη ροή</string>\n    <string name=\"error_no_internet\">Δεν υπάρχει σύνδεση δικτύου</string>\n    <string name=\"error_timeout\">Τέλος χρονικού ορίου</string>\n    <string name=\"error_unknown\">Άγνωστο σφάλμα</string>\n    <string name=\"action_like\">Αγαπημένο</string>\n    <string name=\"action_like_all\">Αγαπημένα όλα</string>\n    <string name=\"action_remove_like\">Αφαίρεση αγαπημένο</string>\n    <string name=\"action_remove_like_all\">Αφαίρεση όλων των αγαπημένων</string>\n    <string name=\"action_shuffle_on\">Ανακάτεμα ενεργό</string>\n    <string name=\"action_shuffle_off\">Ανακάτεμα ανενεργό</string>\n    <string name=\"repeat_mode_off\">Λειτουργία επανάληψης ανενεργό</string>\n    <string name=\"repeat_mode_one\">Επανάληψη τρέχοντος τραγουδιού</string>\n    <string name=\"repeat_mode_all\">Επανάληψη ουράς</string>\n    <string name=\"queue_all_songs\">Όλα τα τραγούδια</string>\n    <string name=\"queue_searched_songs\">Αναζητημένα τραγούδια</string>\n    <string name=\"music_player\">Αναπαραγωγέας Μουσικής</string>\n    <string name=\"settings\">Ρυθμίσεις</string>\n    <string name=\"appearance\">Εμφάνιση</string>\n    <string name=\"theme\">Θέμα</string>\n    <string name=\"enable_dynamic_theme\">Ενεργοποίηση δυναμικού θέματος</string>\n    <string name=\"dark_theme\">Σκοτεινό θέμα</string>\n    <string name=\"dark_theme_on\">Ενεργό</string>\n    <string name=\"dark_theme_off\">Ανενεργό</string>\n    <string name=\"dark_theme_follow_system\">Ακολουθεί το σύστημα</string>\n    <string name=\"pure_black\">Καθαρό μαύρο</string>\n    <string name=\"customize_navigation_tabs\">Προσαρμογή καρτελών πλοήγησης</string>\n    <string name=\"player\">Aναπαραγωγέας</string>\n    <string name=\"player_text_alignment\">Ευθυγράμμιση κειμένου αναπαραγωγέα</string>\n    <string name=\"lyrics_text_position\">Θέση κειμένου στίχων</string>\n    <string name=\"sided\">Πλευρά</string>\n    <string name=\"left\">Αριστερά</string>\n    <string name=\"center\">Κέντρο</string>\n    <string name=\"right\">Δεξιά</string>\n    <string name=\"player_slider_style\">Στυλ ρυθμιστικού αναπαραγωγέα</string>\n    <string name=\"default_\">Προεπιλογή</string>\n    <string name=\"squiggly\">Στριφογυριστό</string>\n    <string name=\"misc\">Διάφορα</string>\n    <string name=\"default_open_tab\">Προεπιλεγμένη ανοικτή καρτέλα</string>\n    <string name=\"grid_cell_size\">Μέγεθος κελιού πλέγματος</string>\n    <string name=\"small\">Μικρό</string>\n    <string name=\"big\">Μεγάλο</string>\n    <string name=\"content\">Περιεχόμενο</string>\n    <string name=\"login\">Σύνδεση</string>\n    <string name=\"not_logged_in\">Δεν έχετε συνδεθεί</string>\n    <string name=\"content_language\">Προεπιλεγμένη γλώσσα περιεχομένου</string>\n    <string name=\"content_country\">Προεπιλεγμένη χώρα περιεχομένου</string>\n    <string name=\"system_default\">Προεπιλογή συστήματος</string>\n    <string name=\"enable_proxy\">Ενεργοποίηση μεσολάβησης</string>\n    <string name=\"proxy_type\">Τύπος μεσολάβησης</string>\n    <string name=\"proxy_url\">URL μεσολάβησης</string>\n    <string name=\"restart_to_take_effect\">Επανεκκίνηση για να τεθεί σε ισχύ</string>\n    <string name=\"player_and_audio\">Αναπαραγωγέας και ήχος</string>\n    <string name=\"audio_quality\">Ποιότητα ήχου</string>\n    <string name=\"audio_quality_auto\">Αυτόματο</string>\n    <string name=\"audio_quality_high\">Υψηλή</string>\n    <string name=\"audio_quality_low\">Χαμηλή</string>\n    <string name=\"queue\">Ουρά</string>\n    <string name=\"persistent_queue\">Επίμονη ουρά</string>\n    <string name=\"persistent_queue_desc\">Επαναγορά τελευταίας ουράς κατά την εκκίνηση εφαρμογής</string>\n    <string name=\"skip_silence\">Παράλειψη σιωπής</string>\n    <string name=\"audio_normalization\">Κανονικοποίηση ήχου</string>\n    <string name=\"auto_skip_next_on_error\">Αυτόματη μετάβαση στο επόμενο τραγούδι όταν προκύψει σφάλμα</string>\n    <string name=\"auto_skip_next_on_error_desc\">Εξασφάλιση της συνεχούς εμπειρίας αναπαραγωγής σας</string>\n    <string name=\"stop_music_on_task_clear\">Διακοπή μουσικής στην εργασία εκκαθάριση</string>\n    <string name=\"equalizer\">Ισοσταθμιστής</string>\n    <string name=\"storage\">Αποθήκευση</string>\n    <string name=\"cache\">Κρυφή μνήμη</string>\n    <string name=\"image_cache\">Κρυφή Μνήμη Εικόνας</string>\n    <string name=\"song_cache\">Κρυφή Μνήμη Τραγουδιών</string>\n    <string name=\"max_cache_size\">Μέγιστο μέγεθος κρυφής μνήμης</string>\n    <string name=\"unlimited\">Απεριόριστα</string>\n    <string name=\"clear_all_downloads\">Εκκαθάριση όλων των λήψεων</string>\n    <string name=\"max_image_cache_size\">Μέγιστο μέγεθος κρυφής μνήμης εικόνων</string>\n    <string name=\"clear_image_cache\">Εκκαθάριση προσωρινής μνήμης εικόνας</string>\n    <string name=\"max_song_cache_size\">Μέγιστο μέγεθος κρυφής μνήμης τραγουδιού</string>\n    <string name=\"clear_song_cache\">Εκκαθάριση προσωρινής μνήμης τραγουδιών</string>\n    <string name=\"size_used\">%s χρησιμοποιείται</string>\n    <string name=\"privacy\">Απόρρητο</string>\n    <string name=\"pause_listen_history\">Παύση ιστορικού ακρόασης</string>\n    <string name=\"clear_listen_history\">Εκκαθάριση ιστορικού ακρόασης</string>\n    <string name=\"clear_listen_history_confirm\">Είστε σίγουροι ότι θέλετε να διαγράψετε όλο το ιστορικό ακροάσεων;</string>\n    <string name=\"pause_search_history\">Παύση ιστορικού αναζήτησης</string>\n    <string name=\"clear_search_history\">Εκκαθάριση ιστορικού αναζήτησης</string>\n    <string name=\"clear_search_history_confirm\">Είστε σίγουροι ότι θέλετε να διαγράψετε όλο το ιστορικό αναζήτησης;</string>\n    <string name=\"enable_lrclib\">Ενεργοποίηση του παρόχου στίχων LrcLib</string>\n    <string name=\"enable_kugou\">Ενεργοποίηση παρόχου στίχων KuGou</string>\n    <string name=\"hide_explicit\">Απόκρυψη ρητού περιεχομένου</string>\n    <string name=\"backup_restore\">Αντίγραφα ασφ. και επαναφορά</string>\n    <string name=\"action_backup\">Αντίγραφο ασφαλείας</string>\n    <string name=\"action_restore\">Επαναφορά</string>\n    <string name=\"imported_playlist\">Έγινε εισαγωγή της λίστας</string>\n    <string name=\"backup_create_success\">Δημιουργία αντιγράφου ασφ. με επιτυχία</string>\n    <string name=\"backup_create_failed\">Αδυναμία δημιουργίας αντιγράφων ασφαλείας</string>\n    <string name=\"restore_failed\">Αποτυχία επαναφοράς αντιγράφου ασφαλείας</string>\n    <string name=\"discord_integration\">Ενσωμάτωση Discord</string>\n    <string name=\"discord_information\">Το Metrolist χρησιμοποιεί τη βιβλιοθήκη KizzyRPC για να ρυθμίσει την κατάσταση του λογαριασμού σας στο Discord. Αυτό περιλαμβάνει τη χρήση της σύνδεσης Discord Gateway, η οποία μπορεί να θεωρηθεί παραβίαση των TOS του Discord. Ωστόσο, δεν υπάρχουν γνωστές περιπτώσεις αναστολής λογαριασμών χρηστών για αυτόν τον λόγο. Η χρήση γίνεται με δική σας ευθύνη.\\n\\nΤο Metrolist θα εξάγει μόνο το token σας και όλα τα υπόλοιπα αποθηκεύονται τοπικά.</string>\n    <string name=\"dismiss\">Απόρριψη</string>\n    <string name=\"options\">Επιλογές</string>\n    <string name=\"preview\">Προεπισκόπηση</string>\n    <string name=\"login_failed\">Η σύνδεση απέτυχε</string>\n    <string name=\"action_logout\">Αποσύνδεση</string>\n    <string name=\"enable_discord_rpc\">Ενεργοποίηση Πλούσιας Παρουσίας</string>\n    <string name=\"about\">Σχετικά</string>\n    <string name=\"app_version\">Έκδοση εφαρμογής</string>\n    <string name=\"new_version_available\">Νέα διαθέσιμη έκδοση</string>\n    <string name=\"translation_models\">Μοντέλα Μετάφρασης</string>\n    <string name=\"clear_translation_models\">Εκκάθαριση μοντέλων μετάφρασης</string>\n    <string name=\"auto_load_more_desc\">Αυτόματη προσθήκη περισσότερων τραγουδιών όταν το τέλος της ουράς επιτυγχάνεται, αν είναι δυνατόν</string>\n    <string name=\"listen_history\">Ιστορικό ακρόασης</string>\n    <string name=\"disable_screenshot_desc\">Όταν αυτή η επιλογή είναι ενεργή, τα στιγμιότυπα και η Πρόσφατη προβολή εφαρμογών θα είναι ανενεργή.</string>\n    <string name=\"your_youtube_playlists\">Οι λίστες YouTube</string>\n    <string name=\"other_versions\">Άλλες εκδόσεις</string>\n    <string name=\"remove_all_from_library\">Αφαίρεση όλων από την βιβλιοθήκη</string>\n    <string name=\"add_all_to_library\">Προσθήκη όλων στη βιβλιοθήκη</string>\n    <string name=\"auto_load_more\">Αυτόματη φόρτωση περισσότερων τραγουδιών</string>\n    <string name=\"search_history\">Ιστορικό αναζήτησης</string>\n    <string name=\"disable_screenshot\">Απενεργοποίηση στιγμιότυπων οθόνης</string>\n    <string name=\"action_login\">Συνδεθείτε</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-en-rCA/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"forgotten_favorites\">Forgotten favourites</string>\n    <string name=\"center\">Centre</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-es/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">Local</string>\n    <string name=\"remote_history\">Remoto</string>\n    <string name=\"charts\">Listas de éxitos</string>\n    <string name=\"back_button_desc\">Atrás</string>\n    <string name=\"album_cover_desc\">Portada del álbum</string>\n    <string name=\"top_music_videos\">Top vídeos musicales</string>\n    <string name=\"trending\">Tendencias</string>\n    <string name=\"weeks\">Semanas</string>\n    <string name=\"months\">Meses</string>\n    <string name=\"years\">Años</string>\n    <string name=\"continuous\">Continuo</string>\n    <string name=\"liked\">Canciones que me gustan</string>\n    <string name=\"offline\">Descargado</string>\n    <string name=\"my_top\">Mi Top</string>\n    <string name=\"cached_playlist\">En caché</string>\n    <string name=\"sync_playlist\">Sincronizar lista de reproducción</string>\n    <string name=\"sync_disabled\">Sincronización deshabilitada</string>\n    <string name=\"allows_for_sync_witch_youtube\">Nota: Esto permite la sincronización con YouTube Music. Esto NO se puede cambiar más tarde.</string>\n    <string name=\"remove_from_cache\">Eliminar de la caché</string>\n    <string name=\"copy_link\">Copiar enlace</string>\n    <string name=\"select\">Seleccionar todo</string>\n    <string name=\"like_all\">Me gusta a todo</string>\n    <string name=\"dislike_all\">No me gusta a todo</string>\n    <string name=\"sort_by_last_updated\">Fecha de actualización</string>\n    <string name=\"link_copied\">Enlace copiado al portapapeles</string>\n    <string name=\"lyrics\">Letra</string>\n    <string name=\"already_in_playlist\">Ya está en la lista de reproducción:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d vez</item>\n        <item quantity=\"many\">%d de veces</item>\n        <item quantity=\"other\">%d veces</item>\n    </plurals>\n    <string name=\"similar_content\">Contenido similar</string>\n    <string name=\"player_background_style\">Estilo de fondo del reproductor</string>\n    <string name=\"follow_theme\">Seguir tema</string>\n    <string name=\"gradient\">Gradiente</string>\n    <string name=\"player_background_blur\">Desenfoque</string>\n    <string name=\"player_buttons_style\">Colores de los botones del reproductor</string>\n    <string name=\"default_style\">Predeterminado</string>\n    <string name=\"enable_swipe_thumbnail\">Habilitar deslizar para cambiar de canción</string>\n    <string name=\"swipe_song_to_add\">Deslizar la canción a la derecha para reproducirla a continuación o a la izquierda para añadirla a la cola</string>\n    <string name=\"lyrics_click_change\">Cambiar letra al hacer clic</string>\n    <string name=\"slim\">Delgado</string>\n    <string name=\"slim_navbar\">Barra de navegación inferior delgada</string>\n    <string name=\"auto_playlists\">Listas de reproducción automáticas</string>\n    <string name=\"show_liked_playlist\">Ver lista de reproducción \\\"Canciones que me gustan\\\"</string>\n    <string name=\"show_downloaded_playlist\">Ver lista de reproducción \\\"Descargado\\\"</string>\n    <string name=\"show_top_playlist\">Ver lista de reproducción \\\"Top\\\"</string>\n    <string name=\"show_cached_playlist\">Ver lista de reproducción \\\"En caché\\\"</string>\n    <string name=\"advanced_login\">Iniciar sesión con token</string>\n    <string name=\"token_hidden\">Toca para mostrar el token</string>\n    <string name=\"token_shown\">Toca de nuevo para copiar o editar</string>\n    <string name=\"token_adv_login_description\">Este es un método de inicio de sesión AVANZADO. Como alternativa al portal web, puedes introducir o actualizar directamente tu token de inicio de sesión aquí. Por ejemplo, esto puede acelerar el inicio de sesión en algunos dispositivos. Ten en cuenta que cualquier formato de token inválido que la aplicación no pueda analizar no será aceptado</string>\n    <string name=\"general\">General</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"default_lib_chips\">Cambiar chip de biblioteca predeterminado</string>\n    <string name=\"set_quick_picks\">Establecer selecciones rápidas</string>\n    <string name=\"last_song_listened\">Basado en la última canción escuchada</string>\n    <string name=\"app_language\">Idioma de la aplicación</string>\n    <string name=\"enable_similar_content\">Habilitar contenido similar</string>\n    <string name=\"similar_content_desc\">Añadir automáticamente más canciones similares cuando se alcance el final de la cola</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"clear_song_cache_dialog\">¿Estás seguro de que quieres borrar todas las canciones en caché?</string>\n    <string name=\"clear_downloads_dialog\">¿Estás seguro de que quieres borrar todas las descargas?</string>\n    <string name=\"not_logged_in_youtube\">No has iniciado sesión en YouTube</string>\n    <string name=\"default_links\">Abrir enlaces compatibles</string>\n    <string name=\"open_app_settings_error\">No se pudieron abrir los ajustes de la aplicación</string>\n    <string name=\"release_notes\">Notas de la versión</string>\n    <string name=\"all_time\">Desde siempre</string>\n    <string name=\"past_24_hours\">Últimas 24 horas</string>\n    <string name=\"past_week\">Semana pasada</string>\n    <string name=\"past_month\">Mes pasado</string>\n    <string name=\"past_year\">Año pasado</string>\n    <string name=\"top_length\">Longitud de la lista Mi Top</string>\n    <string name=\"history_duration\">Duración del historial</string>\n    <string name=\"information\">Información</string>\n    <string name=\"description\">Descripción</string>\n    <string name=\"views\">Visitas</string>\n    <string name=\"likes\">Me gustas</string>\n    <string name=\"dislikes\">No me gustas</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">1 segundo</item>\n        <item quantity=\"many\">%d de segundos</item>\n        <item quantity=\"other\">%d segundos</item>\n    </plurals>\n    <string name=\"please_wait\">Por favor, espera</string>\n    <string name=\"cancel\">Cancelar</string>\n    <string name=\"share_lyrics\">Compartir letra</string>\n    <string name=\"share_as_text\">Compartir como texto</string>\n    <string name=\"share_as_image\">Compartir como imagen</string>\n    <string name=\"customize_colors\">Personalizar colores</string>\n    <string name=\"background_color\">Color de fondo</string>\n    <string name=\"auto_download_on_like\">Descargar automáticamente al dar me gusta</string>\n    <string name=\"generating_image\">Generando imagen</string>\n    <string name=\"text_color\">Color del texto</string>\n    <string name=\"auto_download_on_like_desc\">Descargar canciones automáticamente al darle a me gusta</string>\n    <string name=\"max_selection_limit\">Límite máximo de selección</string>\n    <string name=\"share_selected\">Compartir selección</string>\n    <string name=\"secondary_text_color\">Color del texto secundario</string>\n    <string name=\"lyrics_auto_scroll\">Desplazamiento automático de la letra</string>\n    <string name=\"import_csv\">Importar listas de reproducción CSV</string>\n    <string name=\"import_online\">Importar listas de reproducción M3U</string>\n    <string name=\"playlist_add_local_to_synced_note\">Nota: No se permite agregar canciones locales a listas de reproducción sincronizadas o remotas. Cualquier otra combinación es válida</string>\n    <string name=\"new_player_design\">Nuevo diseño del reproductor</string>\n    <string name=\"lyrics_romanize_japanese\">Romanizar letras japonesas</string>\n    <string name=\"lyrics_romanize_korean\">Romanizar letras coreanas</string>\n    <string name=\"yt_sync\">Sincronización automática con la cuenta</string>\n    <string name=\"more_content\">Más contenido</string>\n    <string name=\"swipe_sensitivity\">Sensibilidad al deslizar el mini reproductor</string>\n    <string name=\"clear_image_cache_dialog\">¿Estás seguro de que deseas borrar todas las imágenes almacenadas en caché?</string>\n    <string name=\"disable\">Deshabilitar</string>\n    <string name=\"subscribe\">Suscribirme</string>\n    <string name=\"subscribed\">Suscrito</string>\n    <string name=\"new_mini_player_design\">Nuevo diseño del mini reproductor</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"now_playing\">Reproduciendo</string>\n    <string name=\"seek_forward_dynamic\">+%1$d segundos hacia adelante</string>\n    <string name=\"seek_backward_dynamic\">-%1$d segundos hacia atrás</string>\n    <string name=\"seek_seconds_addup\">Búsqueda progresiva</string>\n    <string name=\"seek_seconds_addup_description\">Si está habilitado, agrega 5 segundos adicionales de forma incremental en cada salto de búsqueda</string>\n    <string name=\"disable_load_more_when_repeat_all\">Deshabilitar cargar más al repetir todo</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">No cargar automáticamente más canciones ni contenido similar cuando el modo repetir todo esté habilitado</string>\n    <string name=\"close\">Cerrar</string>\n    <string name=\"hide_player_thumbnail\">Ocultar miniatura del reproductor</string>\n    <string name=\"hide_player_thumbnail_desc\">Reemplazar la carátula del álbum con el logotipo de la aplicación en el reproductor</string>\n    <string name=\"settings_section_ui\">Interfaz</string>\n    <string name=\"settings_section_privacy\">Privacidad y seguridad</string>\n    <string name=\"settings_section_player_content\">Reproductor y contenido</string>\n    <string name=\"settings_section_storage\">Almacenamiento y datos</string>\n    <string name=\"settings_section_system\">Sistema y Acerca de</string>\n    <string name=\"starting_radio\">Iniciando radio</string>\n    <string name=\"config_proxy\">Configurar proxy</string>\n    <string name=\"proxy_username\">Nombre de usuario proxy</string>\n    <string name=\"proxy_password\">Contraseña de proxy</string>\n    <string name=\"enable_authentication\">Habilitar autenticación</string>\n    <string name=\"lyrics_romanization_cyrillic\">Cirílico</string>\n    <string name=\"lyrics_romanize_title\">Romanización</string>\n    <string name=\"lyrics_romanization\">Romanización de letras</string>\n    <string name=\"lyrics_romanize_russian\">Romanizar letras en ruso</string>\n    <string name=\"lyrics_romanize_ukrainian\">Romanizar letras en ucraniano</string>\n    <string name=\"lyrics_romanize_belarusian\">Romanizar letras en bielorruso</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Romanizar la letra del kirguís</string>\n    <string name=\"lyrics_romanize_serbian\">Romanizar letras en serbio</string>\n    <string name=\"lyrics_romanize_bulgarian\">Romanizar letras en búlgaro</string>\n    <string name=\"line_by_line_option_title\">EXPERIMENTAL: Detectar el lenguaje línea por línea</string>\n    <string name=\"line_by_line_option_desc\">El idioma cirílico se detectará línea por línea en lugar de toda la canción.</string>\n    <string name=\"line_by_line_dialog_title\">¿Está seguro?</string>\n    <string name=\"line_by_line_dialog_desc\">Esta función es experimental y puede tener sus fallos.\\n\\nPor defecto, el idioma se determina a partir de la canción completa, pero con esta opción activada, se determinará línea por línea. Esto permitirá que las canciones multilingües funcionen, PERO el idioma podría no ser siempre correcto (por ejemplo, si hay una letra en ucraniano que no contiene letras específicas del idioma, podría romanizarse al ruso).\\n\\nSi no tienes problemas, se recomienda desactivar esta opción.</string>\n    <string name=\"romanize_current_track\">Romanizar la pista actual</string>\n    <string name=\"edit_playlist_cover\">Editar la portada de la lista de reproducción</string>\n    <string name=\"edit_playlist_cover_note\">Nota: Tu cuenta debe estar vinculada a un número de teléfono y verificada en YouTube Music para cambiar la portada de la lista de reproducción.</string>\n    <string name=\"edit_playlist_cover_note_wait\">Después de seleccionar una imagen, espera un momento hasta que la nueva portada aparezca en la lista de reproducción.</string>\n    <string name=\"choose_from_library\">Elija de la biblioteca</string>\n    <string name=\"remove_custom_image\">Eliminar imagen personalizada</string>\n    <string name=\"audio_offload\">Activar descarga (offload)</string>\n    <string name=\"audio_offload_description\">Usa la ruta de descarga de audio (offload) para la reproducción. Desactivar esta opción podría aumentar el consumo de energía, pero puede ser útil si experimentas problemas con la reproducción o el posprocesamiento de audio</string>\n    <string name=\"uploaded_playlist\">Subidas</string>\n    <string name=\"filter_uploaded\">Subido</string>\n    <string name=\"show_uploaded_playlist\">Mostrar lista de reproducción \\\"Subidas\\\"</string>\n    <string name=\"updater\">Actualizador</string>\n    <string name=\"check_for_updates\">Buscar actualizaciones automáticamente</string>\n    <string name=\"lyrics_romanize_macedonian\">Romanizar letras en macedonio</string>\n    <string name=\"discord_use_details\">Usar detalles en lugar de estado</string>\n    <string name=\"discord_use_details_description\">Mostrar el título de la canción de forma destacada en lugar de los nombres de los artistas</string>\n    <string name=\"update_notifications\">Habilitar notificaciones de actualización</string>\n    <string name=\"update_available_title\">Actualización disponible</string>\n    <string name=\"update_channel_name\">Actualizaciones de la app</string>\n    <string name=\"update_channel_desc\">Notificaciones sobre nuevas versiones</string>\n    <string name=\"integrations\">Integraciones</string>\n    <string name=\"username\">Nombre de usuario</string>\n    <string name=\"password\">Contraseña</string>\n    <string name=\"lastfm_integration\">Integración con Last.fm</string>\n    <string name=\"enable_scrobbling\">Habilitar scrobbling</string>\n    <string name=\"lastfm_now_playing\">Enviar Reproduciendo ahora</string>\n    <string name=\"scrobbling_configuration\">Configuración de scrobbling</string>\n    <string name=\"scrobble_min_track_duration\">Scrobble a canciones más largas que</string>\n    <string name=\"scrobble_delay_percent\">Porcentaje de retraso de Scrobble</string>\n    <string name=\"scrobble_delay_minutes\">Minutos de retraso de Scrobble</string>\n    <string name=\"swipe_song_to_remove\">Deslizar la canción para quitarla de la lista de reproducción</string>\n    <string name=\"last_fm_send_likes\">Enviar Me gusta/No me gusta</string>\n    <string name=\"last_fm_send_likes_description\">Marcar como Me gusta o No me gusta las canciones en Last.fm cuando aparecen marcadas como Me gusta o No me gusta en Metrolist</string>\n    <string name=\"primary_color_style\">Color primario</string>\n    <string name=\"tertiary_color_style\">Color terciario</string>\n    <string name=\"auto_scroll\">Volver a sincronizar</string>\n    <string name=\"google_cast\">Transmitir con Google Cast</string>\n    <string name=\"google_cast_description\">Habilitar la transmisión de audio a Chromecast y otros dispositivos compatibles con Cast</string>\n    <string name=\"hide_video_songs\">Ocultar canciones de vídeo</string>\n    <string name=\"details_desc\">Ver la información de la canción</string>\n    <string name=\"edit_desc\">Cambiar el título o el artista</string>\n    <string name=\"start_radio_desc\">Crea una estación basada en este elemento</string>\n    <string name=\"play_next_desc\">Añadir al inicio de la cola</string>\n    <string name=\"add_to_queue_desc\">Añadir al final de la cola</string>\n    <string name=\"add_to_library_desc\">Guardar en la biblioteca</string>\n    <string name=\"download_desc\">Hacer disponible para reproducir sin conexión</string>\n    <string name=\"add_to_playlist_desc\">Añadir a una de tus listas de reproducción</string>\n    <string name=\"refetch_desc\">Obtener los metadatos más recientes de YouTube Music</string>\n    <string name=\"share_desc\">Compartir un enlace a este elemento</string>\n    <string name=\"delete_desc\">Eliminar este elemento de forma permanente</string>\n    <string name=\"advanced_desc\">Cambiar el tempo y el tono de la canción</string>\n    <string name=\"equalizer_desc\">Ajustar el ecualizador de audio</string>\n    <string name=\"enable_dynamic_icon\">Habilitar icono dinámico</string>\n    <string name=\"mini_player\">Minirreproductor</string>\n    <string name=\"pure_black_mini_player\">Minirreproductor negro puro</string>\n    <string name=\"cache_size_warning_title\">¡Espera!</string>\n    <string name=\"cache_size_warning_message\">Has elegido un límite de tamaño de caché menor que el que usa la aplicación (%1$s). Si continúas, la aplicación podría eliminar algunos %2$s de la caché para que coincidan con el nuevo límite. ¿Continuar de todos modos?</string>\n    <string name=\"cache_size_warning_confirm\">Continuar</string>\n    <string name=\"lyrics_romanize_chinese\">Romanizar letras chinas</string>\n    <string name=\"logging_in\">Iniciando sesión…</string>\n    <string name=\"download_playlist_desc\">Descargar todas las canciones para reproducirlas sin conexión</string>\n    <string name=\"remove_download_playlist_desc\">Eliminar todas las canciones descargadas de esta lista de reproducción</string>\n    <string name=\"download_in_progress_desc\">La descarga está en proceso</string>\n    <string name=\"share_playlist_desc\">Compartir esta lista de reproducción con otros</string>\n    <string name=\"delete_playlist_desc\">Eliminar esta lista de reproducción de forma permanente</string>\n    <string name=\"sync_playlist_desc\">Sincronizar lista de reproducción con YouTube Music</string>\n    <string name=\"enable_better_lyrics\">Habilitar Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">Letras sincronizadas por sílabas para cualquier canción, ideales para karaoke</string>\n    <string name=\"lyrics_animation_style\">Estilo de animación de la letra</string>\n    <string name=\"none\">Ninguno</string>\n    <string name=\"lyrics_text_size\">Tamaño del texto de la letra</string>\n    <string name=\"shuffle_playlist_first\">Aleatorizar lista de reproducción/álbum primero</string>\n    <string name=\"shuffle_playlist_first_desc\">Al reproducir aleatoriamente, reproduce primero todas las canciones de la lista de reproducción/álbum original y luego el contenido similar</string>\n    <string name=\"show_wrapped_card\">Mostrar tarjeta Recap</string>\n    <string name=\"lyrics_line_spacing\">Interlineado de la letra</string>\n    <string name=\"album_art_for\">Carátula del álbum de %s</string>\n    <string name=\"wrapped_total_albums_title\">Ya has escuchado</string>\n    <string name=\"wrapped_total_albums_subtitle\">álbumes únicos</string>\n    <string name=\"wrapped_top_album_title\">Tu álbum favorito es</string>\n    <string name=\"wrapped_playlist_ready\">Tu lista de reproducción personal está lista</string>\n    <string name=\"wrapped_top_5_albums_title\">Tus 5 mejores álbumes</string>\n    <string name=\"wrapped_album_listening_time\">Has escuchado este álbum durante %d minutos</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d minutos</string>\n    <string name=\"wrapped_no_data\">No hay datos</string>\n    <string name=\"wrapped_top_5_artists_title\">Tus mejores artistas del año</string>\n    <string name=\"wrapped_artist_listening_time\">%d minutos</string>\n    <string name=\"wrapped_top_5_songs_title\">Tus mejores canciones del año</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">Portada del álbum</string>\n    <string name=\"wrapped_top_artist_title\">Tu artista favorito del año es</string>\n    <string name=\"wrapped_top_artist_image_content_description\">Imagen del mejor artista</string>\n    <string name=\"wrapped_top_artist_listening_time\">Los has escuchado durante %d minutos</string>\n    <string name=\"wrapped_top_song_title\">Tu canción más reproducida es</string>\n    <string name=\"wrapped_top_song_listening_time\">Has escuchado durante %d minutos</string>\n    <string name=\"wrapped_total_artists_title\">Escuchaste a</string>\n    <string name=\"wrapped_total_artists_subtitle\">artistas únicos</string>\n    <string name=\"wrapped_total_songs_title\">Escuchaste</string>\n    <string name=\"wrapped_total_songs_subtitle\">canciones únicas</string>\n    <string name=\"wrapped_intro_subtitle\">Es hora de ver lo que has estado escuchando</string>\n    <string name=\"wrapped_intro_button\">¡vamos!</string>\n    <string name=\"wrapped_ready_title\">¡TU RECAP ESTÁ LISTO!</string>\n    <string name=\"wrapped_ready_subtitle\">Es hora de ver lo que te gustó este año.</string>\n    <string name=\"wrapped_thank_you\">Gracias por escuchar</string>\n    <string name=\"wrapped_special_thanks\">Un agradecimiento especial a MO Agamy por crear Metrolist</string>\n    <string name=\"wrapped_close\">Cerrar Recap</string>\n    <string name=\"wrapped_playlist_title\">Tu Recap %s</string>\n    <string name=\"wrapped_create_playlist\">Crear lista de reproducción</string>\n    <string name=\"wrapped_playlist_saved\">Lista de reproducción guardada</string>\n    <string name=\"casting_to\">Transmitiendo a %s</string>\n    <string name=\"progress_percent\">Progreso %s%%</string>\n    <string name=\"listening_to_metrolist\">Escuchando Metrolist</string>\n    <string name=\"open\">Abrir</string>\n    <string name=\"failed_to_create_image\">No se pudo crear la imagen: %s</string>\n    <string name=\"copied_title\">Título copiado</string>\n    <string name=\"copied_artist\">Artista copiado</string>\n    <string name=\"error_playing\">Error al reproducir</string>\n    <string name=\"failed_to_parse_proxy\">Error al analizar la URL del proxy.</string>\n    <string name=\"lyrics_glow_effect\">Habilitar efecto de brillo de la letra</string>\n    <string name=\"lyrics_glow_effect_desc\">Añadir animación de brillo y efecto de rebote a la letra activa</string>\n    <string name=\"fade\">Difuminar</string>\n    <string name=\"glow\">Brillar</string>\n    <string name=\"slide\">Deslizar</string>\n    <string name=\"karaoke\">Karaoke</string>\n    <string name=\"apple_music_style\">Estilo Apple Music</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_logo_content_description\">Logo de Metrolist</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"show_artist_description\">Mostrar descripción del artista</string>\n    <string name=\"show_artist_subscriber_count\">Ver numero de suscriptores</string>\n    <string name=\"show_artist_monthly_listeners\">Ver oyentes mensuales</string>\n    <string name=\"lyrics_offset\">Desfase de la letra</string>\n    <string name=\"about_artist\">Acerca de</string>\n    <string name=\"show_more\">Mostrar más</string>\n    <string name=\"show_less\">Mostrar menos</string>\n    <string name=\"artist_page_settings\">Página del Artista</string>\n    <string name=\"wavy\">Ondulado</string>\n    <string name=\"enable_simpmusic\">Habilitar letras de SimpMusic</string>\n    <string name=\"enable_simpmusic_desc\">Letras obtenidas automáticamente de Musixmatch y YouTube Transcript</string>\n    <string name=\"skip_silence_desc\">Avanzar rápidamente por las partes silenciosas de las canciones</string>\n    <string name=\"skip_silence_instant\">Saltar el silencio al instante</string>\n    <string name=\"skip_silence_instant_desc\">Salta hacia adelante durante los momentos de silencio en lugar de acelerar la reproducción</string>\n    <string name=\"remember_shuffle_and_repeat\">Recuerda mezclar y repetir</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Recuerda el modo aleatorio y repetir al reiniciar la aplicación</string>\n    <string name=\"pause_music_when_media_is_muted\">Pausar la música cuando se silencia el medio</string>\n    <string name=\"equalizer_header\">Ecualizador</string>\n    <string name=\"no_profiles\">Sin perfiles de ecualizador</string>\n    <string name=\"import_profile\">Importar perfil</string>\n    <string name=\"system_equalizer\">Ecualizador del sistema</string>\n    <string name=\"eq_disabled\">Deshabilitado</string>\n    <string name=\"delete_profile_desc\">Eliminar Perfil</string>\n    <string name=\"delete_profile_confirmation\">¿Está seguro de que desea eliminar %1$s? Esta acción no se puede deshacer.</string>\n    <string name=\"error_file_read\">No se pudo leer el archivo</string>\n    <string name=\"error_file_open\">No se pudo abrir el archivo: %1$s</string>\n    <string name=\"import_error_title\">Error de Importación</string>\n    <string name=\"album_art\">Portada del álbum</string>\n    <string name=\"no_song_playing\">No se reproduce ninguna canción</string>\n    <string name=\"tap_to_open\">Toca para abrir Metrolist</string>\n    <string name=\"previous\">Anterior</string>\n    <string name=\"play_pause\">Reproducir/Pausar</string>\n    <string name=\"next\">Siguiente</string>\n    <string name=\"like\">Me gusta</string>\n    <string name=\"widget_description\">Widget reproductor de música con controles de reproducción</string>\n    <string name=\"turntable_widget_description\">Widget musical circular con controles de reproducir y me gusta</string>\n    <string name=\"crop_album_art\">Recortar portada del álbum</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"one\">%d Perfil</item>\n        <item quantity=\"many\">%d Perfiles</item>\n        <item quantity=\"other\">%d Perfiles</item>\n    </plurals>\n    <plurals name=\"band_count\">\n        <item quantity=\"one\">%d banda</item>\n        <item quantity=\"many\">%d de bandas</item>\n        <item quantity=\"other\">%d bandas</item>\n    </plurals>\n    <string name=\"error_title\">Error</string>\n    <string name=\"error_eq_apply_failed\">Error al aplicar el perfil de ecualización: %1$s</string>\n    <string name=\"error_playback_failed\">Reproducción fallida</string>\n    <string name=\"crop_album_art_desc\">Forzar una relación de aspecto cuadrada recortando miniaturas de vídeo</string>\n    <string name=\"persistent_shuffle_title\">Aleatorio persistente</string>\n    <string name=\"persistent_shuffle_desc\">Mantener la reproducción aleatoria habilitada al iniciar nuevas canciones o listas de reproducción</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">Mantener la pantalla encendida cuando el reproductor está expandido</string>\n    <string name=\"listen_together\">Escuchar juntos</string>\n    <string name=\"listen_together_server_url\">URL del servidor</string>\n    <string name=\"listen_together_username\">Nombre de usuario</string>\n    <string name=\"listen_together_connected\">Conectado</string>\n    <string name=\"listen_together_reconnecting\">Reconectando…</string>\n    <string name=\"listen_together_disconnected\">Desconectado</string>\n    <string name=\"listen_together_connecting\">Conectando…</string>\n    <string name=\"listen_together_error\">Error de conexión</string>\n    <string name=\"listen_together_create_room\">Crear sala</string>\n    <string name=\"listen_together_create_room_desc\">Crea una sala y comparte el código con tus amigos</string>\n    <string name=\"listen_together_join_room\">Unirse a la sala</string>\n    <string name=\"listen_together_room_code\">Código de la sala</string>\n    <string name=\"listen_together_you_are_host\">Eres el anfitrión</string>\n    <string name=\"listen_together_you_are_guest\">Eres un invitado</string>\n    <string name=\"listen_together_join_requests\">Solicitudes de conexión</string>\n    <string name=\"listen_together_view_logs\">Ver registros</string>\n    <string name=\"listen_together_view_logs_desc\">Depurar conexión y mensajes</string>\n    <string name=\"listen_together_logs\">Registros de conexión</string>\n    <string name=\"listen_together_no_logs\">Aún no hay registros</string>\n    <string name=\"listen_together_description\">Escucha música con tus amigos en tiempo real. Crea una sala para ser el anfitrión o únete a una sala existente con un código.</string>\n    <string name=\"listen_together_background_disconnect_note\">Nota: Es posible que se desconecte si crea una sala mientras no se está reproduciendo música y luego cambia a otra aplicación.</string>\n    <string name=\"listen_together_not_configured\">Listen Together no está configurado. Configure la URL del servidor en Configuración → Integraciones → Listen Together.</string>\n    <string name=\"listen_together_suggestion_received\">%1$s solicitó %2$s</string>\n    <string name=\"listen_together_suggestion_sent\">Sugerencia enviada al anfitrión!</string>\n    <string name=\"listen_together_join_request_notification\">%1$s quiere unirse a la sala</string>\n    <string name=\"listen_together_notification_channel_name\">Escuchar juntos</string>\n    <string name=\"listen_together_notification_channel_desc\">Notificaciones de eventos de Escuchar juntos</string>\n    <string name=\"listen_together_room_created\">Sala creada: %s</string>\n    <string name=\"listen_together_cannot_edit_username_in_room\">No se puede editar el nombre de usuario mientras se está en una sala</string>\n    <string name=\"waiting_for_approval\">Esperando la aprobación del anfitrión</string>\n    <string name=\"invalid_room_code\">Código de sala no válido</string>\n    <string name=\"join_request_denied\">Solicitud de unión denegada</string>\n    <string name=\"join_existing_room\">Unirse a una sala existente</string>\n    <string name=\"room_code\">Código de sala</string>\n    <string name=\"leave_room\">Salir de la sala</string>\n    <string name=\"join_room\">Unirse</string>\n    <string name=\"create_room\">Crear</string>\n    <string name=\"joining_room\">Uniéndose a la sala %s…</string>\n    <string name=\"creating_room\">Creando sala…</string>\n    <string name=\"connect\">Conectar</string>\n    <string name=\"disconnect\">Desconectar</string>\n    <string name=\"create\">Crear</string>\n    <string name=\"join\">Unirse</string>\n    <string name=\"approve\">Aprobar</string>\n    <string name=\"reject\">Rechazar</string>\n    <string name=\"clear\">Limpiar</string>\n    <string name=\"copy\">Copiar</string>\n    <string name=\"copied_to_clipboard\">Copiado al portapapeles</string>\n    <string name=\"not_set\">No establecido</string>\n    <string name=\"hosting_room\">Alojar sala</string>\n    <string name=\"in_room\">En sala</string>\n    <string name=\"pending_requests\">Solicitudes pendientes</string>\n    <string name=\"pending_suggestions\">Sugerencias pendientes</string>\n    <string name=\"suggest_to_host\">Sugerir alojar</string>\n    <string name=\"kick_user\">Expulsar</string>\n    <string name=\"host_label\">Anfitrión</string>\n    <string name=\"you_label\">Tú</string>\n    <string name=\"connected_users\">Usuarios conectados</string>\n    <string name=\"enter_username\">Ingresar nombre de usuario</string>\n    <string name=\"error_username_empty\">Se requiere nombre de usuario.</string>\n    <string name=\"resync\">Re sincronizar</string>\n    <string name=\"mute\">Silenciar</string>\n    <string name=\"unmute\">Activar sonido</string>\n    <string name=\"crash_title\">La app falló</string>\n    <string name=\"crash_description\">Ocurrió un error inesperado. Por favor comparte el informe de fallos para ayudarnos a solucionar el problema.</string>\n    <string name=\"crash_share_logs\">Compartir registros</string>\n    <string name=\"crash_share_title\">Compartir informe de fallos</string>\n    <string name=\"crash_report_subject\">Informe de fallos de Metrolist</string>\n    <string name=\"crash_close\">Cerrar</string>\n    <string name=\"crash_no_log\">No hay registro de fallos disponible</string>\n    <string name=\"palette_dynamic\">Dinámico</string>\n    <string name=\"palette_crimson\">Carmesí</string>\n    <string name=\"palette_rose\">Rosa</string>\n    <string name=\"palette_purple\">Morado</string>\n    <string name=\"palette_deep_purple\">Púrpura intenso</string>\n    <string name=\"palette_indigo\">Índigo</string>\n    <string name=\"palette_blue\">Azul</string>\n    <string name=\"palette_sky_blue\">Celeste</string>\n    <string name=\"palette_cyan\">Cian</string>\n    <string name=\"palette_teal\">Verde azulado</string>\n    <string name=\"palette_green\">Verde</string>\n    <string name=\"palette_light_green\">Verde claro</string>\n    <string name=\"palette_lime\">Lima</string>\n    <string name=\"palette_yellow\">Amarillo</string>\n    <string name=\"palette_amber\">Ámbar</string>\n    <string name=\"palette_orange\">Naranja</string>\n    <string name=\"palette_deep_orange\">Naranja intenso</string>\n    <string name=\"palette_brown\">Marrón</string>\n    <string name=\"palette_grey\">Gris</string>\n    <string name=\"palette_blue_grey\">Gris azulado</string>\n    <string name=\"cd_back\">Atrás</string>\n    <string name=\"cd_pure_black_mode\">Modo negro puro</string>\n    <string name=\"cd_light_mode\">Modo claro</string>\n    <string name=\"cd_dark_mode\">Modo oscuro</string>\n    <string name=\"cd_system_mode\">Modo del sistema</string>\n    <string name=\"cd_palette_item\">Paleta %1$s</string>\n    <string name=\"not_playing\">No hay canción en reproducción</string>\n    <string name=\"tap_to_play\">Toca para abrir Metrolist</string>\n    <string name=\"widget_music_player\">Reproductor de música</string>\n    <string name=\"widget_turntable\">Tocadiscos</string>\n    <string name=\"listen_together_choose_server\">Elegir servidor</string>\n    <string name=\"listen_together_custom_server\">Servidor personalizado</string>\n    <string name=\"listen_together_use_custom_server\">Usar servidor personalizado</string>\n    <string name=\"listen_together_auto_approval_joins\">Aprobar automáticamente solicitudes de conexión</string>\n    <string name=\"listen_together_auto_approval_joins_desc\">Aprobar automáticamente solicitudes de conexión en lugar de revisarlas manualmente</string>\n    <string name=\"listen_together_sync_volume\">Sincronizar volumen del anfitrión</string>\n    <string name=\"listen_together_sync_volume_desc\">Los invitados siguen el nivel de volumen del anfitrión</string>\n    <string name=\"copy_code\">Copiar código</string>\n    <string name=\"kick_user_desc\">Eliminar a esta persona de la sesión</string>\n    <string name=\"permanently_kick_user\">Bloquear permanentemente</string>\n    <string name=\"permanently_kick_user_desc\">Bloquear las solicitudes de unión de esta persona y ocultar sus sugerencias</string>\n    <string name=\"transfer_ownership\">Transferir propiedad</string>\n    <string name=\"transfer_ownership_desc\">Convertir a esta persona en el anfitrión de la sala</string>\n    <string name=\"manage_user\">Administrar usuario</string>\n    <string name=\"listen_together_blocked_users\">Usuarios bloqueados</string>\n    <string name=\"listen_together_blocked_users_count\">%d usuario(s) bloqueado(s)</string>\n    <string name=\"listen_together_no_blocked_users\">No hay usuarios bloqueados</string>\n    <string name=\"unblock\">Desbloquear</string>\n    <string name=\"user_blocked_by_host\">Usuario bloqueado por el anfitrión</string>\n    <string name=\"together\">Juntos</string>\n    <string name=\"enter_room_code\">Ingresar código de sala</string>\n    <string name=\"listen_together_settings_desc\">Configurar servidor, nombre de usuario y más</string>\n    <string name=\"ai_lyrics_translation\">Traducción de letras con IA</string>\n    <string name=\"ai_translating_lyrics\">Traduciendo letras...</string>\n    <string name=\"ai_lyrics_translated\">Letras traducidas</string>\n    <string name=\"ai_provider\">Proveedor</string>\n    <string name=\"ai_base_url\">URL base</string>\n    <string name=\"ai_api_key\">Clave API</string>\n    <string name=\"ai_model\">Modelo</string>\n    <string name=\"ai_translation_mode\">Modo de traducción</string>\n    <string name=\"ai_target_language\">Idioma de destino</string>\n    <string name=\"ai_setup_guide\">Credenciales de API</string>\n    <string name=\"ai_translation_literal\">Traducción</string>\n    <string name=\"ai_translation_transcribed\">Transcrito</string>\n    <string name=\"ai_api_key_required\">Clave API requerida</string>\n    <string name=\"ai_error_api_key_required\">Clave API obligatoria</string>\n    <string name=\"ai_error_no_lyrics\">No hay letras para traducir</string>\n    <string name=\"ai_error_lyrics_empty\">Las letras están vacías</string>\n    <string name=\"ai_error_language_required\">El idioma de destino es obligatorio</string>\n    <string name=\"ai_error_unexpected\">Resultado de traducción inesperado</string>\n    <string name=\"ai_error_unknown\">Error desconocido ocurrido</string>\n    <string name=\"ai_error_translation_failed\">Traducción fallida</string>\n    <string name=\"play_all\">Reproducir todo</string>\n    <string name=\"recognize_music\">Identificar música</string>\n    <string name=\"youtube_url_column\">Columna de URL de YouTube (Opcional)</string>\n    <string name=\"re_listen\">Volver a escuchar</string>\n    <string name=\"clear_recognition_history_confirm\">¿Estás seguro de que quieres borrar todo el historial de identificación?</string>\n    <string name=\"no_match_found\">No se encontró ninguna coincidencia</string>\n    <string name=\"delete_from_history\">Eliminar del historial</string>\n    <string name=\"artist_name_column\">Columna de nombre del artista</string>\n    <string name=\"processing\">Procesando…</string>\n    <string name=\"clear_recognition_history\">Borrar historial de identificación</string>\n    <string name=\"map_csv_columns\">Mapear columnas CSV</string>\n    <string name=\"column_label\">Col %d</string>\n    <string name=\"recognition_error\">Error de identificación</string>\n    <string name=\"enable_high_refresh_rate_desc\">Forzar a la pantalla a funcionar a la frecuencia de actualización más alta permitida (p. ej., 120Hz)</string>\n    <string name=\"first_row_is_header\">La primera fila es el encabezado</string>\n    <string name=\"try_again\">Inténtalo de nuevo</string>\n    <string name=\"tap_to_recognize\">Toca para identificar</string>\n    <string name=\"recognition_history\">Historial de identificación</string>\n    <string name=\"enable_high_refresh_rate\">Habilitar alta frecuencia de actualización</string>\n    <string name=\"song_title_column\">Columna de título de la canción</string>\n    <string name=\"recently_converted\">Recientemente convertidos</string>\n    <string name=\"importing_csv\">Importando CSV</string>\n    <string name=\"play_on_app\">Reproducir en Metrolist</string>\n    <string name=\"listening\">Escuchando…</string>\n    <string name=\"continue_action\">Continuar</string>\n    <string name=\"enable\">Habilitar</string>\n    <string name=\"crossfade_desc\">Transición suave entre canciones</string>\n    <string name=\"crossfade_duration\">Duración de la transición suave</string>\n    <string name=\"crossfade_gapless\">Desactivar para álbumes sin pausas</string>\n    <string name=\"crossfade_gapless_desc\">No hacer transición suave si el álbum es sin pausas</string>\n    <string name=\"crossfade_beta_title\">Función beta</string>\n    <string name=\"crossfade_beta_message\">La transición suave es una función nueva y puede tener errores. Si experimentas algún problema, por favor repórtalo. \\n\\nEsta función desactiva la descarga de audio debido a limitaciones técnicas.</string>\n    <string name=\"audio_offload_disabled_by_crossfade\">Desactivado porque la transición suave está activa</string>\n    <string name=\"crossfade\">Transición suave</string>\n    <string name=\"hide_youtube_shorts\">Ocultar YouTube Shorts</string>\n    <string name=\"listen_together_in_top_bar\">Escuchar juntos en la barra superior</string>\n    <string name=\"listen_together_in_top_bar_desc\">Mostrar Escuchar juntos en la barra superior en lugar de en la barra de navegación</string>\n    <string name=\"prevent_duplicate_tracks_in_queue\">Evitar pistas duplicadas en la cola</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">Al agregar una pista a la cola, elimínela de su posición anterior si ya está presente</string>\n    <string name=\"ai_translation_transcribed_desc\">Convertir pronunciación al script de destino</string>\n    <string name=\"ai_provider_help\">Obtener Llaves API</string>\n    <string name=\"discord_status\">Status</string>\n    <string name=\"discord_status_online\">Online</string>\n    <string name=\"discord_activity_competing\">Compitiendo</string>\n    <string name=\"discord_presence\">Presencia</string>\n    <string name=\"player_background_solid\">Sólido</string>\n    <string name=\"resume_on_bluetooth_connect\">Reanudar al conectar Bluetooth</string>\n    <string name=\"lyrics_romanize_hindi\">Romanizar letras en hindi</string>\n    <string name=\"lyrics_romanize_punjabi\">Romanizar letras en panyabí</string>\n    <string name=\"lyrics_romanize_as_main\">Mostrar letras romanizadas como principales</string>\n    <string name=\"ai_translation_literal_desc\">Traducir el significado al idioma de destino</string>\n    <string name=\"ai_provider_openrouter_help\">Visita https://openrouter.ai para modelos gratuitos y de pago</string>\n    <string name=\"ai_provider_openai_help\">Visita https://platform.openai.com/api-keys</string>\n    <string name=\"ai_provider_claude_help\">Visita https://console.anthropic.com/settings/keys</string>\n    <string name=\"ai_provider_gemini_help\">Visita https://aistudio.google.com/apikey</string>\n    <string name=\"ai_provider_perplexity_help\">Visita https://perplexity.ai/settings/api</string>\n    <string name=\"ai_provider_xai_help\">Visita https://console.x.ai</string>\n    <string name=\"ai_provider_deepl_help\">Visita https://deepl.com/pro-api para claves gratuitas y de pago</string>\n    <string name=\"ai_deepl_formality\">Formalidad</string>\n    <string name=\"ai_deepl_formality_default\">Predeterminado</string>\n    <string name=\"ai_deepl_formality_more\">Más formal</string>\n    <string name=\"ai_deepl_formality_less\">Menos formal</string>\n    <string name=\"discord_status_idle\">Ausente</string>\n    <string name=\"discord_status_dnd\">No molestar</string>\n    <string name=\"discord_buttons\">Botones</string>\n    <string name=\"discord_button_1\">Botón 1</string>\n    <string name=\"discord_button_2\">Botón 2</string>\n    <string name=\"login_successful\">¡Inicio de sesión con éxito!</string>\n    <string name=\"discord_information_warning\">Esta función utiliza la biblioteca KizzyRPC para conectarse a la pasarela (Gateway) de Discord y establecer tu estado de Rich Presence. Aunque no se conocen suspensiones de cuentas por un uso similar, este método no cuenta con el soporte oficial de Discord y podría considerarse una infracción de sus Términos de Servicio. Tu token se extrae localmente y nunca se envía a servidores de terceros. Procede bajo tu propia responsabilidad.</string>\n    <string name=\"discord_activity_type\">Tipo de actividad</string>\n    <string name=\"discord_activity_playing\">Jugando</string>\n    <string name=\"discord_activity_listening\">Escuchando</string>\n    <string name=\"discord_activity_watching\">Viendo</string>\n    <string name=\"discord_button_text_variables\">Variables: {song_name}, {artist_name}, {album_name}</string>\n    <string name=\"discord_rpc_preview\">Vista previa de Rich Presence</string>\n    <string name=\"discord_connect_description\">Inicia sesión con Discord para compartir lo que estás escuchando</string>\n    <string name=\"discord_playing_metrolist\">Reproduciendo Metrolist</string>\n    <string name=\"discord_watching_metrolist\">Viendo Metrolist</string>\n    <string name=\"discord_competing_metrolist\">Compitiendo en Metrolist</string>\n    <string name=\"discord_activity_name\">Nombre de la actividad</string>\n    <string name=\"discord_activity_name_description\">Nombre personalizado para la actividad (dejar vacío para el valor predeterminado)</string>\n    <string name=\"discord_advanced_mode\">Modo avanzado</string>\n    <string name=\"discord_advanced_mode_description\">Mostrar opciones de personalización adicionales para Rich Presence</string>\n    <string name=\"display_density\">Densidad de pantalla</string>\n    <string name=\"restart\">Reiniciar</string>\n    <string name=\"restart_required\">Se requiere reiniciar</string>\n    <string name=\"density_restart_message\">El cambio en la densidad de pantalla se aplicará después de reiniciar la aplicación. ¿Quieres reiniciarla ahora?</string>\n    <string name=\"found_in_settings_content\">Se encuentra en Ajustes &gt; Contenido</string>\n    <string name=\"plays\">reproducciones</string>\n    <string name=\"speed_dial\">Acceso rápido</string>\n    <string name=\"pin_to_speed_dial\">Fijar al acceso rápido</string>\n    <string name=\"unpin_from_speed_dial\">Quitar del acceso rápido</string>\n    <string name=\"randomize_home_order\">Aleatorizar el orden de la pantalla de inicio</string>\n    <string name=\"randomize_home_order_desc\">Reordenar aleatoriamente las secciones de la pantalla de inicio según prioridades ponderadas</string>\n    <string name=\"daily_discover_sounds_like\">Suena como %1$s</string>\n    <string name=\"daily_discover_because_you_listen_to\">Porque escuchas a %1$s</string>\n    <string name=\"daily_discover_similar_to\">Similar a %1$s</string>\n    <string name=\"daily_discover_based_on\">Basado en %1$s</string>\n    <string name=\"daily_discover_for_fans_of\">Para fans de %1$s</string>\n    <string name=\"from_the_community\">De la comunidad</string>\n    <string name=\"enable_lrclib_desc\">Base de datos de letras sincronizadas impulsada por la comunidad</string>\n    <string name=\"enable_kugou_desc\">Obtiene las letras de KuGou, una popular plataforma de música china</string>\n    <string name=\"youtube_music_lyrics_note\">NOTA: Las letras de YouTube Music se mostrarán automáticamente cuando no haya otras letras disponibles. Las letras de YTM no suelen estar sincronizadas.</string>\n    <string name=\"enable_lyricsplus\">Activar LyricsPlus</string>\n    <string name=\"enable_lyricsplus_desc\">Letras sincronizadas de múltiples fuentes</string>\n    <string name=\"lyrics_provider_selection\">Selección de proveedor</string>\n    <string name=\"lyrics_provider_selection_desc\">Elige qué proveedores de letras están activados</string>\n    <string name=\"lyrics_provider_priority\">Prioridad de proveedores de letras</string>\n    <string name=\"lyrics_provider_priority_desc\">Arrastra para reordenar los proveedores por preferencia. Una posición más alta indica una mayor prioridad.</string>\n    <string name=\"changelog\">Registro de cambios</string>\n    <string name=\"changelog_empty\">No hay registros de cambios disponibles</string>\n    <string name=\"github_releases_url\">https://github.com/MetrolistGroup/Metrolist/releases</string>\n    <string name=\"list_bullet\">•</string>\n    <string name=\"view_on_github\">Ver en GitHub</string>\n    <string name=\"current_version\">Versión actual</string>\n    <string name=\"version_format\">Versión: %s</string>\n    <string name=\"update_settings\">Ajustes de actualización</string>\n    <string name=\"check_for_updates_title\">Buscar actualizaciones</string>\n    <string name=\"checking_for_updates\">Buscando actualizaciones…</string>\n    <string name=\"latest_version_format\">Última: %s</string>\n    <string name=\"check_for_updates_button\">Buscar actualizaciones</string>\n    <string name=\"hide_changelog\">Ocultar registro de cambios</string>\n    <string name=\"view_changelog\">Ver registro de cambios</string>\n    <string name=\"failed_to_check_updates\">Error al buscar actualizaciones: %s</string>\n    <string name=\"set_as_default\">Establecer como predeterminado</string>\n    <string name=\"sleep_timer_default_set\">Temporizador de apagado predeterminado establecido en %d min</string>\n    <string name=\"error_episode_save\">No se pudo guardar el episodio</string>\n    <string name=\"error_episode_remove\">No se pudo eliminar el episodio</string>\n    <string name=\"error_podcast_subscribe\">Error al suscribirse al pódcast</string>\n    <string name=\"error_podcast_unsubscribe\">Error al cancelar la suscripción al pódcast</string>\n    <string name=\"listen_together_auto_approval_suggestions\">Aprobar automáticamente las sugerencias de canciones</string>\n    <string name=\"listen_together_auto_approval_suggestions_desc\">Aprobar automáticamente y añadir a la cola las sugerencias de canciones de invitados</string>\n    <string name=\"importing_playlist\">Importando lista de reproducción</string>\n    <string name=\"logout_dialog_title\">¿Conservar los datos de la biblioteca?</string>\n    <string name=\"logout_dialog_message\">¿Quieres conservar tus listas de reproducción y los datos de tu biblioteca? Las canciones descargadas se conservarán igualmente.</string>\n    <string name=\"logout_keep\">Conservar</string>\n    <string name=\"logout_clear\">Borrar</string>\n    <string name=\"credits_lead_developer\">Desarrollador principal</string>\n    <string name=\"credits_collaborator\">Colaborador</string>\n    <string name=\"credits_collaborators_section\">Colaboradores</string>\n    <string name=\"credits_license_name\">GNU General Public License v3.0</string>\n    <string name=\"credits_license_desc\">Software libre y de código abierto. Puedes usarlo, estudiarlo, compartirlo y mejorarlo.</string>\n    <string name=\"credits_discord\">Servidor de Discord</string>\n    <string name=\"credits_telegram\">Canal de Telegram</string>\n    <string name=\"credits_website\">Sitio web</string>\n    <string name=\"credits_instagram\">Instagram</string>\n    <string name=\"credits_github\">GitHub</string>\n    <string name=\"credits_view_repo\">Ver repositorio</string>\n    <string name=\"app_version_info\">%1$s • %2$s</string>\n    <string name=\"like_what_i_do\">¿Te gusta lo que hago?</string>\n    <string name=\"buy_mo_a_coffee\">Invítame un café</string>\n    <string name=\"community_and_info\">Comunidad e información</string>\n    <string name=\"metrolist\">METROLIST</string>\n    <string name=\"wanna_play_favorite_song\">¿Quieres reproducir su canción favorita?</string>\n    <string name=\"yeah\">Sí</string>\n    <string name=\"stands_with_palestine\">Este proyecto apoya a Palestina 🇵🇸</string>\n    <string name=\"filter_podcasts\">Podcasts</string>\n    <string name=\"view_podcast\">Ver podcast</string>\n    <string name=\"podcast_channels\">Canales de podcast</string>\n    <string name=\"latest_episodes\">Últimos episodios</string>\n    <string name=\"your_shows\">Tus programas</string>\n    <string name=\"new_episodes\">Nuevos episodios</string>\n    <string name=\"episodes_for_later\">Episodios para más tarde</string>\n    <string name=\"save_episode_for_later\">Guardar para más tarde</string>\n    <string name=\"save_episode_for_later_desc\">Añadir a tu lista de Episodios para más tarde</string>\n    <string name=\"remove_episode_from_saved\">Quitar de guardados</string>\n    <string name=\"subscribe_to_podcast\">Guardar podcast en la biblioteca</string>\n    <plurals name=\"n_episode\">\n        <item quantity=\"one\">%d episodio</item>\n        <item quantity=\"many\">%d episodios</item>\n        <item quantity=\"other\">%d episodios</item>\n    </plurals>\n    <string name=\"restore_confirm_title\">¿Restaurar copia de seguridad?</string>\n    <string name=\"restore_confirm_message\">Esto restaurará los datos de la aplicación desde la copia de seguridad.</string>\n    <string name=\"restore_account_warning\">Deberás iniciar sesión de nuevo tras la restauración. Se cerrará la sesión de la siguiente cuenta:</string>\n    <string name=\"restore\">Restaurar</string>\n    <string name=\"checking_previous_account\">Buscando una cuenta anterior…</string>\n    <string name=\"no_account_found\">No se encontró ninguna cuenta</string>\n    <string name=\"widget_recognizer_name\">Identificador de música</string>\n    <string name=\"widget_recognizer_description\">Identifica canciones que suenan a tu alrededor directamente desde tu pantalla de inicio</string>\n    <string name=\"widget_recognizer_tap_to_search\">Toca para identificar la canción</string>\n    <string name=\"widget_recognizer_listening\">Escuchando…</string>\n    <string name=\"widget_recognizer_processing\">Identificando…</string>\n    <string name=\"widget_recognizer_no_match\">No se encontraron coincidencias. Inténtalo de nuevo</string>\n    <string name=\"widget_recognizer_error\">Identificación fallida</string>\n    <string name=\"widget_recognizer_error_generic\">Ocurrió un error. Inténtalo de nuevo</string>\n    <string name=\"widget_recognizer_unknown_song\">Canción desconocida</string>\n    <string name=\"widget_recognizer_unknown_artist\">Artista desconocido</string>\n    <string name=\"widget_recognizer_mic_desc\">Identificar canción</string>\n    <string name=\"widget_recognizer_channel_name\">Identificación de música</string>\n    <string name=\"widget_recognizer_channel_desc\">Muestra una notificación mientras se identifica una canción desde el widget</string>\n    <string name=\"widget_recognizer_notification_text\">Grabando audio para identificar la canción…</string>\n    <string name=\"filter_episodes\">Episodios</string>\n    <string name=\"filter_channels\">Canales</string>\n    <string name=\"auto_playlist\">Lista de reproducción automática</string>\n    <string name=\"downloaded_episodes\">Episodios descargados</string>\n    <string name=\"no_subscribed_channels\">No hay canales suscritos</string>\n    <string name=\"no_downloaded_episodes\">No hay episodios descargados</string>\n    <plurals name=\"n_channel\">\n        <item quantity=\"one\">%d canal</item>\n        <item quantity=\"many\">%d canales</item>\n        <item quantity=\"other\">%d canales</item>\n    </plurals>\n    <string name=\"view_channel\">Ver canal</string>\n    <string name=\"filter_profiles\">Perfiles</string>\n    <string name=\"enable_automatic_sleeptimer\">Activar temporizador de apagado automático</string>\n    <string name=\"sleeptimer_description\">Activa automáticamente el temporizador de apagado usando el valor predeterminado a una hora personalizada</string>\n    <string name=\"sleep_timer_repeat_description\">Establece un día y una hora personalizados para que el temporizador de apagado se active automáticamente</string>\n    <string name=\"sleep_timer_repeat\">Repetir</string>\n    <string name=\"sleep_timer_daily\">Diario</string>\n    <string name=\"sleep_timer_weekdays\">De lunes a viernes</string>\n    <string name=\"sleep_timer_weekdays_weekends\">Entre semana / Fin de semana</string>\n    <string name=\"sleep_timer_weekends\">Fines de semana (sáb–dom)</string>\n    <string name=\"sleep_timer_custom\">Personalizado</string>\n    <string name=\"sleep_timer_start_time\">Hora de inicio</string>\n    <string name=\"sleep_timer_end_time\">Hora de fin</string>\n    <string name=\"sleep_timer_monday\">Lunes</string>\n    <string name=\"sleep_timer_tuesday\">Martes</string>\n    <string name=\"sleep_timer_wednesday\">Miércoles</string>\n    <string name=\"sleep_timer_thursday\">Jueves</string>\n    <string name=\"sleep_timer_friday\">Viernes</string>\n    <string name=\"sleep_timer_saturday\">Sábado</string>\n    <string name=\"sleep_timer_sunday\">Domingo</string>\n    <string name=\"sleep_timer_stop_after_current_song\">Detener al final de la canción actual cuando termine el temporizador</string>\n    <string name=\"sleep_timer_fade_out\">Atenuar el volumen en el último minuto</string>\n    <string name=\"upload_songs\">Subir canciones</string>\n    <string name=\"uploading\">Subiendo…</string>\n    <string name=\"upload_progress\">%1$d de %2$d</string>\n    <string name=\"upload_complete\">Subida completada</string>\n    <string name=\"upload_failed\">Subida fallida</string>\n    <string name=\"upload_file_too_large\">Archivo demasiado grande (máx. 300 MB)</string>\n    <string name=\"upload_unsupported_format\">Formato no compatible. Usa mp3, m4a, wma, flac u ogg</string>\n    <string name=\"delete_uploaded_song\">Eliminar canción subida</string>\n    <string name=\"delete_uploaded_song_confirm\">¿Seguro que quieres eliminar esta canción subida? Esta acción no se puede deshacer.</string>\n    <string name=\"delete_uploaded_song_success\">Canción subida eliminada</string>\n    <string name=\"delete_uploaded_song_failed\">No se pudo eliminar la canción subida</string>\n    <string name=\"delete_uploaded_songs\">Eliminar canciones subidas</string>\n    <string name=\"delete_uploaded_songs_confirm\">¿Seguro que quieres eliminar %1$d canciones subidas? Esta acción no se puede deshacer.</string>\n    <string name=\"deleted_n_songs\">Se eliminaron %1$d canciones</string>\n    <string name=\"deleting\">Eliminando…</string>\n    <string name=\"export_playlist\">Exportar lista de reproducción</string>\n    <string name=\"export_as_csv\">Exportar como CSV</string>\n    <string name=\"export_as_m3u\">Exportar como M3U</string>\n    <string name=\"export_success\">Lista de reproducción exportada correctamente</string>\n    <string name=\"export_failed\">No se pudo exportar la lista de reproducción</string>\n    <string name=\"export_option_share\">Compartir</string>\n    <string name=\"export_option_save\">Guardar en Documentos</string>\n    <string name=\"qs_tile_music_recognizer\">Identificar música</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-es/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Inicio</string>\n    <string name=\"songs\">Canciones</string>\n    <string name=\"artists\">Artistas</string>\n    <string name=\"albums\">Álbumes</string>\n    <string name=\"playlists\">Listas de reproducción</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d seleccionada</item>\n        <item quantity=\"many\">%d de seleccionadas</item>\n        <item quantity=\"other\">%d seleccionadas</item>\n    </plurals>\n    <string name=\"history\">Historial</string>\n    <string name=\"stats\">Estadísticas</string>\n    <string name=\"mood_and_genres\">Estado de ánimo y géneros</string>\n    <string name=\"account\">Cuenta</string>\n    <string name=\"quick_picks\">Selecciones rápidas</string>\n    <string name=\"quick_picks_empty\">Escuche canciones para generar sus selecciones rápidas</string>\n    <string name=\"forgotten_favorites\">Favoritos olvidados</string>\n    <string name=\"keep_listening\">Seguir escuchando</string>\n    <string name=\"similar_to\">Similares a</string>\n    <string name=\"new_release_albums\">Álbumes recién lanzados</string>\n    <string name=\"today\">Hoy</string>\n    <string name=\"yesterday\">Ayer</string>\n    <string name=\"this_week\">Esta semana</string>\n    <string name=\"last_week\">La semana pasada</string>\n    <string name=\"most_played_songs\">Canciones más reproducidas</string>\n    <string name=\"most_played_artists\">Artistas más reproducidos</string>\n    <string name=\"most_played_albums\">Álbumes más reproducidos</string>\n    <string name=\"search\">Buscar</string>\n    <string name=\"search_yt_music\">Buscar en YouTube Music…</string>\n    <string name=\"search_library\">Buscar en la biblioteca…</string>\n    <string name=\"filter_library\">Biblioteca</string>\n    <string name=\"filter_liked\">Me gusta</string>\n    <string name=\"filter_downloaded\">Descargado</string>\n    <string name=\"filter_all\">Todo</string>\n    <string name=\"filter_songs\">Canciones</string>\n    <string name=\"filter_videos\">Vídeos</string>\n    <string name=\"filter_albums\">Álbumes</string>\n    <string name=\"filter_artists\">Artistas</string>\n    <string name=\"filter_playlists\">Listas de reproducción</string>\n    <string name=\"filter_community_playlists\">Listas de la comunidad</string>\n    <string name=\"filter_featured_playlists\">Listas destacadas</string>\n    <string name=\"filter_bookmarked\">En marcadores</string>\n    <string name=\"no_results_found\">No se han encontrado resultados</string>\n    <string name=\"library_song_empty\">Las canciones de la biblioteca aparecerán aquí</string>\n    <string name=\"library_artist_empty\">Los artistas de la biblioteca aparecerán aquí</string>\n    <string name=\"library_album_empty\">Los álbumes de la biblioteca aparecerán aquí</string>\n    <string name=\"library_playlist_empty\">Tus listas de reproducción aparecerán aquí</string>\n    <string name=\"from_your_library\">De tu biblioteca</string>\n    <string name=\"liked_songs\">Canciones que te gustan</string>\n    <string name=\"downloaded_songs\">Canciones descargadas</string>\n    <string name=\"playlist_is_empty\">La lista de reproducción está vacía</string>\n    <string name=\"remove_download_playlist_confirm\">¿Realmente quiere quitar todas las canciones de la lista de reproducción «%s» del almacenamiento de canciones descargadas?</string>\n    <string name=\"delete_playlist_confirm\">¿Confirma que quiere eliminar la lista de reproducción «%s»?</string>\n    <string name=\"retry\">Reintentar</string>\n    <string name=\"radio\">Radio</string>\n    <string name=\"shuffle\">Aleatorio</string>\n    <string name=\"reset\">Restablecer</string>\n    <string name=\"details\">Detalles</string>\n    <string name=\"edit\">Editar</string>\n    <string name=\"start_radio\">Iniciar radio</string>\n    <string name=\"play\">Reproducir</string>\n    <string name=\"play_next\">Reproducir a continuación</string>\n    <string name=\"add_to_queue\">Añadir a la cola</string>\n    <string name=\"add_to_library\">Añadir a la biblioteca</string>\n    <string name=\"remove_from_library\">Quitar de la biblioteca</string>\n    <string name=\"action_download\">Descargar</string>\n    <string name=\"downloading\">Descargando</string>\n    <string name=\"remove_download\">Eliminar descarga</string>\n    <string name=\"import_playlist\">Importar lista de reproducción</string>\n    <string name=\"add_to_playlist\">Añadir a lista de reproducción</string>\n    <string name=\"view_artist\">Ver artista</string>\n    <string name=\"view_album\">Ver álbum</string>\n    <string name=\"refetch\">Cargar de nuevo</string>\n    <string name=\"share\">Compartir</string>\n    <string name=\"delete\">Eliminar</string>\n    <string name=\"remove_from_history\">Eliminar del historial</string>\n    <string name=\"remove_from_playlist\">Quitar de la lista de reproducción</string>\n    <string name=\"remove_from_queue\">Quitar de la cola</string>\n    <string name=\"search_online\">Buscar en línea</string>\n    <string name=\"action_sync\">Sincronizar</string>\n    <string name=\"advanced\">Avanzado</string>\n    <string name=\"tempo_and_pitch\">Tempo y tono</string>\n    <string name=\"sort_by_create_date\">Fecha añadida</string>\n    <string name=\"sort_by_name\">Nombre</string>\n    <string name=\"sort_by_artist\">Artista</string>\n    <string name=\"sort_by_year\">Año</string>\n    <string name=\"sort_by_song_count\">Número de canciones</string>\n    <string name=\"sort_by_length\">Duración</string>\n    <string name=\"sort_by_play_time\">Tiempo de reproducción</string>\n    <string name=\"sort_by_custom\">Orden personalizado</string>\n    <string name=\"media_id\">ID multimedia</string>\n    <string name=\"mime_type\">Tipo de MIME</string>\n    <string name=\"codecs\">Códecs</string>\n    <string name=\"bitrate\">Tasa de bits</string>\n    <string name=\"sample_rate\">Frecuencia</string>\n    <string name=\"loudness\">Intensidad</string>\n    <string name=\"volume\">Volumen</string>\n    <string name=\"file_size\">Tamaño del archivo</string>\n    <string name=\"unknown\">Desconocido</string>\n    <string name=\"copied\">Copiado al portapapeles</string>\n    <string name=\"edit_lyrics\">Editar letra</string>\n    <string name=\"search_lyrics\">Buscar letra</string>\n    <string name=\"edit_song\">Editar canción</string>\n    <string name=\"song_title\">Título</string>\n    <string name=\"song_artists\">Artistas</string>\n    <string name=\"error_song_title_empty\">El título de la canción no puede estar vacío.</string>\n    <string name=\"error_song_artist_empty\">El artista de la canción no puede estar vacío.</string>\n    <string name=\"save\">Guardar</string>\n    <string name=\"choose_playlist\">Elegir lista de reproducción</string>\n    <string name=\"edit_playlist\">Editar lista de reproducción</string>\n    <string name=\"create_playlist\">Crear lista de reproducción</string>\n    <string name=\"playlist_name\">Nombre de la lista de reproducción</string>\n    <string name=\"error_playlist_name_empty\">El nombre de la lista de reproducción no puede estar vacío.</string>\n    <string name=\"edit_artist\">Editar artista</string>\n    <string name=\"artist_name\">Nombre del artista</string>\n    <string name=\"error_artist_name_empty\">El nombre del artista no puede estar vacío.</string>\n    <string name=\"duplicates\">Duplicados</string>\n    <string name=\"skip_duplicates\">Saltar duplicados</string>\n    <string name=\"add_anyway\">Añadir de todas formas</string>\n    <string name=\"duplicates_description_single\">Esta canción ya está en su lista de reproducción</string>\n    <string name=\"duplicates_description_multiple\">%d canciones ya están en su lista de reproducción</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d canción</item>\n        <item quantity=\"many\">%d de canciones</item>\n        <item quantity=\"other\">%d canciones</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d artista</item>\n        <item quantity=\"many\">%d de artistas</item>\n        <item quantity=\"other\">%d artistas</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d álbum</item>\n        <item quantity=\"many\">%d de álbumes</item>\n        <item quantity=\"other\">%d álbumes</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d lista de reproducción</item>\n        <item quantity=\"many\">%d listas de reproducción</item>\n        <item quantity=\"other\">%d listas de reproducción</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d semana</item>\n        <item quantity=\"many\">%d de semanas</item>\n        <item quantity=\"other\">%d semanas</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d mes</item>\n        <item quantity=\"many\">%d de meses</item>\n        <item quantity=\"other\">%d meses</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d año</item>\n        <item quantity=\"many\">%d de años</item>\n        <item quantity=\"other\">%d años</item>\n    </plurals>\n    <string name=\"playlist_imported\">Lista de reproducción importada</string>\n    <string name=\"removed_song_from_playlist\">Eliminado «%s» de la lista de reproducción</string>\n    <string name=\"playlist_synced\">Lista de reproducción sincronizada</string>\n    <string name=\"undo\">Deshacer</string>\n    <string name=\"lyrics_not_found\">Letras no encontradas</string>\n    <string name=\"sleep_timer\">Temporizador de apagado</string>\n    <string name=\"end_of_song\">Al finalizar la canción</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">1 minuto</item>\n        <item quantity=\"many\">%d de minutos</item>\n        <item quantity=\"other\">%d minutos</item>\n    </plurals>\n    <string name=\"error_no_stream\">No hay un stream disponible</string>\n    <string name=\"error_no_internet\">No hay conexión a internet</string>\n    <string name=\"error_timeout\">Tiempo de espera agotado</string>\n    <string name=\"error_unknown\">Error desconocido</string>\n    <string name=\"action_like\">Me gusta</string>\n    <string name=\"action_like_all\">Marcar todo como me gusta</string>\n    <string name=\"action_remove_like\">Quitar me gusta</string>\n    <string name=\"action_remove_like_all\">Eliminar todos los me gusta</string>\n    <string name=\"action_shuffle_on\">Activar aleatorio</string>\n    <string name=\"action_shuffle_off\">Desactivar aleatorio</string>\n    <string name=\"repeat_mode_off\">Repetición desactivada</string>\n    <string name=\"repeat_mode_one\">Repetir canción actual</string>\n    <string name=\"repeat_mode_all\">Repetir cola</string>\n    <string name=\"queue_all_songs\">Todas las canciones</string>\n    <string name=\"queue_searched_songs\">Canciones buscadas</string>\n    <string name=\"music_player\">Reproductor de música</string>\n    <string name=\"settings\">Ajustes</string>\n    <string name=\"appearance\">Apariencia</string>\n    <string name=\"theme\">Tema</string>\n    <string name=\"enable_dynamic_theme\">Habilitar tema dinámico</string>\n    <string name=\"dark_theme\">Tema oscuro</string>\n    <string name=\"dark_theme_on\">Activado</string>\n    <string name=\"dark_theme_off\">Desactivado</string>\n    <string name=\"dark_theme_follow_system\">Tema del sistema</string>\n    <string name=\"pure_black\">Negro puro</string>\n    <string name=\"customize_navigation_tabs\">Personalizar pestañas de navegación</string>\n    <string name=\"player\">Reproductor</string>\n    <string name=\"player_text_alignment\">Alineación de texto del reproductor</string>\n    <string name=\"lyrics_text_position\">Posición de la letra de la canción</string>\n    <string name=\"sided\">Lateralmente</string>\n    <string name=\"left\">Izquierda</string>\n    <string name=\"center\">Centro</string>\n    <string name=\"right\">Derecha</string>\n    <string name=\"player_slider_style\">Estilo de la barra del reproductor</string>\n    <string name=\"default_\">Predeterminado</string>\n    <string name=\"squiggly\">Ondulado</string>\n    <string name=\"misc\">Otros</string>\n    <string name=\"default_open_tab\">Pestaña abierta predeterminada</string>\n    <string name=\"grid_cell_size\">Tamaño de la celda de la cuadrícula</string>\n    <string name=\"small\">Pequeño</string>\n    <string name=\"big\">Grande</string>\n    <string name=\"content\">Contenido</string>\n    <string name=\"login\">Cuenta</string>\n    <string name=\"not_logged_in\">Sesión no iniciada</string>\n    <string name=\"content_language\">Idioma de contenido predeterminado</string>\n    <string name=\"content_country\">País de contenido predeterminado</string>\n    <string name=\"system_default\">Predeterminado del sistema</string>\n    <string name=\"enable_proxy\">Activar «proxy»</string>\n    <string name=\"proxy_type\">Tipo de «proxy»</string>\n    <string name=\"proxy_url\">URL del «proxy»</string>\n    <string name=\"restart_to_take_effect\">Reinicie para aplicar los cambios</string>\n    <string name=\"player_and_audio\">Reproductor y sonido</string>\n    <string name=\"audio_quality\">Calidad de sonido</string>\n    <string name=\"audio_quality_auto\">Automática</string>\n    <string name=\"audio_quality_high\">Alta</string>\n    <string name=\"audio_quality_low\">Baja</string>\n    <string name=\"queue\">Cola</string>\n    <string name=\"persistent_queue\">Cola persistente</string>\n    <string name=\"persistent_queue_desc\">Restaura tu última cola al iniciar la aplicación</string>\n    <string name=\"skip_silence\">Omitir silencios</string>\n    <string name=\"audio_normalization\">Normalización del audio</string>\n    <string name=\"auto_skip_next_on_error\">Saltar automáticamente a la siguiente canción cuando se produce un error</string>\n    <string name=\"auto_skip_next_on_error_desc\">Asegura una experiencia de reproducción continua</string>\n    <string name=\"stop_music_on_task_clear\">Detener la música cuando se cierre la aplicación</string>\n    <string name=\"equalizer\">Ecualizador</string>\n    <string name=\"storage\">Almacenamiento</string>\n    <string name=\"cache\">Caché</string>\n    <string name=\"image_cache\">Caché de imágenes</string>\n    <string name=\"song_cache\">Caché de canciones</string>\n    <string name=\"max_cache_size\">Tamaño máximo de la caché</string>\n    <string name=\"unlimited\">Ilimitado</string>\n    <string name=\"clear_all_downloads\">Borrar todas las descargas</string>\n    <string name=\"max_image_cache_size\">Tamaño máximo de la caché de la imagen</string>\n    <string name=\"clear_image_cache\">Borrar caché de imágenes</string>\n    <string name=\"max_song_cache_size\">Tamaño máximo del caché de canciones</string>\n    <string name=\"clear_song_cache\">Borrar caché de canciones</string>\n    <string name=\"size_used\">%s usado</string>\n    <string name=\"privacy\">Privacidad</string>\n    <string name=\"pause_listen_history\">Pausar historial de reproducciones</string>\n    <string name=\"clear_listen_history\">Borrar historial de reproducciones</string>\n    <string name=\"clear_listen_history_confirm\">¿Confirma que quiere borrar todo el historial de reproducciones?</string>\n    <string name=\"pause_search_history\">Pausar historial de búsquedas</string>\n    <string name=\"clear_search_history\">Borrar historial de búsquedas</string>\n    <string name=\"clear_search_history_confirm\">¿Confirma que quiere borrar todo el historial de búsquedas?</string>\n    <string name=\"enable_lrclib\">Activar proveedor de letras LrcLib</string>\n    <string name=\"enable_kugou\">Activar proveedor de letras KuGou</string>\n    <string name=\"hide_explicit\">Ocultar contenido explícito</string>\n    <string name=\"backup_restore\">Copias de seguridad y restauración</string>\n    <string name=\"action_backup\">Respaldar</string>\n    <string name=\"action_restore\">Restaurar</string>\n    <string name=\"imported_playlist\">Lista de reproducción importada</string>\n    <string name=\"backup_create_success\">Copia de seguridad creada con éxito</string>\n    <string name=\"backup_create_failed\">No se ha podido crear la copia de seguridad</string>\n    <string name=\"restore_failed\">Error al restaurar la copia de seguridad</string>\n    <string name=\"discord_integration\">Integración con Discord</string>\n    <string name=\"discord_information\">Metrolist utiliza la biblioteca KizzyRPC para establecer el estado de su cuenta de Discord. Esto implica el uso de la conexión Discord Gateway, lo que podría considerarse una violación de las condiciones del servicio de Discord. Sin embargo, no se conocen casos de cuentas de usuario suspendidas por este motivo. Úselo bajo su propio riesgo.\\n\\nMetrolist solo extraerá su ficha; todo lo demás se almacena localmente.</string>\n    <string name=\"dismiss\">Descartar</string>\n    <string name=\"options\">Opciones</string>\n    <string name=\"preview\">Vista previa</string>\n    <string name=\"login_failed\">Error al acceder a la cuenta</string>\n    <string name=\"action_logout\">Finalizar sesión</string>\n    <string name=\"enable_discord_rpc\">Activar Rich Presence</string>\n    <string name=\"about\">Acerca de</string>\n    <string name=\"app_version\">Versión de la app</string>\n    <string name=\"new_version_available\">Nueva versión disponible</string>\n    <string name=\"translation_models\">Modelos de traducción</string>\n    <string name=\"clear_translation_models\">Borrar modelos de traducción</string>\n    <string name=\"listen_history\">Historial de reproducciones</string>\n    <string name=\"your_youtube_playlists\">Sus listas de reproducción de YouTube</string>\n    <string name=\"remove_all_from_library\">Quitar todo de la biblioteca</string>\n    <string name=\"search_history\">Historial de búsqueda</string>\n    <string name=\"disable_screenshot\">Desactivar captura de pantalla</string>\n    <string name=\"auto_load_more\">Cargar automáticamente más canciones</string>\n    <string name=\"disable_screenshot_desc\">Cuando esta opción está activada, las capturas de pantalla y la vista de la aplicación en Recientes estará deshabilitada.</string>\n    <string name=\"auto_load_more_desc\">Añade automáticamente más canciones cuando llegues al final de la cola, si es posible</string>\n    <string name=\"other_versions\">Otras versiones</string>\n    <string name=\"add_all_to_library\">Añadir todo a la biblioteca</string>\n    <string name=\"use_login_for_browse\">Utilice el inicio de sesión para ver el contenido</string>\n    <string name=\"use_login_for_browse_desc\">Esto puede influir en el contenido que ve y, por ejemplo, muestra álbumes exclusivos si ha iniciado sesión con una cuenta Premium</string>\n    <string name=\"action_login\">Acceder</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-es-rUS/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"lyrics_auto_scroll\">Letras sincronizadas</string>\n    <string name=\"advanced_login\">Inicio de sesión con token</string>\n    <string name=\"token_hidden\">Presiona para mostrar el token</string>\n    <string name=\"token_shown\">Presiona de nuevo para copiar o editar</string>\n    <string name=\"general\">General</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"default_lib_chips\">Cambiar pestaña predeterminada de la biblioteca</string>\n    <string name=\"set_quick_picks\">Establecer selecciones rápidas</string>\n    <string name=\"last_song_listened\">Basado en canciones escuchadas últimamente</string>\n    <string name=\"app_language\">Idioma de la aplicación</string>\n    <string name=\"similar_content_desc\">Agregar automáticamente más canciones similares cuando la cola se termine</string>\n    <string name=\"all_time\">Todo el tiempo</string>\n    <string name=\"past_24_hours\">Últimas 24 horas</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"open_app_settings_error\">No se pudo abrir la configuración de la aplicación</string>\n    <string name=\"top_length\">Longitud de mi lista de favoritos</string>\n    <string name=\"information\">Información</string>\n    <string name=\"auto_playlists\">Listas de reproducción automáticas</string>\n    <string name=\"show_liked_playlist\">Mostrar \\\"Mis me gusta\\\"</string>\n    <string name=\"show_downloaded_playlist\">Mostrar \\\"Música descargada\\\"</string>\n    <string name=\"show_top_playlist\">Mostrar \\\"Top de reproducción\\\"</string>\n    <string name=\"show_cached_playlist\">Mostrar la lista de reproducción “En caché”</string>\n    <string name=\"enable_similar_content\">Habilitar contenido similar</string>\n    <string name=\"import_online\">Importar una lista \\\"m3u\\\"</string>\n    <string name=\"import_csv\">Importar una lista \\\"csv\\\"</string>\n    <string name=\"playlist_add_local_to_synced_note\">Nota: Añadir canciones locales a listas de reproducción sincronizadas/remotas no está soportado, cualquier otra combinación es valida</string>\n    <string name=\"release_notes\">Notas de actualizaciones</string>\n    <string name=\"past_week\">Semana pasada</string>\n    <string name=\"past_month\">Mes pasado</string>\n    <string name=\"description\">Descripción</string>\n    <string name=\"views\">Vistas</string>\n    <string name=\"likes\">Me gustas</string>\n    <string name=\"history_duration\">Duración del historial</string>\n    <string name=\"local_history\">Local</string>\n    <string name=\"remote_history\">Remoto</string>\n    <string name=\"charts\">Lista de éxitos</string>\n    <string name=\"back_button_desc\">Atrás</string>\n    <string name=\"album_cover_desc\">Portada de álbum</string>\n    <string name=\"top_music_videos\">Top de vídeos músicales</string>\n    <string name=\"weeks\">Semanas</string>\n    <string name=\"years\">Años</string>\n    <string name=\"continuous\">Continuo</string>\n    <string name=\"liked\">Tus \\\"me gusta\\\"</string>\n    <string name=\"offline\">Descargados</string>\n    <string name=\"cached_playlist\">Guardado en la caché</string>\n    <string name=\"sync_playlist\">Sincronizar lista de reproducción</string>\n    <string name=\"sync_disabled\">Sincronización desactivada</string>\n    <string name=\"allows_for_sync_witch_youtube\">Nota: Esto permite sincronizarse con YouTube Music. NO se puede cambiar después.</string>\n    <string name=\"generating_image\">Generando imagen</string>\n    <string name=\"please_wait\">Por favor espera</string>\n    <string name=\"cancel\">Cancelar</string>\n    <string name=\"my_top\">Mis favoritos</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d vez</item>\n        <item quantity=\"many\">%d veces</item>\n        <item quantity=\"other\">%d veces</item>\n    </plurals>\n    <string name=\"share_lyrics\">Compartir letras</string>\n    <string name=\"share_as_text\">Compartir como texto</string>\n    <string name=\"share_as_image\">Compartir como imagen</string>\n    <string name=\"max_selection_limit\">Se alcanzó el límite en la selección</string>\n    <string name=\"share_selected\">Compartir lo seleccionado</string>\n    <string name=\"customize_colors\">Personalizar colores</string>\n    <string name=\"text_color\">Color del texto</string>\n    <string name=\"secondary_text_color\">Color secundario del texto</string>\n    <string name=\"background_color\">Color del fondo</string>\n    <string name=\"remove_from_cache\">Eliminar de la caché</string>\n    <string name=\"copy_link\">Copiar enlace</string>\n    <string name=\"select\">Seleccionar todos</string>\n    <string name=\"like_all\">Dar me gusta a todo</string>\n    <string name=\"dislike_all\">Dar no me gusta a todo</string>\n    <string name=\"sort_by_last_updated\">Fecha actualizada</string>\n    <string name=\"lyrics\">Letras</string>\n    <string name=\"already_in_playlist\">Actualmente en la lista de reproducción:</string>\n    <string name=\"similar_content\">Contenido similar</string>\n    <string name=\"player_background_style\">Estilo del reproductor de fondo</string>\n    <string name=\"follow_theme\">Seguir tema</string>\n    <string name=\"gradient\">Gradiente</string>\n    <string name=\"player_background_blur\">Desenfoque</string>\n    <string name=\"player_buttons_style\">Colores de los botones del reproductor</string>\n    <string name=\"default_style\">Por defecto</string>\n    <string name=\"enable_swipe_thumbnail\">Activar deslizar para pasar canción</string>\n    <string name=\"swipe_song_to_add\">Desliza la canción hacia la izquierda para agregarla a la cola, o hacia la derecha para reproducirla a continuación</string>\n    <string name=\"lyrics_click_change\">Cambiar letra al presionar</string>\n    <string name=\"auto_download_on_like\">Descargar automáticamente al dar \\\"me gusta\\\"</string>\n    <string name=\"auto_download_on_like_desc\">Automáticamente se descargarán las canciones cuando le des me gusta</string>\n    <string name=\"clear_song_cache_dialog\">¿Estás seguro de eliminar todas las canciones de la caché?</string>\n    <string name=\"clear_downloads_dialog\">¿Estás seguro de eliminar todas las canciones descargadas?</string>\n    <string name=\"not_logged_in_youtube\">No haz iniciado sesión en YouTube</string>\n    <string name=\"default_links\">Abrir links soportados</string>\n    <string name=\"dislikes\">No me gusta</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">1 segundo</item>\n        <item quantity=\"many\">%d segundos</item>\n        <item quantity=\"other\">%d segundos</item>\n    </plurals>\n    <string name=\"trending\">Tendencia</string>\n    <string name=\"link_copied\">Link copiado al portapapeles</string>\n    <string name=\"token_adv_login_description\">Este es un método de inicio de sesión AVANZADO. Como una alternativa de la página web, puedes entrar directamente o actualizar tu token de sesión aquí. Por ejemplo, esto puede acelerar tu inicio de sesión en múltiples dispositivos. Ten en cuenta que cualquier formato invalido del token no es aceptado</string>\n    <string name=\"months\">Meses</string>\n    <string name=\"past_year\">Año pasado</string>\n    <string name=\"slim\">Delgado</string>\n    <string name=\"slim_navbar\">Barra de navegación inferior delgada</string>\n    <string name=\"now_playing\">Reproduciendo Ahora</string>\n    <string name=\"close\">Cerrar</string>\n    <string name=\"hide_player_thumbnail\">Ocultar carátula del reproductor</string>\n    <string name=\"hide_player_thumbnail_desc\">Reemplaza la carátula del álbum por el logo de la app en el reproductor</string>\n    <string name=\"seek_forward_dynamic\">+%1$d segundos adelante</string>\n    <string name=\"seek_backward_dynamic\">-%1$d segundos hacia atrás</string>\n    <string name=\"seek_seconds_addup\">Búsqueda progresiva</string>\n    <string name=\"seek_seconds_addup_description\">Si se activa, incrementará 5 segundos en cada salto de búsqueda</string>\n    <string name=\"new_player_design\">Nuevo diseño del reproductor</string>\n    <string name=\"new_mini_player_design\">Nuevo diseño del mini reproductor</string>\n    <string name=\"lyrics_romanize_japanese\">Romanizar letras japonesas</string>\n    <string name=\"lyrics_romanize_korean\">Romanizar letras coreanas</string>\n    <string name=\"yt_sync\">Auto-sincronizar con cuenta</string>\n    <string name=\"more_content\">Más contenido</string>\n    <string name=\"swipe_sensitivity\">Sensibilidad de deslizamiento del mini reproductor</string>\n    <string name=\"clear_image_cache_dialog\">¿Estás seguro de querer limpiar todas las imágenes en caché?</string>\n    <string name=\"disable\">Desactivar</string>\n    <string name=\"subscribe\">Suscribir</string>\n    <string name=\"subscribed\">Suscrito</string>\n    <string name=\"disable_load_more_when_repeat_all\">Deshabilitar cargar más al repetir todo</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">No precargar más canciones ni contenido similar cuando repetir todo está activado</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"starting_radio\">Iniciando radio</string>\n    <string name=\"settings_section_ui\">Interfaz</string>\n    <string name=\"settings_section_privacy\">Privacidad y seguridad</string>\n    <string name=\"settings_section_player_content\">Reproductor y contenido</string>\n    <string name=\"settings_section_storage\">Almacenamiento y datos</string>\n    <string name=\"settings_section_system\">Sistema e información</string>\n    <string name=\"edit_playlist_cover\">Editar carátula de la lista de reproducción</string>\n    <string name=\"edit_playlist_cover_note\">Nota: Tu cuenta debe estar vinculada a un número de teléfono y verificada en YouTube Music para modificar la carátula de la lista de reproducción.</string>\n    <string name=\"edit_playlist_cover_note_wait\">Luego de seleccionar una imagen, deberás esperar un momento para que la nueva carátula aparezca en tu lista de reproducción.</string>\n    <string name=\"config_proxy\">Configurar proxy</string>\n    <string name=\"proxy_username\">Nombre de usuario del proxy</string>\n    <string name=\"proxy_password\">Contraseña del proxy</string>\n    <string name=\"enable_authentication\">Habilitar autenticación</string>\n    <string name=\"lyrics_romanization_cyrillic\">Cirílico</string>\n    <string name=\"lyrics_romanize_title\">Romanización</string>\n    <string name=\"lyrics_romanization\">Romanización de letras</string>\n    <string name=\"lyrics_romanize_russian\">Romanizar letras en ruso</string>\n    <string name=\"lyrics_romanize_ukrainian\">Romanizar letras en ucraniano</string>\n    <string name=\"lyrics_romanize_belarusian\">Romanizar letras en bielorruso</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Romanizar letras en kirguís</string>\n    <string name=\"lyrics_romanize_serbian\">Romanizar letras en serbio</string>\n    <string name=\"lyrics_romanize_bulgarian\">Romanizar letras en búlgaro</string>\n    <string name=\"line_by_line_option_title\">EXPERIMENTAL: Detectar idioma línea por línea</string>\n    <string name=\"line_by_line_option_desc\">El idioma cirílico será detectado línea por línea en lugar de toda la canción.</string>\n    <string name=\"line_by_line_dialog_title\">¿Estás seguro?</string>\n    <string name=\"line_by_line_dialog_desc\">Esta función es experimental y propensa a fallos.\\n\\nPor defecto, el idioma es determinado a partir de la canción completa, pero al activar esta opción, será determinado línea por línea. Esto permitirá que las canciones multilingüe funcionen, PERO el idioma podría no ser siempre el correcto (por ejemplo, si hay alguna letra en ucraniano que no contiene caracteres específico de este idioma, podría ser romanizada al ruso).\\n\\nSi no tienes inconvenientes, es recomendable que dejes esta opción desactivada.</string>\n    <string name=\"romanize_current_track\">Romanizar pista actual</string>\n    <string name=\"uploaded_playlist\">Subidas</string>\n    <string name=\"swipe_song_to_remove\">Desliza la canción para eliminarla de la playlist</string>\n    <string name=\"show_uploaded_playlist\">Mostrar listas de reproducción \\\"Subidas\\\"</string>\n    <string name=\"choose_from_library\">Elija de la biblioteca</string>\n    <string name=\"remove_custom_image\">Eliminar imagen personalizada</string>\n    <string name=\"discord_use_details\">Usar detalles en lugar de estado</string>\n    <string name=\"filter_uploaded\">Subido</string>\n    <string name=\"download_playlist_desc\">Descargar todas las canciones para reproducirlas sin conexión</string>\n    <string name=\"remove_download_playlist_desc\">Remover todas las canciones descargadas de esta lista de reproducción</string>\n    <string name=\"download_in_progress_desc\">La descarga está en progreso</string>\n    <string name=\"share_playlist_desc\">Compartir esta lista de reproducción con otros</string>\n    <string name=\"delete_playlist_desc\">Eliminar esta lista de reproducción de forma permanente</string>\n    <string name=\"sync_playlist_desc\">Sincronizar la lista de reproducción con YouTube Music</string>\n    <string name=\"primary_color_style\">Color primario</string>\n    <string name=\"tertiary_color_style\">Color terciario</string>\n    <string name=\"lyrics_glow_effect\">Habilitar efecto de brillo de la letra</string>\n    <string name=\"lyrics_glow_effect_desc\">Añadir animación de brillo y efecto de rebote a la letra activa</string>\n    <string name=\"enable_better_lyrics\">Habilitar Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">Usar el proveedor Better Lyrics para letras sincronizadas palabra a palabra</string>\n    <string name=\"auto_scroll\">Sincronizar de nuevo</string>\n    <string name=\"shuffle_playlist_first\">Aleatorizar lista de reproducción/álbum primero</string>\n    <string name=\"shuffle_playlist_first_desc\">Al aleatorizar, reproducir primero todas las canciones de la lista de reproducción/álbum original, luego el contenido similar</string>\n    <string name=\"show_wrapped_card\">Mostrar tarjeta de Recap</string>\n    <string name=\"discord_use_details_description\">Mostrar resaltado el título de la canción en lugar de los nombres de los artistas</string>\n    <string name=\"lyrics_romanize_chinese\">Romanizar letras chinas</string>\n    <string name=\"updater\">Actualizador</string>\n    <string name=\"check_for_updates\">Buscar actualizaciones automáticamente</string>\n    <string name=\"update_notifications\">Habilitar las notificaciones de actualización</string>\n    <string name=\"update_available_title\">Actualización disponible</string>\n    <string name=\"update_channel_name\">Actualizaciones de la app</string>\n    <string name=\"update_channel_desc\">Notificaciones de nuevas versiones</string>\n    <string name=\"audio_offload\">Habilitar descarga de audio</string>\n    <string name=\"audio_offload_description\">Usar la ruta de descarga de audio para la reproducción. Desactivar esto puede aumentar el consumo de batería, pero podría ser útil si estás experimentando problemas con la reproducción o posprocesado de audio</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"google_cast_description\">Habilitar la transmisión de audio a Chromecast y otros dispositivos compatibles con Cast</string>\n    <string name=\"lyrics_romanize_macedonian\">Romanizar letras en macedonio</string>\n    <string name=\"integrations\">Integraciones</string>\n    <string name=\"username\">Nombre de usuario</string>\n    <string name=\"password\">Contraseña</string>\n    <string name=\"lastfm_integration\">Integración con Last.fm</string>\n    <string name=\"enable_scrobbling\">Habilitar scrobbling</string>\n    <string name=\"lastfm_now_playing\">Enviar lo que estoy escuchando</string>\n    <string name=\"last_fm_send_likes\">Enviar Me gusta/No me gusta</string>\n    <string name=\"last_fm_send_likes_description\">Marcar como Me gusta/No me gusta las canciones en Last.fm cuando son marcadas como Me gusta/No me gusta en Metrolist</string>\n    <string name=\"logging_in\">Iniciando sesión…</string>\n    <string name=\"wavy\">Ondulado</string>\n    <string name=\"enable_simpmusic\">Habilitar letras SimpMusic</string>\n    <string name=\"enable_simpmusic_desc\">Usar el proveedor SimpMusic para letras sincronizadas</string>\n    <string name=\"remember_shuffle_and_repeat\">Recordar aleatorio y repetir</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Recordar el estado de los modos aleatorio y repetir al reiniciar la app</string>\n    <string name=\"pause_music_when_media_is_muted\">Pausar la música al silenciar la multimedia</string>\n    <string name=\"lyrics_offset\">Compensación de letras</string>\n    <string name=\"scrobbling_configuration\">Configuración de scrobbling</string>\n    <string name=\"scrobble_min_track_duration\">Hacer scrobble a canciones más largas que</string>\n    <string name=\"scrobble_delay_percent\">Porcentaje de retraso de scrobble</string>\n    <string name=\"scrobble_delay_minutes\">Minutos de retraso de scrobble</string>\n    <string name=\"hide_video_songs\">Ocultar canciones de video</string>\n    <string name=\"details_desc\">Ver la información de la canción</string>\n    <string name=\"edit_desc\">Cambiar el título o artista</string>\n    <string name=\"start_radio_desc\">Crear una estación basada en este elemento</string>\n    <string name=\"play_next_desc\">Añadir al inicio de la cola</string>\n    <string name=\"add_to_queue_desc\">Añadir al final de la cola</string>\n    <string name=\"add_to_library_desc\">Guardar en tu biblioteca</string>\n    <string name=\"download_desc\">Hacer disponible para reproducir sin conexión</string>\n    <string name=\"show_less\">Mostrar menos</string>\n    <string name=\"show_more\">Mostrar más</string>\n    <string name=\"about_artist\">Acerca</string>\n    <string name=\"artist_page_settings\">Página del artista</string>\n    <string name=\"show_artist_description\">Mostrar descripción del artista</string>\n    <string name=\"show_artist_monthly_listeners\">Mostrar oyentes mensuales</string>\n    <string name=\"show_artist_subscriber_count\">Mostrar cantidad de suscriptores</string>\n    <string name=\"skip_silence_desc\">Avanzar rápidamente por las partes silenciosas de las canciones</string>\n    <string name=\"skip_silence_instant\">Saltar silencio de forma instantánea</string>\n    <string name=\"skip_silence_instant_desc\">Saltar hacia adelante durante los momentos silenciosos en lugar de acelerar la reproducción</string>\n    <string name=\"persistent_shuffle_title\">Aleatorio persistente</string>\n    <string name=\"persistent_shuffle_desc\">Mantener el modo aleatorio activado al iniciar nuevas canciones o listas de reproducción</string>\n    <string name=\"add_to_playlist_desc\">Añadir a una de tus listas de reproducción</string>\n    <string name=\"refetch_desc\">Obtener los metadatos más recientes de YouTube Music</string>\n    <string name=\"share_desc\">Compartir un enlace a este elemento</string>\n    <string name=\"delete_desc\">Remover este elemento permanentemente</string>\n    <string name=\"advanced_desc\">Cambiar la velocidad y el tono de la canción</string>\n    <string name=\"equalizer_desc\">Ajustar el ecualizador de audio</string>\n    <string name=\"enable_dynamic_icon\">Habilitar icono dinámico</string>\n    <string name=\"mini_player\">Mini reproductor</string>\n    <string name=\"pure_black_mini_player\">Mini reproductor completamente negro</string>\n    <string name=\"cache_size_warning_title\">¡Espera!</string>\n    <string name=\"cache_size_warning_message\">Elegiste un límite de tamaño de caché menor que el usado actualmente por la app (%1$s). Si continúas, la app podría remover algunos %2$s de la caché para ajustarse al nuevo límite. ¿Proceder de todas formas?</string>\n    <string name=\"cache_size_warning_confirm\">Continuar</string>\n    <string name=\"lyrics_animation_style\">Estilo de animación palabra a palabra</string>\n    <string name=\"none\">Ninguno</string>\n    <string name=\"fade\">Difuminar</string>\n    <string name=\"glow\">Brillar</string>\n    <string name=\"slide\">Deslizar</string>\n    <string name=\"karaoke\">Karaoke</string>\n    <string name=\"apple_music_style\">Apple Music</string>\n    <string name=\"lyrics_text_size\">Tamaño del texto de la letra</string>\n    <string name=\"lyrics_line_spacing\">Interlineado de la letra</string>\n    <string name=\"album_art_for\">Carátula del álbum para %s</string>\n    <string name=\"wrapped_total_albums_title\">Ya escuchaste</string>\n    <string name=\"wrapped_total_albums_subtitle\">álbumes únicos</string>\n    <string name=\"wrapped_top_album_title\">Tu álbum favorito es</string>\n    <string name=\"wrapped_playlist_ready\">Tu lista de reproducción personal está lista</string>\n    <string name=\"wrapped_top_5_albums_title\">Tus 5 álbumes favoritos</string>\n    <string name=\"wrapped_album_listening_time\">Escuchaste este álbum durante %d minutos</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d minutos</string>\n    <string name=\"wrapped_no_data\">Sin datos</string>\n    <string name=\"wrapped_top_5_artists_title\">Tus artistas favoritos del año</string>\n    <string name=\"wrapped_artist_listening_time\">%d minutos</string>\n    <string name=\"wrapped_top_5_songs_title\">Tus canciones favoritas del año</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">Carátula del álbum</string>\n    <string name=\"wrapped_top_artist_title\">Tu artista favorito del año es</string>\n    <string name=\"wrapped_top_artist_image_content_description\">Imagen del artista favorito</string>\n    <string name=\"wrapped_top_artist_listening_time\">Los escuchaste durante %d minutos</string>\n    <string name=\"wrapped_top_song_title\">Tu canción más reproducida es</string>\n    <string name=\"wrapped_top_song_listening_time\">Escuchaste durante %d minutos</string>\n    <string name=\"wrapped_total_artists_title\">Escuchaste a</string>\n    <string name=\"wrapped_total_artists_subtitle\">artistas únicos</string>\n    <string name=\"wrapped_total_songs_title\">Escuchaste</string>\n    <string name=\"wrapped_total_songs_subtitle\">canciones únicas</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_intro_subtitle\">es hora de ver lo que estuviste escuchando</string>\n    <string name=\"wrapped_intro_button\">¡vamos!</string>\n    <string name=\"wrapped_logo_content_description\">Logo de Metrolist</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"wrapped_ready_title\">¡TU RECAP ESTÁ LISTO!</string>\n    <string name=\"wrapped_ready_subtitle\">Hora de ver lo que te gustó este año.</string>\n    <string name=\"wrapped_thank_you\">Gracias por escuchar</string>\n    <string name=\"wrapped_special_thanks\">Agradecimiento especial a MO Agamy por crear Metrolist</string>\n    <string name=\"wrapped_close\">Cerrar Recap</string>\n    <string name=\"wrapped_playlist_title\">Tu Recap %s</string>\n    <string name=\"wrapped_create_playlist\">Crear lista de reproducción</string>\n    <string name=\"wrapped_playlist_saved\">Lista de reproducción guardada</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"one\">%d perfil</item>\n        <item quantity=\"many\">%d de perfiles</item>\n        <item quantity=\"other\">%d perfiles</item>\n    </plurals>\n    <string name=\"equalizer_header\">Ecualizador</string>\n    <string name=\"no_profiles\">No hay perfiles de ecualizador</string>\n    <string name=\"import_profile\">Importar perfil</string>\n    <string name=\"system_equalizer\">Ecualizador del sistema</string>\n    <string name=\"eq_disabled\">Desactivado</string>\n    <plurals name=\"band_count\">\n        <item quantity=\"one\">%d bandas</item>\n        <item quantity=\"many\">%d de bandas</item>\n        <item quantity=\"other\">%d bandas</item>\n    </plurals>\n    <string name=\"delete_profile_desc\">Eliminar perfil</string>\n    <string name=\"delete_profile_confirmation\">¿Estás seguro de que quieres eliminar %1$s? Esta acción no se puede deshacer.</string>\n    <string name=\"error_file_read\">No se pudo leer el archivo</string>\n    <string name=\"error_file_open\">No se pudo abrir el archivo: %1$s</string>\n    <string name=\"import_error_title\">Error al importar</string>\n    <string name=\"casting_to\">Transmitiendo a %s</string>\n    <string name=\"listening_to_metrolist\">Escuchando Metrolist</string>\n    <string name=\"open\">Abrir</string>\n    <string name=\"failed_to_create_image\">No se pudo crear la imagen: %s</string>\n    <string name=\"copied_title\">Se copió el título</string>\n    <string name=\"copied_artist\">Se copió el artista</string>\n    <string name=\"error_playing\">Error al reproducir</string>\n    <string name=\"failed_to_parse_proxy\">No se pudo analizar la URL del proxy.</string>\n    <string name=\"album_art\">Carátula del álbum</string>\n    <string name=\"no_song_playing\">Ninguna canción en reproducción</string>\n    <string name=\"tap_to_open\">Toca para abrir Metrolist</string>\n    <string name=\"previous\">Anterior</string>\n    <string name=\"play_pause\">Reproducir/pausar</string>\n    <string name=\"next\">Siguiente</string>\n    <string name=\"like\">Me Gusta</string>\n    <string name=\"widget_description\">Widget de reproductor de música con controles de reproducción</string>\n    <string name=\"turntable_widget_description\">Widget de música circular con controles de reproducción y me gusta</string>\n    <string name=\"progress_percent\">Progreso %s%%</string>\n    <string name=\"error_playback_failed\">Falló la reproducción</string>\n    <string name=\"crop_album_art\">Recortar carátula del álbum</string>\n    <string name=\"crop_album_art_desc\">Recortar las miniaturas de los videos para forzar una relación de aspecto cuadrada</string>\n    <string name=\"error_title\">Error</string>\n    <string name=\"error_eq_apply_failed\">No se pudo aplicar el perfil de ecualización: %1$s</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">Mantener la pantalla encendida cuando el reproductor esté expandido</string>\n    <string name=\"listen_together\">Escuchar juntos</string>\n    <string name=\"listen_together_server_url\">URL del servidor</string>\n    <string name=\"listen_together_username\">Nombre de usuario</string>\n    <string name=\"listen_together_connected\">Conectado</string>\n    <string name=\"listen_together_reconnecting\">Reconectando…</string>\n    <string name=\"listen_together_disconnected\">Desconectado</string>\n    <string name=\"listen_together_connecting\">Conectando…</string>\n    <string name=\"listen_together_error\">Error de conexión</string>\n    <string name=\"listen_together_create_room\">Crear sala</string>\n    <string name=\"listen_together_create_room_desc\">Crea una sala y comparte el código con tus amigos</string>\n    <string name=\"listen_together_join_room\">Unirse a la sala</string>\n    <string name=\"listen_together_room_code\">Código de la sala</string>\n    <string name=\"listen_together_you_are_host\">Eres el anfitrión</string>\n    <string name=\"listen_together_you_are_guest\">Eres un invitado</string>\n    <string name=\"listen_together_join_requests\">Solicitudes para unirse</string>\n    <string name=\"listen_together_view_logs\">Ver registros</string>\n    <string name=\"listen_together_view_logs_desc\">Depurar conexión y mensajes</string>\n    <string name=\"listen_together_logs\">Registros de conexión</string>\n    <string name=\"listen_together_no_logs\">Aún no hay registros</string>\n    <string name=\"listen_together_description\">Escucha música con tus amigos en tiempo real. Crea una sala para ser el anfitrión o únete a una sala existente con un código.</string>\n    <string name=\"listen_together_background_disconnect_note\">Nota: Es posible que se te desconecte si creas una sala mientras no hay música reproduciéndose y luego cambias a otra aplicación.</string>\n    <string name=\"listen_together_not_configured\">Escuchar juntos no está configurado. Por favor, configura la URL del servidor en Ajustes → Integraciones → Escuchar juntos.</string>\n    <string name=\"listen_together_suggestion_received\">%1$s solicitó %2$s</string>\n    <string name=\"listen_together_suggestion_sent\">¡Sugerencia enviada al anfitrión!</string>\n    <string name=\"listen_together_join_request_notification\">%1$s quiere unirse a la sala</string>\n    <string name=\"listen_together_notification_channel_name\">Escuchar juntos</string>\n    <string name=\"listen_together_notification_channel_desc\">Notificaciones para eventos de Escuchar juntos</string>\n    <string name=\"listen_together_room_created\">Sala creada: %s</string>\n    <string name=\"listen_together_cannot_edit_username_in_room\">No puedes editar el nombre de usuario mientras estás en una sala</string>\n    <string name=\"waiting_for_approval\">Esperando aprobación del anfitrión</string>\n    <string name=\"invalid_room_code\">Código de sala inválido</string>\n    <string name=\"join_request_denied\">Solicitud para unirse rechazada</string>\n    <string name=\"join_existing_room\">Unirse a una sala existente</string>\n    <string name=\"room_code\">Código de la sala</string>\n    <string name=\"leave_room\">Salir de la sala</string>\n    <string name=\"join_room\">Unirse</string>\n    <string name=\"create_room\">Crear</string>\n    <string name=\"joining_room\">Uniéndose a la sala %s…</string>\n    <string name=\"creating_room\">Creando sala…</string>\n    <string name=\"connect\">Conectar</string>\n    <string name=\"disconnect\">Desconectar</string>\n    <string name=\"create\">Crear</string>\n    <string name=\"join\">Unirse</string>\n    <string name=\"approve\">Aprobar</string>\n    <string name=\"reject\">Rechazar</string>\n    <string name=\"clear\">Limpiar</string>\n    <string name=\"copy\">Copiar</string>\n    <string name=\"copied_to_clipboard\">Copiado al portapapeles</string>\n    <string name=\"not_set\">No configurado</string>\n    <string name=\"hosting_room\">Alojando la sala</string>\n    <string name=\"in_room\">En la sala</string>\n    <string name=\"pending_requests\">Solicitudes pendientes</string>\n    <string name=\"pending_suggestions\">Sugerencias pendientes</string>\n    <string name=\"suggest_to_host\">Sugerir al anfitrión</string>\n    <string name=\"kick_user\">Expulsar</string>\n    <string name=\"host_label\">Anfitrión</string>\n    <string name=\"you_label\">Tú</string>\n    <string name=\"connected_users\">Usuarios conectados</string>\n    <string name=\"enter_username\">Introducir nombre de usuario</string>\n    <string name=\"error_username_empty\">El nombre de usuario es obligatorio.</string>\n    <string name=\"resync\">Volver a sincronizar</string>\n    <string name=\"enable\">Habilitar</string>\n    <string name=\"player_background_solid\">Sólido</string>\n    <string name=\"prevent_duplicate_tracks_in_queue\">Previene el duplicado de canciones en la cola de reproducción</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">Al añadir una pista a la cola, elimínela de su posición anterior si ya está presente</string>\n    <string name=\"resume_on_bluetooth_connect\">Resume al conectar Bluetooth</string>\n    <string name=\"crossfade\">Transición suave</string>\n    <string name=\"crossfade_desc\">Transición suave entre canciones</string>\n    <string name=\"crossfade_duration\">Duración del desvanecido</string>\n    <string name=\"crossfade_gapless\">Desactivar para álbumes sin pausas</string>\n    <string name=\"crossfade_gapless_desc\">No desvanecer si el álbum no tiene pausas</string>\n    <string name=\"crossfade_beta_title\">Funciones Beta</string>\n    <string name=\"crossfade_beta_message\">Transición suave es una nueva función y puede tener errores. Si experimentas algún problema, por favor, infórmanos.\\n\\nEsta función desactiva la descarga de audio debido a limitaciones técnicas.</string>\n    <string name=\"lyrics_romanize_hindi\">Romanizar letras Hindi</string>\n    <string name=\"lyrics_romanize_punjabi\">Romanizar letras Punjabi</string>\n    <string name=\"audio_offload_disabled_by_crossfade\">Deshabilitado porque el desvanecido esta activo</string>\n    <string name=\"lyrics_romanize_as_main\">Mostrar letras romanizadas como principales</string>\n    <string name=\"hide_youtube_shorts\">Ocultar Youtube Shorts</string>\n    <string name=\"not_playing\">No se está reproduciendo ninguna canción</string>\n    <string name=\"tap_to_play\">Toca para abrir Metrolist</string>\n    <string name=\"widget_music_player\">Reproductor de Música</string>\n    <string name=\"widget_turntable\">Disco giratorio</string>\n    <string name=\"together\">En compañia</string>\n    <string name=\"listen_together_choose_server\">Elije el servidor</string>\n    <string name=\"listen_together_custom_server\">Servidor personalizado</string>\n    <string name=\"listen_together_use_custom_server\">Usar servidor personalizado</string>\n    <string name=\"mute\">Silenciar</string>\n    <string name=\"unmute\">Activar sonido</string>\n    <string name=\"listen_together_auto_approval_joins\">Aprobar automáticamente las solicitudes de ingreso</string>\n    <string name=\"listen_together_auto_approval_joins_desc\">Aprobar automáticamente las solicitudes de ingreso en lugar de revisarlas manualmente</string>\n    <string name=\"listen_together_sync_volume\">Sincronizar volumen del anfitrión</string>\n    <string name=\"listen_together_sync_volume_desc\">Los invitados siguen el nivel de volumen del anfitrión</string>\n    <string name=\"listen_together_in_top_bar\">Mover Escuchar Juntos a la barra superior</string>\n    <string name=\"listen_together_in_top_bar_desc\">Mostrar Escuchar Juntos en la barra superior en lugar de la barra de navegación</string>\n    <string name=\"enter_room_code\">Ingresa el código de la sala</string>\n    <string name=\"listen_together_settings_desc\">Configurar servidor, nombre de usuario y más</string>\n    <string name=\"copy_code\">Copiar código</string>\n    <string name=\"kick_user_desc\">Remover a esta persona de la sesión</string>\n    <string name=\"permanently_kick_user\">Bloquear permanentemente</string>\n    <string name=\"permanently_kick_user_desc\">Bloquear y ocultar las solicitudes de ingreso para esta persona</string>\n    <string name=\"transfer_ownership\">Transferir propiedad</string>\n    <string name=\"transfer_ownership_desc\">Convertir esta persona en el anfitrión de la sala</string>\n    <string name=\"manage_user\">Gestionar usuario</string>\n    <string name=\"listen_together_blocked_users\">Usuarios bloqueados</string>\n    <string name=\"listen_together_blocked_users_count\">%d usuario(s) bloqueado(s)</string>\n    <string name=\"listen_together_no_blocked_users\">No hay usuarios bloqueados</string>\n    <string name=\"unblock\">Desbloquear</string>\n    <string name=\"user_blocked_by_host\">Usuario bloqueado por el anfitrión</string>\n    <string name=\"ai_lyrics_translation\">Traducción de letras con IA</string>\n    <string name=\"ai_translating_lyrics\">Traduciendo letras...</string>\n    <string name=\"ai_lyrics_translated\">Letras traducidas</string>\n    <string name=\"ai_provider\">Proovedor</string>\n    <string name=\"ai_base_url\">URL base</string>\n    <string name=\"ai_api_key\">Clave API</string>\n    <string name=\"ai_model\">Modelo</string>\n    <string name=\"ai_translation_mode\">Modo de traducción</string>\n    <string name=\"ai_target_language\">Idioma de destino</string>\n    <string name=\"ai_setup_guide\">Credenciales de la API</string>\n    <string name=\"ai_translation_literal\">Traducción</string>\n    <string name=\"ai_translation_literal_desc\">Traducir el significado al idioma de destino</string>\n    <string name=\"ai_translation_transcribed\">Transcripción</string>\n    <string name=\"ai_translation_transcribed_desc\">Convertir la pronunciación al alfabeto de destino</string>\n    <string name=\"ai_api_key_required\">Clave API requerida</string>\n    <string name=\"ai_error_api_key_required\">Clave API es requerida</string>\n    <string name=\"ai_error_no_lyrics\">Sin letras para traducir</string>\n    <string name=\"ai_error_lyrics_empty\">Las letras están vacías</string>\n    <string name=\"ai_error_language_required\">Es requerido un idioma de destino</string>\n    <string name=\"ai_error_unexpected\">Resultado de traducción inesperado</string>\n    <string name=\"ai_error_unknown\">Ocurrió un error desconocido</string>\n    <string name=\"ai_error_translation_failed\">Traducción fallida</string>\n    <string name=\"ai_provider_help\">Obtener claves API</string>\n    <string name=\"ai_provider_openrouter_help\">Visita openrouter.ai para modelos gratuitos y de pago</string>\n    <string name=\"ai_provider_openai_help\">Visita platform.openai.com/api-keys</string>\n    <string name=\"ai_provider_claude_help\">Visita console.anthropic.com/settings/keys</string>\n    <string name=\"ai_provider_gemini_help\">Visita aistudio.google.com/apikey</string>\n    <string name=\"ai_provider_perplexity_help\">Visita perplexity.ai/settings/api</string>\n    <string name=\"ai_provider_xai_help\">Visita console.x.ai</string>\n    <string name=\"ai_provider_deepl_help\">Visita deepl.com/pro-api para modelos gratuitos y de pago</string>\n    <string name=\"ai_deepl_formality\">Formalidad</string>\n    <string name=\"ai_deepl_formality_default\">Por defecto</string>\n    <string name=\"ai_deepl_formality_more\">Más formal</string>\n    <string name=\"ai_deepl_formality_less\">Menos formal</string>\n    <string name=\"crash_title\">La aplicación se cerró</string>\n    <string name=\"crash_description\">Se ha producido un error inesperado. Por favor, comparta el informe de errores para ayudarnos a solucionar el problema.</string>\n    <string name=\"crash_share_logs\">Compartir Registros</string>\n    <string name=\"crash_share_title\">Compartir informe de errores</string>\n    <string name=\"crash_report_subject\">Informe de errores de Metrolist</string>\n    <string name=\"crash_close\">Cerrar</string>\n    <string name=\"crash_no_log\">No hay registro de fallos disponible</string>\n    <string name=\"palette_dynamic\">Dinámico</string>\n    <string name=\"palette_crimson\">Carmesí</string>\n    <string name=\"palette_rose\">Rosa</string>\n    <string name=\"palette_purple\">Púrpura</string>\n    <string name=\"palette_deep_purple\">Púrpura Intenso</string>\n    <string name=\"palette_indigo\">Indigo</string>\n    <string name=\"palette_blue\">Azul</string>\n    <string name=\"palette_sky_blue\">Azul Cielo</string>\n    <string name=\"palette_cyan\">Cian</string>\n    <string name=\"palette_teal\">Verde Azulado</string>\n    <string name=\"palette_green\">Verde</string>\n    <string name=\"palette_light_green\">Verde Claro</string>\n    <string name=\"palette_lime\">Verde Lima</string>\n    <string name=\"palette_yellow\">Amarillo</string>\n    <string name=\"palette_amber\">Ámbar</string>\n    <string name=\"palette_orange\">Naranja</string>\n    <string name=\"palette_deep_orange\">Naranja Intenso</string>\n    <string name=\"palette_brown\">Café</string>\n    <string name=\"palette_grey\">Gris</string>\n    <string name=\"palette_blue_grey\">Azul Grisáceo</string>\n    <string name=\"cd_back\">Volver</string>\n    <string name=\"cd_pure_black_mode\">Modo Negro Puro</string>\n    <string name=\"cd_light_mode\">Modo claro</string>\n    <string name=\"cd_dark_mode\">Modo oscuro</string>\n    <string name=\"cd_system_mode\">Sistema</string>\n    <string name=\"cd_palette_item\">Paleta %1$s</string>\n    <string name=\"play_all\">Reproducir todo</string>\n    <string name=\"enable_high_refresh_rate\">Habilitar alta tasa de refresco</string>\n    <string name=\"enable_high_refresh_rate_desc\">Obliga a la pantalla a funcionar con la frecuencia de actualización más alta compatible (por ejemplo, 120 Hz)</string>\n    <string name=\"recognize_music\">Identificar Canción</string>\n    <string name=\"tap_to_recognize\">Toca para identificar</string>\n    <string name=\"listening\">Escuchando…</string>\n    <string name=\"processing\">Procesando…</string>\n    <string name=\"no_match_found\">No se ha encontrado ninguna coincidencia</string>\n    <string name=\"recognition_error\">Error de reconocimiento</string>\n    <string name=\"try_again\">Intenta de nuevo</string>\n    <string name=\"recognition_history\">Historial de reconocimientos</string>\n    <string name=\"clear_recognition_history\">Limpiar el historial de reconocimientos</string>\n    <string name=\"clear_recognition_history_confirm\">¿Estás seguro de que deseas borrar todo el historial de reconocimiento?</string>\n    <string name=\"delete_from_history\">Eliminar del historial</string>\n    <string name=\"re_listen\">Volver a escuchar</string>\n    <string name=\"play_on_app\">Escuchar en Metrolist</string>\n    <string name=\"map_csv_columns\">Asignar columnas CSV</string>\n    <string name=\"first_row_is_header\">La primera fila es el encabezado</string>\n    <string name=\"artist_name_column\">Columna del nombre del artista</string>\n    <string name=\"song_title_column\">Columna del título de la canción</string>\n    <string name=\"youtube_url_column\">Columna URL de YouTube (opcional)</string>\n    <string name=\"continue_action\">Continuar</string>\n    <string name=\"importing_csv\">Importando CSV</string>\n    <string name=\"recently_converted\">Convertidos Recientemente</string>\n    <string name=\"column_label\">Columna %d</string>\n    <string name=\"discord_status\">Estado</string>\n    <string name=\"discord_status_online\">En línea</string>\n    <string name=\"discord_status_idle\">Inactivo</string>\n    <string name=\"discord_status_dnd\">No Molestar</string>\n    <string name=\"discord_buttons\">Botones</string>\n    <string name=\"discord_button_1\">Botón 1</string>\n    <string name=\"discord_button_2\">Botón 2</string>\n    <string name=\"login_successful\">¡Inicio de sesión exitoso!</string>\n    <string name=\"discord_information_warning\">Esta función utiliza la biblioteca KizzyRPC para conectarse a la puerta de enlace de Discord y configurar tu estado de presencia enriquecida. Aunque no se conoce ningún caso de suspensión de cuentas por un uso similar, este método no está oficialmente respaldado por Discord y puede considerarse una infracción de los Términos de servicio. Tu token se extrae localmente y nunca se envía a servidores de terceros. Procede bajo tu propia responsabilidad.</string>\n    <string name=\"discord_activity_type\">Tipo de actividad</string>\n    <string name=\"discord_activity_playing\">Reproduciendo</string>\n    <string name=\"discord_activity_listening\">Escuchando</string>\n    <string name=\"discord_activity_watching\">Viendo</string>\n    <string name=\"discord_activity_competing\">Compitiendo</string>\n    <string name=\"discord_button_text_variables\">Variables: {nombre_de_la_canción}, {nombre_del_artista}, {nombre_del_álbum}</string>\n    <string name=\"discord_rpc_preview\">Previsualizar Presencia</string>\n    <string name=\"discord_presence\">Presencia</string>\n    <string name=\"discord_connect_description\">Inicia sesión con Discord para compartir lo que estás escuchando</string>\n    <string name=\"discord_playing_metrolist\">Reproduciendo en Metrolist</string>\n    <string name=\"discord_watching_metrolist\">Viendo en Metrolist</string>\n    <string name=\"discord_competing_metrolist\">Compitiendo en Metrolist</string>\n    <string name=\"discord_activity_name\">Nombre de la actividad</string>\n    <string name=\"discord_activity_name_description\">Nombre personalizado para la actividad (déjelo vacío para usar el nombre predeterminado)</string>\n    <string name=\"discord_advanced_mode\">Modo avanzado</string>\n    <string name=\"discord_advanced_mode_description\">Mostrar opciones de personalización adicionales para la Presencia Enriquecida</string>\n    <string name=\"display_density\">Densidad de la pantalla</string>\n    <string name=\"restart\">Reiniciar</string>\n    <string name=\"restart_required\">Reinicio requerido</string>\n    <string name=\"density_restart_message\">El cambio en la densidad de la pantalla tendrá efecto después de reiniciar la aplicación. ¿Quieres reiniciarla ahora?</string>\n    <string name=\"found_in_settings_content\">Encontrado en ajustes &gt; Contenido</string>\n    <string name=\"plays\">reproducciones</string>\n    <string name=\"speed_dial\">Velocidad de reproducción</string>\n    <string name=\"randomize_home_order\">Aleatorizar el orden de la pantalla de inicio</string>\n    <string name=\"randomize_home_order_desc\">Reordenar aleatoriamente las secciones de la pantalla de inicio según prioridad</string>\n    <string name=\"daily_discover_because_you_listen_to\">Porque escuchas a %1$s</string>\n    <string name=\"daily_discover_similar_to\">Similar a %1$s</string>\n    <string name=\"daily_discover_based_on\">Basado en %1$s</string>\n    <string name=\"daily_discover_for_fans_of\">Para fans de %1$s</string>\n    <string name=\"from_the_community\">De la comunidad</string>\n    <string name=\"daily_discover_sounds_like\">Sonidos como %1$s</string>\n    <string name=\"deleting\">Borrando…</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-es-rUS/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"mood_and_genres\">Estado de ánimo y géneros</string>\n    <string name=\"account\">Cuenta</string>\n    <string name=\"quick_picks\">Selecciones rápidas</string>\n    <string name=\"quick_picks_empty\">Escucha canciones para generar tus selecciones rápidas</string>\n    <string name=\"forgotten_favorites\">Favoritos olvidados</string>\n    <string name=\"keep_listening\">Sigue escuchando</string>\n    <string name=\"your_youtube_playlists\">Tus listas de reproducción de YouTube</string>\n    <string name=\"similar_to\">Similar a</string>\n    <string name=\"new_release_albums\">Nuevos álbumes de lanzamiento</string>\n    <string name=\"media_id\">ID Multimedia</string>\n    <string name=\"home\">Inicio</string>\n    <string name=\"songs\">Canciones</string>\n    <string name=\"artists\">Artistas</string>\n    <string name=\"albums\">Álbumes</string>\n    <string name=\"playlists\">Listas de reproducción</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d seleccionada</item>\n        <item quantity=\"many\">%d de seleccionadas</item>\n        <item quantity=\"other\">%d seleccionadas</item>\n    </plurals>\n    <string name=\"history\">Historial</string>\n    <string name=\"stats\">Estadísticas</string>\n    <string name=\"today\">Hoy</string>\n    <string name=\"yesterday\">Ayer</string>\n    <string name=\"this_week\">Esta semana</string>\n    <string name=\"last_week\">La semana pasada</string>\n    <string name=\"most_played_songs\">Canciones más reproducidas</string>\n    <string name=\"most_played_artists\">Artistas más reproducidos</string>\n    <string name=\"most_played_albums\">Álbumes más reproducidos</string>\n    <string name=\"search\">Buscar</string>\n    <string name=\"search_yt_music\">Buscar música en YouTube…</string>\n    <string name=\"search_library\">Buscar biblioteca…</string>\n    <string name=\"filter_library\">Biblioteca</string>\n    <string name=\"filter_liked\">Favoritos</string>\n    <string name=\"filter_downloaded\">Descargado</string>\n    <string name=\"filter_all\">Todo</string>\n    <string name=\"filter_songs\">Canciones</string>\n    <string name=\"filter_videos\">Vídeos</string>\n    <string name=\"filter_albums\">Álbumes</string>\n    <string name=\"filter_artists\">Artistas</string>\n    <string name=\"filter_playlists\">Listas de reproducción</string>\n    <string name=\"filter_community_playlists\">Listas de reproducción de la comunidad</string>\n    <string name=\"filter_featured_playlists\">Listas de reproducción destacadas</string>\n    <string name=\"filter_bookmarked\">Marcado como favorito</string>\n    <string name=\"no_results_found\">No se encontraron resultados</string>\n    <string name=\"library_song_empty\">Las canciones de la biblioteca aparecerán aquí</string>\n    <string name=\"library_artist_empty\">La biblioteca de artistas aparecerán aquí</string>\n    <string name=\"library_album_empty\">Los álbumes de la biblioteca aparecerán aquí</string>\n    <string name=\"library_playlist_empty\">Las listas de reproducción aparecerán aquí</string>\n    <string name=\"from_your_library\">Desde tu biblioteca</string>\n    <string name=\"other_versions\">Otras versiones</string>\n    <string name=\"liked_songs\">Canciones favoritas</string>\n    <string name=\"downloaded_songs\">Canciones descargadas</string>\n    <string name=\"playlist_is_empty\">La lista de reproducción está vacía</string>\n    <string name=\"remove_download_playlist_confirm\">¿Realmente quieres eliminar todas las canciones de la lista de reproducción \\\"%s\\\" del almacenamiento de canciones descargadas?</string>\n    <string name=\"delete_playlist_confirm\">¿Realmente quieres eliminar la lista de reproducción \\\"%s\\\"?</string>\n    <string name=\"retry\">Volver a intentar</string>\n    <string name=\"radio\">Radio</string>\n    <string name=\"shuffle\">Aleatorio</string>\n    <string name=\"reset\">Reiniciar</string>\n    <string name=\"details\">Detalles</string>\n    <string name=\"edit\">Editar</string>\n    <string name=\"sort_by_name\">Nombre</string>\n    <string name=\"start_radio\">Iniciar radio</string>\n    <string name=\"play\">Reproducir</string>\n    <string name=\"play_next\">Reproducir siguiente</string>\n    <string name=\"add_to_queue\">Añadir a la cola</string>\n    <string name=\"add_to_library\">Añadir a la biblioteca</string>\n    <string name=\"add_all_to_library\">Añadir todo a la biblioteca</string>\n    <string name=\"remove_from_library\">Quitar de la biblioteca</string>\n    <string name=\"remove_all_from_library\">Eliminar todo de la biblioteca</string>\n    <string name=\"action_download\">Descargar</string>\n    <string name=\"downloading\">Descargando</string>\n    <string name=\"remove_download\">Eliminar descarga</string>\n    <string name=\"import_playlist\">Importar lista de reproducción</string>\n    <string name=\"add_to_playlist\">Agregar a la lista de reproducción</string>\n    <string name=\"view_artist\">Ver artista</string>\n    <string name=\"view_album\">Ver álbum</string>\n    <string name=\"refetch\">Recargar</string>\n    <string name=\"share\">Compartir</string>\n    <string name=\"delete\">Borrar</string>\n    <string name=\"remove_from_history\">Eliminar del historial</string>\n    <string name=\"remove_from_playlist\">Eliminar de la lista de reproducción</string>\n    <string name=\"remove_from_queue\">Quitar de la cola</string>\n    <string name=\"search_online\">Buscar en línea</string>\n    <string name=\"action_sync\">Sincronizar</string>\n    <string name=\"advanced\">Avanzado</string>\n    <string name=\"tempo_and_pitch\">Tempo y tono</string>\n    <string name=\"sort_by_create_date\">Fecha añadida</string>\n    <string name=\"sort_by_artist\">Artista</string>\n    <string name=\"sort_by_year\">Año</string>\n    <string name=\"sort_by_song_count\">Número de canciones</string>\n    <string name=\"sort_by_length\">Duración</string>\n    <string name=\"loudness\">Intensidad</string>\n    <string name=\"volume\">Volumen</string>\n    <string name=\"file_size\">Tamaño del archivo</string>\n    <string name=\"unknown\">Desconocido</string>\n    <string name=\"copied\">Copiado al portapapeles</string>\n    <string name=\"edit_lyrics\">Editar letra</string>\n    <string name=\"sort_by_play_time\">Tiempo de reproducción</string>\n    <string name=\"sort_by_custom\">Orden personalizado</string>\n    <string name=\"mime_type\">Tipo de MIME</string>\n    <string name=\"codecs\">Códecs</string>\n    <string name=\"bitrate\">Tasa de bits</string>\n    <string name=\"sample_rate\">Frecuencia de muestreo</string>\n    <string name=\"song_artists\">Artista/s de la canción</string>\n    <string name=\"playlist_name\">Nombre para la lista de reproducción</string>\n    <string name=\"error_song_title_empty\">El título de la canción no puede estar vacío.</string>\n    <string name=\"create_playlist\">Crear lista de reproducción</string>\n    <string name=\"error_playlist_name_empty\">Debes poner un nombre a la lista de reproducción.</string>\n    <string name=\"error_song_artist_empty\">No puede estar vacío el artista de la canción.</string>\n    <string name=\"song_title\">Título de la canción</string>\n    <string name=\"save\">Guardar</string>\n    <string name=\"choose_playlist\">Escoger lista de reproducción</string>\n    <string name=\"edit_playlist\">Editar lista de reproducción</string>\n    <string name=\"search_lyrics\">Buscar letra</string>\n    <string name=\"edit_song\">Editar canción</string>\n    <string name=\"edit_artist\">Editar artista</string>\n    <string name=\"artist_name\">Nombre del artista</string>\n    <string name=\"error_artist_name_empty\">Debe haber un nombre para el artista.</string>\n    <string name=\"duplicates\">Duplicados</string>\n    <string name=\"skip_duplicates\">Saltar duplicados</string>\n    <string name=\"add_anyway\">Agregar de todos modos</string>\n    <string name=\"duplicates_description_single\">Esta canción ya está en tu lista de reproducción</string>\n    <string name=\"duplicates_description_multiple\">%d canciones ya están en tu lista de reproducción</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d canción</item>\n        <item quantity=\"many\">%d canciones</item>\n        <item quantity=\"other\">%d canciones</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d artista</item>\n        <item quantity=\"many\">%d de artistas</item>\n        <item quantity=\"other\">%d artistas</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d álbum</item>\n        <item quantity=\"many\">%d de álbumes</item>\n        <item quantity=\"other\">%d álbumes</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d lista de reproducción</item>\n        <item quantity=\"many\">%d de listas de reproducción</item>\n        <item quantity=\"other\">%d listas de reproducción</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d semana</item>\n        <item quantity=\"many\">%d de semanas</item>\n        <item quantity=\"other\">%d semanas</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d mes</item>\n        <item quantity=\"many\">%d meses</item>\n        <item quantity=\"other\">%d meses</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d año</item>\n        <item quantity=\"many\">%d años</item>\n        <item quantity=\"other\">%d años</item>\n    </plurals>\n    <string name=\"playlist_imported\">Lista de reproducción importada</string>\n    <string name=\"removed_song_from_playlist\">Se ha eliminado «%s» de la lista de reproducción</string>\n    <string name=\"playlist_synced\">Lista de reproducción sincronizada</string>\n    <string name=\"undo\">Deshacer</string>\n    <string name=\"lyrics_not_found\">No se han encontrado letras</string>\n    <string name=\"end_of_song\">Fin de la canción</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">1 minuto</item>\n        <item quantity=\"many\">%d minutos</item>\n        <item quantity=\"other\">%d minutos</item>\n    </plurals>\n    <string name=\"error_no_stream\">No hay transmisión disponible</string>\n    <string name=\"error_no_internet\">Sin conexión a la red</string>\n    <string name=\"error_timeout\">Tiempo de espera</string>\n    <string name=\"error_unknown\">Error desconocido</string>\n    <string name=\"action_like\">Me gusta</string>\n    <string name=\"action_remove_like\">Eliminar «Me gusta»</string>\n    <string name=\"action_remove_like_all\">Eliminar todos los «Me gusta»</string>\n    <string name=\"action_shuffle_on\">Mezclar activado</string>\n    <string name=\"action_shuffle_off\">Mezclar apagado</string>\n    <string name=\"repeat_mode_one\">Repetir la canción actual</string>\n    <string name=\"repeat_mode_all\">Repetir cola</string>\n    <string name=\"queue_all_songs\">Todas las canciones</string>\n    <string name=\"queue_searched_songs\">Canciones buscadas</string>\n    <string name=\"music_player\">Reproductor de música</string>\n    <string name=\"settings\">Configuración</string>\n    <string name=\"theme\">Tema</string>\n    <string name=\"enable_dynamic_theme\">Activar tema dinámico</string>\n    <string name=\"dark_theme\">Tema oscuro</string>\n    <string name=\"dark_theme_follow_system\">Seguir el sistema</string>\n    <string name=\"pure_black\">Negro puro</string>\n    <string name=\"customize_navigation_tabs\">Personalizar pestañas de navegación</string>\n    <string name=\"player\">Reproductor</string>\n    <string name=\"player_text_alignment\">Alineación del texto del reproductor</string>\n    <string name=\"lyrics_text_position\">Posición del texto de la letra</string>\n    <string name=\"left\">Izquierda</string>\n    <string name=\"center\">Centro</string>\n    <string name=\"right\">Derecha</string>\n    <string name=\"player_slider_style\">Estilo del control deslizante del reproductor</string>\n    <string name=\"misc\">Varios</string>\n    <string name=\"default_open_tab\">Pestaña predeterminada abierta</string>\n    <string name=\"small\">Pequeña</string>\n    <string name=\"big\">Grande</string>\n    <string name=\"content\">Contenido</string>\n    <string name=\"action_logout\">Cerrar sesión</string>\n    <string name=\"action_login\">Iniciar sesión</string>\n    <string name=\"sleep_timer\">Temporizador de apagado</string>\n    <string name=\"repeat_mode_off\">Modo de repetición desactivado</string>\n    <string name=\"appearance\">Apariencia</string>\n    <string name=\"dark_theme_on\">Encendido</string>\n    <string name=\"dark_theme_off\">Apagado</string>\n    <string name=\"sided\">con bordes</string>\n    <string name=\"default_\">Predeterminado</string>\n    <string name=\"grid_cell_size\">Tamaño de celda de la cuadrícula</string>\n    <string name=\"login\">Inicio</string>\n    <string name=\"not_logged_in\">No ha iniciado sesión</string>\n    <string name=\"login_failed\">Error al iniciar sesión</string>\n    <string name=\"content_language\">Idioma predeterminado del contenido</string>\n    <string name=\"content_country\">País predeterminado del contenido</string>\n    <string name=\"system_default\">Predeterminado del sistema</string>\n    <string name=\"enable_proxy\">Activar proxy</string>\n    <string name=\"proxy_type\">Tipo de proxy</string>\n    <string name=\"proxy_url\">URL de proxy</string>\n    <string name=\"restart_to_take_effect\">Reiniciar para que surta efecto</string>\n    <string name=\"player_and_audio\">Reproductor y audio</string>\n    <string name=\"audio_quality\">Calidad de audio</string>\n    <string name=\"audio_quality_auto\">Automático</string>\n    <string name=\"audio_quality_high\">Alto</string>\n    <string name=\"audio_quality_low\">Bajo</string>\n    <string name=\"queue\">Cola</string>\n    <string name=\"persistent_queue\">Cola persistente</string>\n    <string name=\"persistent_queue_desc\">Restaurar la última cola al iniciar la aplicación</string>\n    <string name=\"auto_load_more\">Cargar automáticamente más canciones</string>\n    <string name=\"auto_load_more_desc\">Añadir automáticamente más canciones cuando se llegue al final de la cola, si es posible</string>\n    <string name=\"skip_silence\">Saltar silencio</string>\n    <string name=\"audio_normalization\">Normalización del audio</string>\n    <string name=\"auto_skip_next_on_error\">Saltar automáticamente a la siguiente canción cuando se produce un error</string>\n    <string name=\"auto_skip_next_on_error_desc\">Asegúrate de que la reproducción no se interrumpa</string>\n    <string name=\"stop_music_on_task_clear\">Detener la música al finalizar la tarea</string>\n    <string name=\"equalizer\">Ecualizador</string>\n    <string name=\"storage\">Almacenamiento</string>\n    <string name=\"cache\">Caché</string>\n    <string name=\"image_cache\">Caché de imágenes</string>\n    <string name=\"song_cache\">Caché de canciones</string>\n    <string name=\"max_cache_size\">Tamaño máximo de la caché</string>\n    <string name=\"unlimited\">Ilimitado</string>\n    <string name=\"clear_all_downloads\">Borrar todas las descargas</string>\n    <string name=\"max_image_cache_size\">Tamaño máximo de la caché de imágenes</string>\n    <string name=\"clear_image_cache\">Borrar caché de imágenes</string>\n    <string name=\"max_song_cache_size\">Tamaño máximo de la caché de canciones</string>\n    <string name=\"clear_song_cache\">Borrar caché de canciones</string>\n    <string name=\"size_used\">%s utilizado</string>\n    <string name=\"privacy\">Privacidad</string>\n    <string name=\"listen_history\">Escuchar historial</string>\n    <string name=\"pause_listen_history\">Pausar historial de escucha</string>\n    <string name=\"clear_listen_history\">Borrar historial de escucha</string>\n    <string name=\"clear_listen_history_confirm\">¿Estás seguro de que quieres borrar todo el historial de escucha?</string>\n    <string name=\"search_history\">Historial de búsqueda</string>\n    <string name=\"pause_search_history\">Pausar el historial de búsqueda</string>\n    <string name=\"clear_search_history\">Borrar historial de búsqueda</string>\n    <string name=\"clear_search_history_confirm\">¿Estás seguro de que quieres borrar todo el historial de búsqueda?</string>\n    <string name=\"use_login_for_browse\">Utiliza el inicio de sesión para navegar por el contenido</string>\n    <string name=\"use_login_for_browse_desc\">Esto puede influir en el contenido que ves y, por ejemplo, muestra álbumes exclusivos para usuarios Premium si has iniciado sesión con una cuenta Premium</string>\n    <string name=\"disable_screenshot\">Desactivar captura de pantalla</string>\n    <string name=\"disable_screenshot_desc\">Cuando esta opción está activada, las capturas de pantalla y la vista de la aplicación en Recientes están desactivadas.</string>\n    <string name=\"enable_lrclib\">Activitar proveedor de letras LrcLib</string>\n    <string name=\"enable_kugou\">Activitar proveedor de letras KuGou</string>\n    <string name=\"hide_explicit\">Ocultar contenido explícito</string>\n    <string name=\"backup_restore\">Copia de seguridad y restauración</string>\n    <string name=\"action_backup\">Copia de seguridad</string>\n    <string name=\"action_restore\">Restaurar</string>\n    <string name=\"imported_playlist\">Lista de reproducción importada</string>\n    <string name=\"backup_create_success\">Copia de seguridad creada correctamente</string>\n    <string name=\"backup_create_failed\">No se pudo crear la copia de seguridad</string>\n    <string name=\"restore_failed\">No se pudo restaurar la copia de seguridad</string>\n    <string name=\"discord_integration\">Integración con Discord</string>\n    <string name=\"discord_information\">Metrolist utiliza la biblioteca KizzyRPC para configurar el estado de tu cuenta de Discord. Esto implica el uso de la conexión Discord Gateway, lo que puede considerarse una violación de los Términos de servicio de Discord. Sin embargo, no se conocen casos de cuentas de usuario suspendidas por este motivo. Úsalo bajo tu propia responsabilidad.\\n\\nMetrolist solo extraerá tu token, y todo lo demás se almacena localmente.</string>\n    <string name=\"dismiss\">Desestimar</string>\n    <string name=\"options\">Opciones</string>\n    <string name=\"preview\">Vista previa</string>\n    <string name=\"enable_discord_rpc\">Activar Presencia Enriquecida</string>\n    <string name=\"about\">Sobre</string>\n    <string name=\"app_version\">Versión de la aplicación</string>\n    <string name=\"new_version_available\">Nueva versión disponible</string>\n    <string name=\"translation_models\">Modelos de traducción</string>\n    <string name=\"clear_translation_models\">Modelos de traducción claros</string>\n    <string name=\"action_like_all\">Me gustan todas</string>\n    <string name=\"squiggly\">Ondulado</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-et/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d kord</item>\n        <item quantity=\"other\">%d korda</item>\n    </plurals>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">1 sekund</item>\n        <item quantity=\"other\">%d sekundit</item>\n    </plurals>\n    <string name=\"please_wait\">Palun oota</string>\n    <string name=\"cancel\">Katkesta</string>\n    <string name=\"share_lyrics\">Jaga laulusõnu</string>\n    <string name=\"share_as_text\">Jaga tekstina</string>\n    <string name=\"share_as_image\">Jaga pildina</string>\n    <string name=\"copy_link\">Kopeeri link</string>\n    <string name=\"select\">Vali kõik</string>\n    <string name=\"like_all\">Märgi kõik meeldivaks</string>\n    <string name=\"dislike_all\">Märgi kõik mittemeeldivaks</string>\n    <string name=\"sort_by_last_updated\">Uuendamise kuupäev</string>\n    <string name=\"link_copied\">Link on kopeeritud lõikelauale</string>\n    <string name=\"lyrics\">Laulusõnad</string>\n    <string name=\"already_in_playlist\">Juba on esitusloendis:</string>\n    <string name=\"album_cover_desc\">Albumi kaanepilt</string>\n    <string name=\"local_history\">Kohalik</string>\n    <string name=\"remote_history\">Serveris</string>\n    <string name=\"charts\">Edetabelid</string>\n    <string name=\"back_button_desc\">Tagasi</string>\n    <string name=\"top_music_videos\">Populaarsed muusikavideod</string>\n    <string name=\"trending\">Populaarsust koguvad lood</string>\n    <string name=\"weeks\">Nädalaid</string>\n    <string name=\"months\">Kuid</string>\n    <string name=\"years\">Aastaid</string>\n    <string name=\"continuous\">Järjepidev</string>\n    <string name=\"liked\">Meeldivaks märgitud</string>\n    <string name=\"offline\">Allalaaditud</string>\n    <string name=\"my_top\">Minu lemmikud</string>\n    <string name=\"cached_playlist\">Puhverdatud</string>\n    <string name=\"sync_playlist\">Sünkroniseeri esitusloend</string>\n    <string name=\"sync_disabled\">Sünkroniseerimine pole kasutusel</string>\n    <string name=\"allows_for_sync_witch_youtube\">Märkus: see võimaldab sünkroonimist YouTube Musicuga. Sa EI SAA seda seadistust hiljem muuta.</string>\n    <string name=\"generating_image\">Loon pilti</string>\n    <string name=\"max_selection_limit\">Valiku ülempiir</string>\n    <string name=\"share_selected\">Jaga valitut</string>\n    <string name=\"customize_colors\">Kohenda värve</string>\n    <string name=\"text_color\">Teksti värv</string>\n    <string name=\"secondary_text_color\">Teisane tekstivärv</string>\n    <string name=\"background_color\">Taustavärv</string>\n    <string name=\"remove_from_cache\">Kustuta puhverdatud andmetest</string>\n    <string name=\"similar_content\">Sarnane sisu</string>\n    <string name=\"player_background_style\">Meediaesitaja tausta stiil</string>\n    <string name=\"follow_theme\">Järgi kujundust</string>\n    <string name=\"player_background_blur\">Hägustatud taust</string>\n    <string name=\"new_player_design\">Meediaesitaja uus kujundus</string>\n    <string name=\"gradient\">Gradient</string>\n    <string name=\"player_buttons_style\">Meediaesitaja nuppude värvid</string>\n    <string name=\"enable_swipe_thumbnail\">Kasuta loo vahetamisel viipamist</string>\n    <string name=\"swipe_song_to_add\">Vasakule viibates saa lisada loo esitusjärjekorda ning paremale viibates esitatakse teda järgmisena</string>\n    <string name=\"default_style\">Vaikimisi</string>\n    <string name=\"lyrics_click_change\">Vaheta laulusõnu klõpsimisega</string>\n    <string name=\"lyrics_auto_scroll\">Keri laulusõnu automaatselt</string>\n    <string name=\"lyrics_romanize_japanese\">Latiniseeri jaapanikeelseid laulusõnu</string>\n    <string name=\"lyrics_romanize_korean\">Latiniseeri koreakeelseid laulusõnu</string>\n    <string name=\"show_liked_playlist\">Näita esitusloendit „Meeldib“</string>\n    <string name=\"show_downloaded_playlist\">Näita esitusloendit „Allalaaditud“</string>\n    <string name=\"show_top_playlist\">Näita esitusloendit „Populaarne“</string>\n    <string name=\"show_cached_playlist\">Näita esitusloendit „Puhverdatud“</string>\n    <string name=\"advanced_login\">Logi sisse tunnusloaga</string>\n    <string name=\"token_hidden\">Tunnusloaga vaatamiseks klõpsi</string>\n    <string name=\"token_shown\">Klõpsi muutmiseks või kopeerimiseks</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"import_online\">Impordi m3u vormingus esitusloendeid</string>\n    <string name=\"import_csv\">Impordi csv vormingus esitusloendeid</string>\n    <string name=\"all_time\">Läbi aegade</string>\n    <string name=\"past_24_hours\">Viimase 24 tunni jooksul</string>\n    <string name=\"past_week\">Viimase nädala jooksul</string>\n    <string name=\"past_month\">Viimase kuu jooksul</string>\n    <string name=\"past_year\">Viimase aasta jooksul</string>\n    <string name=\"app_language\">Rakenduse keel</string>\n    <string name=\"auto_playlists\">Automaatsed esitusloendid</string>\n    <string name=\"last_song_listened\">Põhineb viimasel kuulatud lool</string>\n    <string name=\"open_app_settings_error\">Rakenduse seadistuste avamine ei õnnestunud</string>\n    <string name=\"release_notes\">Muudatuste logi</string>\n    <string name=\"history_duration\">Ajaloo kestus</string>\n    <string name=\"information\">Teave</string>\n    <string name=\"description\">Kirjeldus</string>\n    <string name=\"views\">Vaatamisi</string>\n    <string name=\"likes\">Meeldimisi</string>\n    <string name=\"dislikes\">Mittemeeldimisi</string>\n    <string name=\"subscribe\">Telli</string>\n    <string name=\"subscribed\">Tellitud</string>\n    <string name=\"top_length\">Minu parimate lugude loendi pikkus</string>\n    <string name=\"starting_radio\">Käivitan raadiot</string>\n    <string name=\"now_playing\">Praegu esitamisel</string>\n    <string name=\"close\">Sulge</string>\n    <string name=\"seek_forward_dynamic\">+%1$d sekundit edasi</string>\n    <string name=\"seek_backward_dynamic\">-%1$d sekundit tagasi</string>\n    <string name=\"new_mini_player_design\">Meediaesitaja uus kompaktne kujundus</string>\n    <string name=\"uploaded_playlist\">Üleslaaditud</string>\n    <string name=\"filter_uploaded\">Üleslaaditud</string>\n    <string name=\"general\">Üldised</string>\n    <string name=\"show_uploaded_playlist\">Näita esitusloendit „Üleslaaditud“</string>\n    <string name=\"more_content\">Täiendav sisu</string>\n    <string name=\"edit_playlist_cover\">Muuda esitusloendi kaanepilti</string>\n    <string name=\"choose_from_library\">Vali meediakogust</string>\n    <string name=\"remove_custom_image\">Eemalda sinu valitud pilt</string>\n    <string name=\"set_quick_picks\">Lisa kiirvalikud</string>\n    <string name=\"config_proxy\">Seadista proksiserverit</string>\n    <string name=\"proxy_username\">Puhverserveri kasutajanimi</string>\n    <string name=\"proxy_password\">Puhverserveri salasõna</string>\n    <string name=\"settings_section_ui\">Kasutajaliides</string>\n    <string name=\"settings_section_privacy\">Privaatsus ja turvalisus</string>\n    <string name=\"updater\">Uuendaja</string>\n    <string name=\"check_for_updates\">Kontrolli uuendusi automaatselt</string>\n    <string name=\"update_notifications\">Lülita uuenduste teavitused sisse</string>\n    <string name=\"update_available_title\">Uus versioon on saadaval</string>\n    <string name=\"update_channel_name\">Rakenduse uuendused</string>\n    <string name=\"integrations\">Lõimingud</string>\n    <string name=\"username\">Kasutajanimi</string>\n    <string name=\"password\">Salasõna</string>\n    <string name=\"lastfm_integration\">Last.fm-i lõiming</string>\n    <string name=\"enable_scrobbling\">Lülita kraasimine sisse</string>\n    <string name=\"lastfm_now_playing\">Saada hetkel esitatava loo andmed</string>\n    <string name=\"last_fm_send_likes\">Saada meeldimised/mittemeeldimised</string>\n    <string name=\"scrobbling_configuration\">Kraasimise seadistused</string>\n    <string name=\"show_artist_description\">Kuva artisti kirjeldust</string>\n    <string name=\"show_artist_subscriber_count\">Kuva tellijate arvu</string>\n    <string name=\"show_artist_monthly_listeners\">Kuva igakuine kuulajate arv</string>\n    <string name=\"show_less\">Kuva vähem</string>\n    <string name=\"show_more\">Kuva rohkem</string>\n    <string name=\"artist_page_settings\">Artisti leht</string>\n    <string name=\"about_artist\">Teave</string>\n    <string name=\"download_playlist_desc\">Laadi alla kõik lood võrguühenduseta taasesituseks</string>\n    <string name=\"remove_download_playlist_desc\">Eemalda kõik alla laetud lood sellest esitlusloendist</string>\n    <string name=\"download_in_progress_desc\">Allalaadimine on pooleli</string>\n    <string name=\"share_playlist_desc\">Jaga esitlusloendit teistega</string>\n    <string name=\"delete_playlist_desc\">Kustuta see esitlusloend jäädavalt</string>\n    <string name=\"sync_playlist_desc\">Sünkroniseeri esitlusloend Youtube Music\\'ga</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-et/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"playlists\">Esitusloendid</string>\n    <string name=\"history\">Ajalugu</string>\n    <string name=\"stats\">Statistika</string>\n    <string name=\"mood_and_genres\">Meeleolu ja žanrid</string>\n    <string name=\"account\">Kasutajakonto</string>\n    <string name=\"quick_picks_empty\">Kiirvalikute loomiseks kuula lugusid</string>\n    <string name=\"forgotten_favorites\">Unustatud lemmikud</string>\n    <string name=\"new_release_albums\">Uued avaldatud albumid</string>\n    <string name=\"today\">Täna</string>\n    <string name=\"yesterday\">Eile</string>\n    <string name=\"most_played_songs\">Kõige rohkem esitatud lood</string>\n    <string name=\"most_played_artists\">Kõige rohkem esitatud esinejad</string>\n    <string name=\"search_yt_music\">Otsi YouTube Musicust…</string>\n    <string name=\"search_library\">Otsi muusikakogust…</string>\n    <string name=\"filter_library\">Muusikakogu</string>\n    <string name=\"filter_liked\">Meeldivaks märgitud</string>\n    <string name=\"filter_downloaded\">Allalaaditud</string>\n    <string name=\"filter_all\">Kõik</string>\n    <string name=\"filter_songs\">Lood</string>\n    <string name=\"filter_videos\">Videod</string>\n    <string name=\"filter_albums\">Albumid</string>\n    <string name=\"filter_artists\">Esitajaid</string>\n    <string name=\"filter_playlists\">Esitusloendid</string>\n    <string name=\"filter_community_playlists\">Kogukonna esitusloendid</string>\n    <string name=\"filter_featured_playlists\">Esiletõstetud esitusloendid</string>\n    <string name=\"no_results_found\">Tulemusi ei leidu</string>\n    <string name=\"library_song_empty\">Muusikakogu lood saavad olema kuvatud siin</string>\n    <string name=\"library_artist_empty\">Muusikakogu esitajad saavad olema kuvatud siin</string>\n    <string name=\"library_album_empty\">Muusikakogu albumid saavad olema kuvatud siin</string>\n    <string name=\"library_playlist_empty\">Muusikakogu esitusloendid saavad olema kuvatud siin</string>\n    <string name=\"remove_download_playlist_confirm\">Kas sa kindlasti soovid kõik „%s“ esitusloendi lood eemaldada allalaaditud lugude andmekogust?</string>\n    <string name=\"delete_playlist_confirm\">Kas sa kindlasti soovid kustutada „%s“ esitusloendi?</string>\n    <string name=\"retry\">Proovi uuesti</string>\n    <string name=\"radio\">Raadio</string>\n    <string name=\"shuffle\">Sega lood</string>\n    <string name=\"edit\">Muuda</string>\n    <string name=\"add_all_to_library\">Lisa kõik muusikakogusse</string>\n    <string name=\"remove_from_library\">Eemalda muusikakogust</string>\n    <string name=\"remove_all_from_library\">Eemalda kõik muusikakogust</string>\n    <string name=\"action_download\">Laadi alla</string>\n    <string name=\"downloading\">Laadime alla</string>\n    <string name=\"remove_download\">Eemalda allalaaditu</string>\n    <string name=\"import_playlist\">Impordi esitusloend</string>\n    <string name=\"view_artist\">Vaata esitajat</string>\n    <string name=\"share\">Jaga</string>\n    <string name=\"delete\">Kustuta</string>\n    <string name=\"remove_from_history\">Eemalda ajaloost</string>\n    <string name=\"remove_from_playlist\">Eemalda esitusloendist</string>\n    <string name=\"remove_from_queue\">Eemalda esitusjärjekorrast</string>\n    <string name=\"search_online\">Otsi veebist</string>\n    <string name=\"action_sync\">Sünkroniseeri</string>\n    <string name=\"advanced\">Lisavalikud</string>\n    <string name=\"home\">Avaleht</string>\n    <string name=\"albums\">Albumid</string>\n    <string name=\"songs\">Lood</string>\n    <string name=\"artists\">Esitajaid</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d valitud</item>\n        <item quantity=\"other\">%d valitud</item>\n    </plurals>\n    <string name=\"quick_picks\">Kiirvalikud</string>\n    <string name=\"your_youtube_playlists\">Sinu YouTube\\'i esitusloendid</string>\n    <string name=\"keep_listening\">Jätka kuulamist</string>\n    <string name=\"similar_to\">Sarnane nagu</string>\n    <string name=\"this_week\">Sel nädalal</string>\n    <string name=\"last_week\">Eelmisel nädalal</string>\n    <string name=\"most_played_albums\">Kõige rohkem esitatud albumid</string>\n    <string name=\"search\">Otsi</string>\n    <string name=\"filter_bookmarked\">Lisatud järjehoidjaks</string>\n    <string name=\"from_your_library\">Sinu muusikakogust</string>\n    <string name=\"other_versions\">Muud versioonid</string>\n    <string name=\"liked_songs\">Meeldivaks märgitud lood</string>\n    <string name=\"downloaded_songs\">Allalaaditud lood</string>\n    <string name=\"playlist_is_empty\">Esitusloend on tühi</string>\n    <string name=\"start_radio\">Pane raadio tööle</string>\n    <string name=\"view_album\">Vaata albumit</string>\n    <string name=\"reset\">Lähtesta</string>\n    <string name=\"details\">Üksikasjad</string>\n    <string name=\"play\">Esita</string>\n    <string name=\"play_next\">Esita järgmisena</string>\n    <string name=\"refetch\">Laadi uuesti</string>\n    <string name=\"add_to_queue\">Lisa esitusjärjekorda</string>\n    <string name=\"add_to_library\">Lisa muusikakogusse</string>\n    <string name=\"add_to_playlist\">Lisa esitusloendisse</string>\n    <string name=\"tempo_and_pitch\">Tempo and helikõrgus</string>\n    <string name=\"sort_by_length\">Pikkuse alusel</string>\n    <string name=\"sort_by_play_time\">Esitusaja järgi</string>\n    <string name=\"sort_by_custom\">Kohandatud järjekorras</string>\n    <string name=\"media_id\">Meedia tunnus</string>\n    <string name=\"mime_type\">Meedia tüüp</string>\n    <string name=\"volume\">Helitugevus</string>\n    <string name=\"song_title\">Loo pealkiri</string>\n    <string name=\"song_artists\">Loo esitaja</string>\n    <string name=\"error_song_title_empty\">Loo pealkiri ei saa olla tühi.</string>\n    <string name=\"create_playlist\">Loo esitusloend</string>\n    <string name=\"playlist_name\">Esitusloendi nimi</string>\n    <string name=\"edit_artist\">Muuda esitajat</string>\n    <string name=\"artist_name\">Esitaja nimi</string>\n    <string name=\"error_artist_name_empty\">Esitaja nimi ei saa olla tühi.</string>\n    <string name=\"duplicates\">Topeltlood</string>\n    <string name=\"add_anyway\">Lisa ikkagi</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d lugu</item>\n        <item quantity=\"other\">%d lugu</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d esitaja</item>\n        <item quantity=\"other\">%d esitajat</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d nädal</item>\n        <item quantity=\"other\">%d nädalat</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d kuu</item>\n        <item quantity=\"other\">%d kuud</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d aasta</item>\n        <item quantity=\"other\">%d aastat</item>\n    </plurals>\n    <string name=\"playlist_synced\">Esitusloend on sünkroniseeritud</string>\n    <string name=\"undo\">Pööra tegevus tagasi</string>\n    <string name=\"lyrics_not_found\">Laulusõnu ei leidu</string>\n    <string name=\"end_of_song\">Loo lõpp</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">1 minut</item>\n        <item quantity=\"other\">%d minutit</item>\n    </plurals>\n    <string name=\"error_no_stream\">Meediavoogu pole saadaval</string>\n    <string name=\"action_remove_like_all\">Eemalda kõik meeldivaks märkimised</string>\n    <string name=\"action_shuffle_on\">Lugude segamine on kasutusel</string>\n    <string name=\"action_shuffle_off\">Lugude segamine pole kasutusel</string>\n    <string name=\"repeat_mode_off\">Kordus pole kasutusel</string>\n    <string name=\"repeat_mode_one\">Korda seda lugu</string>\n    <string name=\"repeat_mode_all\">Korda esitusjärjekorda</string>\n    <string name=\"queue_all_songs\">Kõik lood</string>\n    <string name=\"queue_searched_songs\">Otsitud lood</string>\n    <string name=\"music_player\">Muusikamängija</string>\n    <string name=\"settings\">Seadistused</string>\n    <string name=\"appearance\">Välimus</string>\n    <string name=\"theme\">Kujundus</string>\n    <string name=\"enable_dynamic_theme\">Kasuta dünaamilist kujundust</string>\n    <string name=\"dark_theme\">Tume kujundus</string>\n    <string name=\"pure_black\">Õige must kujundus</string>\n    <string name=\"customize_navigation_tabs\">Kohenda vahekaarte</string>\n    <string name=\"player\">Muusikamängija</string>\n    <string name=\"player_text_alignment\">Muusikamängija teksti joondumine</string>\n    <string name=\"lyrics_text_position\">Laulusõnade asukoht</string>\n    <string name=\"sided\">Küljel</string>\n    <string name=\"left\">Vasakul</string>\n    <string name=\"center\">Keskel</string>\n    <string name=\"right\">Paremal</string>\n    <string name=\"player_slider_style\">Muusikamängija liugurnupu stiil</string>\n    <string name=\"default_\">Vaikimisi</string>\n    <string name=\"squiggly\">Väänlev</string>\n    <string name=\"misc\">Varia</string>\n    <string name=\"default_open_tab\">Vaikimisi avatav vahekaart</string>\n    <string name=\"content_country\">Vaikimisi eelistatav riik sisu kuvamisel</string>\n    <string name=\"content_language\">Vaikimisi eelistatav keel sisu kuvamisel</string>\n    <string name=\"system_default\">Süsteemi vaikeseadistused</string>\n    <string name=\"enable_proxy\">Kasuta proksiserverit</string>\n    <string name=\"proxy_type\">Proksiserveri tüüp</string>\n    <string name=\"proxy_url\">Proksiserveri aadress</string>\n    <string name=\"restart_to_take_effect\">Muudatuse jõustamiseks käivita rakendus uuesti</string>\n    <string name=\"player_and_audio\">Meediamängija ja heli</string>\n    <string name=\"auto_load_more\">Laadi automaatselt täiendavaid lugusid</string>\n    <string name=\"storage\">Failid ja meedia</string>\n    <string name=\"cache\">Vahemälu</string>\n    <string name=\"image_cache\">Piltide vahemälu</string>\n    <string name=\"song_cache\">Lugude vahemälu</string>\n    <string name=\"max_cache_size\">Vahemälu suuruse ülempiir</string>\n    <string name=\"unlimited\">Piiramatu</string>\n    <string name=\"clear_all_downloads\">Eemalda kõik allalaaditud lood</string>\n    <string name=\"max_image_cache_size\">Piltide vahemälu suuruse ülempiir</string>\n    <string name=\"clear_image_cache\">Eemalda vahemälust pildid</string>\n    <string name=\"max_song_cache_size\">Lugude vahemälu suuruse ülempiir</string>\n    <string name=\"clear_song_cache\">Eemalda vahemälust lood</string>\n    <string name=\"size_used\">%s kasutusel</string>\n    <string name=\"privacy\">Privaatsus</string>\n    <string name=\"listen_history\">Kuulamiste ajalugu</string>\n    <string name=\"pause_listen_history\">Peata kuulamiste ajaloo salvestamine</string>\n    <string name=\"clear_listen_history\">Kustuta kuulamiste ajalugu</string>\n    <string name=\"clear_listen_history_confirm\">Kas sa oled kindel, et soovid kustutada kogu kuulamiste ajaloo?</string>\n    <string name=\"search_history\">Otsingute ajalugu</string>\n    <string name=\"pause_search_history\">Peata otsingute ajaloo salvestamine</string>\n    <string name=\"clear_search_history\">Kustuta otsingute ajalugu</string>\n    <string name=\"clear_search_history_confirm\">Kas sa oled kindel, et soovid kustutada kogu otsingute ajaloo?</string>\n    <string name=\"disable_screenshot\">Keela ekraanitõmmised</string>\n    <string name=\"disable_screenshot_desc\">Kui see eelistus on kasutusel, siis ekraanitõmmiste tegemine ja rakenduse vaade Viimaste rakenduste all on keelatud.</string>\n    <string name=\"enable_lrclib\">Kasuta laulusõnade kuvamiseks teenusepakkujat LrcLib</string>\n    <string name=\"dismiss\">Loobu</string>\n    <string name=\"preview\">Eelvaade</string>\n    <string name=\"enable_discord_rpc\">Näita oma tegevust Metrolist rakenduses Discordi olekuteatena</string>\n    <string name=\"sort_by_create_date\">Lisamise kuupäev</string>\n    <string name=\"sort_by_name\">Nime alusel</string>\n    <string name=\"sort_by_artist\">Esitaja järgi</string>\n    <string name=\"sort_by_year\">Aasta järgi</string>\n    <string name=\"sort_by_song_count\">Lugude arvu alusel</string>\n    <string name=\"codecs\">Koodekid</string>\n    <string name=\"sample_rate\">Diskreetimissagedus</string>\n    <string name=\"file_size\">Faili suurus</string>\n    <string name=\"edit_lyrics\">Muuda laulusõnu</string>\n    <string name=\"bitrate\">Bitikiirus</string>\n    <string name=\"search_lyrics\">Otsi laulusõnu</string>\n    <string name=\"unknown\">Teadmata</string>\n    <string name=\"loudness\">Valjus</string>\n    <string name=\"copied\">Kopeeritud lõikelauale</string>\n    <string name=\"edit_song\">Muuda lugu</string>\n    <string name=\"skip_duplicates\">Jäta topeltlood vahele</string>\n    <string name=\"error_song_artist_empty\">Loo esitaja ei saa olla tühi.</string>\n    <string name=\"error_timeout\">Aegumine</string>\n    <string name=\"save\">Salvesta</string>\n    <string name=\"error_playlist_name_empty\">Esitusloendi nimi ei saa olla tühi.</string>\n    <string name=\"choose_playlist\">Vali esitusloend</string>\n    <string name=\"edit_playlist\">Muuda esitusloendit</string>\n    <string name=\"duplicates_description_single\">See lugu on sinu esitusloendis juba olemas</string>\n    <string name=\"error_unknown\">Tundmatu viga</string>\n    <string name=\"duplicates_description_multiple\">%d lugu on sinu esitusloendis juba olemas</string>\n    <string name=\"sleep_timer\">Unetaimer</string>\n    <string name=\"removed_song_from_playlist\">Eemaldasime „%s“ esitusloendist</string>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d album</item>\n        <item quantity=\"other\">%d albumit</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d esitusloend</item>\n        <item quantity=\"other\">%d esitusloendit</item>\n    </plurals>\n    <string name=\"playlist_imported\">Esitusloend on imporditud</string>\n    <string name=\"error_no_internet\">Võrguühendus puudub</string>\n    <string name=\"action_like_all\">Kõik meeldivad</string>\n    <string name=\"action_like\">Meeldib</string>\n    <string name=\"action_remove_like\">Eemalda meeldivaks märkimine</string>\n    <string name=\"dark_theme_follow_system\">Järgi süsteemi kujundust</string>\n    <string name=\"dark_theme_on\">Kasutusel</string>\n    <string name=\"dark_theme_off\">Pole kasutusel</string>\n    <string name=\"audio_quality_low\">Madal</string>\n    <string name=\"audio_quality\">Helikvaliteet</string>\n    <string name=\"audio_quality_auto\">Automaatne</string>\n    <string name=\"audio_quality_high\">Kõrge</string>\n    <string name=\"persistent_queue\">Püsiv esitusjärjekotrd</string>\n    <string name=\"persistent_queue_desc\">Rakenduse uuesti käivitamisel taasta viimatikasutatud esitusjärjekord</string>\n    <string name=\"queue\">Esitusjärjekord</string>\n    <string name=\"auto_load_more_desc\">Esitusjärjekorra lõppedes, kui vähegi võimalik, siis laadi automaatselt täiendavaid lugusid</string>\n    <string name=\"auto_skip_next_on_error\">Vea puhul hüppa automaatselt järgmise loo juurde</string>\n    <string name=\"skip_silence\">Jäta vaikus vahele</string>\n    <string name=\"stop_music_on_task_clear\">Tegumist eemaldamisel peata muusika esitamine</string>\n    <string name=\"audio_normalization\">Heli normaliseerimine</string>\n    <string name=\"auto_skip_next_on_error_desc\">Taga jätkuv taasesitus</string>\n    <string name=\"equalizer\">Ekvalaiser</string>\n    <string name=\"about\">Rakenduse teave</string>\n    <string name=\"translation_models\">Tõlkemudelid</string>\n    <string name=\"app_version\">Rakenduse versioon</string>\n    <string name=\"new_version_available\">Uus versioon on saadaval</string>\n    <string name=\"clear_translation_models\">Eemalda tõlkemudelid</string>\n    <string name=\"small\">Väike</string>\n    <string name=\"grid_cell_size\">Ruudustiku elemendi suurus</string>\n    <string name=\"big\">Suur</string>\n    <string name=\"content\">Sisu</string>\n    <string name=\"login\">Kasutajanimi</string>\n    <string name=\"not_logged_in\">Pole sisselogitud</string>\n    <string name=\"enable_kugou\">Kasuta laulusõnade kuvamiseks teenusepakkujat KuGou</string>\n    <string name=\"hide_explicit\">Peida ebasobilik sisu</string>\n    <string name=\"backup_restore\">Varukoopia ja taastamine</string>\n    <string name=\"action_backup\">Varunda</string>\n    <string name=\"action_restore\">Taasta</string>\n    <string name=\"imported_playlist\">Imporditud esitusloend</string>\n    <string name=\"backup_create_success\">Varukoopia tegemine õnnestus</string>\n    <string name=\"backup_create_failed\">Varukoopia tegemine ei õnnestunud</string>\n    <string name=\"restore_failed\">Varukoopiast taastamine ei õnnestunud</string>\n    <string name=\"discord_integration\">Lõiming Discordiga</string>\n    <string name=\"discord_information\">Metrolist kasutab Discordi kasutajakonto oleku kuvamiseks KizzyRPC teeki. See eeldab Discord Gateway ühenduse kasutamist ja seda loetakse Discordi kasutustingimuste rikkumiseks. Aga pole teada ühtegi juhust, kus kasutajakonto oleks sel põhjusel keelatud. Kasuta seda teenust omal vastutsusel.\\n\\nMetrolist kasutab ainult sinu tunnusluba, kõike muud hoitakse kohalikus seadmes.</string>\n    <string name=\"options\">Valikud</string>\n    <string name=\"action_logout\">Logi välja</string>\n    <string name=\"login_failed\">Sisselogimine ei õnnestunud</string>\n    <string name=\"use_login_for_browse\">Sisu sirvimiseks logi sisse</string>\n    <string name=\"use_login_for_browse_desc\">See võib mõjutada mis sisu sa näed, sealhulgas kui kasutad YouTube Premium-kontot, siis YouTube Premiumi jaoks mõeldud albumeid</string>\n    <string name=\"action_login\">Logi sisse</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-eu/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"charts\">Arrakasta Zerrendak</string>\n    <string name=\"back_button_desc\">Atzera</string>\n    <string name=\"album_cover_desc\">Albumaren azala</string>\n    <string name=\"top_music_videos\">Musika bideo onenak</string>\n    <string name=\"trending\">Joera</string>\n    <string name=\"weeks\">Asteak</string>\n    <string name=\"months\">Hilabeteak</string>\n    <string name=\"years\">Urteak</string>\n    <string name=\"continuous\">Etengabe</string>\n    <string name=\"liked\">Gustoko</string>\n    <string name=\"offline\">Jaiskia</string>\n    <string name=\"my_top\">Nire onenak</string>\n    <string name=\"cached_playlist\">Cachean gordeta</string>\n    <string name=\"sync_playlist\">Zerrenda sinkronizatu</string>\n    <string name=\"sync_disabled\">Sinkronizazioa itzalita</string>\n    <string name=\"allows_for_sync_witch_youtube\">Oharra: Honek YouTube Music-ekin sinkronizatzea ahalbidetzen du. Ondoren ezin izango da aldatu.</string>\n    <string name=\"generating_image\">Irudia sortzen</string>\n    <string name=\"please_wait\">Itxaron, mesedez</string>\n    <string name=\"cancel\">Utzi</string>\n    <string name=\"local_history\">Lokala</string>\n    <string name=\"remote_history\">Urrutiko Historia</string>\n    <string name=\"share_lyrics\">Letrak partekatu</string>\n    <string name=\"share_as_text\">Testu moduan partekatu</string>\n    <string name=\"share_as_image\">Irudi moduan partekatu</string>\n    <string name=\"max_selection_limit\">Gehienezko hautaketa</string>\n    <string name=\"share_selected\">Hautatuak partekatu</string>\n    <string name=\"customize_colors\">Koloreak aldatu</string>\n    <string name=\"text_color\">Testu kolorea</string>\n    <string name=\"secondary_text_color\">Bigarren testu kolorea</string>\n    <string name=\"background_color\">Atzealde kolorea</string>\n    <string name=\"remove_from_cache\">\\\"Cache\\\"tik ezabatu</string>\n    <string name=\"copy_link\">Link-a kopiatu</string>\n    <string name=\"select\">Hautatu dena</string>\n    <string name=\"like_all\">Dena gustoko</string>\n    <string name=\"sort_by_last_updated\">Eguneratze data</string>\n    <string name=\"link_copied\">Link-a arbelera kopiatuta</string>\n    <string name=\"starting_radio\">Irratia pizten</string>\n    <string name=\"now_playing\">Orain jotzen</string>\n    <string name=\"lyrics\">Letrak</string>\n    <string name=\"close\">Itxi</string>\n    <string name=\"hide_player_thumbnail\">Miniatura ezkutatu</string>\n    <string name=\"hide_player_thumbnail_desc\">Album irudia aplikazio logoarekin aldatu</string>\n    <string name=\"already_in_playlist\">Erreprodukzio listan:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">Aldi %d</item>\n        <item quantity=\"other\">%d aldi</item>\n    </plurals>\n    <string name=\"seek_forward_dynamic\">%1$d segundu aurrera</string>\n    <string name=\"seek_backward_dynamic\">-%1$d segundu atzera</string>\n    <string name=\"seek_seconds_addup\">Bilaketa progresiboa</string>\n    <string name=\"seek_seconds_addup_description\">Gaituta badago, bilaketa-jauzi bakoitzean 5 segundo extra gehitu</string>\n    <string name=\"similar_content\">Antzeko edukia</string>\n    <string name=\"dislike_all\">Ez gustoko denak</string>\n    <string name=\"player_background_style\">Erreproduzitzaile atzeko estiloa</string>\n    <string name=\"follow_theme\">Jarraitu gaia</string>\n    <string name=\"gradient\">Gradientea</string>\n    <string name=\"new_player_design\">Erreproduzitzaile diseinu berria</string>\n    <string name=\"new_mini_player_design\">Mini-erreproduzitzaile diseinu berria</string>\n    <string name=\"player_background_blur\">Lauso-efektua</string>\n    <string name=\"player_buttons_style\">Erreproduzitzaile botoi kolorea</string>\n    <string name=\"default_style\">Lehenetsia</string>\n    <string name=\"enable_swipe_thumbnail\">Irristatu abestia aldatzeko gaitu</string>\n    <string name=\"swipe_song_to_add\">Abestia ezkerretara irristatu ilara gehitzeko, edo eskuinera hurrengoan jotzeko</string>\n    <string name=\"lyrics_click_change\">Klik egitean letrak aldatu</string>\n    <string name=\"lyrics_auto_scroll\">Letren mugimendu automatikoa</string>\n    <string name=\"slim\">Argala</string>\n    <string name=\"slim_navbar\">Nabigazio beheko barra argala</string>\n    <string name=\"auto_playlists\">Erreproduzio-zerrenda automatikoa</string>\n    <string name=\"show_liked_playlist\">\\\"Gustokoak\\\" zerrenda ikusi</string>\n    <string name=\"show_downloaded_playlist\">\\\"Deskargatuak\\\" zerrenda ikusk</string>\n    <string name=\"show_top_playlist\">\\\"Top\\\" zerrenda ikusi</string>\n    <string name=\"show_cached_playlist\">\\\"Cache-an\\\" zerrenda ikusi</string>\n    <string name=\"advanced_login\">Token-arekin saioa hasi</string>\n    <string name=\"token_hidden\">Sakatu \\\"token\\\"-a ikusteko</string>\n    <string name=\"token_shown\">Sakatu berriro kopiatu edo editatzeko</string>\n    <string name=\"token_adv_login_description\">Hau SARRERA AURRERATUAREN metodoa da. Web-portalaren alternatiba gisa, hemen zuzenean sartu edo eguneratu dezakezu zure sarrera-tokena. Adibidez, honek hainbat gailutan sartzea azkartzen lagun dezake. Kontuan izan aplikazioak ez dituen token-formatu baliogabeak ez direla onartuko</string>\n    <string name=\"yt_sync\">Kontuarekin sinkronizazio automatikoa</string>\n    <string name=\"more_content\">Eduki gehiago</string>\n    <string name=\"edit_playlist_cover\">Editatu erreprodukzio-zerrendaren azala</string>\n    <string name=\"edit_playlist_cover_note\">Oharra: Zure kontua telefono-zenbaki batekin lotuta egon behar da eta YouTube Music-en egiaztatuta egon behar du erreprodukzio-zerrendaren azala aldatzeko.</string>\n    <string name=\"edit_playlist_cover_note_wait\">Irudi bat hautatu ondoren, mesedez itxaron une batez zure erreprodukzio-zerrendan azala agertu dadin.</string>\n    <string name=\"choose_from_library\">Liburutegitik hautatu</string>\n    <string name=\"remove_custom_image\">Irudi pertzonalizatua kendu</string>\n    <string name=\"general\">Orokorra</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"default_lib_chips\">Liburutegi lehenetsiaren chip-a aldatu</string>\n    <string name=\"set_quick_picks\">Hautatze arinak ezarri</string>\n    <string name=\"last_song_listened\">Azken abestian oinarritua</string>\n    <string name=\"app_language\">Aplikazio hizkuntza</string>\n    <string name=\"config_proxy\">Proxy-a ezarri</string>\n    <string name=\"proxy_username\">Proxy erabiltzaile izena</string>\n    <string name=\"proxy_password\">Proxy pasahitza</string>\n    <string name=\"enable_authentication\">Autentikazioa gaitu</string>\n    <string name=\"enable_similar_content\">Antzeko edukia gaitu</string>\n    <string name=\"similar_content_desc\">Ilara amaitzerakoan, antzeko abestiak gehitu</string>\n    <string name=\"percentage_format\">%%%d</string>\n    <string name=\"import_online\">\\\"m3u\\\" zerrenda gehitu</string>\n    <string name=\"import_csv\">\\\"csv\\\" zerrenda gehitu</string>\n    <string name=\"playlist_add_local_to_synced_note\">Oharra: Tokiko abestiak sinkronizatutako / urrutiko erreprodukzio-zerrendetara gehitzea ez da onartzen. Beste edozein konbinazio baliogarria da</string>\n    <string name=\"auto_download_on_like\">Gustokoak automatikoki deskargatu</string>\n    <string name=\"auto_download_on_like_desc\">Gustoko sakatzean abestiak automatikoki deskargatu</string>\n    <string name=\"swipe_sensitivity\">Mini-erreproduzitzaile irrista-sentikortasuna</string>\n    <string name=\"sensitivity_percentage\">%%%1$d</string>\n    <string name=\"clear_song_cache_dialog\">Ziur zaude \\\"cache\\\"-an dauden abesti guztiak ezabatzea?</string>\n    <string name=\"clear_image_cache_dialog\">Ziur zaude \\\"cache\\\"-an dauden irudi guztiak ezabatzea?</string>\n    <string name=\"clear_downloads_dialog\">Ziur zaude deskarga guztiak ezabatu nahi dituzula?</string>\n    <string name=\"disable\">Desgaitu</string>\n    <string name=\"not_logged_in_youtube\">YouTube-n saioa ez hasita</string>\n    <string name=\"default_links\">Ireki onartutako estekak</string>\n    <string name=\"open_app_settings_error\">Aplikazio ezarpenak ezin dira ireki</string>\n    <string name=\"release_notes\">Bertsio oharrak</string>\n    <string name=\"all_time\">Denbora guztia</string>\n    <string name=\"past_24_hours\">Azken 24 orduak</string>\n    <string name=\"past_week\">Azken astea</string>\n    <string name=\"past_month\">Azken hilabetea</string>\n    <string name=\"past_year\">Azken urtea</string>\n    <string name=\"top_length\">Nire Top zerrenda luzera</string>\n    <string name=\"history_duration\">Historia iraupena</string>\n    <string name=\"information\">Informazioa</string>\n    <string name=\"description\">Deskribapena</string>\n    <string name=\"views\">Bistak</string>\n    <string name=\"likes\">Gustokoak</string>\n    <string name=\"dislikes\">Ez gustokoak</string>\n    <string name=\"subscribe\">Harpidetu</string>\n    <string name=\"subscribed\">Harpidetzatua</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">Segundu %d</item>\n        <item quantity=\"other\">%d segundu</item>\n    </plurals>\n    <string name=\"disable_load_more_when_repeat_all\">\\\"Errepikatu guztiak\\\" moduan gehiago kargatzea desgaitu</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Ez kargatu automatikoki abesti gehiago eta antzeko edukia ‘Errepikatu guztiak’ modua aktibatuta dagoenean</string>\n    <string name=\"lyrics_romanization_cyrillic\">Kirilikoa</string>\n    <string name=\"lyrics_romanize_title\">Erromanizazio</string>\n    <string name=\"lyrics_romanization\">Letren erromanizazioa</string>\n    <string name=\"lyrics_romanize_japanese\">Japoniar letrak erromanizatu</string>\n    <string name=\"lyrics_romanize_korean\">Korear letrak erromanizatu</string>\n    <string name=\"lyrics_romanize_russian\">Errusiar letrak erromanizatu</string>\n    <string name=\"lyrics_romanize_ukrainian\">Ukraniar letrak erromanizatu</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Kyrgizar letrak erromanizatu</string>\n    <string name=\"lyrics_romanize_serbian\">Serbiar letrak erromanizatu</string>\n    <string name=\"line_by_line_option_title\">EXPERIMENTALA: Lerroz-lerro hizkuntza detektatu</string>\n    <string name=\"lyrics_romanize_belarusian\">Bielorrusiar letrak erromanizatu</string>\n    <string name=\"lyrics_romanize_bulgarian\">Bulgariar letrak erromanizatu</string>\n    <string name=\"line_by_line_option_desc\">Kiriliko hizkuntza lerroz-lerro detektatuko da, abesti osoaren ordez.</string>\n    <string name=\"line_by_line_dialog_title\">Ziur zaude?</string>\n    <string name=\"line_by_line_dialog_desc\">Ezaugarri esperimental hau ez da beti fidagarria.\\n\\nLehenetsita, hizkuntza abesti osoaren arabera zehazten da, baina aukera hau gaituta, lerroka zehaztuko da. Honek hizkuntza anitzeko abestien funtzionamendua ahalbidetuko du, BAINA hizkuntza ez da beti zuzena izango (adibidez, ukrainierazko lerro bat ez badu letra berezirik, errusieraz erromanizatua izan daiteke).\\n\\nArazoik ez baduzu, gomendagarria da aukera hau desgaituta uztea.</string>\n    <string name=\"romanize_current_track\">Oraingo abestia erromanizatu</string>\n    <string name=\"settings_section_ui\">Interfazea</string>\n    <string name=\"settings_section_privacy\">Pribatutasuna eta Segurtasuna</string>\n    <string name=\"settings_section_player_content\">Jotzailea eta Edukia</string>\n    <string name=\"settings_section_storage\">Biltegia eta Datuak</string>\n    <string name=\"settings_section_system\">Sistema eta Honi buruz</string>\n    <string name=\"audio_offload\">Offload desgaitu</string>\n    <string name=\"audio_offload_description\">Erabili offload audio-bidea audioa erreproduzitzeko. Hau desgaitzeak energia-kontsumoa handitu dezake, baina erabilgarria izan daiteke audioa erreproduzitzean edo post-prozesamenduan arazoak badituzu</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-fa/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"text_color\">رنگ متن</string>\n    <string name=\"clear_downloads_dialog\">آیا مطمئن هستید که می خواهید همه موسیقی‌های دریافت شده را پاک کنید؟</string>\n    <string name=\"cancel\">لغو</string>\n    <string name=\"share_as_image\">اشتراک گذاری به عنوان تصویر</string>\n    <string name=\"auto_download_on_like_desc\">دریافت خودکار موسیقی‌ها زمانی که آنها را پسندیدید</string>\n    <string name=\"follow_theme\">پیروی از پوسته</string>\n    <string name=\"player_background_style\">سبک پس‌زمینه پخش‌کننده</string>\n    <string name=\"past_24_hours\">۲۴ ساعت گذشته</string>\n    <string name=\"enable_swipe_thumbnail\">تغییر آهنگ با کشیدن را فعال کنید</string>\n    <string name=\"show_top_playlist\">نمایش برترین لیست پخش‌ها</string>\n    <string name=\"generating_image\">درحال ساخت تصویر</string>\n    <string name=\"please_wait\">لطفا منتظر بمانید</string>\n    <string name=\"share_lyrics\">اشتراک گذاری متن آهنگ</string>\n    <string name=\"share_as_text\">اشتراک به عنوان متن</string>\n    <string name=\"max_selection_limit\">حداکثر محدودیت انتخاب</string>\n    <string name=\"share_selected\">به اشتراک گذاری انتخاب شده‌ها</string>\n    <string name=\"customize_colors\">شخصی سازی رنگ‌ها</string>\n    <string name=\"secondary_text_color\">رنگ متن دوم</string>\n    <string name=\"background_color\">رنگ پس‌زمینه</string>\n    <string name=\"lyrics\">متن آهنگ</string>\n    <string name=\"already_in_playlist\">همین الان در لیست پخش است:</string>\n    <string name=\"similar_content\">محتوای مشابه</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d بار</item>\n        <item quantity=\"other\">%d بار</item>\n    </plurals>\n    <string name=\"gradient\">طیف رنگی</string>\n    <string name=\"player_buttons_style\">رنگ دکمه‌های پخش کننده</string>\n    <string name=\"default_style\">پیش فرض</string>\n    <string name=\"player_background_blur\">تار</string>\n    <string name=\"swipe_song_to_add\">آهنگ را برای پخش بعدی به راست یا برای افزودن به صف‌پخش به چپ بکشید</string>\n    <string name=\"lyrics_click_change\">تغییر متن ترانه با کلیک</string>\n    <string name=\"slim\">لاغر</string>\n    <string name=\"slim_navbar\">برچسب‌های نوار پیمایش پایین را پنهان کنید</string>\n    <string name=\"auto_playlists\">لیست پخش خودکار</string>\n    <string name=\"show_liked_playlist\">نمایش لیست پخش پسند‌ شده‌ها</string>\n    <string name=\"show_downloaded_playlist\">نمایش لیست پخش دانلود شده‌ها</string>\n    <string name=\"enable_similar_content\">فعال کردن محتوای مشابه</string>\n    <string name=\"similar_content_desc\">هنگامی که به پایان صف رسید، آهنگ های مشابه بیشتری به صورت خودکار اضافه شود</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"auto_download_on_like\">دریافت خودکار در صورت پسندیدن</string>\n    <string name=\"clear_song_cache_dialog\">از پاک کردن همه آهنگ‌های کَش شده اطمینان دارید؟</string>\n    <string name=\"not_logged_in_youtube\">به یوتیوب وارد نشده‌اید</string>\n    <string name=\"default_links\">بازکردن پیوندهای پشتیبانی شده</string>\n    <string name=\"open_app_settings_error\">نمی‌توان تنظیمات برنامه را باز کرد</string>\n    <string name=\"release_notes\">یادداشت‌های انتشار</string>\n    <string name=\"all_time\">درکل</string>\n    <string name=\"past_week\">هفته گذشته</string>\n    <string name=\"past_month\">ماه گذشته</string>\n    <string name=\"past_year\">سال گذشته</string>\n    <string name=\"information\">اطلاعات</string>\n    <string name=\"description\">توضیحات</string>\n    <string name=\"likes\">پسندها</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">%d ثانیه</item>\n        <item quantity=\"other\">%d ثانیه</item>\n    </plurals>\n    <string name=\"dislikes\">نپسندیدن</string>\n    <string name=\"local_history\">محلی</string>\n    <string name=\"back_button_desc\">بازگشت</string>\n    <string name=\"album_cover_desc\">کاور آلبوم</string>\n    <string name=\"top_music_videos\">موزیک ویدیوهای برتر</string>\n    <string name=\"trending\">پرطرفدارها</string>\n    <string name=\"weeks\">هفته‌ها</string>\n    <string name=\"months\">ماه‌ها</string>\n    <string name=\"years\">سالها</string>\n    <string name=\"offline\">دریافت شده‌ها</string>\n    <string name=\"my_top\">برترین‌های من</string>\n    <string name=\"remote_history\">دستگاه راه دور</string>\n    <string name=\"continuous\">مستمر</string>\n    <string name=\"liked\">پسندیده</string>\n    <string name=\"sync_playlist\">همگام‌سازی لیست پخش</string>\n    <string name=\"sync_disabled\">همگام‌سازی غیرفعال شد</string>\n    <string name=\"remove_from_cache\">حدف از حافظه نهان</string>\n    <string name=\"copy_link\">رونوشت از پیوند</string>\n    <string name=\"select\">انتخاب همه</string>\n    <string name=\"like_all\">پسندیدن همه</string>\n    <string name=\"dislike_all\">نپسندیدن همه</string>\n    <string name=\"sort_by_last_updated\">تاریخ بروزرسانی شد</string>\n    <string name=\"link_copied\">پیوند کپی شد</string>\n    <string name=\"allows_for_sync_witch_youtube\">توجه: این مورد اجازه همگام‌سازی با یوتیوب موزیک را فراهم می کند. این مورد بعداً قابل تغییر نیست.</string>\n    <string name=\"token_shown\">برای رونوشت یا ویرایش دوباره ضربه بزنید</string>\n    <string name=\"general\">عمومی</string>\n    <string name=\"proxy\">پروکسی</string>\n    <string name=\"last_song_listened\">بر اساس آخرین آهنگ شنیده شده</string>\n    <string name=\"app_language\">زبان برنامه</string>\n    <string name=\"advanced_login\">ورود پیشرفته (توکن)</string>\n    <string name=\"token_hidden\">ضربه برای نمایش توکن</string>\n    <string name=\"set_quick_picks\">تنظیم انتخاب سریع</string>\n    <string name=\"views\">بازدید‌ها</string>\n    <string name=\"charts\">چارت‌ها</string>\n    <string name=\"cached_playlist\">کش شده</string>\n    <string name=\"now_playing\">در حال پخش</string>\n    <string name=\"seek_forward_dynamic\">ثانیه به جلو</string>\n    <string name=\"seek_backward_dynamic\">ثانیه به عقب</string>\n    <string name=\"enable_simpmusic\">فعال‌سازی simpMusic Lyrics</string>\n    <string name=\"close\">بستن</string>\n    <string name=\"seek_seconds_addup_description\">درصورت فعال بودن، با هر بار پرش، ۵ ثانیه به‌صورت تدریجی اضافه می‌شود</string>\n    <string name=\"lyrics_auto_scroll\">اسکرول خودکار متن‌ترانه</string>\n    <string name=\"swipe_song_to_remove\">برای حذف آهنگ از لیست پخش، آن را بکشید</string>\n    <string name=\"artist_page_settings\">صفحه هنرمند</string>\n    <string name=\"show_artist_description\">نمایش توضیحات هنرمند</string>\n    <string name=\"new_player_design\">طراحی جدید پخش‌کننده</string>\n    <string name=\"lyrics_glow_effect_desc\">­افزودن انیمیشن درخشش و افکت پرش به متن فعال ترانه</string>\n    <string name=\"crop_album_art\">برش تصویر البوم</string>\n    <string name=\"hide_player_thumbnail_desc\">جایگزین کردن کاور آلبوم با لوگوی برنامه در پخش‌کننده</string>\n    <string name=\"show_artist_monthly_listeners\">نمایش شنوندگان ماهانه</string>\n    <string name=\"lyrics_glow_effect\">فعال‌سازی حالت نورانی متن ترانه</string>\n    <string name=\"show_more\">نمایش بیشتر</string>\n    <string name=\"crop_album_art_desc\">برش مربعی تصویر ویدیو</string>\n    <string name=\"sync_playlist_desc\">همگام سازی این لیست پخش با یوتیوب موزیک</string>\n    <string name=\"remove_download_playlist_desc\">حذف تمام اهنگ های دانلود شده از این لیست پخش</string>\n    <string name=\"primary_color_style\">رنگ اصلی</string>\n    <string name=\"enable_simpmusic_desc\">استفاده از ارائه‌دهنده SimpMusic Lyrics برای متن ترانه همگام‌شده</string>\n    <string name=\"new_mini_player_design\">طراحی جدید مینی پخش‌کننده</string>\n    <string name=\"starting_radio\">درحال شروع رادیو</string>\n    <string name=\"download_in_progress_desc\">دانلود در جریان است</string>\n    <string name=\"auto_scroll\">باز‌همگام سازی</string>\n    <string name=\"download_playlist_desc\">دانلود تمام آهنگ‌ها برای پخش آفلاین</string>\n    <string name=\"share_playlist_desc\">اشتراک این لیست پخش با دیگران</string>\n    <string name=\"show_artist_subscriber_count\">نمایش تعداد دنبال کنندگان</string>\n    <string name=\"delete_playlist_desc\">حذف این لیست پخش برای همیشه</string>\n    <string name=\"wavy\">موجی</string>\n    <string name=\"enable_better_lyrics\">فعال‌سازی ترانه بهتر</string>\n    <string name=\"tertiary_color_style\">رنگ ثالث</string>\n    <string name=\"enable_better_lyrics_desc\">«استفاده از ارائه‌دهنده \\\"ترانه بهتر\\\" برای متن ترانه همگام‌شده کلمه‌-به‌-کلمه</string>\n    <string name=\"show_less\">نمایش کمتر</string>\n    <string name=\"seek_seconds_addup\">افزایش تدریجی زمان پرش</string>\n    <string name=\"about_artist\">درباره</string>\n    <string name=\"hide_player_thumbnail\">پنهان کردن تصویر پخش کننده</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-fa/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <string name=\"home\">خانه</string>\n    <string name=\"songs\">ترانه‌ها</string>\n    <string name=\"artists\">هنرمندها</string>\n    <string name=\"albums\">مجموعه‌ها</string>\n    <string name=\"playlists\">فهرست‌پخش‌ها</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d انتخاب‌شده</item>\n        <item quantity=\"other\">%d انتخاب‌شده‌اند</item>\n    </plurals>\n    <string name=\"history\">تاریخچه</string>\n    <string name=\"stats\">آمار</string>\n    <string name=\"mood_and_genres\">حالت و سبک</string>\n    <string name=\"account\">حساب</string>\n    <string name=\"quick_picks\">انتخاب‌های سریع</string>\n    <string name=\"quick_picks_empty\">به ترانه‌ها گوش دهید تا انتخاب سریع خود ساخته شود</string>\n    <string name=\"new_release_albums\">مجموعه‌های منتشرشده‌ی جدید</string>\n    <string name=\"today\">امروز</string>\n    <string name=\"yesterday\">دیروز</string>\n    <string name=\"this_week\">این هفته</string>\n    <string name=\"last_week\">هفته‌ی گذشته</string>\n    <string name=\"most_played_songs\">بیشترین ترانه‌های پخش‌شده</string>\n    <string name=\"most_played_artists\">بیشترین هنرمندهای پخش‌شده</string>\n    <string name=\"most_played_albums\">بیشترین مجموعه‌های پخش‌شده</string>\n    <string name=\"search\">جستجو</string>\n    <string name=\"search_yt_music\">جستجو در یوتیوب موزیک…</string>\n    <string name=\"search_library\">جستجوی کتاب‌خانه…</string>\n    <string name=\"filter_library\">کتاب‌خانه</string>\n    <string name=\"filter_liked\">پسندشده</string>\n    <string name=\"filter_downloaded\">بارگیری‌شده</string>\n    <string name=\"filter_all\">همه</string>\n    <string name=\"filter_songs\">ترانه‌ها</string>\n    <string name=\"filter_videos\">فیلم‌ها</string>\n    <string name=\"filter_albums\">مجموعه‌ها</string>\n    <string name=\"filter_artists\">هنرمندها</string>\n    <string name=\"filter_playlists\">فهرست‌پخش‌ها</string>\n    <string name=\"filter_community_playlists\">فهرست‌پخش‌های انجمن</string>\n    <string name=\"filter_featured_playlists\">فهرست‌پخش‌های ویژه</string>\n    <string name=\"filter_bookmarked\">نشانک‌گذاری‌شده</string>\n    <string name=\"no_results_found\">نتیجه‌ای پیدا نشد</string>\n    <string name=\"from_your_library\">از کتابخانه شما</string>\n    <string name=\"liked_songs\">آهنگ های پسندیده شده</string>\n    <string name=\"downloaded_songs\">آهنگ های دانلود شده</string>\n    <string name=\"playlist_is_empty\">فهرست‌ پخش خالی است</string>\n    <string name=\"retry\">تلاش‌ مجدد</string>\n    <string name=\"radio\">رادیو</string>\n    <string name=\"shuffle\">مخلوط</string>\n    <string name=\"reset\">بازنشانی</string>\n    <string name=\"details\">جزئیات</string>\n    <string name=\"edit\">ویرایش</string>\n    <string name=\"start_radio\">شروع رادیو</string>\n    <string name=\"play\">پخش</string>\n    <string name=\"play_next\">پخش بعدی</string>\n    <string name=\"add_to_queue\">اضافه‌کردن به صف</string>\n    <string name=\"add_to_library\">اضافه‌کردن به کتاب‌خانه</string>\n    <string name=\"remove_from_library\">حذف از کتاب‌ خانه</string>\n    <string name=\"action_download\">دانلود</string>\n    <string name=\"downloading\">درحال دانلود</string>\n    <string name=\"remove_download\">لغو دانلود</string>\n    <string name=\"import_playlist\">واردکردن فهرست‌ پخش</string>\n    <string name=\"add_to_playlist\">اضافه‌کردن به فهرست‌ پخش</string>\n    <string name=\"view_artist\">مشاهده‌ هنرمند</string>\n    <string name=\"view_album\">مشاهده‌ مجموعه</string>\n    <string name=\"refetch\">نوسازی</string>\n    <string name=\"share\">هم‌رسانی</string>\n    <string name=\"delete\">حذف</string>\n    <string name=\"remove_from_history\">حذف از تاریخچه</string>\n    <string name=\"search_online\">جستجوی برخط</string>\n    <string name=\"action_sync\">همگام‌سازی</string>\n    <string name=\"advanced\">پیشرفته</string>\n    <string name=\"sort_by_create_date\">تاریخ اضافه‌شده</string>\n    <string name=\"sort_by_name\">نام</string>\n    <string name=\"sort_by_artist\">هنرمند</string>\n    <string name=\"sort_by_year\">سال</string>\n    <string name=\"sort_by_song_count\">تعداد آهنگ</string>\n    <string name=\"sort_by_length\">طول</string>\n    <string name=\"sort_by_play_time\">زمان پخش</string>\n    <string name=\"sort_by_custom\">ترتیب سفارشی</string>\n    <string name=\"media_id\">شناسه‌ی رسانه</string>\n    <string name=\"mime_type\">نوع رسانه</string>\n    <string name=\"codecs\">رمزگشاها</string>\n    <string name=\"bitrate\">نرخ‌ذره</string>\n    <string name=\"sample_rate\">نرخ نمونه</string>\n    <string name=\"loudness\">بلندی</string>\n    <string name=\"volume\">حجم</string>\n    <string name=\"file_size\">اندازه‌ی پرونده</string>\n    <string name=\"unknown\">ناشناخته</string>\n    <string name=\"copied\">در بُریده‌دان رونوشت‌شد</string>\n    <string name=\"edit_lyrics\">ویرایش متن‌ترانه</string>\n    <string name=\"search_lyrics\">جستجوی متن‌ترانه</string>\n    <string name=\"edit_song\">ویرایش ترانه</string>\n    <string name=\"song_title\">عنوان ترانه</string>\n    <string name=\"song_artists\">هنرمند ترانه</string>\n    <string name=\"error_song_title_empty\">عنوان ترانه نمی تواند خالی باشد.</string>\n    <string name=\"error_song_artist_empty\">هنرمند ترانه نمی تواند خالی باشد.</string>\n    <string name=\"save\">ذخیره</string>\n    <string name=\"choose_playlist\">انتخاب فهرست‌پخش</string>\n    <string name=\"edit_playlist\">ویرایش فهرست‌پخش</string>\n    <string name=\"create_playlist\">ایجاد فهرست‌پخش</string>\n    <string name=\"playlist_name\">نام فهرست‌پخش</string>\n    <string name=\"error_playlist_name_empty\">نام فهرست‌پخش نمی تواند خالی باشد.</string>\n    <string name=\"edit_artist\">ویرایش هنرمند</string>\n    <string name=\"artist_name\">نام هنرمند</string>\n    <string name=\"error_artist_name_empty\">نام هنرمند نمی تواند خالی باشد.</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d ترانه</item>\n        <item quantity=\"other\">%d ترانه‌ها</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d هنرمند</item>\n        <item quantity=\"other\">%d هنرمندها</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d مجموعه</item>\n        <item quantity=\"other\">%d مجموعه‌ها</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d فهرست‌پخش</item>\n        <item quantity=\"other\">%d فهرست‌پخش‌ها</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d هفته</item>\n        <item quantity=\"other\">%d هفته‌ها</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d ماه</item>\n        <item quantity=\"other\">%d ماه‌ها</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d سال</item>\n        <item quantity=\"other\">%d سال‌ها</item>\n    </plurals>\n    <string name=\"playlist_imported\">فهرست‌پخش افزوده شد</string>\n    <string name=\"removed_song_from_playlist\">«%s» از فهرست‌پخش حذف شد</string>\n    <string name=\"playlist_synced\">فهرست‌پخش همگام‌شد</string>\n    <string name=\"undo\">واگرد</string>\n    <string name=\"lyrics_not_found\">متن‌ترانه پیدا نشد</string>\n    <string name=\"sleep_timer\">شمارنده‌ی خواب</string>\n    <string name=\"end_of_song\">پایان ترانه</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">%d دقیقه</item>\n        <item quantity=\"other\">%d دقیقه</item>\n    </plurals>\n    <string name=\"error_no_stream\">جریانی در دسترس نیست</string>\n    <string name=\"error_no_internet\">بدون اتصال به تارکده</string>\n    <string name=\"error_timeout\">اتمام وقت</string>\n    <string name=\"error_unknown\">خطای ناشناخته</string>\n    <string name=\"action_like\">پسندیدن</string>\n    <string name=\"action_remove_like\">حذف پسندیدن</string>\n    <string name=\"action_shuffle_on\">بُرزدن روشن</string>\n    <string name=\"action_shuffle_off\">بُرزدن خاموش</string>\n    <string name=\"repeat_mode_off\">حالت تکرار خاموش</string>\n    <string name=\"repeat_mode_one\">تکرار ترانه‌ی فعلی</string>\n    <string name=\"repeat_mode_all\">ترار صف</string>\n    <string name=\"queue_all_songs\">همه‌ی ترانه‌ها</string>\n    <string name=\"queue_searched_songs\">ترانه‌های جستجوشده</string>\n    <string name=\"music_player\">پخش‌کننده‌ی موسیقی</string>\n    <string name=\"settings\">تنظیمات</string>\n    <string name=\"appearance\">ظاهر</string>\n    <string name=\"enable_dynamic_theme\">فعال‌کردن طرح پویا</string>\n    <string name=\"dark_theme\">تم تاریک</string>\n    <string name=\"dark_theme_on\">روشن</string>\n    <string name=\"dark_theme_off\">خاموش</string>\n    <string name=\"dark_theme_follow_system\">پیروی از سیستم</string>\n    <string name=\"pure_black\">مشکی خالص</string>\n    <string name=\"default_open_tab\">زبانه باز پیش‌فرض</string>\n    <string name=\"customize_navigation_tabs\">سفارشی‌کردن زبانه‌های ناوبری</string>\n    <string name=\"lyrics_text_position\">موقعیت متن ترانه</string>\n    <string name=\"left\">چپ</string>\n    <string name=\"center\">وسط</string>\n    <string name=\"right\">راست</string>\n    <string name=\"content\">محتوا</string>\n    <string name=\"login\">ورود</string>\n    <string name=\"content_language\">زبان پیش‌فرض محتوا</string>\n    <string name=\"content_country\">کشور پیش‌فرض محتوا</string>\n    <string name=\"system_default\">پیش فرض سیستم</string>\n    <string name=\"enable_proxy\">فعال‌کردن پروکسی</string>\n    <string name=\"proxy_type\">نوع پروکسی</string>\n    <string name=\"proxy_url\">آدرس‌اینترنتی پروکسی</string>\n    <string name=\"restart_to_take_effect\">راه‌اندازی‌مجدد برای اعمال اثر</string>\n    <string name=\"player_and_audio\">پخش‌کننده و صدا</string>\n    <string name=\"audio_quality\">کیفیت صدا</string>\n    <string name=\"audio_quality_auto\">خودکار</string>\n    <string name=\"audio_quality_high\">بالا</string>\n    <string name=\"audio_quality_low\">پایین</string>\n    <string name=\"persistent_queue\">صف مداوم</string>\n    <string name=\"skip_silence\">گذر از سکوت</string>\n    <string name=\"audio_normalization\">طبیعی‌سازی صدا</string>\n    <string name=\"equalizer\">میزان‌گر</string>\n    <string name=\"storage\">ذخیره‌سازی</string>\n    <string name=\"cache\">حافظه‌ی‌پنهان</string>\n    <string name=\"image_cache\">حافظه‌‌ی‌پنهان تصویر</string>\n    <string name=\"song_cache\">حافظه‌‌ی‌پنهان ترانه</string>\n    <string name=\"max_cache_size\">حداکثر اندازه‌ی حافظه‌ی‌پنهان</string>\n    <string name=\"unlimited\">نامحدود</string>\n    <string name=\"clear_all_downloads\">پاک‌کردن تمامی بارگیری‌ها</string>\n    <string name=\"max_image_cache_size\">بیشترین اندازه‌ی حافظه‌ی‌پنهان تصویر</string>\n    <string name=\"clear_image_cache\">پاک‌کردن حافظه‌‌ی‌پنهان تصویر</string>\n    <string name=\"max_song_cache_size\">بیشترین اندازه‌ی حافظه‌ی‌پنهان ترانه</string>\n    <string name=\"clear_song_cache\">پاک‌کردن حافظه‌‌ی‌پنهان ترانه</string>\n    <string name=\"size_used\">%s استفاده‌شده‌است</string>\n    <string name=\"privacy\">حریم‌خصوصی</string>\n    <string name=\"pause_listen_history\">متوقف‌کردن تاریخچه‌ی گوش‌دادن</string>\n    <string name=\"clear_listen_history\">پاک‌کردن تاریخچه‌ی گوش‌دادن</string>\n    <string name=\"clear_listen_history_confirm\">آیا از پاک‌کردن تمامی سابقه‌ی گوش‌دادن مطمئن هستید؟</string>\n    <string name=\"pause_search_history\">متوقف‌کردن تاریخچه جستجو</string>\n    <string name=\"clear_search_history\">پاک‌کردن تاریخچه جستجو</string>\n    <string name=\"clear_search_history_confirm\">آیا برای پاک‌کردن تمام سابقه جستجو مطمئن هستید؟</string>\n    <string name=\"enable_kugou\">فعال‌کردن ارائه‌دهنده‌ی متن‌ترانه‌ی KuGou</string>\n    <string name=\"backup_restore\">پشتیبان‌گیری و بازگردانی</string>\n    <string name=\"action_backup\">پشتیبان‌گیری</string>\n    <string name=\"action_restore\">بازگردانی</string>\n    <string name=\"imported_playlist\">فهرست‌پخش واردشد</string>\n    <string name=\"backup_create_success\">پشتیبان باموفقیت ایجادشد</string>\n    <string name=\"backup_create_failed\">پشتیبان ایجاد نشد</string>\n    <string name=\"restore_failed\">بازیابی پشتیبان انجام‌نشد</string>\n    <string name=\"about\">درباره</string>\n    <string name=\"app_version\">نسخه‌ی برنامه</string>\n    <string name=\"new_version_available\">نسخه‌ی جدید دردسترس‌است</string>\n    <string name=\"translation_models\">نمونه‌های ترجمه</string>\n    <string name=\"clear_translation_models\">پاک‌کردن نمونه‌های ترجمه</string>\n    <string name=\"forgotten_favorites\">موارد دلخواه فراموش شده</string>\n    <string name=\"keep_listening\">به گوش دادن ادامه بده</string>\n    <string name=\"your_youtube_playlists\">لیست های پخش یوتیوب شما</string>\n    <string name=\"similar_to\">شبیه به</string>\n    <string name=\"library_song_empty\">آهنگ‌ های کتابخانه اینجا نمایش داده می‌ شوند</string>\n    <string name=\"library_artist_empty\">هنرمندان کتابخانه اینجا نمایش داده می شوند</string>\n    <string name=\"library_album_empty\">آلبوم‌ های کتابخانه اینجا نمایش داده می‌ شوند</string>\n    <string name=\"library_playlist_empty\">لیست‌های پخش شما اینجا نمایش داده می‌ شوند</string>\n    <string name=\"other_versions\">نسخه های دیگر</string>\n    <string name=\"remove_download_playlist_confirm\">آیا واقعاً می‌ خواهید همه آهنگ‌های لیست پخش «%s» را از حافظه آهنگ‌ های دانلود شده حذف کنید؟</string>\n    <string name=\"delete_playlist_confirm\">آیا واقعاً می‌ خواهید لیست پخش «%s» را حذف کنید؟</string>\n    <string name=\"add_all_to_library\">همه را به کتابخانه اضافه کنید</string>\n    <string name=\"remove_all_from_library\">همه را از کتابخانه حذف کنید</string>\n    <string name=\"remove_from_playlist\">حذف از لیست پخش</string>\n    <string name=\"remove_from_queue\">حذف از صف</string>\n    <string name=\"tempo_and_pitch\">تمپو و زیر و بمی صدا</string>\n    <string name=\"duplicates\">تکراری ها</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-fi/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <string name=\"home\">Koti</string>\n    <string name=\"songs\">Kappaleet</string>\n    <string name=\"artists\">Artistit</string>\n    <string name=\"albums\">Albumit</string>\n    <string name=\"playlists\">Soittolistat</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d valittu</item>\n        <item quantity=\"other\">%d kappaletta valittu</item>\n    </plurals>\n    <string name=\"history\">Historia</string>\n    <string name=\"stats\">Tilastot</string>\n    <string name=\"mood_and_genres\">Mielialat ja genret</string>\n    <string name=\"account\">Tili</string>\n    <string name=\"quick_picks\">Pikalista</string>\n    <string name=\"quick_picks_empty\">Kuuntele kappaleita luodaksesi pikavalintasi</string>\n    <string name=\"new_release_albums\">Uusia julkaisuja albumeita</string>\n    <string name=\"today\">Tänään</string>\n    <string name=\"yesterday\">Eilen</string>\n    <string name=\"this_week\">Tämä viikko</string>\n    <string name=\"last_week\">Viime viikko</string>\n    <string name=\"most_played_songs\">Eniten soitetut kappaleet</string>\n    <string name=\"most_played_artists\">Eniten soitetut artistit</string>\n    <string name=\"most_played_albums\">Eniten soitetut albumit</string>\n    <string name=\"search\">Etsi</string>\n    <string name=\"search_yt_music\">Hae YouTube Musicista…</string>\n    <string name=\"search_library\">Hae kirjastosta…</string>\n    <string name=\"filter_library\">Kirjasto</string>\n    <string name=\"filter_liked\">Tykätty</string>\n    <string name=\"filter_downloaded\">Ladattu</string>\n    <string name=\"filter_all\">Kaikki</string>\n    <string name=\"filter_songs\">Kappaleet</string>\n    <string name=\"filter_videos\">Videot</string>\n    <string name=\"filter_albums\">Albumit</string>\n    <string name=\"filter_artists\">Artistit</string>\n    <string name=\"filter_playlists\">Soittolistat</string>\n    <string name=\"filter_community_playlists\">Yhteisön soittolistat</string>\n    <string name=\"filter_featured_playlists\">Suositellut soittolistat</string>\n    <string name=\"filter_bookmarked\">Kirjanmerkkeihin lisätty</string>\n    <string name=\"no_results_found\">Tuloksia ei löytynyt</string>\n    <string name=\"from_your_library\">Kirjastostasi</string>\n    <string name=\"liked_songs\">Tykkätyt kappaleet</string>\n    <string name=\"downloaded_songs\">Ladatut kappaleet</string>\n    <string name=\"playlist_is_empty\">Soittolista on tyhjä</string>\n    <string name=\"retry\">Toisto</string>\n    <string name=\"radio\">Radio</string>\n    <string name=\"shuffle\">Sekoita</string>\n    <string name=\"reset\">Nollaa</string>\n    <string name=\"details\">Yksityiskohdat</string>\n    <string name=\"edit\">Muokkaa</string>\n    <string name=\"start_radio\">Käynnistä radio</string>\n    <string name=\"play\">Toista</string>\n    <string name=\"play_next\">Toista seuraavaksi</string>\n    <string name=\"add_to_queue\">Lisää jonoon</string>\n    <string name=\"add_to_library\">Lisää kirjastoon</string>\n    <string name=\"remove_from_library\">Poista kirjastosta</string>\n    <string name=\"action_download\">Lataa</string>\n    <string name=\"downloading\">Lataamassa</string>\n    <string name=\"remove_download\">Poista lataus</string>\n    <string name=\"import_playlist\">Tuo soittolista</string>\n    <string name=\"add_to_playlist\">Lisää soittolistaan</string>\n    <string name=\"view_artist\">Näytä artisti</string>\n    <string name=\"view_album\">Näytä albumi</string>\n    <string name=\"refetch\">Nouda uudelleen</string>\n    <string name=\"share\">Jakaa</string>\n    <string name=\"delete\">Poista</string>\n    <string name=\"remove_from_history\">Poista historiasta</string>\n    <string name=\"search_online\">Etsi verkossa</string>\n    <string name=\"action_sync\">Synkronoi</string>\n    <string name=\"advanced\">Edistynyt</string>\n    <string name=\"sort_by_create_date\">Lisäyspäivä</string>\n    <string name=\"sort_by_name\">Nimi</string>\n    <string name=\"sort_by_artist\">Artisti</string>\n    <string name=\"sort_by_year\">Vuosi</string>\n    <string name=\"sort_by_song_count\">Kappaleiden määrä</string>\n    <string name=\"sort_by_length\">Pituus</string>\n    <string name=\"sort_by_play_time\">Toistoaika</string>\n    <string name=\"sort_by_custom\">Mukautettu tilaus</string>\n    <string name=\"media_id\">Mediatunnus</string>\n    <string name=\"mime_type\">MIME-tyyppi</string>\n    <string name=\"codecs\">Koodekit</string>\n    <string name=\"bitrate\">Bittinopeus</string>\n    <string name=\"loudness\">Äänekkyys</string>\n    <string name=\"volume\">Voimakkuus</string>\n    <string name=\"file_size\">Tiedoston koko</string>\n    <string name=\"unknown\">Tuntematon</string>\n    <string name=\"copied\">Kopioitu leikepöydälle</string>\n    <string name=\"edit_lyrics\">Muokkaa sanoituksia</string>\n    <string name=\"search_lyrics\">Hae sanoituksia</string>\n    <string name=\"edit_song\">Muokkaa kappaletta</string>\n    <string name=\"song_title\">Kappaleen nimi</string>\n    <string name=\"song_artists\">Artisti</string>\n    <string name=\"error_song_title_empty\">Kappaleen nimi ei voi olla tyhjä.</string>\n    <string name=\"error_song_artist_empty\">Kappaleen esittäjä ei voi olla tyhjä.</string>\n    <string name=\"save\">Tallenna</string>\n    <string name=\"choose_playlist\">Valitse soittolista</string>\n    <string name=\"edit_playlist\">Muokkaa soittolistaa</string>\n    <string name=\"create_playlist\">Luo soittolista</string>\n    <string name=\"playlist_name\">Soittolistan nimi</string>\n    <string name=\"error_playlist_name_empty\">Soittolistan nimi ei voi olla tyhjä.</string>\n    <string name=\"edit_artist\">Muokkaa artistia</string>\n    <string name=\"artist_name\">Artistin nimi</string>\n    <string name=\"error_artist_name_empty\">Artistin nimi ei voi olla tyhjä.</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d kappale</item>\n        <item quantity=\"other\">%d kappaletta</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d artisti</item>\n        <item quantity=\"other\">%d artistia</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d albumi</item>\n        <item quantity=\"other\">%d albumia</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d soittolista</item>\n        <item quantity=\"other\">%d soittolistaa</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d viikko</item>\n        <item quantity=\"other\">%d viikkoa</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d kuukausi</item>\n        <item quantity=\"other\">%d kuukautta</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d vuosi</item>\n        <item quantity=\"other\">%d vuotta</item>\n    </plurals>\n    <string name=\"playlist_imported\">Soittolista tuotu</string>\n    <string name=\"removed_song_from_playlist\">\\\"%s\\\" poistettiin soittolistasta</string>\n    <string name=\"playlist_synced\">Soittolista synkronoitu</string>\n    <string name=\"undo\">Kumoa</string>\n    <string name=\"lyrics_not_found\">Sanoitusta ei löytynyt</string>\n    <string name=\"sleep_timer\">Uniajastin</string>\n    <string name=\"end_of_song\">Laulun loppu</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">%d minuutti</item>\n        <item quantity=\"other\">%d minuuttia</item>\n    </plurals>\n    <string name=\"error_no_stream\">Suoratoistoa ei saatavilla</string>\n    <string name=\"error_no_internet\">Ei verkkoyhteyttä</string>\n    <string name=\"error_timeout\">Aikakatkaisu</string>\n    <string name=\"error_unknown\">Tuntematon virhe</string>\n    <string name=\"action_like\">Tykkää</string>\n    <string name=\"action_remove_like\">Poista \\\"tykkää\\\"</string>\n    <string name=\"action_shuffle_on\">Sekoitus päällä</string>\n    <string name=\"action_shuffle_off\">Sekoitus pois päältä</string>\n    <string name=\"repeat_mode_off\">Toistotila pois päältä</string>\n    <string name=\"repeat_mode_one\">Toista nykyinen laulu</string>\n    <string name=\"repeat_mode_all\">Toista jono</string>\n    <string name=\"queue_all_songs\">Kaikki laulut</string>\n    <string name=\"music_player\">Soitin</string>\n    <string name=\"settings\">Asetukset</string>\n    <string name=\"appearance\">Ulkoasu</string>\n    <string name=\"dark_theme\">Tumma teema</string>\n    <string name=\"dark_theme_on\">Käytössä</string>\n    <string name=\"dark_theme_off\">Pois käytöstä</string>\n    <string name=\"dark_theme_follow_system\">Järjestelmän teeman mukainen</string>\n    <string name=\"content\">Sisältö</string>\n    <string name=\"content_language\">Sisällön oletuskieli</string>\n    <string name=\"content_country\">Sisällön oletusmaa</string>\n    <string name=\"system_default\">Järjestelmän oletus</string>\n    <string name=\"about\">Tietoa</string>\n    <string name=\"app_version\">Sovelluksen versio</string>\n    <string name=\"add_all_to_library\">Lisää kaikki kirjastoon</string>\n    <string name=\"remove_all_from_library\">Poista kaikki kirjastosta</string>\n    <string name=\"remove_from_queue\">Poista jonosta</string>\n    <string name=\"remove_from_playlist\">Poista soittolistasta</string>\n    <string name=\"queue\">Jono</string>\n    <string name=\"small\">Pieni</string>\n    <string name=\"not_logged_in\">Ei kirjautunut sisään</string>\n    <string name=\"similar_to\">Samanlaisia kuin</string>\n    <string name=\"tempo_and_pitch\">Tempo ja sävelkorkeus</string>\n    <string name=\"action_like_all\">Tykkää kaikista</string>\n    <string name=\"action_remove_like_all\">Poista kaikki tykkäykset</string>\n    <string name=\"theme\">Teema</string>\n    <string name=\"player\">Soitin</string>\n    <string name=\"sided\">Sivulla</string>\n    <string name=\"auto_load_more\">Automaattisesti lataa lisää kappaleita</string>\n    <string name=\"auto_load_more_desc\">Automaattisesti lisää enemmän kappaleita, kun jonon loppu saavutetaan, jos mahdollista</string>\n    <string name=\"auto_skip_next_on_error\">Automaattisesti ohita seuraavaan kappaleeseen, kun tapahtuu virhe</string>\n    <string name=\"listen_history\">Kuunteluhistoria</string>\n    <string name=\"skip_duplicates\">Ohita kaksoiskappaleet</string>\n    <string name=\"disable_screenshot\">Poista kuvakaappaukset käytöstä</string>\n    <string name=\"discord_integration\">Discord-integraatio</string>\n    <string name=\"big\">Suuri</string>\n    <string name=\"library_artist_empty\">Kirjaston artistit näkyvät täällä</string>\n    <string name=\"keep_listening\">Jatka kuuntelemista</string>\n    <string name=\"your_youtube_playlists\">YouTube-soittolistasi</string>\n    <string name=\"library_song_empty\">Kirjaston kappaleet näkyvät täällä</string>\n    <string name=\"library_album_empty\">Kirjaston albumit näkyvät täällä</string>\n    <string name=\"library_playlist_empty\">Soittolistasi näkyvät täällä</string>\n    <string name=\"other_versions\">Muita versioita</string>\n    <string name=\"remove_download_playlist_confirm\">Haluatko varmasti poistaa kaikki \\\"%s\\\" soittolistan kappaleet Ladattujen kappaleiden tallennustilasta?</string>\n    <string name=\"delete_playlist_confirm\">Haluatko varmasti poistaa soittolistan \\\"%s\\\"?</string>\n    <string name=\"duplicates\">Kaksoiskappaleet</string>\n    <string name=\"add_anyway\">Lisää silti</string>\n    <string name=\"duplicates_description_single\">Laulu on jo soittolistassasi</string>\n    <string name=\"duplicates_description_multiple\">%d kappaleet ovat jo soittolistassasi</string>\n    <string name=\"player_text_alignment\">Soittimen tekstin tasaus</string>\n    <string name=\"player_slider_style\">Soittimen liukusäätimen tyyli</string>\n    <string name=\"default_\">Oletus</string>\n    <string name=\"squiggly\">Koukeroinen</string>\n    <string name=\"grid_cell_size\">Ruudukon solun koko</string>\n    <string name=\"persistent_queue_desc\">Palauta viimeinen jonosi, kun sovellus käynnistyy</string>\n    <string name=\"auto_skip_next_on_error_desc\">Varmista jatkuva toistokokemus</string>\n    <string name=\"stop_music_on_task_clear\">Pysäytä musiikki, kun sovellus suljetaan</string>\n    <string name=\"search_history\">Hakuhistoria</string>\n    <string name=\"disable_screenshot_desc\">Kun tämä asetus on päällä, kuvakaappaukset ja sovelluksen näkyminen äskeisissä sovelluksissa poistetaan käytöstä.</string>\n    <string name=\"enable_lrclib\">Ota LrcLib-lyriikantarjoaja käyttöön</string>\n    <string name=\"hide_explicit\">Piilota sopimaton sisältö</string>\n    <string name=\"dismiss\">Hylkää</string>\n    <string name=\"discord_information\">Metrolist käyttää KizzyRPC-kirjastoa asettaaksesi Discord-tilisi tilan. Tämä edellyttää Discord Gateway-yhteyttä, jota voidaan katsoa Discordin käyttöehtojen rikkomuksena. Ei kuitenkaan ole tiedossa tapauksia, joissa käyttäjätilit olisi jäädytetty tästä syystä. Käytä omalla vastuullasi. \\n \\nMetrolist poimii ainoastaan tunnuksesi, ja kaikki muu säilytetään paikallisesti.</string>\n    <string name=\"options\">Asetukset</string>\n    <string name=\"preview\">Esikatselu</string>\n    <string name=\"login_failed\">Kirjautuminen epäonnistui</string>\n    <string name=\"action_logout\">Kirjaudu ulos</string>\n    <string name=\"enable_discord_rpc\">Tämä on Discord-integraatio-ominaisuus, se tarkoittaa Metrolist-toiminnan näyttämistä Discord-tililläsi</string>\n    <string name=\"forgotten_favorites\">Unohtuneet suosikit</string>\n    <string name=\"sample_rate\">Näytteenottonopeus</string>\n    <string name=\"queue_searched_songs\">Haetut laulut</string>\n    <string name=\"enable_dynamic_theme\">Ota dynaaminen teema käyttöön</string>\n    <string name=\"pure_black\">Puhtaan musta</string>\n    <string name=\"customize_navigation_tabs\">Mukauta navigointivälilehtiä</string>\n    <string name=\"lyrics_text_position\">Sanoitustekstin sijainti</string>\n    <string name=\"left\">Vasen</string>\n    <string name=\"center\">Keskellä</string>\n    <string name=\"right\">Oikea</string>\n    <string name=\"misc\">Sekalaiset</string>\n    <string name=\"default_open_tab\">Oletusarvoinen avausvälilehti</string>\n    <string name=\"action_login\">Kirjaudu sisään</string>\n    <string name=\"login\">Sisäänkirjautuminen</string>\n    <string name=\"enable_proxy\">Ota välityspalvelin käyttöön</string>\n    <string name=\"proxy_type\">Välityspalvelimen tyyppi</string>\n    <string name=\"proxy_url\">Välityspalvelimen URL-osoite</string>\n    <string name=\"restart_to_take_effect\">Käynnistä uudelleen, jotta se tulee voimaan</string>\n    <string name=\"player_and_audio\">Soitin ja ääni</string>\n    <string name=\"audio_quality\">Äänenlaatu</string>\n    <string name=\"audio_quality_auto\">Automaattinen</string>\n    <string name=\"audio_quality_high\">Korkea</string>\n    <string name=\"audio_quality_low\">Matala</string>\n    <string name=\"persistent_queue\">Pysyvä jono</string>\n    <string name=\"skip_silence\">Ohita hiljaisuus</string>\n    <string name=\"audio_normalization\">Äänen normalisointi</string>\n    <string name=\"equalizer\">Taajuuskorjain</string>\n    <string name=\"storage\">Tallennustila</string>\n    <string name=\"cache\">Välimuisti</string>\n    <string name=\"image_cache\">Kuvavälimuisti</string>\n    <string name=\"song_cache\">Lauluvälimuisti</string>\n    <string name=\"max_cache_size\">Välimuistin enimmäiskoko</string>\n    <string name=\"unlimited\">Rajoittamaton</string>\n    <string name=\"clear_all_downloads\">Tyhjennä kaikki lataukset</string>\n    <string name=\"max_image_cache_size\">Kuvavälimuistin enimmäiskoko</string>\n    <string name=\"clear_image_cache\">Tyhjennä kuvavälimuisti</string>\n    <string name=\"max_song_cache_size\">Laulujen välimuistin enimmäiskoko</string>\n    <string name=\"clear_song_cache\">Tyhjennä laulujen välimuisti</string>\n    <string name=\"size_used\">%s käytetty</string>\n    <string name=\"privacy\">Tietosuoja</string>\n    <string name=\"pause_listen_history\">Keskeytä historian kuuntelu</string>\n    <string name=\"clear_listen_history\">Tyhjennä kuunteluhistoria</string>\n    <string name=\"clear_listen_history_confirm\">Oletko varma, että haluat tyhjentää koko kuunteluhistorian?</string>\n    <string name=\"pause_search_history\">Keskeytä hakuhistoria</string>\n    <string name=\"clear_search_history\">Tyhjennä hakuhistoria</string>\n    <string name=\"clear_search_history_confirm\">Oletko varma, että haluat tyhjentää kaiken hakuhistorian?</string>\n    <string name=\"use_login_for_browse\">Käytä kirjautumista sisällön selaamista varten</string>\n    <string name=\"use_login_for_browse_desc\">Tämä voi vaikuttaa näkemääsi sisältöön ja näyttää esimerkiksi vain ensiluokkaisia albumeita, jos olet kirjautunut sisään Premium-tilillä</string>\n    <string name=\"enable_kugou\">Ota KuGou-sanoitukset käyttöön</string>\n    <string name=\"backup_restore\">Varmuuskopiointi ja palautus</string>\n    <string name=\"action_backup\">Varmuuskopio</string>\n    <string name=\"action_restore\">Palauta</string>\n    <string name=\"imported_playlist\">Tuotu soittolista</string>\n    <string name=\"backup_create_success\">Varmuuskopio luotu onnistuneesti</string>\n    <string name=\"backup_create_failed\">Varmuuskopiota ei voitu luoda</string>\n    <string name=\"restore_failed\">Varmuuskopion palauttaminen epäonnistui</string>\n    <string name=\"new_version_available\">Uusi versio saatavilla</string>\n    <string name=\"translation_models\">Käännösmallit</string>\n    <string name=\"clear_translation_models\">Tyhjennä käännösmallit</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-fil/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">Lokal</string>\n    <string name=\"charts\">Mga tsart</string>\n    <string name=\"back_button_desc\">Balik</string>\n    <string name=\"top_music_videos\">Nangungunang music videos</string>\n    <string name=\"trending\">Sumisikat</string>\n    <string name=\"weeks\">Linggo</string>\n    <string name=\"months\">Buwan</string>\n    <string name=\"years\">Taon</string>\n    <string name=\"continuous\">Tuloy tuloy</string>\n    <string name=\"liked\">Nagustuhan</string>\n    <string name=\"offline\">Nadownload</string>\n    <string name=\"my_top\">Aking nangunguna</string>\n    <string name=\"cached_playlist\">Nakacache</string>\n    <string name=\"uploaded_playlist\">Naka upload</string>\n    <string name=\"filter_uploaded\">Naka upload</string>\n    <string name=\"allows_for_sync_witch_youtube\">Tala: Ito ay pumapayag mag sync sa Youtube Music. HINDI PWEDE ibahin to mamaya.</string>\n    <string name=\"generating_image\">Gumagawa ng imahe</string>\n    <string name=\"please_wait\">Pakiantay</string>\n    <string name=\"cancel\">Kanselahin</string>\n    <string name=\"share_lyrics\">Ishare ang lyrics</string>\n    <string name=\"share_as_text\">Ishare bilang teksto</string>\n    <string name=\"share_as_image\">Ishare bilang imahe</string>\n    <string name=\"max_selection_limit\">Todong limit ng seleksyon</string>\n    <string name=\"share_selected\">Ishare ang napili</string>\n    <string name=\"customize_colors\">Icustomize ang kulay</string>\n    <string name=\"text_color\">Kulay ng teksto</string>\n    <string name=\"secondary_text_color\">Kulay ng pangalawang teksto</string>\n    <string name=\"background_color\">Kulay ng paligid</string>\n    <string name=\"remove_from_cache\">Tinanggal mula sa cache</string>\n    <string name=\"copy_link\">Kopyahin ang link</string>\n    <string name=\"select\">Piliin lahat</string>\n    <string name=\"like_all\">Ilike lahat</string>\n    <string name=\"dislike_all\">Idislike lahat</string>\n    <string name=\"sort_by_last_updated\">Petsa ng huling update</string>\n    <string name=\"link_copied\">Kinopya ang link sa clipboard</string>\n    <string name=\"starting_radio\">Sinisimulan ang radyo</string>\n    <string name=\"now_playing\">Tumutugtog ngayon</string>\n    <string name=\"hide_player_thumbnail\">Itago ang thumbnail ng player</string>\n    <string name=\"hide_player_thumbnail_desc\">Palitan ang artwork ng album ng logo ng aplikasyon sa player</string>\n    <string name=\"already_in_playlist\">Nasa playlist na:</string>\n    <string name=\"album_cover_desc\">Cover ng Album</string>\n    <string name=\"remote_history\">Remote</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-fil/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-fr/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"liked\">Favoris</string>\n    <string name=\"offline\">Hors-Ligne</string>\n    <string name=\"my_top\">Mon Top</string>\n    <string name=\"select\">Tout sélectionner</string>\n    <string name=\"like_all\">Tout aimer</string>\n    <string name=\"sort_by_last_updated\">Date de mise à jour</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"default_lib_chips\">Choix de la bibliothèque par défaut</string>\n    <string name=\"set_quick_picks\">Choix des sélections rapides</string>\n    <string name=\"last_song_listened\">Basé sur le dernier titre écouté</string>\n    <string name=\"all_time\">Tout le temps</string>\n    <string name=\"past_24_hours\">Dernières 24 heures</string>\n    <string name=\"past_week\">Semaine dernière</string>\n    <string name=\"past_month\">Mois dernier</string>\n    <string name=\"past_year\">Année dernière</string>\n    <string name=\"top_length\">Longueur de la liste Mon Top</string>\n    <string name=\"remote_history\">Dans d\\'autres applications</string>\n    <string name=\"top_music_videos\">Meilleurs Clips Musicaux</string>\n    <string name=\"allows_for_sync_witch_youtube\">Remarque : Ceci permet la synchronisation avec YouTube Music. Ce paramètre ne peut pas être modifié ultérieurement.</string>\n    <string name=\"copy_link\">Copier le lien</string>\n    <string name=\"dislike_all\">Ne pas aimer tout</string>\n    <string name=\"remove_from_cache\">Retirer du cache</string>\n    <string name=\"link_copied\">Lien copié dans le presse-papiers</string>\n    <string name=\"slim_navbar\">Barre de navigation inférieure mince</string>\n    <string name=\"lyrics\">Paroles</string>\n    <string name=\"already_in_playlist\">Déjà dans la playlist :</string>\n    <string name=\"swipe_song_to_add\">Faites glisser le titre vers la gauche pour l\\'ajouter à la file d\\'attente ou vers la droite pour le lire ensuite</string>\n    <string name=\"show_top_playlist\">Afficher la playlist « Top »</string>\n    <string name=\"show_cached_playlist\">Afficher la playlist « Mise en Cache »</string>\n    <string name=\"token_adv_login_description\">Ceci est une méthode de connexion AVANCÉE. En tant qu\\'alternative au portail web, vous devez directement entrer ou mettre à jour votre jeton de connexion ici. Par exemple, ceci peut accélérer la connexion sur plusieurs appareils. Veuillez noter que tout format de jeton invalide que l\\'application ne parvient pas à analyser ne sera pas accepté</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">%d seconde</item>\n        <item quantity=\"many\">%d secondes</item>\n        <item quantity=\"other\">%d secondes</item>\n    </plurals>\n    <string name=\"open_app_settings_error\">Impossible d\\'ouvrir les paramètres de l\\'application</string>\n    <string name=\"local_history\">Locale</string>\n    <string name=\"charts\">Classements</string>\n    <string name=\"back_button_desc\">Retour</string>\n    <string name=\"album_cover_desc\">Pochette d\\'album</string>\n    <string name=\"trending\">Tendance</string>\n    <string name=\"weeks\">Semaines</string>\n    <string name=\"months\">Mois</string>\n    <string name=\"years\">Années</string>\n    <string name=\"continuous\">Continu</string>\n    <string name=\"cached_playlist\">Mise en Cache</string>\n    <string name=\"sync_playlist\">Synchroniser la Playlist</string>\n    <string name=\"sync_disabled\">Synchronisation désactivée</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d fois</item>\n        <item quantity=\"many\">%d fois</item>\n        <item quantity=\"other\">%d fois</item>\n    </plurals>\n    <string name=\"similar_content\">Contenu similaire</string>\n    <string name=\"player_background_style\">Style de l\\'arrière plan du lecteur</string>\n    <string name=\"follow_theme\">Suivre le thème</string>\n    <string name=\"gradient\">Dégradé</string>\n    <string name=\"player_background_blur\">Flou</string>\n    <string name=\"player_buttons_style\">Couleurs des boutons du lecteur</string>\n    <string name=\"default_style\">Défaut</string>\n    <string name=\"enable_swipe_thumbnail\">Activer le glissement pour changer de titre</string>\n    <string name=\"lyrics_click_change\">Changer les paroles avec un clic</string>\n    <string name=\"slim\">Subtil</string>\n    <string name=\"auto_playlists\">Playlists Automatiques</string>\n    <string name=\"show_liked_playlist\">Afficher la playlist « Favoris »</string>\n    <string name=\"show_downloaded_playlist\">Afficher la playlist « Hors-Ligne »</string>\n    <string name=\"advanced_login\">Connexion avec un jeton</string>\n    <string name=\"token_hidden\">Cliquer pour afficher le jeton</string>\n    <string name=\"token_shown\">Cliquer encore pour copier ou éditer</string>\n    <string name=\"general\">Général</string>\n    <string name=\"app_language\">Langue de l\\'application</string>\n    <string name=\"enable_similar_content\">Activer le contenu similaire</string>\n    <string name=\"similar_content_desc\">Ajoutez automatiquement d\\'autres titres similaires lorsque la fin de la file d\\'attente est atteinte</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"clear_song_cache_dialog\">Êtes-vous sûr de vouloir effacer tous les titres en cache ?</string>\n    <string name=\"clear_downloads_dialog\">Êtes-vous sûr·e de vouloir supprimer tous les téléchargements ?</string>\n    <string name=\"not_logged_in_youtube\">Non connecté à YouTube</string>\n    <string name=\"default_links\">Ouvrir les liens supportés</string>\n    <string name=\"release_notes\">Notes de version</string>\n    <string name=\"history_duration\">Durée de l\\'historique</string>\n    <string name=\"information\">Informations</string>\n    <string name=\"description\">Description</string>\n    <string name=\"views\">Vues</string>\n    <string name=\"likes\">J\\'aime</string>\n    <string name=\"dislikes\">Je n\\'aime pas</string>\n    <string name=\"cancel\">Annuler</string>\n    <string name=\"share_lyrics\">Partager les paroles</string>\n    <string name=\"share_as_text\">Partager comme texte</string>\n    <string name=\"max_selection_limit\">Limite maximale de sélection</string>\n    <string name=\"share_selected\">Partager la sélection</string>\n    <string name=\"customize_colors\">Personnaliser les couleurs</string>\n    <string name=\"text_color\">Couleur du texte</string>\n    <string name=\"secondary_text_color\">Couleur secondaire du texte</string>\n    <string name=\"background_color\">Couleur d\\'arrière-plan</string>\n    <string name=\"auto_download_on_like\">Télécharger automatiquement quand on aime</string>\n    <string name=\"auto_download_on_like_desc\">Téléchargez automatiquement les titres quand vous les aimez</string>\n    <string name=\"generating_image\">Génération de l\\'image</string>\n    <string name=\"share_as_image\">Partager comme image</string>\n    <string name=\"please_wait\">Veuillez patienter</string>\n    <string name=\"lyrics_auto_scroll\">Défilement automatique des paroles</string>\n    <string name=\"playlist_add_local_to_synced_note\">Remarque : L’ajout de titres locaux à des playlists synchronisées/distantes n’est pas pris en charge. Toute autre combinaison est valide.</string>\n    <string name=\"import_online\">Importer une playlist « m3u »</string>\n    <string name=\"import_csv\">Importer des playlists au format CSV</string>\n    <string name=\"lyrics_romanize_japanese\">Romaniser les paroles japonaises</string>\n    <string name=\"lyrics_romanize_korean\">Romaniser les paroles coréennes</string>\n    <string name=\"yt_sync\">Synchronisation automatique avec le compte</string>\n    <string name=\"more_content\">Plus de contenu</string>\n    <string name=\"new_player_design\">Nouveau design du lecteur</string>\n    <string name=\"swipe_sensitivity\">Sensibilité du mini-lecteur au balayage</string>\n    <string name=\"clear_image_cache_dialog\">Êtes-vous sûr de vouloir effacer toutes les images mises en cache ?</string>\n    <string name=\"disable\">Désactiver</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"subscribe\">S\\'abonner</string>\n    <string name=\"subscribed\">Abonné</string>\n    <string name=\"new_mini_player_design\">Nouveau design du mini lecteur</string>\n    <string name=\"now_playing\">En cours de lecture</string>\n    <string name=\"seek_forward_dynamic\">+%1$d secondes en avant</string>\n    <string name=\"seek_backward_dynamic\">-%1$d secondes en arrière</string>\n    <string name=\"seek_seconds_addup\">Recherche progressive</string>\n    <string name=\"seek_seconds_addup_description\">Si cette option est activée, chaque nouvel appui pour avancer ou reculer ajoute 5 secondes de plus que le saut précédent</string>\n    <string name=\"close\">Fermer</string>\n    <string name=\"hide_player_thumbnail\">Masquer la miniature du lecteur</string>\n    <string name=\"hide_player_thumbnail_desc\">Remplacer la pochette de l\\'album par le logo de l\\'application dans le lecteur</string>\n    <string name=\"disable_load_more_when_repeat_all\">Désactiver le chargement supplémentaire lors de la répétition de tout</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Ne chargez pas automatiquement plus de titres et de contenu similaire lorsque le mode répéter tout est activé</string>\n    <string name=\"settings_section_ui\">Interface</string>\n    <string name=\"settings_section_privacy\">Confidentialité &amp; Sécurité</string>\n    <string name=\"settings_section_player_content\">Lecteur &amp; Contenu</string>\n    <string name=\"settings_section_storage\">Stockage &amp; Données</string>\n    <string name=\"settings_section_system\">Système &amp; À propos</string>\n    <string name=\"starting_radio\">Démarrage de la radio</string>\n    <string name=\"config_proxy\">Configurer le proxy</string>\n    <string name=\"proxy_username\">Nom d\\'utilisateur du proxy</string>\n    <string name=\"proxy_password\">Mot de passe proxy</string>\n    <string name=\"enable_authentication\">Activer l\\'authentification</string>\n    <string name=\"lyrics_romanization_cyrillic\">Cyrillique</string>\n    <string name=\"lyrics_romanize_title\">Romanisation</string>\n    <string name=\"lyrics_romanization\">Romanisation des paroles</string>\n    <string name=\"lyrics_romanize_russian\">Romaniser les paroles russes</string>\n    <string name=\"lyrics_romanize_ukrainian\">Romaniser les paroles ukrainiennes</string>\n    <string name=\"lyrics_romanize_belarusian\">Romaniser les paroles biélorusses</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Romaniser les paroles kirghizes</string>\n    <string name=\"lyrics_romanize_serbian\">Romaniser les paroles serbes</string>\n    <string name=\"lyrics_romanize_bulgarian\">Romaniser les paroles bulgares</string>\n    <string name=\"line_by_line_option_title\">EXPÉRIMENTAL : Détecter la langue ligne par ligne</string>\n    <string name=\"line_by_line_option_desc\">La langue cyrillique sera détectée ligne par ligne au lieu du titre entier.</string>\n    <string name=\"line_by_line_dialog_title\">Êtes-vous sûr ?</string>\n    <string name=\"line_by_line_dialog_desc\">Il s\\'agit d\\'une fonctionnalité expérimentale aléatoire.\\n\\nPar défaut, la langue est déterminée à partir du titre entier, mais avec cette option activée, il sera déterminé ligne par ligne. Cela permettra aux titres multilingues de fonctionner, mais la langue pourrait ne pas être toujours correcte (par exemple, si des paroles en ukrainien ne contiennent aucune lettre spécifique à l\\'ukrainien, elles pourraient être romanisées en russe).\\n\\nSi vous ne rencontrez pas de problème, il est recommandé de désactiver cette option.</string>\n    <string name=\"romanize_current_track\">Romaniser la piste actuelle</string>\n    <string name=\"edit_playlist_cover\">Modifier la couverture de la playlist</string>\n    <string name=\"edit_playlist_cover_note\">Remarque : votre compte doit être lié à un numéro de téléphone et vérifié sur YouTube Music pour modifier la couverture de la playlist.</string>\n    <string name=\"edit_playlist_cover_note_wait\">Après avoir sélectionné une image, veuillez patienter un instant pour que la nouvelle couverture apparaisse dans votre playlist.</string>\n    <string name=\"choose_from_library\">Choisissez dans la bibliothèque</string>\n    <string name=\"remove_custom_image\">Supprimer l\\'image personnalisée</string>\n    <string name=\"audio_offload\">Activer le déchargement</string>\n    <string name=\"audio_offload_description\">Utilisez le chemin audio de déchargement pour la lecture audio. Désactiver cette option peut augmenter la consommation d\\'énergie, mais peut s\\'avérer utile en cas de problèmes de lecture audio ou de post-traitement</string>\n    <string name=\"show_uploaded_playlist\">Afficher la playlist « Téléchargés »</string>\n    <string name=\"uploaded_playlist\">Téléchargés</string>\n    <string name=\"filter_uploaded\">Téléchargés</string>\n    <string name=\"updater\">Mise à jour</string>\n    <string name=\"check_for_updates\">Vérifier automatiquement les mises à jour</string>\n    <string name=\"lyrics_romanize_macedonian\">Romaniser les paroles en macédonien</string>\n    <string name=\"update_notifications\">Activer les notifications de mise à jour</string>\n    <string name=\"update_available_title\">Mise à jour disponible</string>\n    <string name=\"update_channel_name\">Mises à jour de l\\'application</string>\n    <string name=\"update_channel_desc\">Notifications sur les nouvelles versions</string>\n    <string name=\"discord_use_details\">Utiliser les détails au lieu de l\\'état</string>\n    <string name=\"discord_use_details_description\">Afficher le titre de la chanson en évidence au lieu des noms des artistes</string>\n    <string name=\"integrations\">Intégrations</string>\n    <string name=\"username\">Nom d\\'utilisateur</string>\n    <string name=\"password\">Mot de passe</string>\n    <string name=\"lastfm_integration\">Intégration Last.fm</string>\n    <string name=\"enable_scrobbling\">Activer le scrobbling</string>\n    <string name=\"lastfm_now_playing\">Envoyer en cours de lecture</string>\n    <string name=\"scrobbling_configuration\">Configuration du scrobbling</string>\n    <string name=\"scrobble_min_track_duration\">Chansons de Scrobble plus longues que</string>\n    <string name=\"scrobble_delay_percent\">Pourcentage de retard du Scrobble</string>\n    <string name=\"scrobble_delay_minutes\">Minutes de retard du Scrobble</string>\n    <string name=\"swipe_song_to_remove\">Faites glisser le titre pour le supprimer de la playlist</string>\n    <string name=\"last_fm_send_likes\">Envoyer des mentions « J’aime »/« Je n’aime pas »</string>\n    <string name=\"last_fm_send_likes_description\">Titres aimés/détestés sur Last.fm selon qu\\'ils soient aimés/détestés dans Metrolist</string>\n    <string name=\"lyrics_romanize_chinese\">Paroles chinoises romanisées</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"google_cast_description\">Activer la diffusion audio sur Chromecast et autres appareils compatibles Cast</string>\n    <string name=\"hide_video_songs\">Masquer les chansons vidéo</string>\n    <string name=\"primary_color_style\">Couleur primaire</string>\n    <string name=\"auto_scroll\">Resynchroniser</string>\n    <string name=\"details_desc\">Consultez les informations sur le titre</string>\n    <string name=\"edit_desc\">Modifier le titre ou l\\'artiste</string>\n    <string name=\"start_radio_desc\">Créez une station basée sur cet élément</string>\n    <string name=\"play_next_desc\">Ajouter en haut de votre file d\\'attente</string>\n    <string name=\"add_to_queue_desc\">Ajouter en bas de votre file d\\'attente</string>\n    <string name=\"add_to_library_desc\">Enregistrer dans votre bibliothèque</string>\n    <string name=\"download_desc\">Rendre disponible pour la lecture hors ligne</string>\n    <string name=\"add_to_playlist_desc\">Ajouter à l\\'une de vos playlists</string>\n    <string name=\"refetch_desc\">Récupérez les métadonnées les plus récentes de YouTube Music</string>\n    <string name=\"share_desc\">Partager un lien vers cet élément</string>\n    <string name=\"delete_desc\">Supprimer définitivement cet élément</string>\n    <string name=\"advanced_desc\">Modifiez le tempo et la hauteur du titre</string>\n    <string name=\"equalizer_desc\">Réglez l\\'égaliseur audio</string>\n    <string name=\"enable_dynamic_icon\">Activer l\\'icône dynamique</string>\n    <string name=\"mini_player\">Mini-lecteur</string>\n    <string name=\"pure_black_mini_player\">Mini-lecteur noir pur</string>\n    <string name=\"cache_size_warning_title\">Attendez !</string>\n    <string name=\"cache_size_warning_message\">Vous avez choisi une limite de taille de cache inférieure à celle actuellement utilisée par l\\'application (%1$s). Si vous continuez, l\\'application risque de supprimer une partie du cache %2$s pour respecter la nouvelle limite. Voulez-vous continuer malgré tout ?</string>\n    <string name=\"cache_size_warning_confirm\">Continuer</string>\n    <string name=\"tertiary_color_style\">Couleur tertiaire</string>\n    <string name=\"logging_in\">Connexion…</string>\n    <string name=\"download_playlist_desc\">Télécharger tous les titres pour une écoute hors ligne</string>\n    <string name=\"remove_download_playlist_desc\">Supprimer tous les titres téléchargés de cette playlist</string>\n    <string name=\"download_in_progress_desc\">Le téléchargement est en cours</string>\n    <string name=\"share_playlist_desc\">Partager cette playlist avec d\\'autres personnes</string>\n    <string name=\"delete_playlist_desc\">Supprimer définitivement cette playlist</string>\n    <string name=\"sync_playlist_desc\">Synchroniser la playlist avec YouTube Music</string>\n    <string name=\"enable_better_lyrics\">Activer Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">Paroles synchronisées syllabe par syllabe pour n\\'importe quelle chanson, pour le karaoké</string>\n    <string name=\"lyrics_animation_style\">Style d\\'animation mot à mot</string>\n    <string name=\"none\">Aucun</string>\n    <string name=\"fade\">Disparition</string>\n    <string name=\"glow\">Brillance</string>\n    <string name=\"slide\">Glissement</string>\n    <string name=\"karaoke\">Karaoké</string>\n    <string name=\"apple_music_style\">Apple Music</string>\n    <string name=\"lyrics_text_size\">Taille du texte des paroles</string>\n    <string name=\"lyrics_line_spacing\">Espacement des lignes des paroles</string>\n    <string name=\"shuffle_playlist_first\">Lecture aléatoire de la playlist/de l\\'album en premier</string>\n    <string name=\"shuffle_playlist_first_desc\">En mode aléatoire, lire d\\'abord tous les titres de la playlist/album d\\'origine, puis les titres similaires</string>\n    <string name=\"show_wrapped_card\">Montrer le résumé</string>\n    <string name=\"album_art_for\">Pochette d\\'album pour %s</string>\n    <string name=\"wrapped_total_albums_title\">Vous avez écouté</string>\n    <string name=\"wrapped_total_albums_subtitle\">albums uniques</string>\n    <string name=\"wrapped_top_album_title\">Votre album préféré est</string>\n    <string name=\"wrapped_playlist_ready\">Votre playlist personnelle est prête</string>\n    <string name=\"wrapped_top_5_albums_title\">Vos 5 albums préférés</string>\n    <string name=\"wrapped_album_listening_time\">Vous avez écouté cet album pendant %d minutes</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d minutes</string>\n    <string name=\"wrapped_no_data\">Aucune donnée</string>\n    <string name=\"wrapped_top_5_artists_title\">Vos artistes préférés de l\\'année</string>\n    <string name=\"wrapped_artist_listening_time\">%d minutes</string>\n    <string name=\"wrapped_top_5_songs_title\">Vos titres préférés de l\\'année</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">Pochette d\\'album</string>\n    <string name=\"wrapped_top_artist_title\">Votre artiste préféré de l\\'année est</string>\n    <string name=\"wrapped_top_artist_image_content_description\">Image de l\\'artiste principal</string>\n    <string name=\"wrapped_top_artist_listening_time\">Vous les avez écoutés pendant %d minutes</string>\n    <string name=\"wrapped_top_song_title\">Votre titre le plus écouté est</string>\n    <string name=\"wrapped_top_song_listening_time\">Vous avez écouté pendant %d minutes</string>\n    <string name=\"wrapped_total_artists_title\">Vous avez écouté</string>\n    <string name=\"wrapped_total_artists_subtitle\">artistes uniques</string>\n    <string name=\"wrapped_total_songs_title\">Vous avez écouté</string>\n    <string name=\"wrapped_total_songs_subtitle\">titres uniques</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_intro_subtitle\">Il est temps de voir ce que vous avez écouté</string>\n    <string name=\"wrapped_intro_button\">allons-y !</string>\n    <string name=\"wrapped_logo_content_description\">Logo Metrolist</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"wrapped_ready_title\">VOTRE CADEAU EST PRÊT À ÊTRE DÉVOILÉ !</string>\n    <string name=\"wrapped_ready_subtitle\">Il est temps de voir ce que vous avez aimé cette année.</string>\n    <string name=\"wrapped_thank_you\">Merci de votre écoute</string>\n    <string name=\"wrapped_special_thanks\">Remerciements particuliers à MO Agamy pour la création de Metrolist</string>\n    <string name=\"wrapped_close\">Fermer le résumé</string>\n    <string name=\"wrapped_playlist_title\">Votre %s en résumé</string>\n    <string name=\"wrapped_create_playlist\">Créer une playlist</string>\n    <string name=\"wrapped_playlist_saved\">Playlist enregistrée</string>\n    <string name=\"casting_to\">Conversion en %s</string>\n    <string name=\"progress_percent\">Progression %s%%</string>\n    <string name=\"listening_to_metrolist\">Écouter Metrolist</string>\n    <string name=\"open\">Ouvrir</string>\n    <string name=\"failed_to_create_image\">Échec de la création de l\\'image : %s</string>\n    <string name=\"copied_title\">Titre copié</string>\n    <string name=\"copied_artist\">Artiste copié</string>\n    <string name=\"error_playing\">Erreur de lecture</string>\n    <string name=\"failed_to_parse_proxy\">Impossible d\\'analyser l\\'URL du proxy.</string>\n    <string name=\"lyrics_glow_effect\">Activer l\\'effet de paroles lumineuses</string>\n    <string name=\"lyrics_glow_effect_desc\">Ajouter une animation lumineuse et un effet de rebond aux paroles actives</string>\n    <string name=\"wavy\">Ondulé</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"one\">%d Profil</item>\n        <item quantity=\"many\">%d Profils</item>\n        <item quantity=\"other\">%d Profils</item>\n    </plurals>\n    <string name=\"equalizer_header\">Égaliseur</string>\n    <string name=\"no_profiles\">Aucun profil d\\'égaliseur</string>\n    <string name=\"import_profile\">Importer un profil</string>\n    <string name=\"eq_disabled\">Désactivé</string>\n    <string name=\"delete_profile_desc\">Supprimer le profil</string>\n    <string name=\"delete_profile_confirmation\">Êtes-vous sûr de vouloir supprimer %1$s ? Cette action est irréversible.</string>\n    <string name=\"error_file_read\">Impossible de lire le fichier</string>\n    <string name=\"error_file_open\">Impossible d\\'ouvrir le fichier : %1$s</string>\n    <string name=\"import_error_title\">Erreur d\\'importation</string>\n    <plurals name=\"band_count\">\n        <item quantity=\"one\">%d bande</item>\n        <item quantity=\"many\">%d bandes</item>\n        <item quantity=\"other\">%d bandes</item>\n    </plurals>\n    <string name=\"pause_music_when_media_is_muted\">Mettre la musique en pause lorsque le son est coupé</string>\n    <string name=\"enable_simpmusic\">Activer les paroles de SimpMusic</string>\n    <string name=\"enable_simpmusic_desc\">Paroles automatiquement fournies par Musixmatch et YouTube Transcript</string>\n    <string name=\"system_equalizer\">Égaliseur système</string>\n    <string name=\"album_art\">Pochette d\\'album</string>\n    <string name=\"no_song_playing\">Aucun titre en cours de lecture</string>\n    <string name=\"tap_to_open\">Appuyez pour ouvrir Metrolist</string>\n    <string name=\"previous\">Précédent</string>\n    <string name=\"play_pause\">Lecture/Pause</string>\n    <string name=\"next\">Suivant</string>\n    <string name=\"widget_description\">Widget de lecteur de musique avec commandes de lecture</string>\n    <string name=\"turntable_widget_description\">Widget musical circulaire avec commandes de lecture et d\\'appréciation</string>\n    <string name=\"like\">J\\'aime</string>\n    <string name=\"remember_shuffle_and_repeat\">Se souvenir de la lecture aléatoire et de la répétition</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Se souvenir du mode aléatoire et répétition lors du redémarrage de l\\'application</string>\n    <string name=\"lyrics_offset\">Décalage des paroles</string>\n    <string name=\"about_artist\">À propos</string>\n    <string name=\"show_more\">Afficher plus</string>\n    <string name=\"show_less\">Afficher moins</string>\n    <string name=\"artist_page_settings\">Page de l\\'artiste</string>\n    <string name=\"show_artist_description\">Afficher la description de l\\'artiste</string>\n    <string name=\"show_artist_subscriber_count\">Afficher le nombre d\\'abonnés</string>\n    <string name=\"show_artist_monthly_listeners\">Afficher les auditeurs mensuels</string>\n    <string name=\"skip_silence_desc\">Avance rapide pendant les parties silencieuses des titres</string>\n    <string name=\"skip_silence_instant\">Ignorer instantanément les silences</string>\n    <string name=\"skip_silence_instant_desc\">Avancez pendant les silences au lieu d\\'accélérer la lecture</string>\n    <string name=\"persistent_shuffle_desc\">Gardez la lecture aléatoire activée lorsque vous démarrez de nouveaux titres ou playlists</string>\n    <string name=\"persistent_shuffle_title\">Lecture aléatoire continue</string>\n    <string name=\"error_playback_failed\">Échec de la lecture</string>\n    <string name=\"error_title\">Erreur</string>\n    <string name=\"error_eq_apply_failed\">Échec de l\\'application du profil d\\'égalisation : %1$s</string>\n    <string name=\"crop_album_art\">Recadrer la pochette d\\'album</string>\n    <string name=\"crop_album_art_desc\">Forcer un format carré en recadrant les miniatures vidéo</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">Garder l\\'écran allumé lorsque le lecteur est agrandi</string>\n    <string name=\"listen_together\">Écouter ensemble</string>\n    <string name=\"listen_together_server_url\">URL du serveur</string>\n    <string name=\"listen_together_username\">Nom d\\'utilisateur</string>\n    <string name=\"listen_together_connected\">Connecté</string>\n    <string name=\"listen_together_reconnecting\">Reconnexion…</string>\n    <string name=\"listen_together_disconnected\">Déconnecté</string>\n    <string name=\"listen_together_connecting\">Connexion en cours…</string>\n    <string name=\"listen_together_error\">Erreur de connexion</string>\n    <string name=\"listen_together_create_room\">Créer une salle</string>\n    <string name=\"listen_together_create_room_desc\">Créez une salle et partagez le code avec vos amis</string>\n    <string name=\"listen_together_join_room\">Rejoindre la salle</string>\n    <string name=\"listen_together_room_code\">Code de la salle</string>\n    <string name=\"listen_together_you_are_host\">Vous êtes l\\'hôte</string>\n    <string name=\"listen_together_you_are_guest\">Vous êtes un invité</string>\n    <string name=\"listen_together_join_requests\">Demandes d\\'adhésion</string>\n    <string name=\"listen_together_view_logs\">Afficher les journaux</string>\n    <string name=\"listen_together_view_logs_desc\">Débogage de la connexion et des messages</string>\n    <string name=\"listen_together_logs\">Journaux de connexion</string>\n    <string name=\"listen_together_no_logs\">Pas encore de commentaires</string>\n    <string name=\"listen_together_description\">Écoutez de la musique avec vos amis en temps réel. Créez une salle pour en être l\\'hôte ou rejoignez une salle existante à l\\'aide d\\'un code.</string>\n    <string name=\"listen_together_background_disconnect_note\">Remarque : vous risquez d\\'être déconnecté si vous créez une salle alors qu\\'aucune musique n\\'est en cours de lecture, puis que vous passez à une autre application.</string>\n    <string name=\"listen_together_not_configured\">L\\'option « Écouter ensemble » n\\'est pas configurée. Veuillez configurer l\\'URL du serveur dans Paramètres → Intégrations → Écouter ensemble.</string>\n    <string name=\"listen_together_suggestion_received\">%1$s a demandé %2$s</string>\n    <string name=\"listen_together_suggestion_sent\">Suggestion envoyée à l\\'hôte !</string>\n    <string name=\"listen_together_join_request_notification\">%1$s souhaite rejoindre la salle</string>\n    <string name=\"listen_together_notification_channel_name\">Écouter ensemble</string>\n    <string name=\"listen_together_notification_channel_desc\">Notifications pour les événements « Écouter ensemble »</string>\n    <string name=\"listen_together_room_created\">Salle créée : %s</string>\n    <string name=\"listen_together_cannot_edit_username_in_room\">Impossible de modifier le nom d\\'utilisateur lorsque vous êtes dans une salle</string>\n    <string name=\"waiting_for_approval\">En attente de l\\'approbation de l\\'hôte</string>\n    <string name=\"invalid_room_code\">Code de salle invalide</string>\n    <string name=\"join_request_denied\">Demande d\\'adhésion refusée</string>\n    <string name=\"join_existing_room\">Rejoindre une salle existante</string>\n    <string name=\"room_code\">Code de la salle</string>\n    <string name=\"leave_room\">Quitter la salle</string>\n    <string name=\"join_room\">Rejoindre</string>\n    <string name=\"create_room\">Créer</string>\n    <string name=\"joining_room\">Rejoindre la salle %s…</string>\n    <string name=\"creating_room\">Création de la salle…</string>\n    <string name=\"disconnect\">Déconnecter</string>\n    <string name=\"create\">Créer</string>\n    <string name=\"join\">Rejoindre</string>\n    <string name=\"approve\">Approuver</string>\n    <string name=\"reject\">Rejeter</string>\n    <string name=\"copy\">Copier</string>\n    <string name=\"copied_to_clipboard\">Copié dans le presse-papiers</string>\n    <string name=\"not_set\">Non défini</string>\n    <string name=\"hosting_room\">Salle d\\'accueil</string>\n    <string name=\"pending_requests\">Demandes en attente</string>\n    <string name=\"pending_suggestions\">Suggestions en attente</string>\n    <string name=\"suggest_to_host\">Suggérer à l\\'hôte</string>\n    <string name=\"host_label\">Hôte</string>\n    <string name=\"you_label\">Vous</string>\n    <string name=\"connected_users\">Utilisateurs connectés</string>\n    <string name=\"enter_username\">Entrez votre nom d\\'utilisateur</string>\n    <string name=\"error_username_empty\">Un nom d\\'utilisateur est requis.</string>\n    <string name=\"resync\">Resynchronisation</string>\n    <string name=\"connect\">Connecter</string>\n    <string name=\"clear\">Effacer</string>\n    <string name=\"in_room\">Dans la salle</string>\n    <string name=\"kick_user\">Coup</string>\n    <string name=\"mute\">Muet</string>\n    <string name=\"unmute\">Réactiver le son</string>\n    <string name=\"crash_title\">L\\'application a planté</string>\n    <string name=\"crash_description\">Une erreur inattendue s\\'est produite. Veuillez partager le rapport d\\'erreur afin de nous aider à résoudre le problème.</string>\n    <string name=\"crash_share_logs\">Partager les journaux</string>\n    <string name=\"crash_share_title\">Partager le rapport d\\'incident</string>\n    <string name=\"crash_report_subject\">Rapport d\\'incident Metrolist</string>\n    <string name=\"crash_close\">Fermer</string>\n    <string name=\"crash_no_log\">Aucun rapport d\\'incident disponible</string>\n    <string name=\"palette_dynamic\">Dynamique</string>\n    <string name=\"palette_crimson\">Cramoisi</string>\n    <string name=\"palette_rose\">Rose</string>\n    <string name=\"palette_purple\">Violet</string>\n    <string name=\"palette_deep_purple\">Violet Profond</string>\n    <string name=\"palette_indigo\">Indigo</string>\n    <string name=\"palette_blue\">Bleu</string>\n    <string name=\"palette_sky_blue\">Bleu Ciel</string>\n    <string name=\"palette_cyan\">Cyan</string>\n    <string name=\"palette_teal\">Sarcelle</string>\n    <string name=\"palette_green\">Vert</string>\n    <string name=\"palette_light_green\">Vert Clair</string>\n    <string name=\"palette_lime\">Citron Vert</string>\n    <string name=\"palette_yellow\">Jaune</string>\n    <string name=\"palette_amber\">Ambre</string>\n    <string name=\"palette_orange\">Orange</string>\n    <string name=\"palette_deep_orange\">Orange Foncé</string>\n    <string name=\"palette_brown\">Marron</string>\n    <string name=\"palette_grey\">Gris</string>\n    <string name=\"palette_blue_grey\">Bleu Gris</string>\n    <string name=\"cd_back\">Retour</string>\n    <string name=\"cd_pure_black_mode\">Mode Noir Profond</string>\n    <string name=\"cd_light_mode\">Mode Clair</string>\n    <string name=\"cd_dark_mode\">Mode Sombre</string>\n    <string name=\"cd_system_mode\">Mode Système</string>\n    <string name=\"cd_palette_item\">Palette %1$s</string>\n    <string name=\"listen_together_choose_server\">Choisir un serveur</string>\n    <string name=\"listen_together_custom_server\">Serveur personnalisé</string>\n    <string name=\"listen_together_use_custom_server\">Utiliser un serveur personnalisé</string>\n    <string name=\"listen_together_auto_approval_joins\">Approuver automatiquement les demandes d\\'adhésion</string>\n    <string name=\"listen_together_auto_approval_joins_desc\">Approuver automatiquement les demandes d\\'adhésion au lieu de les examiner manuellement</string>\n    <string name=\"listen_together_sync_volume_desc\">Les invités suivent le volume sonore indiqué par l\\'hôte</string>\n    <string name=\"listen_together_sync_volume\">Synchroniser le volume hôte</string>\n    <string name=\"copy_code\">Copier le code</string>\n    <string name=\"kick_user_desc\">Supprimer cette personne de la session</string>\n    <string name=\"permanently_kick_user\">Bloquer définitivement</string>\n    <string name=\"permanently_kick_user_desc\">Bloquez les demandes d\\'adhésion de cette personne et masquez ses suggestions</string>\n    <string name=\"transfer_ownership\">Transfert de propriété</string>\n    <string name=\"transfer_ownership_desc\">Désignez cette personne comme l\\'hôte de la salle</string>\n    <string name=\"manage_user\">Gérer les utilisateurs</string>\n    <string name=\"listen_together_blocked_users\">Utilisateurs bloqués</string>\n    <string name=\"listen_together_blocked_users_count\">%d utilisateur(s) bloqué(s)</string>\n    <string name=\"listen_together_no_blocked_users\">Aucun utilisateur bloqué</string>\n    <string name=\"unblock\">Débloquer</string>\n    <string name=\"user_blocked_by_host\">Utilisateur bloqué par l\\'hôte</string>\n    <string name=\"not_playing\">Aucun titre en cours de lecture</string>\n    <string name=\"tap_to_play\">Appuyez pour ouvrir Metrolist</string>\n    <string name=\"widget_music_player\">Lecteur de Musique</string>\n    <string name=\"widget_turntable\">Platine vinyle</string>\n    <string name=\"together\">Ensemble</string>\n    <string name=\"enter_room_code\">Entrez le code de la salle</string>\n    <string name=\"listen_together_settings_desc\">Configurer le serveur, le nom d\\'utilisateur, et plus encore</string>\n    <string name=\"ai_lyrics_translation\">Traduction des paroles par IA</string>\n    <string name=\"ai_translating_lyrics\">Traduction des paroles...</string>\n    <string name=\"ai_lyrics_translated\">Paroles traduites</string>\n    <string name=\"ai_provider\">Fournisseur</string>\n    <string name=\"ai_base_url\">URL de base</string>\n    <string name=\"ai_api_key\">Clé API</string>\n    <string name=\"ai_model\">Modèle</string>\n    <string name=\"ai_translation_mode\">Mode de traduction</string>\n    <string name=\"ai_target_language\">Langue cible</string>\n    <string name=\"ai_setup_guide\">Identifiants API</string>\n    <string name=\"ai_translation_literal\">Traduction</string>\n    <string name=\"ai_translation_transcribed\">Transcription</string>\n    <string name=\"ai_api_key_required\">Clé API requise</string>\n    <string name=\"ai_error_api_key_required\">Une clé API est requise</string>\n    <string name=\"ai_error_no_lyrics\">Aucune parole à traduire</string>\n    <string name=\"ai_error_lyrics_empty\">Les paroles sont vides</string>\n    <string name=\"ai_error_language_required\">La langue cible est requise</string>\n    <string name=\"ai_error_unexpected\">Résultat de traduction inattendu</string>\n    <string name=\"ai_error_unknown\">Une erreur inconnue s\\'est produite</string>\n    <string name=\"ai_error_translation_failed\">Échec de la traduction</string>\n    <string name=\"play_all\">Tout lire</string>\n    <string name=\"recognize_music\">Reconnaître la musique</string>\n    <string name=\"youtube_url_column\">Colonne URL YouTube (Facultatif)</string>\n    <string name=\"re_listen\">Réécouter</string>\n    <string name=\"clear_recognition_history_confirm\">Êtes-vous sûr de vouloir effacer tout l\\'historique de reconnaissance ?</string>\n    <string name=\"no_match_found\">Aucun résultat trouvé</string>\n    <string name=\"delete_from_history\">Supprimer de l\\'historique</string>\n    <string name=\"artist_name_column\">Colonne Nom de l\\'artiste</string>\n    <string name=\"processing\">Traitement en cours…</string>\n    <string name=\"clear_recognition_history\">Effacer l\\'historique de reconnaissance</string>\n    <string name=\"map_csv_columns\">Colonnes CSV de la carte</string>\n    <string name=\"column_label\">Col %d</string>\n    <string name=\"recognition_error\">Erreur de reconnaissance</string>\n    <string name=\"enable_high_refresh_rate_desc\">Forcer l\\'affichage à fonctionner à la fréquence de rafraîchissement maximale prise en charge (par exemple, 120 Hz)</string>\n    <string name=\"first_row_is_header\">La première ligne est l\\'en-tête</string>\n    <string name=\"try_again\">Réessayer</string>\n    <string name=\"tap_to_recognize\">Appuyez pour reconnaître</string>\n    <string name=\"recognition_history\">Historique de reconnaissance</string>\n    <string name=\"enable_high_refresh_rate\">Activer le taux de rafraîchissement élevé</string>\n    <string name=\"song_title_column\">Colonne Titre de la chanson</string>\n    <string name=\"recently_converted\">Récemment converti</string>\n    <string name=\"importing_csv\">Importer un fichier CSV</string>\n    <string name=\"play_on_app\">Jouer sur Metrolist</string>\n    <string name=\"listening\">Écoute…</string>\n    <string name=\"continue_action\">Continuer</string>\n    <string name=\"enable\">Activer</string>\n    <string name=\"crossfade\">Fondu enchaîné</string>\n    <string name=\"crossfade_desc\">Fondu enchaîné entre les chansons</string>\n    <string name=\"crossfade_duration\">Durée du fondu enchaîné</string>\n    <string name=\"crossfade_gapless\">Désactiver pour les albums sans interruption</string>\n    <string name=\"crossfade_gapless_desc\">N\\'utilisez pas de fondu enchaîné si l\\'album est sans interruption</string>\n    <string name=\"crossfade_beta_title\">Fonctionnalité Bêta</string>\n    <string name=\"crossfade_beta_message\">Le fondu enchaîné est une nouvelle fonctionnalité qui peut présenter des bugs. Si vous rencontrez des problèmes, veuillez les signaler.\\n\\nCette fonctionnalité désactive le déchargement audio en raison de limitations techniques.</string>\n    <string name=\"audio_offload_disabled_by_crossfade\">Désactivé car le fondu enchaîné est actif</string>\n    <string name=\"hide_youtube_shorts\">Masquer YouTube Shorts</string>\n    <string name=\"listen_together_in_top_bar\">Écouter ensemble dans la barre supérieure</string>\n    <string name=\"listen_together_in_top_bar_desc\">Afficher « Écouter ensemble » dans la barre d\\'application supérieure au lieu de la barre de navigation</string>\n    <string name=\"prevent_duplicate_tracks_in_queue\">Empêcher les doublons dans la file d\\'attente</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">Lorsque vous ajoutez un titre à la file d\\'attente, supprimez-le de sa position précédente s\\'il y figure déjà</string>\n    <string name=\"ai_translation_literal_desc\">Traduire le sens dans la langue cible</string>\n    <string name=\"ai_translation_transcribed_desc\">Convertir la prononciation en script cible</string>\n    <string name=\"ai_provider_help\">Obtenir les clés API</string>\n    <string name=\"ai_provider_openrouter_help\">Consultez https://openrouter.ai pour accéder à des modèles gratuits et payants</string>\n    <string name=\"ai_provider_openai_help\">Consultez https://platform.openai.com/api-keys</string>\n    <string name=\"ai_provider_claude_help\">Consultez https://console.anthropic.com/settings/keys</string>\n    <string name=\"ai_provider_gemini_help\">Consultez https://aistudio.google.com/apikey</string>\n    <string name=\"ai_provider_perplexity_help\">Consultez https://perplexity.ai/settings/api</string>\n    <string name=\"ai_provider_deepl_help\">Consultez https://deepl.com/pro-api pour obtenir des clés gratuites et payantes</string>\n    <string name=\"ai_provider_xai_help\">Consultez https://console.x.ai</string>\n    <string name=\"ai_deepl_formality\">Formalité</string>\n    <string name=\"ai_deepl_formality_default\">Par défaut</string>\n    <string name=\"ai_deepl_formality_more\">Plus formel</string>\n    <string name=\"ai_deepl_formality_less\">Moins formel</string>\n    <string name=\"discord_status\">Statut</string>\n    <string name=\"discord_status_online\">En ligne</string>\n    <string name=\"discord_status_idle\">Inactif</string>\n    <string name=\"discord_status_dnd\">Ne pas déranger</string>\n    <string name=\"discord_buttons\">Boutons</string>\n    <string name=\"discord_button_1\">Bouton 1</string>\n    <string name=\"discord_button_2\">Bouton 2</string>\n    <string name=\"login_successful\">Connexion réussie !</string>\n    <string name=\"discord_information_warning\">Cette fonctionnalité utilise la bibliothèque KizzyRPC pour se connecter à la passerelle Discord et définir votre statut Rich Presence. Bien qu\\'aucune suspension de compte n\\'ait été signalée suite à une utilisation similaire, cette méthode n\\'est pas officiellement prise en charge par Discord et peut être considérée comme une violation des conditions d\\'utilisation. Votre jeton est extrait localement et n\\'est jamais envoyé à des serveurs tiers. Procédez à votre propre discrétion.</string>\n    <string name=\"discord_activity_type\">Type d\\'activité</string>\n    <string name=\"discord_activity_playing\">Lire</string>\n    <string name=\"discord_activity_listening\">Écouter</string>\n    <string name=\"discord_activity_watching\">Regarder</string>\n    <string name=\"discord_activity_competing\">En compétition</string>\n    <string name=\"discord_button_text_variables\">Variables : {song_name}, {artist_name}, {album_name}</string>\n    <string name=\"discord_rpc_preview\">Aperçu de la présence enrichie</string>\n    <string name=\"discord_presence\">Présence</string>\n    <string name=\"discord_connect_description\">Connectez-vous avec Discord pour partager ce que vous écoutez</string>\n    <string name=\"discord_playing_metrolist\">Lire sur Metrolist</string>\n    <string name=\"discord_watching_metrolist\">Regarder sur Metrolist</string>\n    <string name=\"discord_competing_metrolist\">En compétition dans Metrolist</string>\n    <string name=\"discord_activity_name\">Nom de l\\'activité</string>\n    <string name=\"discord_activity_name_description\">Nom personnalisé pour l\\'activité (laisser vide pour utiliser le nom par défaut)</string>\n    <string name=\"discord_advanced_mode\">Mode avancé</string>\n    <string name=\"discord_advanced_mode_description\">Afficher les options de personnalisation supplémentaires pour la présence enrichie</string>\n    <string name=\"player_background_solid\">Solide</string>\n    <string name=\"resume_on_bluetooth_connect\">Reprise de la connexion Bluetooth</string>\n    <string name=\"lyrics_romanize_hindi\">Romaniser les paroles en hindi</string>\n    <string name=\"lyrics_romanize_punjabi\">Romaniser les paroles en pendjabi</string>\n    <string name=\"lyrics_romanize_as_main\">Afficher les paroles romanisées comme paroles principales</string>\n    <string name=\"credits_license_name\">Licence publique générale GNU v3.0</string>\n    <string name=\"credits_license_desc\">Software gratuit et open source. L\\'utilisation, la recherche, le partage et les améliorations sont autorisés.</string>\n    <string name=\"credits_discord\">Serveur Discord</string>\n    <string name=\"credits_telegram\">Chaîne Telegram</string>\n    <string name=\"credits_website\">Site internet</string>\n    <string name=\"credits_instagram\">Instagram</string>\n    <string name=\"credits_github\">GitHub</string>\n    <string name=\"credits_view_repo\">Consulter le dépôt</string>\n    <string name=\"app_version_info\">%1$s • %2$s</string>\n    <string name=\"metrolist\">METROLIST</string>\n    <string name=\"no_account_found\">Aucun compte trouvé</string>\n    <string name=\"checking_previous_account\">Vérification du compte précédent…</string>\n    <string name=\"restore\">Restaurer</string>\n    <string name=\"restore_account_warning\">Vous devrez vous reconnecter après la restauration. Le compte suivant sera déconnecté :</string>\n    <string name=\"restore_confirm_message\">Cela restaurera les données de votre application à partir de la sauvegarde.</string>\n    <string name=\"restore_confirm_title\">Restaurer la sauvegarde ?</string>\n    <plurals name=\"n_episode\">\n        <item quantity=\"one\">%d épisode</item>\n        <item quantity=\"many\">%d épisodes</item>\n        <item quantity=\"other\">%d épisodes</item>\n    </plurals>\n    <string name=\"display_density\">Densité d\\'affichage</string>\n    <string name=\"restart\">Redémarrer</string>\n    <string name=\"restart_required\">Redémarrage requis</string>\n    <string name=\"density_restart_message\">Le changement de densité d\\'affichage prendra effet après le redémarrage de l\\'application. Voulez-vous redémarrer maintenant ?</string>\n    <string name=\"enable_lrclib_desc\">Base de données communautaire de paroles synchronisées</string>\n    <string name=\"enable_kugou_desc\">Utilise des paroles de KuGou, une plateforme musicale chinoise populaire</string>\n    <string name=\"youtube_music_lyrics_note\">REMARQUE : Les paroles de YouTube Music s’afficheront automatiquement si aucune autre source n’est disponible. Les paroles provenant de YouTube Music ne sont généralement pas synchronisées.</string>\n    <string name=\"enable_lyricsplus\">Activer LyricsPlus</string>\n    <string name=\"enable_lyricsplus_desc\">Paroles synchronisées provenant de plusieurs sources</string>\n    <string name=\"lyrics_provider_selection\">Sélection du fournisseur</string>\n    <string name=\"lyrics_provider_selection_desc\">Choisissez les fournisseurs de paroles activés</string>\n    <string name=\"lyrics_provider_priority\">Priorité du fournisseur de paroles</string>\n    <string name=\"lyrics_provider_priority_desc\">Faites glisser pour réorganiser les fournisseurs selon vos préférences. Position plus élevée -&gt; priorité plus élevée.</string>\n    <string name=\"changelog\">Journal des modifications</string>\n    <string name=\"changelog_empty\">Aucun journal des modifications disponible</string>\n    <string name=\"github_releases_url\">https://github.com/MetrolistGroup/Metrolist/releases</string>\n    <string name=\"list_bullet\">•</string>\n    <string name=\"view_on_github\">Voir sur GitHub</string>\n    <string name=\"current_version\">Version actuelle</string>\n    <string name=\"version_format\">Version : %s</string>\n    <string name=\"update_settings\">Paramètres de mise à jour</string>\n    <string name=\"check_for_updates_title\">Vérification des mises à jour</string>\n    <string name=\"checking_for_updates\">Recherche de mises à jour…</string>\n    <string name=\"latest_version_format\">Dernière mise à jour : %s</string>\n    <string name=\"check_for_updates_button\">Vérifier les mises à jour</string>\n    <string name=\"hide_changelog\">Masquer le journal des modifications</string>\n    <string name=\"view_changelog\">Afficher le journal des modifications</string>\n    <string name=\"failed_to_check_updates\">Échec de la vérification des mises à jour : %s</string>\n    <string name=\"set_as_default\">Définir comme valeur par défaut</string>\n    <string name=\"sleep_timer_default_set\">Minuterie de mise en veille réglée par défaut sur %d min</string>\n    <string name=\"found_in_settings_content\">Situé dans Paramètres &gt; Contenu</string>\n    <string name=\"plays\">lectures</string>\n    <string name=\"error_episode_save\">Impossible de sauvegarder l\\'épisode</string>\n    <string name=\"error_episode_remove\">Impossible de supprimer l\\'épisode</string>\n    <string name=\"error_podcast_subscribe\">Impossible de s\\'abonner au podcast</string>\n    <string name=\"error_podcast_unsubscribe\">Impossible de se désabonner du podcast</string>\n    <string name=\"listen_together_auto_approval_suggestions\">Approuver automatiquement les suggestions de titres</string>\n    <string name=\"listen_together_auto_approval_suggestions_desc\">Approuver automatiquement et mettre en file d\\'attente les suggestions de titres des invités</string>\n    <string name=\"speed_dial\">Accès Rapide</string>\n    <string name=\"pin_to_speed_dial\">Épingler à l\\'Accès Rapide</string>\n    <string name=\"unpin_from_speed_dial\">Détacher de l\\'Accès Rapide</string>\n    <string name=\"randomize_home_order\">Ordre aléatoire de l\\'écran d\\'accueil</string>\n    <string name=\"randomize_home_order_desc\">Réorganiser aléatoirement les sections de l\\'écran d\\'accueil selon des priorités pondérées</string>\n    <string name=\"daily_discover_sounds_like\">Cela ressemble à %1$s</string>\n    <string name=\"daily_discover_because_you_listen_to\">Parce que vous écoutez %1$s</string>\n    <string name=\"daily_discover_similar_to\">Similaire à %1$s</string>\n    <string name=\"daily_discover_based_on\">Basé sur %1$s</string>\n    <string name=\"daily_discover_for_fans_of\">Pour les fans de %1$s</string>\n    <string name=\"from_the_community\">De la communauté</string>\n    <string name=\"logout_dialog_title\">Conserver les données de la bibliothèque ?</string>\n    <string name=\"logout_dialog_message\">Souhaitez-vous conserver vos playlists et les données de votre bibliothèque ? Les titres téléchargés seront conservés dans tous les cas.</string>\n    <string name=\"logout_keep\">Conserver</string>\n    <string name=\"logout_clear\">Effacer</string>\n    <string name=\"credits_lead_developer\">Développeur Principal</string>\n    <string name=\"credits_collaborator\">Collaborateur</string>\n    <string name=\"credits_collaborators_section\">Collaborateurs</string>\n    <string name=\"like_what_i_do\">Vous aimez ce que je fais ?</string>\n    <string name=\"buy_mo_a_coffee\">Offrez-moi un café</string>\n    <string name=\"community_and_info\">Communauté &amp; Informations</string>\n    <string name=\"wanna_play_favorite_song\">Envie de jouer leur titre préféré ?</string>\n    <string name=\"yeah\">Oui</string>\n    <string name=\"stands_with_palestine\">Ce projet soutient la Palestine 🇵🇸</string>\n    <string name=\"filter_podcasts\">Podcasts</string>\n    <string name=\"view_podcast\">Voir le podcast</string>\n    <string name=\"podcast_channels\">Chaînes de podcast</string>\n    <string name=\"latest_episodes\">Derniers épisodes</string>\n    <string name=\"your_shows\">Vos émissions</string>\n    <string name=\"new_episodes\">Nouveaux épisodes</string>\n    <string name=\"episodes_for_later\">Épisodes à venir</string>\n    <string name=\"save_episode_for_later\">Enregistrer pour plus tard</string>\n    <string name=\"save_episode_for_later_desc\">Ajouter à votre playlist « Épisodes à venir »</string>\n    <string name=\"remove_episode_from_saved\">Supprimer des enregistrements</string>\n    <string name=\"subscribe_to_podcast\">Enregistrer le podcast dans la bibliothèque</string>\n    <string name=\"importing_playlist\">Importer une playlist</string>\n    <string name=\"widget_recognizer_name\">Reconnaissance Musicale</string>\n    <string name=\"widget_recognizer_description\">Identifiez les titres diffusés autour de vous directement depuis votre écran d\\'accueil</string>\n    <string name=\"widget_recognizer_tap_to_search\">Appuyez pour identifier le titre</string>\n    <string name=\"widget_recognizer_listening\">En cours d\\'écoute…</string>\n    <string name=\"widget_recognizer_processing\">Identification…</string>\n    <string name=\"widget_recognizer_no_match\">Aucun résultat trouvé. Veuillez réessayer.</string>\n    <string name=\"widget_recognizer_error\">Échec de la reconnaissance</string>\n    <string name=\"widget_recognizer_error_generic\">Une erreur s\\'est produite. Veuillez réessayer.</string>\n    <string name=\"widget_recognizer_unknown_song\">Titre inconnu</string>\n    <string name=\"widget_recognizer_unknown_artist\">Artiste inconnu</string>\n    <string name=\"widget_recognizer_mic_desc\">Identifier un titre</string>\n    <string name=\"widget_recognizer_channel_name\">Reconnaissance Musicale</string>\n    <string name=\"widget_recognizer_channel_desc\">Affiche une notification lors de l\\'identification d\\'un titre à partir du widget</string>\n    <string name=\"widget_recognizer_notification_text\">Enregistrement audio pour identifier le titre…</string>\n    <string name=\"filter_episodes\">Épisodes</string>\n    <string name=\"filter_channels\">Chaînes</string>\n    <string name=\"auto_playlist\">Playlist Automatique</string>\n    <string name=\"downloaded_episodes\">Épisodes téléchargés</string>\n    <string name=\"no_subscribed_channels\">Aucune chaîne abonnée</string>\n    <string name=\"no_downloaded_episodes\">Aucun épisode téléchargé</string>\n    <plurals name=\"n_channel\">\n        <item quantity=\"one\">%d chaîne</item>\n        <item quantity=\"many\">%d chaînes</item>\n        <item quantity=\"other\">%d chaînes</item>\n    </plurals>\n    <string name=\"view_channel\">Voir la chaîne</string>\n    <string name=\"filter_profiles\">Profils</string>\n    <string name=\"enable_automatic_sleeptimer\">Activer la minuterie de mise en veille automatique</string>\n    <string name=\"sleeptimer_description\">Active automatiquement la minuterie de mise en veille avec la valeur par défaut à une durée personnalisée.</string>\n    <string name=\"sleep_timer_repeat_description\">Définissez une date et une heure personnalisées pour l\\'activation automatique de la minuterie de sommeil.</string>\n    <string name=\"sleep_timer_repeat\">Répéter</string>\n    <string name=\"sleep_timer_daily\">Quotidien</string>\n    <string name=\"sleep_timer_weekdays\">Du Lundi au Vendredi</string>\n    <string name=\"sleep_timer_weekdays_weekends\">En semaine / Week-ends</string>\n    <string name=\"sleep_timer_weekends\">Week-ends (Samedi-Dimanche)</string>\n    <string name=\"sleep_timer_custom\">Personnalisé</string>\n    <string name=\"sleep_timer_start_time\">Heure de début</string>\n    <string name=\"sleep_timer_end_time\">Heure de fin</string>\n    <string name=\"sleep_timer_monday\">Lundi</string>\n    <string name=\"sleep_timer_tuesday\">Mardi</string>\n    <string name=\"sleep_timer_wednesday\">Mercredi</string>\n    <string name=\"sleep_timer_thursday\">Jeudi</string>\n    <string name=\"sleep_timer_friday\">Vendredi</string>\n    <string name=\"sleep_timer_saturday\">Samedi</string>\n    <string name=\"sleep_timer_sunday\">Dimanche</string>\n    <string name=\"sleep_timer_stop_after_current_song\">Arrêter à la fin du titre en cours lorsque le minuteur arrive à son terme</string>\n    <string name=\"sleep_timer_fade_out\">S\\'éteint dans la dernière minute</string>\n    <string name=\"upload_songs\">Télécharger des titres</string>\n    <string name=\"uploading\">Téléchargement en cours…</string>\n    <string name=\"upload_progress\">%1$d de %2$d</string>\n    <string name=\"upload_complete\">Téléchargement terminé</string>\n    <string name=\"upload_failed\">Échec du téléchargement</string>\n    <string name=\"upload_file_too_large\">Fichier trop volumineux (max. 300 Mo)</string>\n    <string name=\"upload_unsupported_format\">Format non pris en charge. Utilisez les formats mp3, m4a, wma, flac ou ogg.</string>\n    <string name=\"delete_uploaded_song\">Supprimer le titre téléchargé</string>\n    <string name=\"delete_uploaded_song_confirm\">Êtes-vous sûr de vouloir supprimer ce titre ? Cette action est irréversible.</string>\n    <string name=\"delete_uploaded_song_success\">Titre téléchargé supprimé</string>\n    <string name=\"delete_uploaded_song_failed\">Échec de la suppression du titre téléchargé</string>\n    <string name=\"delete_uploaded_songs\">Supprimer les titres téléchargés</string>\n    <string name=\"delete_uploaded_songs_confirm\">Êtes-vous sûr de vouloir supprimer %1$d titres téléchargés ? Cette action est irréversible.</string>\n    <string name=\"deleted_n_songs\">Suppression de %1$d titres</string>\n    <string name=\"deleting\">Suppression en cours…</string>\n    <string name=\"export_playlist\">Exporter la playlist</string>\n    <string name=\"export_as_csv\">Exporter au format CSV</string>\n    <string name=\"export_as_m3u\">Exporter au format M3U</string>\n    <string name=\"export_success\">Playlist exportée avec succès</string>\n    <string name=\"export_failed\">Échec de l\\'exportation de la playlist</string>\n    <string name=\"export_option_share\">Partager</string>\n    <string name=\"export_option_save\">Enregistrer dans Documents</string>\n    <string name=\"qs_tile_music_recognizer\">Reconnaître la musique</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-fr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Accueil</string>\n    <string name=\"songs\">Titres</string>\n    <string name=\"artists\">Artistes</string>\n    <string name=\"albums\">Albums</string>\n    <string name=\"playlists\">Playlists</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d sélectionné</item>\n        <item quantity=\"many\">%d sélectionnés</item>\n        <item quantity=\"other\">%d sélectionnés</item>\n    </plurals>\n    <string name=\"history\">Historique</string>\n    <string name=\"stats\">Statistiques</string>\n    <string name=\"mood_and_genres\">Humeurs et Genres</string>\n    <string name=\"account\">Compte</string>\n    <string name=\"quick_picks\">Sélection Rapide</string>\n    <string name=\"quick_picks_empty\">Écoutez quelques titres pour générer votre sélection rapide</string>\n    <string name=\"new_release_albums\">Nouveautés</string>\n    <string name=\"today\">Aujourd\\'hui</string>\n    <string name=\"yesterday\">Hier</string>\n    <string name=\"this_week\">Cette semaine</string>\n    <string name=\"last_week\">La semaine dernière</string>\n    <string name=\"most_played_songs\">Titres les plus joués</string>\n    <string name=\"most_played_artists\">Artistes les plus joués</string>\n    <string name=\"most_played_albums\">Albums les plus joués</string>\n    <string name=\"search\">Rechercher</string>\n    <string name=\"search_yt_music\">Rechercher sur YouTube Music…</string>\n    <string name=\"search_library\">Rechercher dans votre bibliothèque…</string>\n    <string name=\"filter_library\">Bibliothèque</string>\n    <string name=\"filter_liked\">Favoris</string>\n    <string name=\"filter_downloaded\">Téléchargés</string>\n    <string name=\"filter_all\">Tout</string>\n    <string name=\"filter_songs\">Titres</string>\n    <string name=\"filter_videos\">Vidéos</string>\n    <string name=\"filter_albums\">Albums</string>\n    <string name=\"filter_artists\">Artistes</string>\n    <string name=\"filter_playlists\">Playlists</string>\n    <string name=\"filter_community_playlists\">Playlists de la communauté</string>\n    <string name=\"filter_featured_playlists\">Playlists mises en avant</string>\n    <string name=\"filter_bookmarked\">Favoris</string>\n    <string name=\"no_results_found\">Aucun résultat trouvé</string>\n    <string name=\"from_your_library\">De votre bibliothèque</string>\n    <string name=\"liked_songs\">Titres favoris</string>\n    <string name=\"downloaded_songs\">Titres téléchargés</string>\n    <string name=\"playlist_is_empty\">La playlist est vide</string>\n    <string name=\"retry\">Réessayer</string>\n    <string name=\"radio\">Radio</string>\n    <string name=\"shuffle\">Lecture aléatoire</string>\n    <string name=\"reset\">Réinitialiser</string>\n    <string name=\"details\">Détails</string>\n    <string name=\"edit\">Modifier</string>\n    <string name=\"start_radio\">Démarrer la radio</string>\n    <string name=\"play\">Lecture</string>\n    <string name=\"play_next\">Suivant</string>\n    <string name=\"add_to_queue\">Ajouter à la file d\\'attente</string>\n    <string name=\"add_to_library\">Ajouter à la bibliothèque</string>\n    <string name=\"remove_from_library\">Supprimer de la bibliothèque</string>\n    <string name=\"action_download\">Télécharger</string>\n    <string name=\"downloading\">Téléchargement</string>\n    <string name=\"remove_download\">Supprimer le téléchargement</string>\n    <string name=\"import_playlist\">Importer une playlist</string>\n    <string name=\"add_to_playlist\">Ajouter à une playlist</string>\n    <string name=\"view_artist\">Voir l’artiste</string>\n    <string name=\"view_album\">Voir l’album</string>\n    <string name=\"refetch\">Récupérer</string>\n    <string name=\"share\">Partager</string>\n    <string name=\"delete\">Effacer</string>\n    <string name=\"remove_from_history\">Supprimer de l’historique</string>\n    <string name=\"search_online\">Rechercher en ligne</string>\n    <string name=\"action_sync\">Synchroniser</string>\n    <string name=\"advanced\">Avancé</string>\n    <string name=\"sort_by_create_date\">Date d’ajout</string>\n    <string name=\"sort_by_name\">Nom</string>\n    <string name=\"sort_by_artist\">Artiste</string>\n    <string name=\"sort_by_year\">Année</string>\n    <string name=\"sort_by_song_count\">Nombre de titres</string>\n    <string name=\"sort_by_length\">Durée</string>\n    <string name=\"sort_by_play_time\">Temps d’écoute</string>\n    <string name=\"sort_by_custom\">Personnalisé</string>\n    <string name=\"media_id\">Identifiant du média</string>\n    <string name=\"mime_type\">Type MIME</string>\n    <string name=\"codecs\">Codecs</string>\n    <string name=\"bitrate\">Débit</string>\n    <string name=\"sample_rate\">Taux d’échantillonnage</string>\n    <string name=\"loudness\">Intensité</string>\n    <string name=\"volume\">Volume</string>\n    <string name=\"file_size\">Taille du fichier</string>\n    <string name=\"unknown\">Inconnu</string>\n    <string name=\"copied\">Copié dans le presse-papiers</string>\n    <string name=\"edit_lyrics\">Modifier les paroles</string>\n    <string name=\"search_lyrics\">Rechercher les paroles</string>\n    <string name=\"edit_song\">Éditer le titre</string>\n    <string name=\"song_title\">Titre</string>\n    <string name=\"song_artists\">Artistes du titre</string>\n    <string name=\"error_song_title_empty\">Le titre de la chanson ne peut pas être vide.</string>\n    <string name=\"error_song_artist_empty\">L’artiste de la chanson ne peut pas être vide.</string>\n    <string name=\"save\">Enregistrer</string>\n    <string name=\"choose_playlist\">Choisir une playlist</string>\n    <string name=\"edit_playlist\">Modifier la playlist</string>\n    <string name=\"create_playlist\">Créer une playlist</string>\n    <string name=\"playlist_name\">Nom de la playlist</string>\n    <string name=\"error_playlist_name_empty\">Le nom de la playlist ne peut pas être vide.</string>\n    <string name=\"edit_artist\">Éditer l’artiste</string>\n    <string name=\"artist_name\">Nom de l’artiste</string>\n    <string name=\"error_artist_name_empty\">Le nom de l’artiste ne peut pas être vide.</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d titre</item>\n        <item quantity=\"many\">%d titres</item>\n        <item quantity=\"other\">%d titres</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d artiste</item>\n        <item quantity=\"many\">%d artistes</item>\n        <item quantity=\"other\">%d artistes</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d album</item>\n        <item quantity=\"many\">%d albums</item>\n        <item quantity=\"other\">%d albums</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d playlist</item>\n        <item quantity=\"many\">%d playlists</item>\n        <item quantity=\"other\">%d playlists</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d semaine</item>\n        <item quantity=\"many\">%d semaines</item>\n        <item quantity=\"other\">%d semaines</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d mois</item>\n        <item quantity=\"many\">%d mois</item>\n        <item quantity=\"other\">%d mois</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d an</item>\n        <item quantity=\"many\">%d ans</item>\n        <item quantity=\"other\">%d ans</item>\n    </plurals>\n    <string name=\"playlist_imported\">Playlist importée</string>\n    <string name=\"removed_song_from_playlist\">« %s » retiré de la playlist</string>\n    <string name=\"playlist_synced\">Playlist synchronisée</string>\n    <string name=\"undo\">Annuler</string>\n    <string name=\"lyrics_not_found\">Paroles introuvables</string>\n    <string name=\"sleep_timer\">Minuterie de Sommeil</string>\n    <string name=\"end_of_song\">Fin du titre</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">%d minute</item>\n        <item quantity=\"many\">%d minutes</item>\n        <item quantity=\"other\">%d minutes</item>\n    </plurals>\n    <string name=\"error_no_stream\">Aucun flux disponible</string>\n    <string name=\"error_no_internet\">Aucune connexion réseau</string>\n    <string name=\"error_timeout\">Temps libre</string>\n    <string name=\"error_unknown\">Erreur inconnue</string>\n    <string name=\"action_like\">J’aime</string>\n    <string name=\"action_remove_like\">Je n’aime plus</string>\n    <string name=\"action_shuffle_on\">Lecture aléatoire activée</string>\n    <string name=\"action_shuffle_off\">Lecture aléatoire désactivée</string>\n    <string name=\"repeat_mode_off\">Répétition désactivée</string>\n    <string name=\"repeat_mode_one\">Répéter le titre actuel</string>\n    <string name=\"repeat_mode_all\">Répéter la liste d’attente</string>\n    <string name=\"queue_all_songs\">Tous les titres</string>\n    <string name=\"queue_searched_songs\">Titres recherchés</string>\n    <string name=\"music_player\">Lecteur de musique</string>\n    <string name=\"settings\">Paramètres</string>\n    <string name=\"appearance\">Apparence</string>\n    <string name=\"enable_dynamic_theme\">Activer le thème dynamique</string>\n    <string name=\"dark_theme\">Thème sombre</string>\n    <string name=\"dark_theme_on\">Activé</string>\n    <string name=\"dark_theme_off\">Désactivé</string>\n    <string name=\"dark_theme_follow_system\">Suivre le système</string>\n    <string name=\"pure_black\">Noir profond</string>\n    <string name=\"default_open_tab\">Menu ouvert par défaut</string>\n    <string name=\"customize_navigation_tabs\">Personnaliser les menus de navigation</string>\n    <string name=\"lyrics_text_position\">Position du texte des paroles</string>\n    <string name=\"left\">Gauche</string>\n    <string name=\"center\">Centre</string>\n    <string name=\"right\">Droite</string>\n    <string name=\"content\">Contenu</string>\n    <string name=\"login\">Connexion</string>\n    <string name=\"content_language\">Langue du contenu par défaut</string>\n    <string name=\"content_country\">Pays du contenu par défaut</string>\n    <string name=\"system_default\">Système par défaut</string>\n    <string name=\"enable_proxy\">Activer un proxy</string>\n    <string name=\"proxy_type\">Type de proxy</string>\n    <string name=\"proxy_url\">URL du proxy</string>\n    <string name=\"restart_to_take_effect\">Redémarrer pour prendre effet</string>\n    <string name=\"player_and_audio\">Lecteur et Audio</string>\n    <string name=\"audio_quality\">Qualité audio</string>\n    <string name=\"audio_quality_auto\">Automatique</string>\n    <string name=\"audio_quality_high\">Élevée</string>\n    <string name=\"audio_quality_low\">Faible</string>\n    <string name=\"persistent_queue\">File d’attente persistante</string>\n    <string name=\"skip_silence\">Ignorer le silence</string>\n    <string name=\"audio_normalization\">Normalisation audio</string>\n    <string name=\"equalizer\">Égaliseur</string>\n    <string name=\"storage\">Stockage</string>\n    <string name=\"cache\">Cache</string>\n    <string name=\"image_cache\">Cache d’images</string>\n    <string name=\"song_cache\">Cache des titres</string>\n    <string name=\"max_cache_size\">Taille maximale du cache</string>\n    <string name=\"unlimited\">Illimitée</string>\n    <string name=\"clear_all_downloads\">Effacer tous les téléchargements</string>\n    <string name=\"max_image_cache_size\">Taille maximale du cache d’images</string>\n    <string name=\"clear_image_cache\">Effacer le cache d\\'images</string>\n    <string name=\"max_song_cache_size\">Taille maximale du cache des titres</string>\n    <string name=\"clear_song_cache\">Effacer le cache des titres</string>\n    <string name=\"size_used\">%s utilisé</string>\n    <string name=\"privacy\">Confidentialité</string>\n    <string name=\"pause_listen_history\">Suspendre l’historique d’écoute</string>\n    <string name=\"clear_listen_history\">Effacer l’historique d’écoute</string>\n    <string name=\"clear_listen_history_confirm\">Voulez-vous vraiment effacer tout l’historique d’écoute ?</string>\n    <string name=\"pause_search_history\">Suspendre l\\'historique de recherche</string>\n    <string name=\"clear_search_history\">Effacer l\\'historique de recherche</string>\n    <string name=\"clear_search_history_confirm\">Voulez-vous vraiment effacer tout l’historique des recherches ?</string>\n    <string name=\"enable_kugou\">Activer le fournisseur de paroles KuGou</string>\n    <string name=\"backup_restore\">Sauvegarder et Restaurer</string>\n    <string name=\"action_backup\">Sauvegarder</string>\n    <string name=\"action_restore\">Restaurer</string>\n    <string name=\"imported_playlist\">Playlist importée</string>\n    <string name=\"backup_create_success\">Sauvegarde créée avec succès</string>\n    <string name=\"backup_create_failed\">Impossible de créer la sauvegarde</string>\n    <string name=\"restore_failed\">Échec de la restauration de la sauvegarde</string>\n    <string name=\"about\">À propos</string>\n    <string name=\"app_version\">Version de l\\'application</string>\n    <string name=\"new_version_available\">Nouvelle version disponible</string>\n    <string name=\"translation_models\">Modèles de traduction</string>\n    <string name=\"clear_translation_models\">Effacer les modèles de traduction</string>\n    <string name=\"not_logged_in\">Non connecté</string>\n    <string name=\"remove_from_playlist\">Retirer de la playlist</string>\n    <string name=\"duplicates\">Doublons</string>\n    <string name=\"skip_duplicates\">Passer les doublons</string>\n    <string name=\"add_anyway\">Ajouter quand même</string>\n    <string name=\"duplicates_description_single\">Le titre est déjà dans votre playlist</string>\n    <string name=\"duplicates_description_multiple\">%d titres sont déjà dans votre playlist</string>\n    <string name=\"player_text_alignment\">Alignement du texte du lecteur</string>\n    <string name=\"sided\">Sur le côté</string>\n    <string name=\"enable_lrclib\">Activer le fournisseur de paroles LrcLib</string>\n    <string name=\"hide_explicit\">Masquer le contenu explicite</string>\n    <string name=\"discord_integration\">Intégration Discord</string>\n    <string name=\"dismiss\">Abandonner</string>\n    <string name=\"options\">Options</string>\n    <string name=\"preview\">Prévisualisation</string>\n    <string name=\"action_logout\">Se déconnecter</string>\n    <string name=\"enable_discord_rpc\">Activer la présence riche</string>\n    <string name=\"login_failed\">Échec de la connexion</string>\n    <string name=\"delete_playlist_confirm\">Voulez-vous vraiment supprimer la playlist « %s » ?</string>\n    <string name=\"listen_history\">Historique d\\'écoute</string>\n    <string name=\"disable_screenshot\">Désactiver les captures d\\'écran</string>\n    <string name=\"search_history\">Historique de recherche</string>\n    <string name=\"disable_screenshot_desc\">Lorsque cette option est activée, les captures d\\'écran et l\\'affichage de l\\'application dans Récents sont désactivés.</string>\n    <string name=\"stop_music_on_task_clear\">Arrêter la musique en fermant la page</string>\n    <string name=\"forgotten_favorites\">Favoris oubliés</string>\n    <string name=\"keep_listening\">Continuez d\\'écouter</string>\n    <string name=\"similar_to\">Similaire à</string>\n    <string name=\"library_song_empty\">Les titres de la bibliothèque s\\'afficheront ici</string>\n    <string name=\"library_artist_empty\">Les artistes de la bibliothèque s\\'afficheront ici</string>\n    <string name=\"library_playlist_empty\">Vos playlists s\\'afficheront ici</string>\n    <string name=\"other_versions\">Autres versions</string>\n    <string name=\"remove_download_playlist_confirm\">Voulez-vous vraiment supprimer tous les titres de la playlist « %s » du stockage des titres téléchargés ?</string>\n    <string name=\"add_all_to_library\">Tout ajouter à la bibliothèque</string>\n    <string name=\"tempo_and_pitch\">Tempo et Pitch</string>\n    <string name=\"action_like_all\">Tout marquer comme aimé</string>\n    <string name=\"action_remove_like_all\">Retirer les j\\'aime</string>\n    <string name=\"theme\">Thème</string>\n    <string name=\"player\">Lecteur</string>\n    <string name=\"default_\">Par défaut</string>\n    <string name=\"squiggly\">Ondulé</string>\n    <string name=\"misc\">Divers</string>\n    <string name=\"grid_cell_size\">Taille des cellules de la grille</string>\n    <string name=\"auto_load_more_desc\">Ajouter automatiquement, si possible, des titres à la fin de la file d\\'attente quand elle est atteinte</string>\n    <string name=\"auto_skip_next_on_error_desc\">Permet d\\'assurer une lecture continue</string>\n    <string name=\"library_album_empty\">Les albums de la bibliothèque s\\'afficheront ici</string>\n    <string name=\"your_youtube_playlists\">Vos playlists YouTube</string>\n    <string name=\"remove_all_from_library\">Tout supprimer de la bibliothèque</string>\n    <string name=\"remove_from_queue\">Retirer de la file d\\'attente</string>\n    <string name=\"small\">Petit</string>\n    <string name=\"player_slider_style\">Style de curseur du lecteur</string>\n    <string name=\"big\">Grand</string>\n    <string name=\"persistent_queue_desc\">Restaurer la file d\\'attente lorsque l\\'application est relancée</string>\n    <string name=\"queue\">File d\\'attente</string>\n    <string name=\"auto_load_more\">Charger automatiquement plus de titres</string>\n    <string name=\"auto_skip_next_on_error\">Aller au titre suivant si une erreur apparaît</string>\n    <string name=\"discord_information\">Metrolist utilise la bibliothèque KizzyRPC pour définir le statut de votre compte Discord. Cela implique l\\'utilisation de la connexion Discord Gateway, ce qui peut être considéré comme une violation des conditions d\\'utilisation de Discord. Cependant, il n\\'existe aucun cas connu de comptes d\\'utilisateurs suspendus pour cette raison. Utilisez-le à vos propres risques. \\n \\nMetrolist extraira uniquement votre jeton, et tout le reste sera stocké localement.</string>\n    <string name=\"use_login_for_browse\">Utiliser le compte connecté pour parcourir le contenu</string>\n    <string name=\"use_login_for_browse_desc\">Ceci peut influencer le contenu que vous voyez et affiche par exemple les albums Premium si vous êtes connecté avec un compte Premium</string>\n    <string name=\"action_login\">Se connecter</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-hi/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">स्थानीय</string>\n    <string name=\"back_button_desc\">पीछे</string>\n    <string name=\"album_cover_desc\">एल्बम आवरण</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-hi/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"songs\">गाने</string>\n    <string name=\"home\">मुख्य स्थान</string>\n    <string name=\"artists\">कलाकार</string>\n    <string name=\"albums\">एलबम</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d चुना गया</item>\n        <item quantity=\"other\">%d चुने गए</item>\n    </plurals>\n    <string name=\"account\">खाता</string>\n    <string name=\"quick_picks\">झटपट चलाने हेतु चुनिंदा गाने</string>\n    <string name=\"quick_picks_empty\">झटपट सूची बनाने के लिए कुछ गाने सुनें</string>\n    <string name=\"forgotten_favorites\">भूले-बिछड़े पसंदीदा गाने</string>\n    <string name=\"history\">इतिहास</string>\n    <string name=\"stats\">आंकड़े</string>\n    <string name=\"mood_and_genres\">मूड और शैलियां</string>\n    <string name=\"playlists\">चाल सूचियां</string>\n    <string name=\"keep_listening\">सुनते रहें</string>\n    <string name=\"your_youtube_playlists\">आपकी यूट्यूब चाल सूचियाँ</string>\n    <string name=\"similar_to\">इसके जैसे</string>\n    <string name=\"today\">आज</string>\n    <string name=\"yesterday\">कल</string>\n    <string name=\"most_played_songs\">अधिकतम सुने गए गाने</string>\n    <string name=\"most_played_artists\">अधिकतम सुने गए कलाकार</string>\n    <string name=\"search\">खोजें</string>\n    <string name=\"search_library\">संग्रह में खोजें…</string>\n    <string name=\"filter_library\">संग्रह</string>\n    <string name=\"filter_liked\">पसंद किए गए</string>\n    <string name=\"filter_downloaded\">डाउनलोड किए गए</string>\n    <string name=\"filter_all\">सब</string>\n    <string name=\"filter_songs\">गाने</string>\n    <string name=\"filter_videos\">वीडिओ</string>\n    <string name=\"filter_albums\">एलबम</string>\n    <string name=\"filter_artists\">कलाकार</string>\n    <string name=\"filter_community_playlists\">सामाज द्वारा चाल सूचियां</string>\n    <string name=\"filter_featured_playlists\">प्रदर्शित चाल सूचियां</string>\n    <string name=\"no_results_found\">कोई परिणाम नहीं</string>\n    <string name=\"library_artist_empty\">संग्रह के कलाकार यहाँ दिखेंगे</string>\n    <string name=\"from_your_library\">आपके संग्रह से</string>\n    <string name=\"other_versions\">अन्य संस्करण</string>\n    <string name=\"liked_songs\">पसंद किए गए गाने</string>\n    <string name=\"downloaded_songs\">डाउनलोड किए गए गाने</string>\n    <string name=\"playlist_is_empty\">चाल सूची खाली है</string>\n    <string name=\"delete_playlist_confirm\">क्या आप वाकई “%s” चाल सूची को हटाना चाहते हैं ?</string>\n    <string name=\"retry\">पुनः करें</string>\n    <string name=\"reset\">रीसेट करें</string>\n    <string name=\"details\">विवरण</string>\n    <string name=\"edit\">बदलाव</string>\n    <string name=\"play\">चलाएं</string>\n    <string name=\"play_next\">अगला इसे बजायें</string>\n    <string name=\"add_to_queue\">कतार में लगाएं</string>\n    <string name=\"add_to_library\">संग्रह में जोड़ें</string>\n    <string name=\"import_playlist\">चाल सूची आयात करें</string>\n    <string name=\"add_to_playlist\">चाल सूची मे जोड़ें</string>\n    <string name=\"refetch\">पुनः निकालें</string>\n    <string name=\"delete\">हटायें</string>\n    <string name=\"search_online\">ऑनलाइन खोजें</string>\n    <string name=\"action_sync\">सिंक</string>\n    <string name=\"this_week\">इस हफ्ते</string>\n    <string name=\"new_release_albums\">नईं रिलीज हुईं एलबम</string>\n    <string name=\"last_week\">पिछले हफ्ते</string>\n    <string name=\"most_played_albums\">अधिकतम सुनीं गईं एलबम</string>\n    <string name=\"search_yt_music\">यूट्यूब म्यूजिक में खोजें…</string>\n    <string name=\"filter_playlists\">चाल सूचियां</string>\n    <string name=\"filter_bookmarked\">चिह्नित</string>\n    <string name=\"library_song_empty\">संग्रह के गाने यहाँ दिखेंगे</string>\n    <string name=\"library_playlist_empty\">आपकी चाल सूचियां यहाँ दिखेंगीं</string>\n    <string name=\"library_album_empty\">संग्रह की एलबमें यहाँ दिखेंगीं</string>\n    <string name=\"remove_download_playlist_confirm\">क्या आप वाकई “%s” चाल सूची के सभी गानों को डाउनलोड किए गए गानों के भंडार से हटाना चाहते हैं ?</string>\n    <string name=\"radio\">आकाशवाणी</string>\n    <string name=\"shuffle\">फेंटें</string>\n    <string name=\"start_radio\">आकाशवाणी आरंभ करें</string>\n    <string name=\"add_all_to_library\">सभी को संग्रह में जोड़ें</string>\n    <string name=\"remove_all_from_library\">सभी को संग्रह से हटायें</string>\n    <string name=\"remove_from_library\">संग्रह से हटाएं</string>\n    <string name=\"action_download\">डाउनलोड करें</string>\n    <string name=\"downloading\">डाउनलोड हो रहा</string>\n    <string name=\"remove_download\">डाउनलोड हटाएँ</string>\n    <string name=\"view_album\">एलबम देखें</string>\n    <string name=\"view_artist\">कलाकार देखें</string>\n    <string name=\"share\">भेजें</string>\n    <string name=\"remove_from_history\">इतिहास से हटायें</string>\n    <string name=\"remove_from_playlist\">चाल सूची से हटायें</string>\n    <string name=\"remove_from_queue\">कतार से निकालें</string>\n    <string name=\"advanced\">उन्नत</string>\n    <string name=\"tempo_and_pitch\">गति व स्वरमान</string>\n    <string name=\"sort_by_create_date\">जुडने की तारीख</string>\n    <string name=\"sort_by_artist\">कलाकार</string>\n    <string name=\"sort_by_length\">लंबाई</string>\n    <string name=\"sort_by_play_time\">बजाए जाने की अवधि</string>\n    <string name=\"sort_by_custom\">आपका क्रम</string>\n    <string name=\"mime_type\">एम आइ एम ई का प्रकार</string>\n    <string name=\"codecs\">कूटलेखक</string>\n    <string name=\"bitrate\">बिटदर</string>\n    <string name=\"sample_rate\">नमूनाकरण दर</string>\n    <string name=\"loudness\">प्रबलता</string>\n    <string name=\"volume\">प्रबलता</string>\n    <string name=\"file_size\">फाइल का माप</string>\n    <string name=\"unknown\">अज्ञात</string>\n    <string name=\"copied\">क्लिपबोर्ड मे लिया गया</string>\n    <string name=\"edit_lyrics\">गीतिकाव्य बदलें</string>\n    <string name=\"edit_song\">गाने मे बदलाव करें</string>\n    <string name=\"song_title\">गाने का नाम</string>\n    <string name=\"error_song_artist_empty\">गाने के कलाकार के नाम को खाली नहीं छोड़ा जा सकता ।</string>\n    <string name=\"choose_playlist\">चाल सूची चुनें</string>\n    <string name=\"add_anyway\">तब भी जोड़ें</string>\n    <string name=\"duplicates_description_multiple\">%d गाने पहले से आपकी चाल सूची में हैं</string>\n    <string name=\"playlist_imported\">चाल सूची आयात की गई</string>\n    <string name=\"removed_song_from_playlist\">\\\"%s\\\" चाल सूची से हटाया गया</string>\n    <string name=\"playlist_synced\">चाल सूची समकालिक की गई</string>\n    <string name=\"undo\">पूर्ववत् करें</string>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d एलबम</item>\n        <item quantity=\"other\">%d एलबमें</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d चाल सूची</item>\n        <item quantity=\"other\">%d चाल सूचियां</item>\n    </plurals>\n    <string name=\"sort_by_name\">नाम</string>\n    <string name=\"sort_by_year\">वर्ष</string>\n    <string name=\"sort_by_song_count\">गीत संख्या</string>\n    <string name=\"media_id\">मीडिया आइडी</string>\n    <string name=\"search_lyrics\">गीतिकाव्य ढूंढें</string>\n    <string name=\"song_artists\">गाने के कलाकार</string>\n    <string name=\"error_song_title_empty\">गाने के नाम को खाली नहीं छोड़ा जा सकता ।</string>\n    <string name=\"create_playlist\">चाल सूची बनाएं</string>\n    <string name=\"save\">सहेजें</string>\n    <string name=\"edit_playlist\">चाल सूची मे बदलाव करें</string>\n    <string name=\"error_playlist_name_empty\">चाल सूची के नाम को खाली नहीं छोड़ा जा सकता ।</string>\n    <string name=\"edit_artist\">कलाकार बदलें</string>\n    <string name=\"artist_name\">कलाकार का नाम</string>\n    <string name=\"playlist_name\">चाल सूची का नाम</string>\n    <string name=\"duplicates_description_single\">यह गाना पहले से आपकी चाल सूची में है</string>\n    <string name=\"error_artist_name_empty\">कलाकार के नाम को खाली नहीं छोड़ा जा सकता ।</string>\n    <string name=\"duplicates\">प्रतिरूप</string>\n    <string name=\"skip_duplicates\">प्रतिरूप छोड़ें</string>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d हफ्ता</item>\n        <item quantity=\"other\">%d हफ्ते</item>\n    </plurals>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d गाना</item>\n        <item quantity=\"other\">%d गाने</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d महीना</item>\n        <item quantity=\"other\">%d महीने</item>\n    </plurals>\n    <string name=\"lyrics_not_found\">गीतिकाव्य नहीं मिला</string>\n    <string name=\"queue_searched_songs\">खोजे गए गाने</string>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d साल</item>\n        <item quantity=\"other\">%d साल</item>\n    </plurals>\n    <string name=\"queue_all_songs\">सभी गाने</string>\n    <string name=\"settings\">सेटिंग्स</string>\n    <string name=\"theme\">थीम</string>\n    <string name=\"dark_theme_off\">बंद</string>\n    <string name=\"error_no_stream\">कोई स्ट्रीम उपलब्ध नहीं है</string>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d कलाकार</item>\n        <item quantity=\"other\">%d कलाकार</item>\n    </plurals>\n    <string name=\"error_no_internet\">कोई नेटवर्क कनेक्शन नहीं</string>\n    <string name=\"dark_theme_on\">चालू</string>\n    <string name=\"sleep_timer\">स्लीप टाइमर</string>\n    <string name=\"end_of_song\">गाने का अंत</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">%d मिनट</item>\n        <item quantity=\"other\">%d मिनट</item>\n    </plurals>\n    <string name=\"error_timeout\">समय सीमा</string>\n    <string name=\"error_unknown\">अज्ञात त्रुटि</string>\n    <string name=\"action_like\">पसंद</string>\n    <string name=\"action_like_all\">सभी को पसंद करे</string>\n    <string name=\"action_remove_like\">पसंदीदा से हटाएं</string>\n    <string name=\"action_remove_like_all\">पसंदीदा से सभी को हटाएं</string>\n    <string name=\"action_shuffle_on\">सफल चालू</string>\n    <string name=\"action_shuffle_off\">सफल बंद</string>\n    <string name=\"repeat_mode_off\">दोहराएं मोड बंद</string>\n    <string name=\"repeat_mode_all\">कतार को दोहराएं</string>\n    <string name=\"player_text_alignment\">प्लेयर टेक्स्ट की स्थिति</string>\n    <string name=\"music_player\">संगीत प्लेयर</string>\n    <string name=\"appearance\">प्रदर्शन</string>\n    <string name=\"dark_theme\">डार्क थीम</string>\n    <string name=\"dark_theme_follow_system\">सिस्टम के अनुसार</string>\n    <string name=\"pure_black\">शुद्ध काला</string>\n    <string name=\"customize_navigation_tabs\">नेविगेशन टैब्स सेट करें</string>\n    <string name=\"repeat_mode_one\">वर्तमान गीत दोहराएं</string>\n    <string name=\"enable_dynamic_theme\">डायनामिक थीम सक्षम करें</string>\n    <string name=\"player\">संगीत प्लेयर</string>\n    <string name=\"lyrics_text_position\">गीतिकाव्य पाठ की स्थिति</string>\n    <string name=\"sided\">दो पक्षीय</string>\n    <string name=\"left\">बाए</string>\n    <string name=\"center\">मध्य</string>\n    <string name=\"right\">दाए</string>\n    <string name=\"player_slider_style\">प्लेयर स्लाइडर का प्रकार</string>\n    <string name=\"small\">छोटा</string>\n    <string name=\"big\">बड़ा</string>\n    <string name=\"clear_all_downloads\">सभी डौन्लोडस निकाल दे</string>\n    <string name=\"misc\">विविध</string>\n    <string name=\"default_open_tab\">डिफ़ॉल्ट खुला टैब</string>\n    <string name=\"default_\">डिफॉल्ट</string>\n    <string name=\"action_logout\">लॉग आउट</string>\n    <string name=\"action_login\">लॉग इन</string>\n    <string name=\"login\">लॉगिन</string>\n    <string name=\"login_failed\">लॉगिन असफल रहा</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-hr/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"charts\">Liste</string>\n    <string name=\"back_button_desc\">Natrag</string>\n    <string name=\"album_cover_desc\">Naslovnica albuma</string>\n    <string name=\"top_music_videos\">Najgledaniji glazbeni spotovi</string>\n    <string name=\"trending\">U trendu</string>\n    <string name=\"weeks\">Tjedni</string>\n    <string name=\"months\">Mjeseci</string>\n    <string name=\"years\">Godine</string>\n    <string name=\"continuous\">Stalno</string>\n    <string name=\"liked\">Lajkano</string>\n    <string name=\"offline\">Preuzeto</string>\n    <string name=\"my_top\">Moja najbolja</string>\n    <string name=\"cached_playlist\">Predmemorirano</string>\n    <string name=\"sync_playlist\">Sinkroniziraj popis za reprodukciju</string>\n    <string name=\"sync_disabled\">Sync onemogućen</string>\n    <string name=\"allows_for_sync_witch_youtube\">Napomena: Ovo omogućuje sinkronizaciju s YouTube Music. Ovo NIJE moguće kasnije promijeniti.</string>\n    <string name=\"generating_image\">Generiram sliku</string>\n    <string name=\"please_wait\">Molim pričekajte</string>\n    <string name=\"cancel\">Odustani</string>\n    <string name=\"share_lyrics\">Podijeli tekst pjesme</string>\n    <string name=\"share_as_text\">Podijeli kao tekst</string>\n    <string name=\"share_as_image\">Podijeli kao sliku</string>\n    <string name=\"local_history\">Lokalno</string>\n    <string name=\"remote_history\">Udaljeno</string>\n    <string name=\"max_selection_limit\">Maksimalno ograničenje odabira</string>\n    <string name=\"share_selected\">Podijeli odabrano</string>\n    <string name=\"customize_colors\">Prilagodi boje</string>\n    <string name=\"text_color\">Tekstualna boja</string>\n    <string name=\"secondary_text_color\">Sekundarna boja teksta</string>\n    <string name=\"background_color\">Boja pozadine</string>\n    <string name=\"remove_from_cache\">Uklonite iz predmemorije</string>\n    <string name=\"copy_link\">Kopiraj vezu</string>\n    <string name=\"select\">Odaberite sve</string>\n    <string name=\"like_all\">Slično svemu</string>\n    <string name=\"dislike_all\">Ne volim sve</string>\n    <string name=\"sort_by_last_updated\">Datum ažuriranja</string>\n    <string name=\"link_copied\">Veza kopirana u međuspremnik</string>\n    <string name=\"lyrics\">Tekst</string>\n    <string name=\"already_in_playlist\">Već je u predlošku:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d put</item>\n        <item quantity=\"few\">%d puta</item>\n        <item quantity=\"other\">%d puta</item>\n    </plurals>\n    <string name=\"similar_content\">Sličan sadržaj</string>\n    <string name=\"player_background_style\">Stil pozadine reproduktora</string>\n    <string name=\"follow_theme\">Pratite temu</string>\n    <string name=\"gradient\">Gradijent</string>\n    <string name=\"new_player_design\">Ogavan izgled reproduktora</string>\n    <string name=\"new_mini_player_design\">Novi izgled minijaturnog reproduktora</string>\n    <string name=\"player_background_blur\">Zamućenje</string>\n    <string name=\"player_buttons_style\">Boje dugmeta reproduktora</string>\n    <string name=\"default_style\">Zadana vrijednost</string>\n    <string name=\"enable_swipe_thumbnail\">Omogući prebacivanje prstom za promjenu pjesme</string>\n    <string name=\"swipe_song_to_add\">Proslijedi pjesmu desnom prstom da je dodaješ u red čekanja ili lijevim prstom da je odmah riješiš</string>\n    <string name=\"lyrics_click_change\">Promijeni stihove po kliku</string>\n    <string name=\"lyrics_auto_scroll\">Automatsko scrollanje teksta pjesme</string>\n    <string name=\"lyrics_romanize_japanese\">Romanizacija japanskog teksta</string>\n    <string name=\"lyrics_romanize_korean\">Romanizacija korejskog teksta</string>\n    <string name=\"slim\">Tanak/a</string>\n    <string name=\"slim_navbar\">Tanka traka navigacije</string>\n    <string name=\"auto_playlists\">Automatski odabrane liste</string>\n    <string name=\"show_liked_playlist\">Prikaži \\\"Voleli\\\" listu</string>\n    <string name=\"show_downloaded_playlist\">Prikaži \\\"Preuzeta\\\" listu pjesama</string>\n    <string name=\"show_top_playlist\">Prikaži \\\"Top\\\" listu</string>\n    <string name=\"show_cached_playlist\">Prikaži \\\"Prije spremljeni\\\" popis</string>\n    <string name=\"advanced_login\">Prijavite se sa žetonom</string>\n    <string name=\"token_hidden\">Ako pritisnite, prikazat će se žeton</string>\n    <string name=\"token_shown\">Pritisni opet da kopiras ili uredi</string>\n    <string name=\"token_adv_login_description\">Ovo je napredan način PRISTUPA LOGINu. Umjesto web portala, možete direktno unijeti ili ažurirati svoj token prijave ovdje. Na primjer, to može ubrzati prijavu na više uređaja. Molimo primijetite da će se bilo koji nevažeći formati tokena koje aplikacija ne može analizirati odbijati</string>\n    <string name=\"yt_sync\">Sinkronizacija automatski sa nalogom</string>\n    <string name=\"more_content\">Više sadržaja</string>\n    <string name=\"general\">Općenito</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"default_lib_chips\">Promijeni zadanu knjižnicu čipa</string>\n    <string name=\"set_quick_picks\">Postavi brze kombinacije</string>\n    <string name=\"last_song_listened\">Na temelju posljednje pjesme čuvane</string>\n    <string name=\"app_language\">Jezik aplikacije</string>\n    <string name=\"enable_similar_content\">Omogućite sličan sadržaj</string>\n    <string name=\"similar_content_desc\">Automatski dodajte više sličnih pjesama kada se dostigne kraj redoslijeda</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"import_online\">Uvozite \\\"m3u\\\" popise</string>\n    <string name=\"import_csv\">Uvozite \\\"csv\\\" datoteke sa listama</string>\n    <string name=\"playlist_add_local_to_synced_note\">Dodavanje lokalnih pjesama na sinkronizirane/udaljene playliste nije podržano. Sve ostale kombinacije su važeće</string>\n    <string name=\"auto_download_on_like\">Automatsko preuzimanje na scroll (prijenos) ili na \\\"like</string>\n    <string name=\"auto_download_on_like_desc\">Automatski preuzmi pjesme kada vam se svide</string>\n    <string name=\"swipe_sensitivity\">Osetljivost poteza minijaturnog reproduktora</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"clear_song_cache_dialog\">Sigurno želite obrisati sve pjesme iz memorije?</string>\n    <string name=\"clear_image_cache_dialog\">Sigurno želite obrisati sve pohranjene slike u cache?</string>\n    <string name=\"clear_downloads_dialog\">Sigurno želite obrisati sve preuzete datoteke?</string>\n    <string name=\"disable\">Onemogući</string>\n    <string name=\"not_logged_in_youtube\">Nije se prijavljen na YouTube</string>\n    <string name=\"default_links\">Otavanje podržanih poveznica</string>\n    <string name=\"open_app_settings_error\">Nemogu otvoriti postavke aplikacije</string>\n    <string name=\"release_notes\">Obavijest o izdanjima [Croatian</string>\n    <string name=\"all_time\">Sve vrijeme</string>\n    <string name=\"past_24_hours\">Prošle 24 sate</string>\n    <string name=\"past_week\">Prošla sedmica</string>\n    <string name=\"past_month\">Prošli mjesec</string>\n    <string name=\"past_year\">Prošla godina</string>\n    <string name=\"top_length\">Moja Top lista dužina</string>\n    <string name=\"history_duration\">Trajanje povijesti</string>\n    <string name=\"information\">Informacija</string>\n    <string name=\"description\">\\\", if the context requires it</string>\n    <string name=\"views\">Pogledi</string>\n    <string name=\"likes\">Lajkovi</string>\n    <string name=\"dislikes\">Nenavoli [English to Croatian translation by translation software</string>\n    <string name=\"subscribe\">Prihvaćanje</string>\n    <string name=\"subscribed\">Pretplaćeni</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">%d sekunda</item>\n        <item quantity=\"few\">%d sekunde</item>\n        <item quantity=\"other\">%d sekundi</item>\n    </plurals>\n    <string name=\"starting_radio\">Pokretanje radija</string>\n    <string name=\"now_playing\">Trenutna reprodukcija</string>\n    <string name=\"close\">Zatvori</string>\n    <string name=\"hide_player_thumbnail\">Sakrij sličicu reproduktora</string>\n    <string name=\"hide_player_thumbnail_desc\">Zamijeni naslovnicu albuma s logotipom aplikacije u reproduktoru</string>\n    <string name=\"seek_forward_dynamic\">+%1$d sek. unaprijed</string>\n    <string name=\"seek_backward_dynamic\">-%1$d sek. unazad</string>\n    <string name=\"seek_seconds_addup\">Progresivno traženje</string>\n    <string name=\"seek_seconds_addup_description\">Ako je omogućeno, dodaje i do 5 dodatnih sek. pri svakom preskakanju traženja</string>\n    <string name=\"disable_load_more_when_repeat_all\">Onemogući učitavanje više prilikom ponavljanja svega</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Ne učitava automatski više pjesama i sličnog sadržaja kad je omogućen način ponavljanja svega</string>\n    <string name=\"settings_section_ui\">Korisničko sučelje</string>\n    <string name=\"settings_section_privacy\">Privatnost i sigurnost</string>\n    <string name=\"settings_section_player_content\">Reproduktor i sadržaj</string>\n    <string name=\"settings_section_storage\">Pohrana i podaci</string>\n    <string name=\"settings_section_system\">Sustav i o aplikaciji</string>\n    <string name=\"updater\">Ažuriranja</string>\n    <string name=\"check_for_updates\">Automatska provjera ažuriranja</string>\n    <string name=\"integrations\">Ugradnja</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-hr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"playlist_synced\">Playlista je sinkronizirana</string>\n    <string name=\"undo\">Poništi</string>\n    <string name=\"lyrics_not_found\">Tekst pjesama nije pronađen</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">%d minuta</item>\n        <item quantity=\"few\">%d minute</item>\n        <item quantity=\"other\">%d minuta</item>\n    </plurals>\n    <string name=\"action_like\">Sviđa mi se</string>\n    <string name=\"action_like_all\">Sviđa mi se sve</string>\n    <string name=\"action_remove_like\">Ukloni sviđanje</string>\n    <string name=\"action_remove_like_all\">Ukloni sva sviđanja</string>\n    <string name=\"center\">Centar</string>\n    <string name=\"sided\">Na strani</string>\n    <string name=\"left\">Lijevo</string>\n    <string name=\"right\">Desno</string>\n    <string name=\"grid_cell_size\">Veličina ćelija mreže</string>\n    <string name=\"small\">Malo</string>\n    <string name=\"content\">Sadržaj</string>\n    <string name=\"content_language\">Zadani jezik sadržaja</string>\n    <string name=\"big\">Veliko</string>\n    <string name=\"content_country\">Zadana zemlja sadržaja</string>\n    <string name=\"audio_quality_high\">Visoka</string>\n    <string name=\"persistent_queue_desc\">Obnovite svoj zadnji red kada se aplikacija ponovo pokrene</string>\n    <string name=\"skip_silence\">Preskoči tišinu</string>\n    <string name=\"audio_normalization\">Normalizacija zvuka</string>\n    <string name=\"equalizer\">Ekvilajzer</string>\n    <string name=\"storage\">Spremište</string>\n    <string name=\"cache\">Predmemorija</string>\n    <string name=\"image_cache\">Predmemorija slika</string>\n    <string name=\"pause_listen_history\">Pauziraj povijest slušanja</string>\n    <string name=\"clear_all_downloads\">Izbriši sva preuzimanja</string>\n    <string name=\"size_used\">%s korišćeno</string>\n    <string name=\"max_image_cache_size\">Maksimalna veličina predmemorije slika</string>\n    <string name=\"clear_song_cache\">Izbriši predmemoriju pjesama</string>\n    <string name=\"listen_history\">Povijest slušanja</string>\n    <string name=\"clear_listen_history\">Izbriši povijest slušanja</string>\n    <string name=\"clear_search_history_confirm\">Jeste li sigurni da želite izbrisati svu povijest pretrage?</string>\n    <string name=\"disable_screenshot_desc\">Kada je ova opcija uključena, snimanje slika ekrana i pregled aplikacije u „Nedavni“ su isključeni.</string>\n    <string name=\"action_backup\">Sigurnosna kopija</string>\n    <string name=\"action_restore\">Obnovi</string>\n    <string name=\"imported_playlist\">Uvezena playlista</string>\n    <string name=\"albums\">Albumi</string>\n    <string name=\"playlists\">Playliste</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d odabran</item>\n        <item quantity=\"few\">%d odabrana</item>\n        <item quantity=\"other\">%d odabrano</item>\n    </plurals>\n    <string name=\"history\">Povijest</string>\n    <string name=\"mood_and_genres\">Ugođaj i žanrovi</string>\n    <string name=\"account\">Račun</string>\n    <string name=\"quick_picks\">Brzi odabiri</string>\n    <string name=\"quick_picks_empty\">Slušajte pjesme za generiranje brzih odabira</string>\n    <string name=\"your_youtube_playlists\">Vaše YouTube playliste</string>\n    <string name=\"similar_to\">Slično kao</string>\n    <string name=\"new_release_albums\">Nova izdanja albuma</string>\n    <string name=\"search_yt_music\">Pretraži YouTube Music …</string>\n    <string name=\"filter_library\">Zbirka</string>\n    <string name=\"filter_liked\">Sviđa mi se</string>\n    <string name=\"filter_downloaded\">Preuzeto</string>\n    <string name=\"filter_videos\">Videa</string>\n    <string name=\"no_results_found\">Nema rezultata</string>\n    <string name=\"library_song_empty\">Pjesme zbirke će se ovdje prikazati</string>\n    <string name=\"library_artist_empty\">Izvođači zbirke će se ovdje pojaviti</string>\n    <string name=\"library_album_empty\">Albumi zbirke će se ovdje pojaviti</string>\n    <string name=\"library_playlist_empty\">Vaše playliste će se ovdje pojaviti</string>\n    <string name=\"from_your_library\">Iz vaše zbirke</string>\n    <string name=\"other_versions\">Druge verzije</string>\n    <string name=\"liked_songs\">Pjesme koje mi se sviđaju</string>\n    <string name=\"delete_playlist_confirm\">Zaista želite izbrisati playlistu „%s“?</string>\n    <string name=\"retry\">Pokušaj ponovo</string>\n    <string name=\"reset\">Resetiraj</string>\n    <string name=\"details\">Detalji</string>\n    <string name=\"edit\">Uredi</string>\n    <string name=\"add_to_queue\">Dodaj u popis</string>\n    <string name=\"add_all_to_library\">Dodaj sve u zbirku</string>\n    <string name=\"remove_from_library\">Ukloni iz zbirke</string>\n    <string name=\"remove_all_from_library\">Ukloni sve iz zbirke</string>\n    <string name=\"action_download\">Preuzmi</string>\n    <string name=\"import_playlist\">Uvezi playlistu</string>\n    <string name=\"add_to_playlist\">Dodaj playlistu</string>\n    <string name=\"view_album\">Prikaži album</string>\n    <string name=\"share\">Dijeli</string>\n    <string name=\"remove_from_queue\">Ukloni iz reda</string>\n    <string name=\"search_online\">Pretraži na internetu</string>\n    <string name=\"action_sync\">Sinkroniziraj</string>\n    <string name=\"advanced\">Napredno</string>\n    <string name=\"tempo_and_pitch\">Tempo i visina tona</string>\n    <string name=\"sort_by_create_date\">Datum dodavanja</string>\n    <string name=\"sort_by_name\">Ime</string>\n    <string name=\"sort_by_artist\">Izvođač</string>\n    <string name=\"sort_by_year\">Godina</string>\n    <string name=\"sort_by_song_count\">Broj pjesama</string>\n    <string name=\"sort_by_length\">Dužina</string>\n    <string name=\"sort_by_play_time\">Vrijeme reprodukcije</string>\n    <string name=\"sort_by_custom\">Prilagođeni redoslijed</string>\n    <string name=\"media_id\">ID medija</string>\n    <string name=\"mime_type\">MIME vrsta</string>\n    <string name=\"bitrate\">Brzina prijenosa</string>\n    <string name=\"loudness\">Glasnoća</string>\n    <string name=\"volume\">Glasnoća</string>\n    <string name=\"file_size\">Veličina datoteke</string>\n    <string name=\"copied\">Kopirano u međuspremnik</string>\n    <string name=\"edit_lyrics\">Uredi tekst</string>\n    <string name=\"edit_song\">Uredi pjesmu</string>\n    <string name=\"error_song_artist_empty\">Izvođač pjesme ne može biti prazan.</string>\n    <string name=\"choose_playlist\">Odaberi playlistu</string>\n    <string name=\"edit_playlist\">Uredi playlistu</string>\n    <string name=\"create_playlist\">Stvori playlistu</string>\n    <string name=\"playlist_name\">Ime playliste</string>\n    <string name=\"error_playlist_name_empty\">Ime playliste ne može biti prazno.</string>\n    <string name=\"edit_artist\">Uredi izvođača</string>\n    <string name=\"duplicates_description_multiple\">%d pjesme su već u vašoj playlisti</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d pjesma</item>\n        <item quantity=\"few\">%d pjesme</item>\n        <item quantity=\"other\">%d pjesama</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d izvođač</item>\n        <item quantity=\"few\">%d izvođača</item>\n        <item quantity=\"other\">%d izvođača</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d playlista</item>\n        <item quantity=\"few\">%d playliste</item>\n        <item quantity=\"other\">%d playlista</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d tjedan</item>\n        <item quantity=\"few\">%d tjedna</item>\n        <item quantity=\"other\">%d tjedana</item>\n    </plurals>\n    <string name=\"sleep_timer\">Spavanje</string>\n    <string name=\"end_of_song\">Kraj pjesme</string>\n    <string name=\"error_no_stream\">Nijedan internetski prijenos nije dostupan</string>\n    <string name=\"error_no_internet\">Nema internet veze</string>\n    <string name=\"error_timeout\">Istek vremena</string>\n    <string name=\"error_unknown\">Nepoznata greška</string>\n    <string name=\"action_shuffle_on\">Miješanje uključeno</string>\n    <string name=\"action_shuffle_off\">Miješanje isključeno</string>\n    <string name=\"repeat_mode_off\">Modus ponavljanja je isključen</string>\n    <string name=\"repeat_mode_one\">Ponavljaj trenutačnu pjesmu</string>\n    <string name=\"repeat_mode_all\">Ponovi red</string>\n    <string name=\"queue_all_songs\">Sve pjesme</string>\n    <string name=\"queue_searched_songs\">Pretražene pjesme</string>\n    <string name=\"music_player\">Plejer glazbe</string>\n    <string name=\"settings\">Postavke</string>\n    <string name=\"appearance\">Izgled</string>\n    <string name=\"theme\">Tema</string>\n    <string name=\"enable_dynamic_theme\">Uključi dinamičnu temu</string>\n    <string name=\"dark_theme\">Tamna tema</string>\n    <string name=\"dark_theme_on\">Uključeno</string>\n    <string name=\"dark_theme_off\">Isključeno</string>\n    <string name=\"dark_theme_follow_system\">Slijedi sustav</string>\n    <string name=\"pure_black\">Potpuno crna</string>\n    <string name=\"customize_navigation_tabs\">Prilagodi navigacijske kartice</string>\n    <string name=\"player\">Plejer</string>\n    <string name=\"player_text_alignment\">Poravnanje teksta u plejeru</string>\n    <string name=\"lyrics_text_position\">Položaj teksta pjesama</string>\n    <string name=\"player_slider_style\">Stil klizača plejera</string>\n    <string name=\"default_\">Zadano</string>\n    <string name=\"squiggly\">Valovito</string>\n    <string name=\"misc\">Razno</string>\n    <string name=\"default_open_tab\">Zadana otvorena kartica</string>\n    <string name=\"login\">Prijava</string>\n    <string name=\"not_logged_in\">Niste prijavljeni</string>\n    <string name=\"system_default\">Zadano sustavom</string>\n    <string name=\"enable_proxy\">Uključi proxy</string>\n    <string name=\"proxy_type\">Vrsta proxya</string>\n    <string name=\"proxy_url\">URL proxyja</string>\n    <string name=\"restart_to_take_effect\">Ponovo pokreni kako bi promjene stupile na snagu</string>\n    <string name=\"player_and_audio\">Plejer i zvuk</string>\n    <string name=\"audio_quality\">Kvaliteta zvuka</string>\n    <string name=\"audio_quality_auto\">Automatski</string>\n    <string name=\"audio_quality_low\">Niska</string>\n    <string name=\"queue\">Red</string>\n    <string name=\"persistent_queue\">Trajni red</string>\n    <string name=\"auto_load_more\">Automatski učitaj više pjesama</string>\n    <string name=\"auto_load_more_desc\">Automatski dodaj više pjesama kada se dosegne kraj reda, ako je moguće</string>\n    <string name=\"auto_skip_next_on_error\">Automatski preskoči do sljedeće pjesme kada dođe do greške</string>\n    <string name=\"auto_skip_next_on_error_desc\">Osigurajte vaše kontinuirano iskustvo reprodukcije</string>\n    <string name=\"stop_music_on_task_clear\">Zaustavi glazbu nakon brisanja zadatka</string>\n    <string name=\"song_cache\">Predmemorija pjesama</string>\n    <string name=\"max_cache_size\">Maksimalna veličina predmemorije</string>\n    <string name=\"unlimited\">Neograničeno</string>\n    <string name=\"clear_image_cache\">Izbriši predmemoriju slika</string>\n    <string name=\"max_song_cache_size\">Maksimalna veličina predmemorije pjesama</string>\n    <string name=\"privacy\">Privatnost</string>\n    <string name=\"clear_listen_history_confirm\">Jeste li sigurni da želite izbrisati svu povijest slušanja?</string>\n    <string name=\"search_history\">Povijest pretrage</string>\n    <string name=\"pause_search_history\">Pauziraj povijest pretrage</string>\n    <string name=\"clear_search_history\">Izbriši povijest pretrage</string>\n    <string name=\"disable_screenshot\">Isključi snimanje slike ekrana</string>\n    <string name=\"enable_lrclib\">Uključi LrcLib dobavljača tekstova pjesama</string>\n    <string name=\"enable_kugou\">Uključi KuGou dobavljača tekstova pjesama</string>\n    <string name=\"hide_explicit\">Sakrij eksplicitan sadržaj</string>\n    <string name=\"backup_restore\">Sigurnosna kopija i obnova</string>\n    <string name=\"backup_create_success\">Sigurnosna kopija je uspješno izrađena</string>\n    <string name=\"backup_create_failed\">Nije bilo moguće stvoriti sigurnosnu kopiju</string>\n    <string name=\"restore_failed\">Neuspjelo obnavljanje sigurnosne kopije</string>\n    <string name=\"discord_integration\">Ugradnja Discord-u</string>\n    <string name=\"dismiss\">Odbaci</string>\n    <string name=\"action_logout\">Odjavi se</string>\n    <string name=\"enable_discord_rpc\">Uključi prikaz aktivnosti</string>\n    <string name=\"about\">Informacije</string>\n    <string name=\"app_version\">Verzija aplikacije</string>\n    <string name=\"clear_translation_models\">Izbriši modele prevođenja</string>\n    <string name=\"home\">Početna</string>\n    <string name=\"artists\">Izvođači</string>\n    <string name=\"songs\">Pjesme</string>\n    <string name=\"stats\">Statistike</string>\n    <string name=\"forgotten_favorites\">Zaboravljeni favoriti</string>\n    <string name=\"this_week\">Ovaj tjedan</string>\n    <string name=\"filter_featured_playlists\">Istaknute playliste</string>\n    <string name=\"search\">Pretraži</string>\n    <string name=\"filter_albums\">Albumi</string>\n    <string name=\"keep_listening\">Nastavite slušati</string>\n    <string name=\"yesterday\">Jučer</string>\n    <string name=\"last_week\">Prošli tjedan</string>\n    <string name=\"most_played_songs\">Najslušanije pjesme</string>\n    <string name=\"most_played_artists\">Najslušaniji izvođači</string>\n    <string name=\"most_played_albums\">Najslušaniji albumi</string>\n    <string name=\"today\">Danas</string>\n    <string name=\"search_library\">Pretraži zbirku …</string>\n    <string name=\"filter_all\">Sve</string>\n    <string name=\"filter_songs\">Pjesme</string>\n    <string name=\"filter_artists\">Izvođači</string>\n    <string name=\"filter_playlists\">Playliste</string>\n    <string name=\"filter_community_playlists\">Playliste zajednice</string>\n    <string name=\"filter_bookmarked\">Zabilježeno</string>\n    <string name=\"downloaded_songs\">Preuzete pjesme</string>\n    <string name=\"playlist_is_empty\">Playlista je prazna</string>\n    <string name=\"shuffle\">Promiješaj</string>\n    <string name=\"remove_download_playlist_confirm\">Zaista želite ukloniti sve pjesme playliste „%s“ iz spremišta „Preuzete pjesme“?</string>\n    <string name=\"play\">Reproduciraj</string>\n    <string name=\"play_next\">Reproduciraj sljedeću</string>\n    <string name=\"radio\">Radio</string>\n    <string name=\"start_radio\">Pokreni radio</string>\n    <string name=\"add_to_library\">Dodaj u zbirku</string>\n    <string name=\"downloading\">Preuzima se</string>\n    <string name=\"remove_download\">Ukloni preuzimanje</string>\n    <string name=\"remove_from_playlist\">Ukloni iz playliste</string>\n    <string name=\"view_artist\">Prikaži izvođača</string>\n    <string name=\"refetch\">Ponovo dohvati</string>\n    <string name=\"delete\">Izbriši</string>\n    <string name=\"remove_from_history\">Ukloni iz povijesti</string>\n    <string name=\"codecs\">Kodeki</string>\n    <string name=\"sample_rate\">Frekvencija</string>\n    <string name=\"unknown\">Nepoznato</string>\n    <string name=\"error_song_title_empty\">Naslov pjesme ne može biti prazan.</string>\n    <string name=\"duplicates_description_single\">Pjesma se već nalazi u vašoj playlisti</string>\n    <string name=\"skip_duplicates\">Preskoči duplikate</string>\n    <string name=\"search_lyrics\">Pretraži tekstove pjesama</string>\n    <string name=\"song_title\">Naslov pjesme</string>\n    <string name=\"save\">Spremi</string>\n    <string name=\"song_artists\">Izvođač pjesme</string>\n    <string name=\"artist_name\">Ime izvođača</string>\n    <string name=\"error_artist_name_empty\">Ime izvođača ne može biti prazno.</string>\n    <string name=\"duplicates\">Duplikati</string>\n    <string name=\"add_anyway\">Dodaj svejedno</string>\n    <string name=\"removed_song_from_playlist\">Pjesma „%s“ je uklonjena iz playliste</string>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d album</item>\n        <item quantity=\"few\">%d albuma</item>\n        <item quantity=\"other\">%d albuma</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d mjesec</item>\n        <item quantity=\"few\">%d mjeseca</item>\n        <item quantity=\"other\">%d mjeseci</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d godina</item>\n        <item quantity=\"few\">%d godine</item>\n        <item quantity=\"other\">%d godina</item>\n    </plurals>\n    <string name=\"playlist_imported\">Playlista je uvezena</string>\n    <string name=\"discord_information\">Metrolist koristi KizzyRPC biblioteku za postavljanje vašeg Discord status. Ovo uključuje korištenje Discord Gateway spajanja, što bi se moglo smatrati prekršajem Discord-ovog TOS-a. Međutim nema poznatih slučajeva gdje su korisnički računi zbog toga bili suspendirani. Korisite na vlastiti rizik.\\n\\nMetrolist će izdvojiti samo vaš žeton, sve drugo se sprema lokalno.</string>\n    <string name=\"options\">Opcije</string>\n    <string name=\"preview\">Pregled</string>\n    <string name=\"login_failed\">Prijava nije uspjela</string>\n    <string name=\"new_version_available\">Dostupna je nova verzija</string>\n    <string name=\"translation_models\">Modeli prevođenja</string>\n    <string name=\"use_login_for_browse\">Koristi prijavu za pregledavanje sadržaja</string>\n    <string name=\"use_login_for_browse_desc\">Ovo može utjecati na sadržaj koji vidiš i, na primjer, prikazuje samo premium albume ako si prijavljen/a s Premium računom</string>\n    <string name=\"action_login\">Prijavi se</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-hu/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">Helyi</string>\n    <string name=\"remote_history\">Távoli</string>\n    <string name=\"charts\">Slágerlisták</string>\n    <string name=\"back_button_desc\">Vissza</string>\n    <string name=\"album_cover_desc\">Album borító</string>\n    <string name=\"top_music_videos\">Top zene klippek</string>\n    <string name=\"trending\">Felkapott</string>\n    <string name=\"weeks\">Hetek</string>\n    <string name=\"months\">Hónapok</string>\n    <string name=\"years\">Évek</string>\n    <string name=\"continuous\">Folyamatos</string>\n    <string name=\"liked\">Kedvelt</string>\n    <string name=\"offline\">Letöltött</string>\n    <string name=\"my_top\">Saját top</string>\n    <string name=\"cached_playlist\">Cache-lt</string>\n    <string name=\"sync_playlist\">Lejátszásilista szinkronizálása</string>\n    <string name=\"sync_disabled\">Szinkronizálás kikapcsolva</string>\n    <string name=\"allows_for_sync_witch_youtube\">Figyelem: Ez lehetővé teszi a YouTube Music-al való szinkronizálást. Ez később NEM megváltoztatható.</string>\n    <string name=\"generating_image\">Kép generálása</string>\n    <string name=\"please_wait\">Kérem várjon</string>\n    <string name=\"cancel\">Mégse</string>\n    <string name=\"share_lyrics\">Dalszöveg megosztása</string>\n    <string name=\"share_as_text\">Megosztás szövegként</string>\n    <string name=\"share_as_image\">Megosztás képként</string>\n    <string name=\"max_selection_limit\">Max kiválasztási határ</string>\n    <string name=\"share_selected\">Kiválasztottak megosztása</string>\n    <string name=\"customize_colors\">Színek testreszabása</string>\n    <string name=\"text_color\">Szöveg szín</string>\n    <string name=\"secondary_text_color\">Másodlagos szöveg szín</string>\n    <string name=\"background_color\">Háttér szín</string>\n    <string name=\"remove_from_cache\">Eltávolítva a cache-ből</string>\n    <string name=\"copy_link\">Hivatkozás másolása</string>\n    <string name=\"select\">Összes kijelölése</string>\n    <string name=\"like_all\">Összes kedvelése</string>\n    <string name=\"dislike_all\">Összes nem kedvelése</string>\n    <string name=\"sort_by_last_updated\">Dátum frissítve</string>\n    <string name=\"link_copied\">Hivatkozás kimásolva a vágólapra</string>\n    <string name=\"lyrics\">Dalszöveg</string>\n    <string name=\"already_in_playlist\">Már a lejátszásilistában:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d alkalom</item>\n        <item quantity=\"other\">%d alkalmak</item>\n    </plurals>\n    <string name=\"similar_content\">Hasonló tartalom</string>\n    <string name=\"player_background_style\">Lejátszó háttér stílus</string>\n    <string name=\"follow_theme\">Téma követése</string>\n    <string name=\"gradient\">Átmenet</string>\n    <string name=\"new_player_design\">Új lejátszó design</string>\n    <string name=\"player_background_blur\">Homályosított</string>\n    <string name=\"player_buttons_style\">Lejátszó gomb színek</string>\n    <string name=\"default_style\">Alapbeállítás</string>\n    <string name=\"enable_swipe_thumbnail\">Bekapcsolja a húzással való váltást</string>\n    <string name=\"swipe_song_to_add\">Húzza a számot balra a listához való hozzáadáshoz, vagy jobbra hogy következőnek azt játssza</string>\n    <string name=\"lyrics_click_change\">Dalszövegek váltása koppintásra</string>\n    <string name=\"lyrics_auto_scroll\">Dalszövegek automata görgetése</string>\n    <string name=\"lyrics_romanize_japanese\">Japán dalszövegek latin betűsítése</string>\n    <string name=\"lyrics_romanize_korean\">Koreai dalszövegek latin betűsítése</string>\n    <string name=\"slim\">Vékony</string>\n    <string name=\"slim_navbar\">Alsó navigációs sáv feliratok elrejtése</string>\n    <string name=\"auto_playlists\">Automata lejátszásilisták</string>\n    <string name=\"show_liked_playlist\">\\\"Kedvelt\\\" lejátszásilista megjelenítése</string>\n    <string name=\"show_downloaded_playlist\">\\\"Letöltött\\\" lejátszásilista megjelenítése</string>\n    <string name=\"show_top_playlist\">\\\"Top\\\" lejátszásilista megjelenítése</string>\n    <string name=\"show_cached_playlist\">\\\"Gyorsítótárazott\\\" lejátszásilista megjelenítése</string>\n    <string name=\"advanced_login\">Bejelentkezés tokennel</string>\n    <string name=\"token_hidden\">Koppintson a token megjelenítéséhez</string>\n    <string name=\"token_shown\">Koppintson még egyszer a másoláshoz vagy szerkesztéshez</string>\n    <string name=\"token_adv_login_description\">Ez egy ÖSSZETETTEBB belépési módszer. A webes portálhoz képest itt közvetlen adhatja vagy frissítheti a belépési tokenjét. Például: ha egyszerre több eszközön akar belépni, ezzel felgyorsíthatja. Vegye azonban figyelembe, hogy minden token visszautasításra kerül, amit az app nem tud visszaigazolni</string>\n    <string name=\"yt_sync\">Automata szinkronizálás a fiókkal</string>\n    <string name=\"more_content\">Több tartalom</string>\n    <string name=\"general\">Általános</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"default_lib_chips\">Alap könyvtár chip változtatása</string>\n    <string name=\"set_quick_picks\">Gyors választások megadása</string>\n    <string name=\"last_song_listened\">Vegye alapul a legutolsó listázott számot</string>\n    <string name=\"app_language\">App nyelve</string>\n    <string name=\"enable_similar_content\">Hasonló tartalom engedélyezése</string>\n    <string name=\"similar_content_desc\">Automatikusan hozzáad hasonló zenéket ha a lista a végéhez ért</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"import_online\">\\\"m3u\\\" lejátszásilista importálása</string>\n    <string name=\"import_csv\">\\\"csv\\\" lejátszásilista importálása</string>\n    <string name=\"playlist_add_local_to_synced_note\">Figyelem: A szinkronizált/távoli lejátszásilistához nem adhat hozzá helyi zenét. Minden más kombináció azonban működik</string>\n    <string name=\"auto_download_on_like\">A like-olt zenék automata letöltése</string>\n    <string name=\"auto_download_on_like_desc\">Ha a szívvel bejelöli kedvenc zenéjét, automatikusan letöltésre kerül</string>\n    <string name=\"swipe_sensitivity\">Mini lejátszó félrehúzás érzékenység</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"clear_song_cache_dialog\">Biztosan ki akarja üríteni az összes gyorsítótárazott zenéjét?</string>\n    <string name=\"clear_image_cache_dialog\">Biztosan ki akarja üríteni az összes gyorsítótárazott képét?</string>\n    <string name=\"clear_downloads_dialog\">Biztosan ki akarja üríteni az összes letöltést?</string>\n    <string name=\"disable\">Kikapcsol</string>\n    <string name=\"not_logged_in_youtube\">Nincs bejelentkezve a YouTube-ba</string>\n    <string name=\"default_links\">Támogatott hivatkozások megnyitása</string>\n    <string name=\"open_app_settings_error\">Nem nyitható meg az app beállítás</string>\n    <string name=\"release_notes\">Változásnapló</string>\n    <string name=\"all_time\">Minden idők</string>\n    <string name=\"past_24_hours\">Elmúlt 24 órában</string>\n    <string name=\"past_week\">Elmúlt héten</string>\n    <string name=\"past_month\">Elmúlt hónapban</string>\n    <string name=\"past_year\">Elmúlt évben</string>\n    <string name=\"top_length\">A toplistám hossza</string>\n    <string name=\"history_duration\">Előzmény hossz</string>\n    <string name=\"information\">Információ</string>\n    <string name=\"description\">Leírás</string>\n    <string name=\"views\">Megtekintések</string>\n    <string name=\"likes\">Tetszések</string>\n    <string name=\"dislikes\">Nemtetszések</string>\n    <string name=\"subscribe\">Feliratkozás</string>\n    <string name=\"subscribed\">Feliratkozva</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">1 másodperc</item>\n        <item quantity=\"other\">%d másodperc</item>\n    </plurals>\n    <string name=\"now_playing\">Most játszva</string>\n    <string name=\"close\">Bezárás</string>\n    <string name=\"hide_player_thumbnail\">A lejátszó gomb elrejtése</string>\n    <string name=\"hide_player_thumbnail_desc\">Az album dizájnjának kicserélése az applikáció logójára</string>\n    <string name=\"seek_forward_dynamic\">%1$dmásodperc előre</string>\n    <string name=\"seek_seconds_addup\">Progresszív keresés</string>\n    <string name=\"seek_seconds_addup_description\">Amennyiben elfogadja, minden egyes átugrás után 5 másodperc összeadódik</string>\n    <string name=\"new_mini_player_design\">Új mini lejátszó dizájn</string>\n    <string name=\"disable_load_more_when_repeat_all\">Lejátszó beállításai</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Ne töltsön be automatikusan több dalt és hasonló tartalmat, amikor a minden újrajátszása mód be van kapcsolva</string>\n    <string name=\"settings_section_privacy\">Titoktartás és biztonság</string>\n    <string name=\"settings_section_player_content\">Lejátszó és tartalom</string>\n    <string name=\"settings_section_storage\">Tároló és adat</string>\n    <string name=\"settings_section_system\">Rendszer és történet</string>\n    <string name=\"seek_backward_dynamic\">%1$d másodperc visszafelé</string>\n    <string name=\"settings_section_ui\">Felület</string>\n    <string name=\"uploaded_playlist\">Feltöltve</string>\n    <string name=\"filter_uploaded\">Feltöltve</string>\n    <string name=\"starting_radio\">Rádió indítása</string>\n    <string name=\"swipe_song_to_remove\">Húzd el a zeneszámot a lejátszási listából való eltávolításhoz</string>\n    <string name=\"show_uploaded_playlist\">\\\"Feltöltött\\\" lejátszási lista mutatása</string>\n    <string name=\"edit_playlist_cover\">Lejátszási lista borítóképének szerkesztése</string>\n    <string name=\"download_playlist_desc\">Összes szám letöltése offline lejátszáshoz</string>\n    <string name=\"remove_download_playlist_desc\">Az összes letöltött szám törlése ebből a listából</string>\n    <string name=\"download_in_progress_desc\">Letöltés folyamatban</string>\n    <string name=\"share_playlist_desc\">Lejátszási lista megosztása</string>\n    <string name=\"delete_playlist_desc\">Lejátszási lista végleges törlése</string>\n    <string name=\"sync_playlist_desc\">Lejátszási lista szinkronizálása a YouTube Music-kal</string>\n    <string name=\"primary_color_style\">Elsődleges szín</string>\n    <string name=\"tertiary_color_style\">Harmadlagos szín</string>\n    <string name=\"lyrics_glow_effect\">Ragyogó dalszöveg</string>\n    <string name=\"lyrics_glow_effect_desc\">Ragyogó és ugráló animáció a dalszöveg aktuális sorára</string>\n    <string name=\"enable_better_lyrics\">Jobb Dalszöveg bekapcsolása</string>\n    <string name=\"enable_better_lyrics_desc\">Használja a Jobb Dalszöveg szolgáltatót a szavanként szinkronizált dalszöveghez</string>\n    <string name=\"auto_scroll\">Újraszinkronizálás</string>\n    <string name=\"shuffle_playlist_first\">Lejátszás keverve</string>\n    <string name=\"shuffle_playlist_first_desc\">Keveréskor először az eredeti lista/album dalait játssza le, majd a hasonló tartalmakat</string>\n    <string name=\"show_wrapped_card\">Wrapped kártya mutatása</string>\n    <string name=\"edit_playlist_cover_note\">Megjegyzés: A lejátszási lista borítójának módosításához össze kell kapcsolnod a fiókodat egy telefonszámmal, és hitelesítened kell a YouTube Musicon.</string>\n    <string name=\"edit_playlist_cover_note_wait\">A kép kiválasztása után várj egy kicsit, amíg az új borító megjelenik a lejátszási listán.</string>\n    <string name=\"choose_from_library\">Választás a galériából</string>\n    <string name=\"remove_custom_image\">Egyedi kép eltávolítása</string>\n    <string name=\"config_proxy\">Proxy beállítása</string>\n    <string name=\"proxy_username\">Proxy felhasználónév</string>\n    <string name=\"proxy_password\">Proxy jelszó</string>\n    <string name=\"enable_authentication\">Hitelesítés bekapcsolása</string>\n    <string name=\"discord_use_details\">Részleteket használjon státusz helyett</string>\n    <string name=\"discord_use_details_description\">A dal címe jelenjen meg kiemelten az előadó neve helyett</string>\n    <string name=\"lyrics_romanization_cyrillic\">Cirill</string>\n    <string name=\"lyrics_romanize_title\">Latin betűs átírás</string>\n    <string name=\"lyrics_romanization\">Dalszöveg latin betűs átírása</string>\n    <string name=\"lyrics_romanize_chinese\">Kínai dalszövegek latin betűs átírása</string>\n    <string name=\"lyrics_romanize_russian\">Orosz dalszövegek latin betűs átírása</string>\n    <string name=\"lyrics_romanize_ukrainian\">Ukrán dalszövegek latin betűs átírása</string>\n    <string name=\"lyrics_romanize_belarusian\">Fehérorosz dalszövegek latin betűs átírása</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Kirgizisztáni dalszövegek latin betűs átírása</string>\n    <string name=\"lyrics_romanize_serbian\">Szerb dalszövegek latin betűs átírása</string>\n    <string name=\"lyrics_romanize_bulgarian\">Bolgár dalszövegek latin betűs átírása</string>\n    <string name=\"line_by_line_option_title\">KÍSÉRLETI: Soronkénti nyelvfelismerés</string>\n    <string name=\"line_by_line_option_desc\">A cirill nyelv soronként lesz felismerve az egész dalszöveg helyett.</string>\n    <string name=\"line_by_line_dialog_title\">Biztos vagy benne?</string>\n    <string name=\"line_by_line_dialog_desc\">Ez egy kísérleti funkció, változó megbízhatósággal.\\n\\nAlapértelmezésben a rendszer a teljes dal alapján határozza meg a nyelvet, ez az opció viszont soronkénti ellenőrzést tesz lehetővé. Ez hasznos a többnyelvű daloknál, DE a nyelvfelismerés nem mindig pontos (például, ha egy ukrán sor nem tartalmaz speciális ukrán karaktereket, a rendszer oroszként írhatja át).\\n\\nHa nem tapasztalsz problémát, javasolt kikapcsolva hagyni ezt az opciót.</string>\n    <string name=\"romanize_current_track\">Aktuális dal latin betűs átírása</string>\n    <string name=\"updater\">Frissítések</string>\n    <string name=\"check_for_updates\">Frissítések automatikus keresése</string>\n    <string name=\"update_notifications\">Frissítések értesítéseinek bekapcsolása</string>\n    <string name=\"update_available_title\">Frissítés elérhető</string>\n    <string name=\"update_channel_name\">Alkalmazás frissítések</string>\n    <string name=\"update_channel_desc\">Értesítés új verziókról</string>\n    <string name=\"audio_offload\">Audio-kiszervezés engedélyezése</string>\n    <string name=\"audio_offload_description\">Hardveres kiszervezés használata. Bár a kikapcsolása jobban merítheti az akkumulátort, segíthet, ha hibát észlelsz a lejátszásban vagy az effekteknél</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"google_cast_description\">Hangátvitel engedélyezése Chromecastra és egyéb Cast-eszközökre</string>\n    <string name=\"lyrics_romanize_macedonian\">Macedón dalszövegek latin betűs átírása</string>\n    <string name=\"integrations\">Integrációk</string>\n    <string name=\"username\">Felhasználónév</string>\n    <string name=\"password\">Jelszó</string>\n    <string name=\"lastfm_integration\">Last.fm integráció</string>\n    <string name=\"enable_scrobbling\">Scrobbling engedélyezése</string>\n    <string name=\"lastfm_now_playing\">\\\"Épp hallgatott\\\" állapot küldése</string>\n    <string name=\"last_fm_send_likes\">Kedvelések küldése</string>\n    <string name=\"last_fm_send_likes_description\">Dalok kedvencnek jelölése/törlése a Last.fm-en, ha a Metrolist-ben kedveled/nem kedveled őket</string>\n    <string name=\"logging_in\">Bejelentkezés…</string>\n    <string name=\"scrobbling_configuration\">Scrobbling beállítása</string>\n    <string name=\"scrobble_min_track_duration\">Ennél hosszabb számok scrobble-olása</string>\n    <string name=\"scrobble_delay_percent\">Scrobble késleltetés százalékban</string>\n    <string name=\"scrobble_delay_minutes\">Scrobble késleltetés percekben</string>\n    <string name=\"hide_video_songs\">Zenei videók elrejtése</string>\n    <string name=\"details_desc\">Zeneszám részleteinek megtekintése</string>\n    <string name=\"about_artist\">Az előadóról</string>\n    <string name=\"show_more\">Több mutatása</string>\n    <string name=\"show_less\">Kevesebb mutatása</string>\n    <string name=\"artist_page_settings\">Előadói oldal</string>\n    <string name=\"show_artist_description\">Előadó leírása</string>\n    <string name=\"show_artist_subscriber_count\">Feliratkozószám mutatása</string>\n    <string name=\"show_artist_monthly_listeners\">Havi hallgatók megjelenítése</string>\n    <string name=\"wavy\">Hullámzó</string>\n    <string name=\"enable_simpmusic\">SimpMusic dalszövegek engedélyezése</string>\n    <string name=\"enable_simpmusic_desc\">A szinkronizált dalszövegekhez a SimpMusic szolgáltató használata</string>\n    <string name=\"skip_silence_desc\">Számok csendes részeinek átugrása</string>\n    <string name=\"skip_silence_instant\">Csend azonnali átugrása</string>\n    <string name=\"skip_silence_instant_desc\">A lejátszás felgyorsítása helyett ugorjon előre a csendes részeknél</string>\n    <string name=\"remember_shuffle_and_repeat\">Keverés és ismétlés megjegyzése</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Keverés és ismétlés megjegyzése az app újraindításakor</string>\n    <string name=\"pause_music_when_media_is_muted\">Zene szüneteltetése némításkor</string>\n    <string name=\"lyrics_offset\">Dalszöveg eltolása</string>\n    <string name=\"edit_desc\">Cím vagy előadó megváltoztatása</string>\n    <string name=\"start_radio_desc\">Rádió indítása ez alapján</string>\n    <string name=\"play_next_desc\">Hozzáadás a lejátszási sor elejéhez</string>\n    <string name=\"add_to_queue_desc\">Hozzáadás a lejátszási sor végéhez</string>\n    <string name=\"add_to_library_desc\">Mentés a könyvtárba</string>\n    <string name=\"download_desc\">Offline lejátszás engedélyezése</string>\n    <string name=\"add_to_playlist_desc\">Hozzáadás lejátszási listához</string>\n    <string name=\"refetch_desc\">Legújabb metaadat lekérése YouTube Music-ról</string>\n    <string name=\"share_desc\">Link megosztása</string>\n    <string name=\"delete_desc\">Elem végleges eltávolítása</string>\n    <string name=\"advanced_desc\">Tempó és hangmagasság módosítása</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-hu/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <string name=\"home\">Kezdőlap</string>\n    <string name=\"songs\">Dalok</string>\n    <string name=\"artists\">Előadók</string>\n    <string name=\"albums\">Albumok</string>\n    <string name=\"playlists\">Lejátszásilisták</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d kijelölve</item>\n        <item quantity=\"other\">%d kijelölve</item>\n    </plurals>\n    <string name=\"history\">Előzmények</string>\n    <string name=\"stats\">Statisztikák</string>\n    <string name=\"mood_and_genres\">Hangulatok és Műfajok</string>\n    <string name=\"account\">Fiók</string>\n    <string name=\"quick_picks\">Gyors választások</string>\n    <string name=\"quick_picks_empty\">Hallgasson zeneszámokat a gyors választások generálásához</string>\n    <string name=\"new_release_albums\">Újonnan megjelent albumok</string>\n    <string name=\"today\">Ma</string>\n    <string name=\"yesterday\">Tegnap</string>\n    <string name=\"this_week\">A héten</string>\n    <string name=\"last_week\">Múlt héten</string>\n    <string name=\"most_played_songs\">Legtöbbet játszott dalok</string>\n    <string name=\"most_played_artists\">Legtöbbet játszott előadók</string>\n    <string name=\"most_played_albums\">Legtöbbet játszott albumok</string>\n    <string name=\"search\">Keresés</string>\n    <string name=\"search_yt_music\">Keresés a YouTube Zenén…</string>\n    <string name=\"search_library\">Keresés a könyvtárban…</string>\n    <string name=\"filter_library\">Könyvtár</string>\n    <string name=\"filter_liked\">Kedvelt</string>\n    <string name=\"filter_downloaded\">Letöltött</string>\n    <string name=\"filter_all\">Összes</string>\n    <string name=\"filter_songs\">Dalok</string>\n    <string name=\"filter_videos\">Videók</string>\n    <string name=\"filter_albums\">Albumok</string>\n    <string name=\"filter_artists\">Előadók</string>\n    <string name=\"filter_playlists\">Listák</string>\n    <string name=\"filter_community_playlists\">Közösségi lejátszási listák</string>\n    <string name=\"filter_featured_playlists\">Kiemelt lejátszási listák</string>\n    <string name=\"filter_bookmarked\">Könyvjelzőzött</string>\n    <string name=\"no_results_found\">Nincs találat</string>\n    <string name=\"from_your_library\">Saját könyvtárból</string>\n    <string name=\"liked_songs\">Kedvelt dalok</string>\n    <string name=\"downloaded_songs\">Letöltött dalok</string>\n    <string name=\"playlist_is_empty\">A lejátszási lista üres</string>\n    <string name=\"retry\">Újra</string>\n    <string name=\"radio\">Rádió</string>\n    <string name=\"shuffle\">Keverés</string>\n    <string name=\"reset\">Visszaállítás</string>\n    <string name=\"details\">Részletek</string>\n    <string name=\"edit\">Szerkesztés</string>\n    <string name=\"start_radio\">Rádió indítása</string>\n    <string name=\"play\">Lejátszás</string>\n    <string name=\"play_next\">Következő</string>\n    <string name=\"add_to_queue\">Listához ad</string>\n    <string name=\"add_to_library\">Könyvtárhoz ad</string>\n    <string name=\"remove_from_library\">Eltávolítás a könyvtárból</string>\n    <string name=\"action_download\">Letölt</string>\n    <string name=\"downloading\">Letöltés alatt</string>\n    <string name=\"remove_download\">Letöltés eltávolítása</string>\n    <string name=\"import_playlist\">Lejátszási lista importálása</string>\n    <string name=\"add_to_playlist\">Hozzáadás lejátszási listához</string>\n    <string name=\"view_artist\">Ugrás az előadóhoz</string>\n    <string name=\"view_album\">Ugrás az albumhoz</string>\n    <string name=\"refetch\">Újrahív</string>\n    <string name=\"share\">Megosztás</string>\n    <string name=\"delete\">Eltávolítás</string>\n    <string name=\"remove_from_history\">Eltávolítás az előzményekből</string>\n    <string name=\"search_online\">Keresés online</string>\n    <string name=\"action_sync\">Szinkronizálás</string>\n    <string name=\"advanced\">Haladó</string>\n    <string name=\"sort_by_create_date\">Hozzáadás dátuma</string>\n    <string name=\"sort_by_name\">Név</string>\n    <string name=\"sort_by_artist\">Előadó</string>\n    <string name=\"sort_by_year\">Év</string>\n    <string name=\"sort_by_song_count\">Dal számláló</string>\n    <string name=\"sort_by_length\">Hossz</string>\n    <string name=\"sort_by_play_time\">Játszott idő</string>\n    <string name=\"sort_by_custom\">Egyedi sorrend</string>\n    <string name=\"media_id\">Média id</string>\n    <string name=\"mime_type\">MIME típus</string>\n    <string name=\"codecs\">Kodekek</string>\n    <string name=\"bitrate\">Bitráta</string>\n    <string name=\"sample_rate\">Mintavétel ráta</string>\n    <string name=\"loudness\">Hangosság</string>\n    <string name=\"volume\">Hangerő</string>\n    <string name=\"file_size\">Fájlméret</string>\n    <string name=\"unknown\">Ismeretlen</string>\n    <string name=\"copied\">Vágólapra másolva</string>\n    <string name=\"edit_lyrics\">Dalszöveg szerkesztése</string>\n    <string name=\"search_lyrics\">Dalszöveg keresése</string>\n    <string name=\"edit_song\">Dal szerkesztése</string>\n    <string name=\"song_title\">Dal címe</string>\n    <string name=\"song_artists\">Dal előadói</string>\n    <string name=\"error_song_title_empty\">Dal cím nem lehet üres.</string>\n    <string name=\"error_song_artist_empty\">Előadó neve nem lehet üres.</string>\n    <string name=\"save\">Mentés</string>\n    <string name=\"choose_playlist\">Lejátszási lista kiválasztása</string>\n    <string name=\"edit_playlist\">Lejátszási lista szerkesztése</string>\n    <string name=\"create_playlist\">Lejátszási lista létrehozása</string>\n    <string name=\"playlist_name\">Lejátszási lista neve</string>\n    <string name=\"error_playlist_name_empty\">A lejátszási lista neve nem lehet üres.</string>\n    <string name=\"edit_artist\">Előadó szerkesztése</string>\n    <string name=\"artist_name\">Előadó neve</string>\n    <string name=\"error_artist_name_empty\">Az előadó neve nem lehet üres.</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d dal</item>\n        <item quantity=\"other\">%d dal</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d előadó</item>\n        <item quantity=\"other\">%d előadó</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d album</item>\n        <item quantity=\"other\">%d album</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d lejátszási lista</item>\n        <item quantity=\"other\">%d lejátszási lista</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d hét</item>\n        <item quantity=\"other\">%d hét</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d hónap</item>\n        <item quantity=\"other\">%d hónap</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d év</item>\n        <item quantity=\"other\">%d év</item>\n    </plurals>\n    <string name=\"playlist_imported\">Lejátszási lista importálva</string>\n    <string name=\"removed_song_from_playlist\">\\\"%s\\\" eltávolítva a lejátszási listából</string>\n    <string name=\"playlist_synced\">Lejátszási lista szinkronizálva</string>\n    <string name=\"undo\">Visszavonás</string>\n    <string name=\"lyrics_not_found\">Dalszöveg nem található</string>\n    <string name=\"sleep_timer\">Alvásidőzítő</string>\n    <string name=\"end_of_song\">A dal vége</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">1 perc</item>\n        <item quantity=\"other\">%d perc</item>\n    </plurals>\n    <string name=\"error_no_stream\">Nincs elérhető adatfolyam</string>\n    <string name=\"error_no_internet\">Nincs hálózati kapcsolat</string>\n    <string name=\"error_timeout\">Időtúllépés</string>\n    <string name=\"error_unknown\">Ismeretlen hiba</string>\n    <string name=\"action_like\">Tetszik</string>\n    <string name=\"action_remove_like\">Tetszés eltávolítása</string>\n    <string name=\"action_shuffle_on\">Keverés BE</string>\n    <string name=\"action_shuffle_off\">Keverés KI</string>\n    <string name=\"repeat_mode_off\">Ismétlő mód KI</string>\n    <string name=\"repeat_mode_one\">Aktuális dal ismétlése</string>\n    <string name=\"repeat_mode_all\">Lista ismétlése</string>\n    <string name=\"queue_all_songs\">Minden dal</string>\n    <string name=\"queue_searched_songs\">Keresett dalok</string>\n    <string name=\"music_player\">Zenelejátszó</string>\n    <string name=\"settings\">Beállítások</string>\n    <string name=\"appearance\">Megjelenés</string>\n    <string name=\"enable_dynamic_theme\">Dinamikus téma bekapcsolása</string>\n    <string name=\"dark_theme\">Sötét téma</string>\n    <string name=\"dark_theme_on\">Be</string>\n    <string name=\"dark_theme_off\">Ki</string>\n    <string name=\"dark_theme_follow_system\">Rendszer téma követése</string>\n    <string name=\"pure_black\">OLED fekete</string>\n    <string name=\"default_open_tab\">Alapértelmezett megjelenő lap</string>\n    <string name=\"customize_navigation_tabs\">A navigációs lapok testreszabása</string>\n    <string name=\"lyrics_text_position\">Dalszöveg pozíciója</string>\n    <string name=\"left\">Balra</string>\n    <string name=\"center\">Középre</string>\n    <string name=\"right\">Jobbra</string>\n    <string name=\"content\">Tartalom</string>\n    <string name=\"login\">Belépés</string>\n    <string name=\"content_language\">Tartalom alapértelmezett nyelve</string>\n    <string name=\"content_country\">Tartalom alapértelmezett országa</string>\n    <string name=\"system_default\">Rendszer alapérték</string>\n    <string name=\"enable_proxy\">Proxy bekapcsolása</string>\n    <string name=\"proxy_type\">Proxy típus</string>\n    <string name=\"proxy_url\">Proxy hivatkozás</string>\n    <string name=\"restart_to_take_effect\">Az alkalmazáshoz újraindítás szükséges</string>\n    <string name=\"player_and_audio\">Lejátszó és hang</string>\n    <string name=\"audio_quality\">Hangminőség</string>\n    <string name=\"audio_quality_auto\">Automata</string>\n    <string name=\"audio_quality_high\">Magas</string>\n    <string name=\"audio_quality_low\">Gyenge</string>\n    <string name=\"persistent_queue\">Állandó lista</string>\n    <string name=\"skip_silence\">Csend átugrása</string>\n    <string name=\"audio_normalization\">Hang normalizálása</string>\n    <string name=\"equalizer\">Hangszínszabályzó</string>\n    <string name=\"storage\">Tárhely</string>\n    <string name=\"cache\">Gyorsítótár</string>\n    <string name=\"image_cache\">Kép gyorsítótár</string>\n    <string name=\"song_cache\">Dal gyorsítótár</string>\n    <string name=\"max_cache_size\">Max. gyorsítótár méret</string>\n    <string name=\"unlimited\">Korlátlan</string>\n    <string name=\"clear_all_downloads\">Minden letöltés törlése</string>\n    <string name=\"max_image_cache_size\">Max. kép gyorsítótár méret</string>\n    <string name=\"clear_image_cache\">Kép gyorsítótár ürítése</string>\n    <string name=\"max_song_cache_size\">Max. dal gyorsítótár méret</string>\n    <string name=\"clear_song_cache\">Dal gyorsítótár ürítése</string>\n    <string name=\"size_used\">%s felhasználva</string>\n    <string name=\"privacy\">Adatvédelem</string>\n    <string name=\"pause_listen_history\">A hallgatási előzmények szüneteltetése</string>\n    <string name=\"clear_listen_history\">Hallgatási előzmények törlése</string>\n    <string name=\"clear_listen_history_confirm\">Biztosan törli az összes hallgatási előzményt?</string>\n    <string name=\"pause_search_history\">Keresési előzmények szüneteltetése</string>\n    <string name=\"clear_search_history\">Keresési előzmények ürítése</string>\n    <string name=\"clear_search_history_confirm\">Biztosan törli az összes keresési előzményt?</string>\n    <string name=\"enable_kugou\">A KuGou dalszöveg szolgáltató engedélyezése</string>\n    <string name=\"backup_restore\">Biztonsági mentés és visszaállítás</string>\n    <string name=\"action_backup\">Biztonsági mentés</string>\n    <string name=\"action_restore\">Visszaállítás</string>\n    <string name=\"imported_playlist\">Importált lejátszási lista</string>\n    <string name=\"backup_create_success\">A biztonsági mentés sikeresen létrehozva</string>\n    <string name=\"backup_create_failed\">Sikertelen biztonsági mentés</string>\n    <string name=\"restore_failed\">Sikertelen visszaállítás</string>\n    <string name=\"about\">Névjegy</string>\n    <string name=\"app_version\">Alkalmazásverzió</string>\n    <string name=\"new_version_available\">Új verzió érhető el</string>\n    <string name=\"translation_models\">Fordítási modellek</string>\n    <string name=\"clear_translation_models\">Fordítási modellek ürítése</string>\n    <string name=\"forgotten_favorites\">Elfelejtett kedvencek</string>\n    <string name=\"keep_listening\">Zene folytatása</string>\n    <string name=\"similar_to\">Hasonló, mint</string>\n    <string name=\"library_song_empty\">A könyvtárában levő dalok itt jelennek meg</string>\n    <string name=\"library_artist_empty\">A könyvtárában levő előadók itt jelennek meg</string>\n    <string name=\"library_album_empty\">A könyvtárában szereplő albumok itt jelennek meg</string>\n    <string name=\"library_playlist_empty\">A lejátszási listái itt jelennek meg</string>\n    <string name=\"other_versions\">Egyéb verziók</string>\n    <string name=\"remove_download_playlist_confirm\">Biztosan el akarja távolítani a \\\"%s\\\" lejátszási listán szereplő összes zenét a Letöltött Zenék tárhelyről?</string>\n    <string name=\"delete_playlist_confirm\">Biztosan törölni szeretné a \\\"%s\\\" lejátszási listát?</string>\n    <string name=\"remove_all_from_library\">Az összes eltávolítása a könyvtárból</string>\n    <string name=\"your_youtube_playlists\">YouTube lejátszási listái</string>\n    <string name=\"add_all_to_library\">Az összes hozzáadása a könyvtárhoz</string>\n    <string name=\"remove_from_playlist\">Eltávolítás lejátszási listáról</string>\n    <string name=\"remove_from_queue\">Eltávolítás listáról</string>\n    <string name=\"tempo_and_pitch\">Tempó és Hangszín</string>\n    <string name=\"duplicates\">Duplikáció</string>\n    <string name=\"skip_duplicates\">Duplikációk átugrása</string>\n    <string name=\"add_anyway\">Adja hozzá mindenképp</string>\n    <string name=\"duplicates_description_single\">A dal már megtalálható a lejátszási listában</string>\n    <string name=\"duplicates_description_multiple\">%d dal már megtalálható a lejátszási listájában</string>\n    <string name=\"action_like_all\">Összes tetszik</string>\n    <string name=\"action_remove_like_all\">Minden tetszés eltávolítása</string>\n    <string name=\"theme\">Téma</string>\n    <string name=\"player\">Lejátszó</string>\n    <string name=\"player_text_alignment\">Lejátszó szöveg igazítás</string>\n    <string name=\"sided\">Oldalra</string>\n    <string name=\"player_slider_style\">Lejátszó sáv stílus</string>\n    <string name=\"default_\">Alapbeállítás</string>\n    <string name=\"squiggly\">Hernyó</string>\n    <string name=\"misc\">Egyéb</string>\n    <string name=\"grid_cell_size\">Cella méret</string>\n    <string name=\"small\">Kicsi</string>\n    <string name=\"big\">Nagy</string>\n    <string name=\"action_logout\">Kijelentkezés</string>\n    <string name=\"action_login\">Bejelentkezés</string>\n    <string name=\"not_logged_in\">Nincs bejelentkezve</string>\n    <string name=\"login_failed\">Sikertelen bejelentkezés</string>\n    <string name=\"queue\">Lista</string>\n    <string name=\"persistent_queue_desc\">Utoljára használt lista visszaállítása app indításnál</string>\n    <string name=\"auto_load_more\">Automatikusan betölt még dalokat</string>\n    <string name=\"auto_load_more_desc\">Ha lehetséges, automatikusan hozzáad dalokat a listához, amint a végéhez ért</string>\n    <string name=\"auto_skip_next_on_error\">Hiba esetén átlép a következő dalra</string>\n    <string name=\"auto_skip_next_on_error_desc\">Biztosítva a folyamatos zene hallgatást</string>\n    <string name=\"stop_music_on_task_clear\">Zene megállítása a feladat ürítéskor</string>\n    <string name=\"listen_history\">Hallgatás előzmény</string>\n    <string name=\"search_history\">Keresési előzmények</string>\n    <string name=\"use_login_for_browse\">Belépés használata a tartalom böngészéséhez</string>\n    <string name=\"use_login_for_browse_desc\">Ez hatással lehet az ajánlott tartalomra. Például megjelenhetnek prémium albumok, ha prémium fiókkal jelentkezik be</string>\n    <string name=\"disable_screenshot\">Képernyőkép készítés kikapcsolása</string>\n    <string name=\"disable_screenshot_desc\">Ha ez be van kapcsolva, a képernyőkép készítés nem lehetséges, és az előzmények sem látszódnak.</string>\n    <string name=\"enable_lrclib\">A LrcLib dalszöveg szolgáltató engedélyezése</string>\n    <string name=\"hide_explicit\">Felnőtt tartalom elrejtése</string>\n    <string name=\"discord_integration\">Discord integráció</string>\n    <string name=\"discord_information\">Az Metrolist a KizzyRPC könyvtárat használja a Discord fiók státusz beállításhoz. Ez a Discord Gateway kapcsolatot használja, amely tekinthető a Discord felhasználási feltételek megszegésének is tágabb értelemben. Habár a múltban nem történt fiók tiltás emiatt, ettől függetlenül kérem használja saját felelősségre.\\n\\nAz Metrolist csak a token-jét fogja kinyerni, minden más helyben tárolt.</string>\n    <string name=\"dismiss\">Elrejtés</string>\n    <string name=\"options\">Opciók</string>\n    <string name=\"preview\">Előnézet</string>\n    <string name=\"enable_discord_rpc\">Rich Presence engedélyezése</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-in/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <plurals name=\"n_time\">\n        <item quantity=\"other\">%d kali</item>\n    </plurals>\n    <string name=\"similar_content\">Konten serupa</string>\n    <string name=\"remote_history\">Online</string>\n    <string name=\"years\">Tahun</string>\n    <string name=\"months\">Bulan</string>\n    <string name=\"please_wait\">Mohon tunggu</string>\n    <string name=\"generating_image\">Membuat gambar</string>\n    <string name=\"charts\">Tangga Lagu</string>\n    <string name=\"back_button_desc\">Kembali</string>\n    <string name=\"album_cover_desc\">Sampul album</string>\n    <string name=\"top_music_videos\">Video musik teratas</string>\n    <string name=\"trending\">Sedang Tren</string>\n    <string name=\"weeks\">Minggu</string>\n    <string name=\"continuous\">Berkelanjutan</string>\n    <string name=\"liked\">Disukai</string>\n    <string name=\"offline\">Diunduh</string>\n    <string name=\"sync_playlist\">Sinkronkan playlist</string>\n    <string name=\"allows_for_sync_witch_youtube\">Catatan: Ini memungkinkan sinkronisasi dengan YouTube Music. Pengaturan ini TIDAK dapat diubah di kemudian hari.</string>\n    <string name=\"share_lyrics\">Bagikan lirik</string>\n    <string name=\"share_as_text\">Bagikan sebagai teks</string>\n    <string name=\"share_as_image\">Bagikan sebagai gambar</string>\n    <string name=\"max_selection_limit\">Batas maksimum pilihan</string>\n    <string name=\"share_selected\">Bagikan yang dipilih</string>\n    <string name=\"customize_colors\">Sesuaikan warna</string>\n    <string name=\"text_color\">Warna teks</string>\n    <string name=\"secondary_text_color\">Warna teks sekunder</string>\n    <string name=\"background_color\">Warna latar belakang</string>\n    <string name=\"remove_from_cache\">Hapus dari cache</string>\n    <string name=\"select\">Pilih semua</string>\n    <string name=\"cancel\">Batal</string>\n    <string name=\"advanced_login\">Login dengan token</string>\n    <string name=\"token_hidden\">Ketuk untuk menampilkan token</string>\n    <string name=\"token_shown\">Ketuk lagi untuk menyalin atau mengubah</string>\n    <string name=\"general\">Umum</string>\n    <string name=\"app_language\">Bahasa aplikasi</string>\n    <string name=\"not_logged_in_youtube\">Belum login ke YouTube</string>\n    <string name=\"release_notes\">Catatan rilis</string>\n    <string name=\"default_links\">Buka link yang didukung</string>\n    <string name=\"open_app_settings_error\">Tidak dapat membuka pengaturan aplikasi</string>\n    <string name=\"all_time\">Sepanjang waktu</string>\n    <string name=\"views\">Tayangan</string>\n    <string name=\"past_24_hours\">24 jam terakhir</string>\n    <string name=\"past_week\">Seminggu terakhir</string>\n    <string name=\"past_month\">Sebulan terakhir</string>\n    <string name=\"past_year\">Setahun terakhir</string>\n    <string name=\"history_duration\">Durasi riwayat</string>\n    <string name=\"information\">Informasi</string>\n    <string name=\"description\">Deskripsi</string>\n    <string name=\"likes\">Suka</string>\n    <string name=\"dislikes\">Tidak Suka</string>\n    <string name=\"clear_downloads_dialog\">Apakah Anda yakin ingin menghapus semua unduhan?</string>\n    <string name=\"token_adv_login_description\">Ini adalah metode login LANJUTAN. Sebagai alternatif dari portal web, Anda dapat langsung memasukkan atau memperbarui token login Anda di sini. Sebagai contoh, ini dapat mempercepat proses login di beberapa perangkat sekaligus. Perlu diperhatikan bahwa format token yang tidak valid dan gagal diproses oleh aplikasi tidak akan diterima</string>\n    <string name=\"local_history\">Lokal</string>\n    <string name=\"sort_by_last_updated\">Tanggal diperbarui</string>\n    <string name=\"link_copied\">Link disalin ke papan klip</string>\n    <string name=\"lyrics\">Lirik</string>\n    <string name=\"already_in_playlist\">Sudah ada di playlist:</string>\n    <string name=\"player_background_style\">Gaya latar belakang pemutar musik</string>\n    <string name=\"my_top\">Teratas Saya</string>\n    <string name=\"cached_playlist\">Tersimpan di Cache</string>\n    <string name=\"sync_disabled\">Sinkronisasi dinonaktifkan</string>\n    <string name=\"copy_link\">Salin link</string>\n    <string name=\"like_all\">Sukai semua</string>\n    <string name=\"dislike_all\">Tidak sukai semua</string>\n    <string name=\"follow_theme\">Ikuti tema</string>\n    <string name=\"gradient\">Gradien</string>\n    <string name=\"new_player_design\">Desain pemutar musik baru</string>\n    <string name=\"player_background_blur\">Buram</string>\n    <string name=\"player_buttons_style\">Warna tombol pemutar musik</string>\n    <string name=\"default_style\">Default</string>\n    <string name=\"enable_swipe_thumbnail\">Aktifkan gestur geser untuk mengganti lagu</string>\n    <string name=\"swipe_song_to_add\">Geser lagu ke kiri untuk menambahkannya ke antrean, atau ke kanan untuk memutar selanjutnya</string>\n    <string name=\"lyrics_click_change\">Ganti lirik dengan ketukan</string>\n    <string name=\"lyrics_auto_scroll\">Gulir lirik otomatis</string>\n    <string name=\"lyrics_romanize_japanese\">Romanisasi lirik Jepang</string>\n    <string name=\"lyrics_romanize_korean\">Romanisasi lirik Korea</string>\n    <string name=\"slim\">Ramping</string>\n    <string name=\"slim_navbar\">Sembunyikan label bilah navigasi bawah</string>\n    <string name=\"auto_playlists\">Playlist otomatis</string>\n    <string name=\"show_liked_playlist\">Tampilkan playlist \\\"Disukai\\\"</string>\n    <string name=\"show_downloaded_playlist\">Tampilkan playlist \\\"Diunduh\\\"</string>\n    <string name=\"show_top_playlist\">Tampilkan playlist \\\"Teratas\\\"</string>\n    <string name=\"show_cached_playlist\">Tampilkan playlist \\\"Tersimpan di Cache\\\"</string>\n    <string name=\"yt_sync\">Sinkronkan otomatis dengan akun</string>\n    <string name=\"more_content\">Konten lainnya</string>\n    <string name=\"proxy\">Proksi</string>\n    <string name=\"default_lib_chips\">Ubah chip pustaka default</string>\n    <string name=\"set_quick_picks\">Atur pilihan cepat</string>\n    <string name=\"last_song_listened\">Berdasarkan lagu yang terakhir didengar</string>\n    <string name=\"enable_similar_content\">Aktifkan konten serupa</string>\n    <string name=\"similar_content_desc\">Otomatis menambahkan lagu serupa saat akhir antrean tercapai</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"import_online\">Impor playlist \\\"m3u\\\"</string>\n    <string name=\"import_csv\">Impor playlist \\\"csv\\\"</string>\n    <string name=\"playlist_add_local_to_synced_note\">Catatan: Menambahkan lagu lokal ke playlist yang tersinkron/online tidak didukung. Kombinasi lainnya tetap valid</string>\n    <string name=\"auto_download_on_like\">Unduh otomatis saat disukai</string>\n    <string name=\"auto_download_on_like_desc\">Unduh lagu secara otomatis saat Anda menyukainya</string>\n    <string name=\"swipe_sensitivity\">Sensitivitas geser pemutar mini</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"clear_song_cache_dialog\">Apakah Anda yakin ingin menghapus semua lagu yang tersimpan di cache?</string>\n    <string name=\"clear_image_cache_dialog\">Apakah Anda yakin ingin menghapus semua gambar yang tersimpan di cache?</string>\n    <string name=\"disable\">Nonaktifkan</string>\n    <string name=\"top_length\">Panjang daftar Teratas Saya</string>\n    <string name=\"subscribe\">Subscribe</string>\n    <string name=\"subscribed\">Disubscribe</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"other\">%d detik</item>\n    </plurals>\n    <string name=\"new_mini_player_design\">Desain pemutar musik mini baru</string>\n    <string name=\"close\">Tutup</string>\n    <string name=\"seek_forward_dynamic\">+%1$d detik maju</string>\n    <string name=\"seek_backward_dynamic\">-%1$d detik mundur</string>\n    <string name=\"disable_load_more_when_repeat_all\">Nonaktifkan muat lebih banyak saat ulangi semua</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Jangan muat otomatis lagu tambahan dan konten serupa saat mode ulangi semua aktif</string>\n    <string name=\"now_playing\">Sedang Diputar</string>\n    <string name=\"seek_seconds_addup\">Lompatan progresif</string>\n    <string name=\"seek_seconds_addup_description\">Jika diaktifkan, menambahkan 5 detik ekstra secara bertahap pada setiap lompatan</string>\n    <string name=\"edit_playlist_cover\">Ubah sampul playlist</string>\n    <string name=\"hide_player_thumbnail\">Sembunyikan Thumbnail Pemutar Musik</string>\n    <string name=\"hide_player_thumbnail_desc\">Ganti sampul album di pemutar musik menjadi logo aplikasi</string>\n    <string name=\"edit_playlist_cover_note\">Catatan: Akun Anda harus terhubung dengan nomor telepon dan telah terverifikasi di YouTube Music untuk mengubah sampul playlist.</string>\n    <string name=\"enable_authentication\">Aktifkan autentikasi</string>\n    <string name=\"line_by_line_dialog_title\">Apakah Anda yakin?</string>\n    <string name=\"line_by_line_dialog_desc\">Fitur ini bersifat eksperimental dan tidak selalu dapat berfungsi dengan baik.\\n\\nSecara default, bahasa ditentukan dari keseluruhan lagu, tetapi dengan fitur ini aktif, akan ditentukan baris per baris. Ini akan memungkinkan lagu multi-bahasa berfungsi TETAPI bahasanya mungkin tidak selalu tepat (misalnya jika ada lirik Ukraina yang tidak mengandung huruf khusus Ukraina, mungkin akan diromanisasi sebagai Rusia).\\n\\nJika Anda tidak sedang mengalami masalah, disarankan untuk menonaktifkan fitur ini.</string>\n    <string name=\"settings_section_privacy\">Privasi &amp; Keamanan</string>\n    <string name=\"settings_section_storage\">Penyimpanan &amp; Data</string>\n    <string name=\"settings_section_system\">Sistem &amp; Tentang</string>\n    <string name=\"starting_radio\">Memulai radio</string>\n    <string name=\"edit_playlist_cover_note_wait\">Setelah memilih gambar, harap tunggu sejenak hingga sampul yang baru muncul di playlist Anda.</string>\n    <string name=\"config_proxy\">Atur proksi</string>\n    <string name=\"proxy_username\">Username proksi</string>\n    <string name=\"proxy_password\">Password proksi</string>\n    <string name=\"lyrics_romanization\">Romanisasi lirik</string>\n    <string name=\"lyrics_romanize_russian\">Romanisasi lirik Rusia</string>\n    <string name=\"lyrics_romanize_ukrainian\">Romanisasi lirik Ukraina</string>\n    <string name=\"lyrics_romanize_belarusian\">Romanisasi lirik Belarus</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Romanisasi lirik Kirgiz</string>\n    <string name=\"lyrics_romanize_serbian\">Romanisasi lirik Serbia</string>\n    <string name=\"lyrics_romanize_bulgarian\">Romanisasi lirik Bulgaria</string>\n    <string name=\"line_by_line_option_title\">EKSPERIMENTAL: Deteksi bahasa baris per baris</string>\n    <string name=\"line_by_line_option_desc\">Bahasa Sirilik akan dideteksi baris per baris alih-alih keseluruhan lagu.</string>\n    <string name=\"romanize_current_track\">Romanisasi lirik lagu saat ini</string>\n    <string name=\"settings_section_ui\">Antarmuka</string>\n    <string name=\"settings_section_player_content\">Pemutar &amp; Konten</string>\n    <string name=\"choose_from_library\">Pilih dari galeri</string>\n    <string name=\"audio_offload\">Aktifkan offload</string>\n    <string name=\"audio_offload_description\">Gunakan jalur audio offload untuk pemutaran audio. Menonaktifkan ini dapat meningkatkan penggunaan daya, tetapi berguna jika Anda sedang mengalami masalah dengan pemutaran audio atau pemrosesan</string>\n    <string name=\"remove_custom_image\">Hapus gambar kustom</string>\n    <string name=\"lyrics_romanize_title\">Romanisasi</string>\n    <string name=\"lyrics_romanization_cyrillic\">Sirilik</string>\n    <string name=\"uploaded_playlist\">Diunggah</string>\n    <string name=\"filter_uploaded\">Diunggah</string>\n    <string name=\"show_uploaded_playlist\">Tampilkan playlist \\\"Diunggah\\\"</string>\n    <string name=\"discord_use_details\">Gunakan detail alih-alih status</string>\n    <string name=\"discord_use_details_description\">Tampilkan judul lagu secara menonjol alih-alih nama artis</string>\n    <string name=\"updater\">Pembaruan</string>\n    <string name=\"check_for_updates\">Periksa pembaruan secara otomatis</string>\n    <string name=\"update_notifications\">Aktifkan notifikasi pembaruan</string>\n    <string name=\"update_available_title\">Pembaruan tersedia</string>\n    <string name=\"lyrics_romanize_macedonian\">Romanisasi lirik Makedonia</string>\n    <string name=\"integrations\">Integrasi</string>\n    <string name=\"lastfm_integration\">Integrasi Last.fm</string>\n    <string name=\"username\">Username</string>\n    <string name=\"password\">Password</string>\n    <string name=\"enable_scrobbling\">Aktifkan scrobbling</string>\n    <string name=\"lastfm_now_playing\">Kirim Now Playing</string>\n    <string name=\"scrobbling_configuration\">Atur Scrobbling</string>\n    <string name=\"scrobble_min_track_duration\">Scrobble lagu yang lebih lama dari</string>\n    <string name=\"update_channel_name\">Pembaruan aplikasi</string>\n    <string name=\"update_channel_desc\">Notifikasi tentang versi terbaru</string>\n    <string name=\"scrobble_delay_minutes\">Menit penundaan scrobble</string>\n    <string name=\"scrobble_delay_percent\">Persentase penundaan scrobble</string>\n    <string name=\"swipe_song_to_remove\">Geser lagu untuk menghapusnya dari playlist</string>\n    <string name=\"download_playlist_desc\">Unduh semua lagu untuk dapat diputar secara offline</string>\n    <string name=\"remove_download_playlist_desc\">Hapus semua lagu yang telah diunduh dari playlist ini</string>\n    <string name=\"download_in_progress_desc\">Pengunduhan sedang berlangsung</string>\n    <string name=\"share_playlist_desc\">Bagikan playlist ini kepada orang lain</string>\n    <string name=\"delete_playlist_desc\">Hapus playlist ini secara permanen</string>\n    <string name=\"sync_playlist_desc\">Sinkronkan playlist dengan YouTube Music</string>\n    <string name=\"primary_color_style\">Warna primer</string>\n    <string name=\"tertiary_color_style\">Warna tersier</string>\n    <string name=\"enable_better_lyrics\">Aktifkan penyedia lirik Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">Lirik sinkron per suku kata untuk semua lagu, cocok untuk karaoke</string>\n    <string name=\"auto_scroll\">Sinkron Ulang</string>\n    <string name=\"share_desc\">Bagikan link ke item ini</string>\n    <string name=\"delete_desc\">Hapus item ini secara permanen</string>\n    <string name=\"advanced_desc\">Ubah tempo dan tinggi nada lagu</string>\n    <string name=\"equalizer_desc\">Sesuaikan ekualiser audio</string>\n    <string name=\"enable_dynamic_icon\">Aktifkan ikon dinamis</string>\n    <string name=\"mini_player\">Pemutar mini</string>\n    <string name=\"pure_black_mini_player\">Pemutar mini berwarna hitam murni</string>\n    <string name=\"cache_size_warning_title\">Tunggu dulu!</string>\n    <string name=\"cache_size_warning_message\">Anda telah memilih batas ukuran cache yang lebih kecil dari yang saat ini digunakan oleh aplikasi (%1$s). Jika Anda melanjutkan, aplikasi mungkin akan menghapus beberapa %2$s yang tersimpan di cache untuk menyesuaikan dengan batas yang baru. Apakah Anda tetap ingin melanjutkan?</string>\n    <string name=\"cache_size_warning_confirm\">Lanjutkan</string>\n    <string name=\"lyrics_animation_style\">Gaya animasi lirik kata per kata</string>\n    <string name=\"none\">Tidak ada</string>\n    <string name=\"fade\">Pudar</string>\n    <string name=\"glow\">Cahaya</string>\n    <string name=\"slide\">Geser</string>\n    <string name=\"karaoke\">Karaoke</string>\n    <string name=\"apple_music_style\">Apple Music</string>\n    <string name=\"lyrics_text_size\">Ukuran teks lirik</string>\n    <string name=\"lyrics_line_spacing\">Spasi baris lirik</string>\n    <string name=\"album_art_for\">Sampul album untuk %s</string>\n    <string name=\"wrapped_total_albums_title\">Anda telah mendengarkan</string>\n    <string name=\"wrapped_total_albums_subtitle\">album unik</string>\n    <string name=\"wrapped_top_album_title\">Album teratas Anda adalah</string>\n    <string name=\"wrapped_playlist_ready\">Playlist personal Anda sudah siap</string>\n    <string name=\"wrapped_top_5_albums_title\">5 album teratas Anda</string>\n    <string name=\"wrapped_album_listening_time\">Anda telah mendengarkan album ini selama %d menit</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d menit</string>\n    <string name=\"wrapped_no_data\">Tidak ada data</string>\n    <string name=\"wrapped_top_5_artists_title\">Artis teratas Anda sepanjang tahun ini</string>\n    <string name=\"wrapped_artist_listening_time\">%d menit</string>\n    <string name=\"wrapped_top_5_songs_title\">Lagu teratas Anda sepanjang tahun ini</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">Sampul album</string>\n    <string name=\"wrapped_top_artist_title\">Artis teratas Anda sepanjang tahun ini adalah</string>\n    <string name=\"wrapped_top_artist_image_content_description\">Gambar artis teratas</string>\n    <string name=\"wrapped_top_artist_listening_time\">Anda telah mendengarkan mereka selama %d menit</string>\n    <string name=\"wrapped_top_song_title\">Lagu yang paling sering Anda dengarkan adalah</string>\n    <string name=\"wrapped_top_song_listening_time\">Anda telah mendengarkan selama %d menit</string>\n    <string name=\"wrapped_total_artists_title\">Anda telah mendengarkan</string>\n    <string name=\"wrapped_total_artists_subtitle\">artis unik</string>\n    <string name=\"wrapped_total_songs_title\">Anda telah mendengarkan</string>\n    <string name=\"wrapped_total_songs_subtitle\">lagu unik</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_intro_subtitle\">saatnya melihat apa yang telah Anda dengarkan</string>\n    <string name=\"wrapped_intro_button\">ayo mulai!</string>\n    <string name=\"wrapped_logo_content_description\">Logo Metrolist</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"wrapped_ready_title\">WRAPPED ANDA SUDAH SIAP!</string>\n    <string name=\"wrapped_ready_subtitle\">Saatnya melihat apa yang Anda sukai sepanjang tahun ini.</string>\n    <string name=\"wrapped_thank_you\">Terima kasih telah mendengarkan</string>\n    <string name=\"wrapped_special_thanks\">Terima kasih khusus kepada MO Agamy karena telah membuat Metrolist</string>\n    <string name=\"wrapped_close\">Tutup wrapped</string>\n    <string name=\"wrapped_playlist_title\">Wrapped %s Anda</string>\n    <string name=\"wrapped_create_playlist\">Buat playlist</string>\n    <string name=\"wrapped_playlist_saved\">Playlist tersimpan</string>\n    <string name=\"casting_to\">Casting ke %s</string>\n    <string name=\"progress_percent\">Progres %s%%</string>\n    <string name=\"listening_to_metrolist\">Mendengarkan Metrolist</string>\n    <string name=\"open\">Buka</string>\n    <string name=\"failed_to_create_image\">Gagal membuat gambar: %s</string>\n    <string name=\"copied_title\">Judul Disalin</string>\n    <string name=\"copied_artist\">Artis Disalin</string>\n    <string name=\"error_playing\">Error saat memutar</string>\n    <string name=\"failed_to_parse_proxy\">Gagal memproses URL proksi.</string>\n    <string name=\"show_wrapped_card\">Tampilkan kartu Wrapped</string>\n    <string name=\"lyrics_romanize_chinese\">Romanisasi lirik Mandarin</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"google_cast_description\">Aktifkan casting audio ke Chromecast dan perangkat Cast lainnya</string>\n    <string name=\"last_fm_send_likes\">Kirim Likes/Unlikes</string>\n    <string name=\"last_fm_send_likes_description\">Love/Unlove lagu di Last.fm saat di-Like/Unlike di Metrolist</string>\n    <string name=\"logging_in\">Sedang login…</string>\n    <string name=\"hide_video_songs\">Sembunyikan video musik</string>\n    <string name=\"details_desc\">Lihat informasi lagu</string>\n    <string name=\"edit_desc\">Ubah judul atau artis</string>\n    <string name=\"start_radio_desc\">Buat stasiun radio berdasarkan item ini</string>\n    <string name=\"play_next_desc\">Tambahkan ke awal antrean Anda</string>\n    <string name=\"add_to_queue_desc\">Tambahkan ke akhir antrean Anda</string>\n    <string name=\"add_to_library_desc\">Simpan ke pustaka Anda</string>\n    <string name=\"download_desc\">Sediakan untuk pemutaran offline</string>\n    <string name=\"add_to_playlist_desc\">Tambahkan ke salah satu playlist Anda</string>\n    <string name=\"refetch_desc\">Ambil metadata terbaru dari YouTube Music</string>\n    <string name=\"lyrics_glow_effect\">Aktifkan efek cahaya lirik</string>\n    <string name=\"lyrics_glow_effect_desc\">Tambahkan animasi bercahaya dan efek memantul pada lirik yang aktif</string>\n    <string name=\"shuffle_playlist_first\">Acak playlist/album terlebih dahulu</string>\n    <string name=\"shuffle_playlist_first_desc\">Saat mengacak, putar semua lagu dari playlist/album asli terlebih dahulu, kemudian konten serupa</string>\n    <string name=\"wavy\">Bergelombang</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"other\">%d Profil</item>\n    </plurals>\n    <string name=\"equalizer_header\">Ekualiser</string>\n    <string name=\"no_profiles\">Tidak ada profil ekualiser</string>\n    <string name=\"import_profile\">Impor Profil</string>\n    <string name=\"eq_disabled\">Dinonaktifkan</string>\n    <plurals name=\"band_count\">\n        <item quantity=\"other\">%d pita frekuensi</item>\n    </plurals>\n    <string name=\"delete_profile_desc\">Hapus Profil</string>\n    <string name=\"delete_profile_confirmation\">Apakah Anda yakin ingin menghapus %1$s? Tindakan ini tidak dapat dibatalkan.</string>\n    <string name=\"error_file_read\">Tidak dapat membaca file</string>\n    <string name=\"error_file_open\">Gagal membuka file: %1$s</string>\n    <string name=\"import_error_title\">Error Impor</string>\n    <string name=\"pause_music_when_media_is_muted\">Jeda musik saat media dibisukan</string>\n    <string name=\"enable_simpmusic\">Aktifkan penyedia lirik SimpMusic Lyrics</string>\n    <string name=\"enable_simpmusic_desc\">Lirik yang diambil secara otomatis dari Musixmatch dan Transkrip YouTube</string>\n    <string name=\"album_art\">Sampul album</string>\n    <string name=\"no_song_playing\">Tidak ada lagu yang sedang diputar</string>\n    <string name=\"tap_to_open\">Ketuk untuk membuka Metrolist</string>\n    <string name=\"previous\">Sebelumnya</string>\n    <string name=\"play_pause\">Putar/Jeda</string>\n    <string name=\"next\">Selanjutnya</string>\n    <string name=\"like\">Suka</string>\n    <string name=\"widget_description\">Widget pemutar musik dengan kontrol pemutaran</string>\n    <string name=\"turntable_widget_description\">Widget musik berbentuk lingkaran dengan kontrol putar dan suka</string>\n    <string name=\"system_equalizer\">Ekualiser Sistem</string>\n    <string name=\"remember_shuffle_and_repeat\">Ingat status acak dan ulang</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Ingat status acak dan ulang saat memulai ulang aplikasi</string>\n    <string name=\"lyrics_offset\">Offset Lirik</string>\n    <string name=\"show_more\">Tampilkan lebih banyak</string>\n    <string name=\"show_less\">Tampilkan lebih sedikit</string>\n    <string name=\"skip_silence_desc\">Lewati bagian hening pada lagu secara otomatis</string>\n    <string name=\"skip_silence_instant\">Lewati keheningan seketika</string>\n    <string name=\"about_artist\">Tentang</string>\n    <string name=\"artist_page_settings\">Halaman artis</string>\n    <string name=\"show_artist_description\">Tampilkan deskripsi artis</string>\n    <string name=\"show_artist_subscriber_count\">Tampilkan jumlah subskriber</string>\n    <string name=\"show_artist_monthly_listeners\">Tampilkan jumlah pendengar bulanan</string>\n    <string name=\"skip_silence_instant_desc\">Langsung melompati bagian hening alih-alih mempercepat pemutaran</string>\n    <string name=\"persistent_shuffle_title\">Acak persisten</string>\n    <string name=\"persistent_shuffle_desc\">Pertahankan acak yang sedang aktif saat memulai lagu atau playlist baru</string>\n    <string name=\"error_playback_failed\">Pemutaran gagal</string>\n    <string name=\"error_title\">Error</string>\n    <string name=\"error_eq_apply_failed\">Gagal menerapkan profil ekualiser: %1$s</string>\n    <string name=\"crop_album_art\">Pangkas Sampul Album</string>\n    <string name=\"crop_album_art_desc\">Paksa rasio aspek persegi dengan memangkas thumbnail video</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">Jaga layar tetap menyala saat pemutar musik dibuka</string>\n    <string name=\"listen_together_reconnecting\">Menghubungkan ulang…</string>\n    <string name=\"listen_together_disconnected\">Terputus</string>\n    <string name=\"listen_together_connecting\">Menghubungkan…</string>\n    <string name=\"listen_together_error\">Kesalahan koneksi</string>\n    <string name=\"listen_together_create_room\">Buat ruangan</string>\n    <string name=\"listen_together_create_room_desc\">Buat sesi dan bagikan kode pada teman</string>\n    <string name=\"listen_together_join_room\">Gabung sesi</string>\n    <string name=\"listen_together_room_code\">Kode sesi</string>\n    <string name=\"listen_together_you_are_host\">Kamu adalah host</string>\n    <string name=\"listen_together_you_are_guest\">Kamu adalah tamu</string>\n    <string name=\"listen_together_join_requests\">Minta bergabung</string>\n    <string name=\"listen_together_view_logs\">Lihat log</string>\n    <string name=\"listen_together_view_logs_desc\">Debug koneksi dan pesan</string>\n    <string name=\"listen_together_logs\">Log koneksi</string>\n    <string name=\"listen_together_no_logs\">Belum ada log</string>\n    <string name=\"listen_together_description\">Dengarkan musik bersama teman secara langsung. Buat sesi untuk menjadi host atau gabung sesi menggunakan kode.</string>\n    <string name=\"listen_together_background_disconnect_note\">Catatan: Kamu bisa terputus jika kamu membuat sesi tanpa musik diputar lalu pindah ke aplikasi lain.</string>\n    <string name=\"listen_together_not_configured\">Listen Together belum dikonfigurasi. Silakan atur URL server di Pengaturan → Integrasi → Listen Together.</string>\n    <string name=\"listen_together_suggestion_sent\">Saran terkirim ke host!</string>\n    <string name=\"listen_together_join_request_notification\">%1$s ingin bergabung ke sesi</string>\n    <string name=\"listen_together_notification_channel_name\">Listen Together</string>\n    <string name=\"listen_together_notification_channel_desc\">Notifikasi untuk acara Listen Together</string>\n    <string name=\"listen_together_room_created\">Sesi dibuat: %s</string>\n    <string name=\"listen_together_cannot_edit_username_in_room\">Tidak dapat mengubah username saat berada di sesi</string>\n    <string name=\"waiting_for_approval\">Menunggu persetujuan dari host</string>\n    <string name=\"invalid_room_code\">Kode sesi tidak valid</string>\n    <string name=\"join_request_denied\">Permintaan bergabung ditolak</string>\n    <string name=\"join_existing_room\">Gabung sesi yang ada</string>\n    <string name=\"room_code\">Kode sesi</string>\n    <string name=\"leave_room\">Keluar sesi</string>\n    <string name=\"join_room\">Gabung</string>\n    <string name=\"create_room\">Buat</string>\n    <string name=\"joining_room\">Bergabung ke sesi %s…</string>\n    <string name=\"creating_room\">Membuat sesi…</string>\n    <string name=\"connect\">Hubungkan</string>\n    <string name=\"disconnect\">Putuskan</string>\n    <string name=\"create\">Buat</string>\n    <string name=\"join\">Gabung</string>\n    <string name=\"approve\">Setujui</string>\n    <string name=\"reject\">Tolak</string>\n    <string name=\"clear\">Hapus</string>\n    <string name=\"copy\">Salin</string>\n    <string name=\"copied_to_clipboard\">Disalin ke papan klip</string>\n    <string name=\"not_set\">Belum diatur</string>\n    <string name=\"listen_together_connected\">Terhubung</string>\n    <string name=\"listen_together\">Dengar bersama</string>\n    <string name=\"listen_together_server_url\">URL Server</string>\n    <string name=\"listen_together_username\">Nama pengguna</string>\n    <string name=\"listen_together_suggestion_received\">%1$s diminta %2$s</string>\n    <string name=\"hosting_room\">Ruang hosting</string>\n    <string name=\"in_room\">Di ruangan</string>\n    <string name=\"pending_requests\">Menunggu permintaan</string>\n    <string name=\"pending_suggestions\">Menunggu saran</string>\n    <string name=\"suggest_to_host\">Sarankan ke host</string>\n    <string name=\"kick_user\">Keluarkan</string>\n    <string name=\"mute\">Bisu</string>\n    <string name=\"unmute\">Bunyikan</string>\n    <string name=\"host_label\">Host</string>\n    <string name=\"you_label\">Anda</string>\n    <string name=\"connected_users\">Pengguna tersambung</string>\n    <string name=\"enter_username\">Masukkan nama pengguna</string>\n    <string name=\"error_username_empty\">Nama pengguna wajib diisi.</string>\n    <string name=\"resync\">Sinkron ulang</string>\n    <string name=\"crash_title\">Aplikasi terhenti</string>\n    <string name=\"crash_description\">Terjadi kesalahan yang tidak terduga. Silakan bagikan laporan crash untuk membantu kami memperbaiki masalah ini.</string>\n    <string name=\"crash_share_logs\">Bagikan log</string>\n    <string name=\"crash_share_title\">Bagikan laporan crash</string>\n    <string name=\"crash_report_subject\">Laporan Crash Metrolist</string>\n    <string name=\"crash_close\">Tutup</string>\n    <string name=\"crash_no_log\">Tidak ada log crash yang tersedia</string>\n    <string name=\"palette_dynamic\">Dinamis</string>\n    <string name=\"palette_crimson\">Merah tua</string>\n    <string name=\"palette_rose\">Merah muda</string>\n    <string name=\"palette_purple\">Ungu</string>\n    <string name=\"palette_deep_purple\">Ungu tua</string>\n    <string name=\"palette_indigo\">Nila</string>\n    <string name=\"palette_blue\">Biru</string>\n    <string name=\"palette_sky_blue\">Biru langit</string>\n    <string name=\"palette_cyan\">Sian</string>\n    <string name=\"palette_teal\">Hijau kebiruan</string>\n    <string name=\"palette_green\">Hijau</string>\n    <string name=\"palette_light_green\">Hijau muda</string>\n    <string name=\"palette_lime\">Hijau limau</string>\n    <string name=\"palette_yellow\">Kuning</string>\n    <string name=\"palette_amber\">Kuning keemasan</string>\n    <string name=\"palette_orange\">Oranye</string>\n    <string name=\"palette_deep_orange\">Oranye tua</string>\n    <string name=\"palette_brown\">Cokelat</string>\n    <string name=\"palette_grey\">Abu-abu</string>\n    <string name=\"palette_blue_grey\">Abu-abu kebiruan</string>\n    <string name=\"cd_back\">Kembali</string>\n    <string name=\"cd_pure_black_mode\">Mode hitam pekat</string>\n    <string name=\"cd_light_mode\">Mode terang</string>\n    <string name=\"cd_dark_mode\">Mode gelap</string>\n    <string name=\"cd_system_mode\">Mode sistem</string>\n    <string name=\"cd_palette_item\">palet %1$s</string>\n    <string name=\"not_playing\">Tidak ada lagu yang diputar</string>\n    <string name=\"tap_to_play\">Ketuk untuk membuka Metrolist</string>\n    <string name=\"widget_music_player\">Pemutar Musik</string>\n    <string name=\"widget_turntable\">Meja putar</string>\n    <string name=\"together\">Bersama</string>\n    <string name=\"listen_together_choose_server\">Pilih server</string>\n    <string name=\"listen_together_custom_server\">Server kustom</string>\n    <string name=\"listen_together_use_custom_server\">Gunakan server kustom</string>\n    <string name=\"listen_together_auto_approval_joins\">Setujui otomatis permintaan bergabung</string>\n    <string name=\"listen_together_auto_approval_joins_desc\">Setujui permintaan bergabung secara otomatis alih-alih meninjaunya secara manual</string>\n    <string name=\"listen_together_sync_volume\">Sinkronkan volume host</string>\n    <string name=\"listen_together_sync_volume_desc\">Tamu mengikuti tingkat volume host</string>\n    <string name=\"enter_room_code\">Masukkan kode ruang</string>\n    <string name=\"listen_together_settings_desc\">Konfigurasikan server, nama pengguna, dan lainnya</string>\n    <string name=\"copy_code\">Salin kode</string>\n    <string name=\"kick_user_desc\">Keluarkan orang ini dari sesi</string>\n    <string name=\"permanently_kick_user\">Blokir permanen</string>\n    <string name=\"permanently_kick_user_desc\">Blokir permintaan bergabung orang ini dan sembunyikan saran mereka</string>\n    <string name=\"transfer_ownership\">Transfer kepemilikan</string>\n    <string name=\"transfer_ownership_desc\">Jadikan orang ini host ruangan</string>\n    <string name=\"manage_user\">Kelola pengguna</string>\n    <string name=\"listen_together_blocked_users\">Pengguna yang diblokir</string>\n    <string name=\"listen_together_blocked_users_count\">%d pengguna diblokir</string>\n    <string name=\"listen_together_no_blocked_users\">Tidak ada pengguna yang diblokir</string>\n    <string name=\"unblock\">Buka blokir</string>\n    <string name=\"user_blocked_by_host\">Pengguna diblokir oleh host</string>\n    <string name=\"ai_lyrics_translation\">Terjemahan Lirik AI</string>\n    <string name=\"ai_translating_lyrics\">Menerjemahkan lirik...</string>\n    <string name=\"ai_lyrics_translated\">Lirik diterjemahkan</string>\n    <string name=\"ai_provider\">Penyedia</string>\n    <string name=\"ai_base_url\">URL dasar</string>\n    <string name=\"ai_api_key\">Kunci API</string>\n    <string name=\"ai_model\">Model</string>\n    <string name=\"ai_translation_mode\">Mode terjemahan</string>\n    <string name=\"ai_target_language\">Bahasa target</string>\n    <string name=\"ai_setup_guide\">Kredensial API</string>\n    <string name=\"ai_translation_literal\">Terjemahan</string>\n    <string name=\"ai_translation_transcribed\">Transkripsi</string>\n    <string name=\"ai_api_key_required\">Kunci API diperlukan</string>\n    <string name=\"ai_error_api_key_required\">Kunci API wajib diisi</string>\n    <string name=\"ai_error_no_lyrics\">Tidak ada lirik untuk diterjemahkan</string>\n    <string name=\"ai_error_lyrics_empty\">Lirik kosong</string>\n    <string name=\"ai_error_language_required\">Bahasa target wajib diisi</string>\n    <string name=\"ai_error_unexpected\">Hasil terjemahan tidak terduga</string>\n    <string name=\"ai_error_unknown\">Terjadi kesalahan yang tidak diketahui</string>\n    <string name=\"ai_error_translation_failed\">Terjemahan gagal</string>\n    <string name=\"play_all\">Putar semua</string>\n    <string name=\"crossfade\">Crossfade</string>\n    <string name=\"crossfade_desc\">Crossfade antar lagu</string>\n    <string name=\"crossfade_duration\">Durasi crossfade</string>\n    <string name=\"crossfade_gapless\">Nonaktifkan untuk album tanpa jeda</string>\n    <string name=\"crossfade_gapless_desc\">Jangan crossfade jika album tanpa jeda</string>\n    <string name=\"crossfade_beta_title\">Fitur Beta</string>\n    <string name=\"crossfade_beta_message\">Crossfade adalah fitur baru dan mungkin memiliki bug. Jika Anda mengalami masalah, silakan laporkan.\\n\\nFitur ini menonaktifkan offload audio karena keterbatasan teknis.</string>\n    <string name=\"audio_offload_disabled_by_crossfade\">Dinonaktifkan karena Crossfade aktif</string>\n    <string name=\"hide_youtube_shorts\">Sembunyikan YouTube Shorts</string>\n    <string name=\"listen_together_in_top_bar\">Listen Together di bilah atas</string>\n    <string name=\"listen_together_in_top_bar_desc\">Tampilkan Listen Together di bilah aplikasi atas alih-alih bilah navigasi</string>\n    <string name=\"ai_translation_literal_desc\">Terjemahkan makna ke bahasa target</string>\n    <string name=\"ai_translation_transcribed_desc\">Ubah pelafalan ke skrip target</string>\n    <string name=\"ai_provider_help\">Dapatkan Kunci API</string>\n    <string name=\"ai_provider_openrouter_help\">Kunjungi [https://openrouter.ai](https://openrouter.ai) untuk model gratis dan berbayar</string>\n    <string name=\"ai_provider_openai_help\">Kunjungi [https://platform.openai.com/api-keys](https://platform.openai.com/api-keys)</string>\n    <string name=\"ai_provider_claude_help\">Kunjungi [https://console.anthropic.com/settings/keys](https://console.anthropic.com/settings/keys)</string>\n    <string name=\"ai_provider_gemini_help\">Kunjungi [https://aistudio.google.com/apikey](https://aistudio.google.com/apikey)</string>\n    <string name=\"ai_provider_perplexity_help\">Kunjungi [https://perplexity.ai/settings/api](https://perplexity.ai/settings/api)</string>\n    <string name=\"ai_provider_xai_help\">Kunjungi [https://console.x.ai](https://console.x.ai)</string>\n    <string name=\"ai_provider_deepl_help\">Kunjungi [https://deepl.com/pro-api](https://deepl.com/pro-api) untuk kunci gratis dan berbayar</string>\n    <string name=\"ai_deepl_formality\">Formalitas</string>\n    <string name=\"ai_deepl_formality_default\">Default</string>\n    <string name=\"ai_deepl_formality_more\">Lebih Formal</string>\n    <string name=\"ai_deepl_formality_less\">Kurang Formal</string>\n    <string name=\"enable_high_refresh_rate\">Aktifkan refresh rate tinggi</string>\n    <string name=\"enable_high_refresh_rate_desc\">Paksa layar untuk berjalan pada refresh rate tertinggi yang didukung (mis. 120Hz)</string>\n    <string name=\"recognize_music\">Kenali Musik</string>\n    <string name=\"tap_to_recognize\">Ketuk untuk mengenali</string>\n    <string name=\"listening\">Mendengarkan…</string>\n    <string name=\"processing\">Memproses…</string>\n    <string name=\"no_match_found\">Tidak ada kecocokan ditemukan</string>\n    <string name=\"recognition_error\">Kesalahan pengenalan</string>\n    <string name=\"try_again\">Coba lagi</string>\n    <string name=\"recognition_history\">Riwayat Pengenalan</string>\n    <string name=\"clear_recognition_history\">Hapus riwayat pengenalan</string>\n    <string name=\"clear_recognition_history_confirm\">Apakah Anda yakin ingin menghapus semua riwayat pengenalan?</string>\n    <string name=\"delete_from_history\">Hapus dari riwayat</string>\n    <string name=\"re_listen\">Dengarkan ulang</string>\n    <string name=\"play_on_app\">Putar di Metrolist</string>\n    <string name=\"map_csv_columns\">Petakan Kolom CSV</string>\n    <string name=\"first_row_is_header\">Baris pertama adalah header</string>\n    <string name=\"artist_name_column\">Kolom Nama Artis</string>\n    <string name=\"song_title_column\">Kolom Judul Lagu</string>\n    <string name=\"youtube_url_column\">Kolom URL YouTube (Opsional)</string>\n    <string name=\"continue_action\">Lanjutkan</string>\n    <string name=\"importing_csv\">Mengimpor CSV</string>\n    <string name=\"recently_converted\">Baru-baru Ini Dikonversi</string>\n    <string name=\"column_label\">Kolom %d</string>\n    <string name=\"discord_status\">Status</string>\n    <string name=\"discord_status_online\">Online</string>\n    <string name=\"discord_status_idle\">Idle</string>\n    <string name=\"discord_status_dnd\">Jangan Ganggu</string>\n    <string name=\"discord_buttons\">Tombol</string>\n    <string name=\"discord_button_1\">Tombol 1</string>\n    <string name=\"discord_button_2\">Tombol 2</string>\n    <string name=\"login_successful\">Login berhasil!</string>\n    <string name=\"discord_information_warning\">Fitur ini menggunakan pustaka KizzyRPC untuk terhubung ke Gateway Discord dan mengatur status Rich Presence Anda. Meskipun tidak ada penangguhan akun yang diketahui dari penggunaan serupa, metode ini tidak didukung secara resmi oleh Discord dan dapat dianggap sebagai pelanggaran Ketentuan Layanan. Token Anda diekstrak secara lokal dan tidak pernah dikirim ke server pihak ketiga. Lanjutkan dengan pertimbangan Anda sendiri.</string>\n    <string name=\"discord_activity_type\">Jenis aktivitas</string>\n    <string name=\"discord_activity_playing\">Bermain</string>\n    <string name=\"discord_activity_listening\">Mendengarkan</string>\n    <string name=\"discord_activity_watching\">Menonton</string>\n    <string name=\"discord_activity_competing\">Berkompetisi</string>\n    <string name=\"discord_button_text_variables\">Variabel: {song_name}, {artist_name}, {album_name}</string>\n    <string name=\"discord_rpc_preview\">Pratinjau Rich Presence</string>\n    <string name=\"discord_presence\">Kehadiran</string>\n    <string name=\"discord_connect_description\">Masuk dengan Discord untuk membagikan apa yang Anda dengarkan</string>\n    <string name=\"discord_playing_metrolist\">Bermain Metrolist</string>\n    <string name=\"discord_watching_metrolist\">Menonton Metrolist</string>\n    <string name=\"discord_competing_metrolist\">Berkompetisi di Metrolist</string>\n    <string name=\"discord_activity_name\">Nama aktivitas</string>\n    <string name=\"discord_activity_name_description\">Nama kustom untuk aktivitas (biarkan kosong untuk default)</string>\n    <string name=\"discord_advanced_mode\">Mode lanjutan</string>\n    <string name=\"discord_advanced_mode_description\">Tampilkan opsi kustomisasi tambahan untuk Rich Presence</string>\n    <string name=\"enable\">Aktifkan</string>\n    <string name=\"prevent_duplicate_tracks_in_queue\">Cegah lagu yang sama di antrean</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">Saat menambahkan lagu ke antrean, hapus kemunculannya sebelumnya jika lagu tersebut sudah ada dalam antrean</string>\n    <string name=\"resume_on_bluetooth_connect\">Lanjutkan saat Bluetooth terhubung</string>\n    <string name=\"player_background_solid\">Pekat</string>\n    <string name=\"lyrics_romanize_hindi\">Romanisasikan lirik Hindi</string>\n    <string name=\"lyrics_romanize_punjabi\">Romanisasikan lirik Punjabi</string>\n    <string name=\"lyrics_romanize_as_main\">Tampilkan lirik yang diromanisasi sebagai utama</string>\n    <string name=\"display_density\">Skala tampilan</string>\n    <string name=\"restart\">Nyalakan ulang</string>\n    <string name=\"restart_required\">Perlu dinyalakan ulang</string>\n    <string name=\"density_restart_message\">Perubahan skala tampilan akan berlaku setelah aplikasi dinyalakan ulang. Ingin menyalakan ulang sekarang?</string>\n    <string name=\"enable_lrclib_desc\">Database lirik sinkron dari komunitas</string>\n    <string name=\"enable_kugou_desc\">Mengambil lirik dari KuGou, platform musik populer asal Cina</string>\n    <string name=\"youtube_music_lyrics_note\">CATATAN: Lirik dari YouTube Music akan otomatis ditampilkan jika lirik lain tidak tersedia. Lirik dari YTM biasanya tidak sinkron.</string>\n    <string name=\"enable_lyricsplus\">Aktifkan LyricsPlus</string>\n    <string name=\"enable_lyricsplus_desc\">Lirik sinkron dari berbagai sumber</string>\n    <string name=\"lyrics_provider_selection\">Pilih Penyedia</string>\n    <string name=\"lyrics_provider_selection_desc\">Pilih penyedia lirik yang ingin diaktifkan</string>\n    <string name=\"lyrics_provider_priority\">Prioritas penyedia lirik</string>\n    <string name=\"lyrics_provider_priority_desc\">Seret untuk mengubah urutan penyedia sesuai preferensi. Posisi lebih atas -&gt; prioritas lebih tinggi.</string>\n    <string name=\"changelog\">Catatan Perubahan</string>\n    <string name=\"changelog_empty\">Tidak ada catatan perubahan</string>\n    <string name=\"github_releases_url\">https://github.com/MetrolistGroup/Metrolist/releases</string>\n    <string name=\"list_bullet\">•</string>\n    <string name=\"view_on_github\">Lihat di GitHub</string>\n    <string name=\"current_version\">Versi saat ini</string>\n    <string name=\"version_format\">Versi: %s</string>\n    <string name=\"update_settings\">Perbarui pengaturan</string>\n    <string name=\"check_for_updates_title\">Periksa pembaruan</string>\n    <string name=\"checking_for_updates\">Sedang memeriksa pembaruan…</string>\n    <string name=\"latest_version_format\">Versi terbaru: %s</string>\n    <string name=\"check_for_updates_button\">Periksa pembaruan</string>\n    <string name=\"hide_changelog\">Sembunyikan catatan perubahan</string>\n    <string name=\"view_changelog\">Lihat catatan perubahan</string>\n    <string name=\"failed_to_check_updates\">Gagal memeriksa pembaruan: %s</string>\n    <string name=\"set_as_default\">Jadikan default</string>\n    <string name=\"sleep_timer_default_set\">Timer tidur default diatur ke %d menit</string>\n    <string name=\"enable_automatic_sleeptimer\">Aktifkan pengatur waktu tidur otomatis</string>\n    <string name=\"sleeptimer_description\">Mengaktifkan pengatur waktu tidur secara otomatis dengan nilai default berdasarkan waktu yang ditentukan</string>\n    <string name=\"sleep_timer_repeat_description\">Tetapkan hari dan waktu khusus saat pengatur waktu tidur harus aktif secara otomatis</string>\n    <string name=\"sleep_timer_repeat\">Ulangi</string>\n    <string name=\"sleep_timer_daily\">Setiap hari</string>\n    <string name=\"sleep_timer_weekdays\">Senin hingga Jumat</string>\n    <string name=\"sleep_timer_weekdays_weekends\">Hari kerja / Akhir pekan</string>\n    <string name=\"sleep_timer_weekends\">Akhir pekan (Sab–Min)</string>\n    <string name=\"sleep_timer_custom\">Kustom</string>\n    <string name=\"sleep_timer_start_time\">Waktu mulai</string>\n    <string name=\"sleep_timer_end_time\">Waktu selesai</string>\n    <string name=\"sleep_timer_monday\">Senin</string>\n    <string name=\"sleep_timer_tuesday\">Selasa</string>\n    <string name=\"sleep_timer_wednesday\">Rabu</string>\n    <string name=\"sleep_timer_thursday\">Kamis</string>\n    <string name=\"sleep_timer_friday\">Jumat</string>\n    <string name=\"sleep_timer_saturday\">Sabtu</string>\n    <string name=\"sleep_timer_sunday\">Minggu</string>\n    <string name=\"sleep_timer_stop_after_current_song\">Berhenti di akhir lagu saat ini ketika pengatur waktu selesai</string>\n    <string name=\"sleep_timer_fade_out\">Redam suara di menit terakhir</string>\n    <string name=\"found_in_settings_content\">Tersedia di Pengaturan &gt; Konten</string>\n    <string name=\"plays\">diputar</string>\n    <string name=\"error_episode_save\">Gagal menyimpan episode</string>\n    <string name=\"error_episode_remove\">Gagal menghapus episode</string>\n    <string name=\"error_podcast_subscribe\">Gagal berlangganan podcast</string>\n    <string name=\"error_podcast_unsubscribe\">Gagal berhenti berlangganan podcast</string>\n    <string name=\"view_channel\">Lihat Saluran</string>\n    <string name=\"widget_recognizer_name\">Pengenal Musik</string>\n    <string name=\"widget_recognizer_description\">Identifikasi lagu yang diputar di sekitar Anda langsung dari layar beranda</string>\n    <string name=\"widget_recognizer_tap_to_search\">Ketuk untuk mengidentifikasi lagu</string>\n    <string name=\"widget_recognizer_listening\">Mendengarkan…</string>\n    <string name=\"widget_recognizer_processing\">Mengidentifikasi…</string>\n    <string name=\"widget_recognizer_no_match\">Tidak ada kecocokan. Coba lagi</string>\n    <string name=\"widget_recognizer_error\">Pengenalan gagal</string>\n    <string name=\"widget_recognizer_error_generic\">Terjadi kesalahan. Silakan coba lagi</string>\n    <string name=\"widget_recognizer_unknown_song\">Lagu tidak dikenal</string>\n    <string name=\"widget_recognizer_unknown_artist\">Artis tidak dikenal</string>\n    <string name=\"widget_recognizer_mic_desc\">Identifikasi lagu</string>\n    <string name=\"widget_recognizer_channel_name\">Pengenalan Musik</string>\n    <string name=\"widget_recognizer_channel_desc\">Menampilkan notifikasi saat mengidentifikasi lagu dari widget</string>\n    <string name=\"widget_recognizer_notification_text\">Merekam audio untuk mengidentifikasi lagu…</string>\n    <string name=\"listen_together_auto_approval_suggestions\">Setujui otomatis saran lagu</string>\n    <string name=\"listen_together_auto_approval_suggestions_desc\">Secara otomatis menyetujui dan mengantrekan saran lagu dari tamu</string>\n    <string name=\"importing_playlist\">Mengimpor Daftar Putar</string>\n    <string name=\"speed_dial\">Panggilan cepat</string>\n    <string name=\"pin_to_speed_dial\">Sematkan ke Panggilan cepat</string>\n    <string name=\"unpin_from_speed_dial\">Lepas dari Panggilan cepat</string>\n    <string name=\"randomize_home_order\">Acak Urutan Layar Beranda</string>\n    <string name=\"randomize_home_order_desc\">Mengacak ulang bagian layar beranda secara acak dengan prioritas berbobot</string>\n    <string name=\"daily_discover_sounds_like\">Terdengar seperti %1$s</string>\n    <string name=\"daily_discover_because_you_listen_to\">Karena Anda mendengarkan %1$s</string>\n    <string name=\"daily_discover_similar_to\">Mirip dengan %1$s</string>\n    <string name=\"daily_discover_based_on\">Berdasarkan %1$s</string>\n    <string name=\"daily_discover_for_fans_of\">Untuk penggemar %1$s</string>\n    <string name=\"from_the_community\">Dari komunitas</string>\n    <string name=\"logout_dialog_title\">Simpan data perpustakaan?</string>\n    <string name=\"logout_dialog_message\">Apakah Anda ingin menyimpan daftar putar dan data perpustakaan Anda? Lagu yang diunduh akan tetap disimpan.</string>\n    <string name=\"logout_keep\">Simpan</string>\n    <string name=\"logout_clear\">Hapus</string>\n    <string name=\"credits_lead_developer\">Pengembang Utama</string>\n    <string name=\"credits_collaborator\">Kolaborator</string>\n    <string name=\"credits_collaborators_section\">Para Kolaborator</string>\n    <string name=\"credits_license_name\">Lisensi Publik Umum GNU v3.0</string>\n    <string name=\"credits_license_desc\">Perangkat lunak gratis dan sumber terbuka. Anda boleh menggunakan, mempelajari, berbagi, dan meningkatkannya.</string>\n    <string name=\"credits_discord\">Server Discord</string>\n    <string name=\"credits_telegram\">Saluran Telegram</string>\n    <string name=\"credits_website\">Situs Web</string>\n    <string name=\"credits_instagram\">Instagram</string>\n    <string name=\"credits_github\">GitHub</string>\n    <string name=\"credits_view_repo\">Lihat Repositori</string>\n    <string name=\"app_version_info\">\"%1$s -  %2$s\"</string>\n    <string name=\"like_what_i_do\">Suka dengan yang saya lakukan?</string>\n    <string name=\"buy_mo_a_coffee\">Belikan saya kopi</string>\n    <string name=\"community_and_info\">Komunitas &amp; Info</string>\n    <string name=\"metrolist\">METROLIST</string>\n    <string name=\"wanna_play_favorite_song\">Ingin memutar lagu favorit mereka?</string>\n    <string name=\"yeah\">Ya</string>\n    <string name=\"stands_with_palestine\">Proyek ini berpihak pada Palestina 🇵🇸</string>\n    <string name=\"filter_podcasts\">Podcast</string>\n    <string name=\"view_podcast\">Lihat podcast</string>\n    <string name=\"podcast_channels\">Saluran Podcast</string>\n    <string name=\"latest_episodes\">Episode Terbaru</string>\n    <string name=\"your_shows\">Acara Anda</string>\n    <string name=\"new_episodes\">Episode Baru</string>\n    <string name=\"episodes_for_later\">Episode untuk Nanti</string>\n    <string name=\"save_episode_for_later\">Simpan untuk nanti</string>\n    <string name=\"save_episode_for_later_desc\">Tambahkan ke daftar putar Episode untuk Nanti Anda</string>\n    <string name=\"remove_episode_from_saved\">Hapus dari tersimpan</string>\n    <string name=\"subscribe_to_podcast\">Simpan podcast ke perpustakaan</string>\n    <plurals name=\"n_episode\">\n        <item quantity=\"other\">%d episode</item>\n    </plurals>\n    <string name=\"filter_episodes\">Episode</string>\n    <string name=\"filter_profiles\">Profil</string>\n    <string name=\"filter_channels\">Saluran</string>\n    <string name=\"auto_playlist\">Daftar putar otomatis</string>\n    <string name=\"downloaded_episodes\">Episode yang diunduh</string>\n    <string name=\"no_subscribed_channels\">Tidak ada saluran yang diikuti</string>\n    <string name=\"no_downloaded_episodes\">Tidak ada episode yang diunduh</string>\n    <plurals name=\"n_channel\">\n        <item quantity=\"other\">%d saluran</item>\n    </plurals>\n    <string name=\"restore_confirm_title\">Pulihkan cadangan?</string>\n    <string name=\"restore_confirm_message\">Ini akan memulihkan data aplikasi Anda dari cadangan.</string>\n    <string name=\"restore_account_warning\">Anda perlu masuk kembali setelah pemulihan. Akun berikut akan keluar:</string>\n    <string name=\"restore\">Pulihkan</string>\n    <string name=\"checking_previous_account\">Memeriksa akun sebelumnya…</string>\n    <string name=\"no_account_found\">Tidak ada akun yang ditemukan</string>\n    <string name=\"upload_songs\">Unggah lagu</string>\n    <string name=\"uploading\">Mengunggah…</string>\n    <string name=\"upload_progress\">%1$d dari %2$d</string>\n    <string name=\"upload_complete\">Unggahan selesai</string>\n    <string name=\"upload_failed\">Unggahan gagal</string>\n    <string name=\"upload_file_too_large\">File terlalu besar (maks 300MB)</string>\n    <string name=\"upload_unsupported_format\">Format tidak didukung. Gunakan mp3, m4a, wma, flac, atau ogg</string>\n    <string name=\"delete_uploaded_song\">Hapus lagu yang diunggah</string>\n    <string name=\"delete_uploaded_song_confirm\">Apakah Anda yakin ingin menghapus lagu yang diunggah ini? Tindakan ini tidak dapat dibatalkan.</string>\n    <string name=\"delete_uploaded_song_success\">Lagu yang diunggah telah dihapus</string>\n    <string name=\"delete_uploaded_song_failed\">Gagal menghapus lagu yang diunggah</string>\n    <string name=\"delete_uploaded_songs\">Hapus lagu yang diunggah</string>\n    <string name=\"delete_uploaded_songs_confirm\">Apakah Anda yakin ingin menghapus %1$d lagu yang diunggah? Tindakan ini tidak dapat dibatalkan.</string>\n    <string name=\"deleted_n_songs\">%1$d lagu dihapus</string>\n    <string name=\"deleting\">Menghapus…</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-in/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Beranda</string>\n    <string name=\"songs\">Lagu</string>\n    <string name=\"artists\">Artis</string>\n    <string name=\"albums\">Album</string>\n    <string name=\"playlists\">Daftar putar</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"other\">%d dipilih</item>\n    </plurals>\n    <string name=\"history\">Riwayat</string>\n    <string name=\"stats\">Statistik</string>\n    <string name=\"mood_and_genres\">Suasana Hati dan Genre</string>\n    <string name=\"account\">Akun</string>\n    <string name=\"quick_picks\">Pilihan cepat</string>\n    <string name=\"quick_picks_empty\">Dengarkan lagu untuk menghasilkan pilihan cepat Anda</string>\n    <string name=\"new_release_albums\">Album yang baru dirilis</string>\n    <string name=\"today\">Hari ini</string>\n    <string name=\"yesterday\">Kemarin</string>\n    <string name=\"this_week\">Minggu ini</string>\n    <string name=\"last_week\">Minggu lalu</string>\n    <string name=\"most_played_songs\">Lagu yang paling sering diputar</string>\n    <string name=\"most_played_artists\">Artis yang paling sering diputar</string>\n    <string name=\"most_played_albums\">Album yang paling sering diputar</string>\n    <string name=\"search\">Mencari</string>\n    <string name=\"search_yt_music\">Cari di YouTube Music…</string>\n    <string name=\"search_library\">Cari di pustaka…</string>\n    <string name=\"filter_library\">Pustaka</string>\n    <string name=\"filter_liked\">Disukai</string>\n    <string name=\"filter_downloaded\">Diunduh</string>\n    <string name=\"filter_all\">Semua</string>\n    <string name=\"filter_songs\">Lagu</string>\n    <string name=\"filter_videos\">Video</string>\n    <string name=\"filter_albums\">Album</string>\n    <string name=\"filter_artists\">Artis</string>\n    <string name=\"filter_playlists\">Daftar putar</string>\n    <string name=\"filter_community_playlists\">Daftar putar komunitas</string>\n    <string name=\"filter_featured_playlists\">Daftar putar unggulan</string>\n    <string name=\"filter_bookmarked\">Ditandai</string>\n    <string name=\"no_results_found\">Tidak ada hasil yang ditemukan</string>\n    <string name=\"from_your_library\">Dari pustaka Anda</string>\n    <string name=\"liked_songs\">Lagu yang disukai</string>\n    <string name=\"downloaded_songs\">Lagu yang diunduh</string>\n    <string name=\"playlist_is_empty\">Daftar putar kosong</string>\n    <string name=\"retry\">Coba lagi</string>\n    <string name=\"radio\">Radio</string>\n    <string name=\"shuffle\">Acak</string>\n    <string name=\"reset\">Atur ulang</string>\n    <string name=\"details\">Rincian</string>\n    <string name=\"edit\">Ubah</string>\n    <string name=\"start_radio\">Mulai radio</string>\n    <string name=\"play\">Putar</string>\n    <string name=\"play_next\">Putar berikutnya</string>\n    <string name=\"add_to_queue\">Tambahkan ke antrean</string>\n    <string name=\"add_to_library\">Tambahkan ke pustaka</string>\n    <string name=\"remove_from_library\">Hapus dari pustaka</string>\n    <string name=\"action_download\">Unduh</string>\n    <string name=\"downloading\">Mengunduh</string>\n    <string name=\"remove_download\">Hapus unduhan</string>\n    <string name=\"import_playlist\">Impor daftar putar</string>\n    <string name=\"add_to_playlist\">Tambahkan ke daftar putar</string>\n    <string name=\"view_artist\">Lihat artis</string>\n    <string name=\"view_album\">Lihat album</string>\n    <string name=\"refetch\">Ambil ulang</string>\n    <string name=\"share\">Bagikan</string>\n    <string name=\"delete\">Hapus</string>\n    <string name=\"remove_from_history\">Hapus dari riwayat</string>\n    <string name=\"search_online\">Cari online</string>\n    <string name=\"action_sync\">Sinkron</string>\n    <string name=\"advanced\">Lanjutan</string>\n    <string name=\"sort_by_create_date\">Tanggal ditambahkan</string>\n    <string name=\"sort_by_name\">Nama</string>\n    <string name=\"sort_by_artist\">Artis</string>\n    <string name=\"sort_by_year\">Tahun</string>\n    <string name=\"sort_by_song_count\">Jumlah lagu</string>\n    <string name=\"sort_by_length\">Durasi</string>\n    <string name=\"sort_by_play_time\">Waktu pemutaran</string>\n    <string name=\"sort_by_custom\">Urutan khusus</string>\n    <string name=\"media_id\">ID media</string>\n    <string name=\"mime_type\">Jenis MIME</string>\n    <string name=\"codecs\">Kodek</string>\n    <string name=\"bitrate\">Laju bit</string>\n    <string name=\"sample_rate\">Laju sampel</string>\n    <string name=\"loudness\">Kelantangan</string>\n    <string name=\"volume\">Volume</string>\n    <string name=\"file_size\">Ukuran berkas</string>\n    <string name=\"unknown\">Tidak diketahui</string>\n    <string name=\"copied\">Disalin ke papan klip</string>\n    <string name=\"edit_lyrics\">Ubah lirik</string>\n    <string name=\"search_lyrics\">Cari lirik</string>\n    <string name=\"edit_song\">Ubah lagu</string>\n    <string name=\"song_title\">Judul lagu</string>\n    <string name=\"song_artists\">Artis lagu</string>\n    <string name=\"error_song_title_empty\">Judul lagu tidak boleh kosong.</string>\n    <string name=\"error_song_artist_empty\">Artis lagu tidak boleh kosong.</string>\n    <string name=\"save\">Simpan</string>\n    <string name=\"choose_playlist\">Pilih daftar putar</string>\n    <string name=\"edit_playlist\">Ubah daftar putar</string>\n    <string name=\"create_playlist\">Buat daftar putar</string>\n    <string name=\"playlist_name\">Nama daftar putar</string>\n    <string name=\"error_playlist_name_empty\">Nama daftar putar tidak boleh kosong.</string>\n    <string name=\"edit_artist\">Ubah artis</string>\n    <string name=\"artist_name\">Nama artis</string>\n    <string name=\"error_artist_name_empty\">Nama artis tidak boleh kosong.</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"other\">%d lagu</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"other\">%d artis</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"other\">%d album</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"other\">%d daftar putar</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"other\">%d minggu</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"other\">%d bulan</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"other\">%d tahun</item>\n    </plurals>\n    <string name=\"playlist_imported\">Daftar putar yang diimpor</string>\n    <string name=\"removed_song_from_playlist\">Menghapus \\\"%s\\\" dari daftar putar</string>\n    <string name=\"playlist_synced\">Daftar putar disinkronkan</string>\n    <string name=\"undo\">Batalkan</string>\n    <string name=\"lyrics_not_found\">Lirik tidak ditemukan</string>\n    <string name=\"sleep_timer\">Pengatur waktu tidur</string>\n    <string name=\"end_of_song\">Akhir lagu</string>\n    <plurals name=\"minute\">\n        <item quantity=\"other\">%d menit</item>\n    </plurals>\n    <string name=\"error_no_stream\">Tidak ada aliran yang tersedia</string>\n    <string name=\"error_no_internet\">Tidak ada sambungan jaringan</string>\n    <string name=\"error_timeout\">Waktu habis</string>\n    <string name=\"error_unknown\">Terjadi kesalahan yang tidak diketahui</string>\n    <string name=\"action_like\">Suka</string>\n    <string name=\"action_remove_like\">Hapus suka</string>\n    <string name=\"action_shuffle_on\">Acak diaktifkan</string>\n    <string name=\"action_shuffle_off\">Acak tidak diaktifkan</string>\n    <string name=\"repeat_mode_off\">Mode ulangi tidak diaktifkan</string>\n    <string name=\"repeat_mode_one\">Ulangi lagu saat ini</string>\n    <string name=\"repeat_mode_all\">Ulangi antrean</string>\n    <string name=\"queue_all_songs\">Semua lagu</string>\n    <string name=\"queue_searched_songs\">Lagu yang dicari</string>\n    <string name=\"music_player\">Pemutar Musik</string>\n    <string name=\"settings\">Pengaturan</string>\n    <string name=\"appearance\">Tampilan</string>\n    <string name=\"enable_dynamic_theme\">Aktifkan tema dinamis</string>\n    <string name=\"dark_theme\">Tema gelap</string>\n    <string name=\"dark_theme_on\">Aktif</string>\n    <string name=\"dark_theme_off\">Nonaktif</string>\n    <string name=\"dark_theme_follow_system\">Ikuti sistem</string>\n    <string name=\"pure_black\">Hitam murni</string>\n    <string name=\"default_open_tab\">Tab terbuka bawaan</string>\n    <string name=\"customize_navigation_tabs\">Sesuaikan tab navigasi</string>\n    <string name=\"lyrics_text_position\">Posisi teks lirik</string>\n    <string name=\"left\">Kiri</string>\n    <string name=\"center\">Tengah</string>\n    <string name=\"right\">Kanan</string>\n    <string name=\"content\">Konten</string>\n    <string name=\"login\">Masuk</string>\n    <string name=\"content_language\">Bahasa konten bawaan</string>\n    <string name=\"content_country\">Negara konten bawaan</string>\n    <string name=\"system_default\">Bawaan sistem</string>\n    <string name=\"enable_proxy\">Aktifkan proksi</string>\n    <string name=\"proxy_type\">Jenis proksi</string>\n    <string name=\"proxy_url\">URL proksi</string>\n    <string name=\"restart_to_take_effect\">Mulai ulang untuk menerapkan perubahan</string>\n    <string name=\"player_and_audio\">Pemutar dan audio</string>\n    <string name=\"audio_quality\">Kualitas audio</string>\n    <string name=\"audio_quality_auto\">Otomatis</string>\n    <string name=\"audio_quality_high\">Tinggi</string>\n    <string name=\"audio_quality_low\">Rendah</string>\n    <string name=\"persistent_queue\">Antrean yang berkelanjutan</string>\n    <string name=\"skip_silence\">Lewati keheningan</string>\n    <string name=\"audio_normalization\">Normalisasi audio</string>\n    <string name=\"equalizer\">Ekualiser</string>\n    <string name=\"storage\">Penyimpanan</string>\n    <string name=\"cache\">Tembolok</string>\n    <string name=\"image_cache\">Tembolok Gambar</string>\n    <string name=\"song_cache\">Tembolok Lagu</string>\n    <string name=\"max_cache_size\">Batas maksimum ukuran tembolok</string>\n    <string name=\"unlimited\">Tak terbatas</string>\n    <string name=\"clear_all_downloads\">Bersihkan semua unduhan</string>\n    <string name=\"max_image_cache_size\">Batas maksimum ukuran tembolok gambar</string>\n    <string name=\"clear_image_cache\">Bersihkan tembolok gambar</string>\n    <string name=\"max_song_cache_size\">Batas maksimum ukuran tembolok lagu</string>\n    <string name=\"clear_song_cache\">Bersihkan tembolok lagu</string>\n    <string name=\"size_used\">%s digunakan</string>\n    <string name=\"privacy\">Privasi</string>\n    <string name=\"pause_listen_history\">Jeda riwayat mendengarkan</string>\n    <string name=\"clear_listen_history\">Bersihkan riwayat mendengarkan</string>\n    <string name=\"clear_listen_history_confirm\">Apakah Anda yakin ingin menghapus semua riwayat mendengarkan?</string>\n    <string name=\"pause_search_history\">Jeda riwayat pencarian</string>\n    <string name=\"clear_search_history\">Bersihkan riwayat pencarian</string>\n    <string name=\"clear_search_history_confirm\">Apakah anda yakin ingin menghapus semua riwayat pencarian?</string>\n    <string name=\"enable_kugou\">Aktifkan penyedia lirik KuGou</string>\n    <string name=\"backup_restore\">Cadangkan dan pulihkan</string>\n    <string name=\"action_backup\">Cadangkan</string>\n    <string name=\"action_restore\">Pulihkan</string>\n    <string name=\"imported_playlist\">Daftar putar yang diimpor</string>\n    <string name=\"backup_create_success\">Cadangan berhasil dibuat</string>\n    <string name=\"backup_create_failed\">Tidak dapat membuat cadangan</string>\n    <string name=\"restore_failed\">Gagal memulihkan cadangan</string>\n    <string name=\"about\">Tentang</string>\n    <string name=\"app_version\">Versi aplikasi</string>\n    <string name=\"new_version_available\">Versi baru tersedia</string>\n    <string name=\"translation_models\">Model Penerjemahan</string>\n    <string name=\"clear_translation_models\">Hapus model penerjemahan</string>\n    <string name=\"forgotten_favorites\">Favorit yang terlupakan</string>\n    <string name=\"keep_listening\">Tetap mendengarkan</string>\n    <string name=\"your_youtube_playlists\">Daftar putar Youtube Anda</string>\n    <string name=\"similar_to\">Serupa dengan</string>\n    <string name=\"other_versions\">Versi lainnya</string>\n    <string name=\"add_all_to_library\">Tambahkan semua ke pustaka</string>\n    <string name=\"delete_playlist_confirm\">Apakah Anda benar-benar ingin menghapus daftar putar \\\"%s\\\"?</string>\n    <string name=\"library_song_empty\">Pustaka lagu akan tampil di sini</string>\n    <string name=\"library_artist_empty\">Pustaka artis akan tampil di sini</string>\n    <string name=\"library_album_empty\">Pustaka album akan tampil di sini</string>\n    <string name=\"library_playlist_empty\">Daftar putar Anda akan tampil di sini</string>\n    <string name=\"remove_download_playlist_confirm\">Apakah Anda benar-benar ingin menghapus semua lagu daftar putar \\\"%s\\\" dari penyimpanan Lagu yang Diunduh?</string>\n    <string name=\"remove_all_from_library\">Hapus semua dari pustaka</string>\n    <string name=\"remove_from_playlist\">Hapus dari daftar putar</string>\n    <string name=\"remove_from_queue\">Hapus dari antrean</string>\n    <string name=\"persistent_queue_desc\">Pulihkan antrean terakhir Anda saat aplikasi dimulai</string>\n    <string name=\"queue\">Antrean</string>\n    <string name=\"hide_explicit\">Sembunyikan konten vulgar</string>\n    <string name=\"auto_skip_next_on_error\">Lewati otomatis ke lagu berikutnya ketika terjadi kesalahan</string>\n    <string name=\"sided\">Samping</string>\n    <string name=\"options\">Pilihan</string>\n    <string name=\"login_failed\">Gagal masuk</string>\n    <string name=\"add_anyway\">Tambahkan saja</string>\n    <string name=\"action_logout\">Keluar</string>\n    <string name=\"misc\">Lainnya</string>\n    <string name=\"tempo_and_pitch\">Kecepatan dan Nada</string>\n    <string name=\"duplicates\">Duplikat</string>\n    <string name=\"skip_duplicates\">Lewati duplikat</string>\n    <string name=\"duplicates_description_single\">Lagu tersebut sudah ada di dalam daftar putar Anda</string>\n    <string name=\"duplicates_description_multiple\">%d lagu sudah ada di dalam daftar putar Anda</string>\n    <string name=\"theme\">Tema</string>\n    <string name=\"action_like_all\">Sukai semua</string>\n    <string name=\"action_remove_like_all\">Hapus semua yang disukai</string>\n    <string name=\"player_text_alignment\">Perataan teks pemutar</string>\n    <string name=\"player_slider_style\">Gaya penggeser pemutar</string>\n    <string name=\"player\">Pemutar</string>\n    <string name=\"default_\">Bawaan</string>\n    <string name=\"squiggly\">Bergelombang</string>\n    <string name=\"grid_cell_size\">Ukuran sel kisi</string>\n    <string name=\"small\">Kecil</string>\n    <string name=\"big\">Besar</string>\n    <string name=\"auto_load_more\">Memuat lebih banyak lagu secara otomatis</string>\n    <string name=\"not_logged_in\">Tidak masuk</string>\n    <string name=\"auto_load_more_desc\">Otomatis menambahkan lebih banyak lagu ketika akhir antrean tercapai, jika memungkinkan</string>\n    <string name=\"auto_skip_next_on_error_desc\">Memastikan pengalaman pemutaran Anda yang berkesinambungan</string>\n    <string name=\"stop_music_on_task_clear\">Hentikan musik saat menghapus tugas</string>\n    <string name=\"listen_history\">Riwayat mendengarkan</string>\n    <string name=\"search_history\">Riwayat pencarian</string>\n    <string name=\"disable_screenshot\">Nonaktifkan tangkapan layar</string>\n    <string name=\"disable_screenshot_desc\">Ketika pilihan ini diaktifkan, tangkapan layar dan tampilan aplikasi di Terkini dinonaktifkan.</string>\n    <string name=\"enable_lrclib\">Aktifkan penyedia lirik LrcLib</string>\n    <string name=\"discord_integration\">Integrasi Discord</string>\n    <string name=\"discord_information\">Metrolist menggunakan pustaka KizzyRPC untuk mengatur status akun Discord Anda. Hal ini melibatkan penggunaan sambungan Discord Gateway, yang dapat dianggap sebagai pelanggaran TOS Discord. Namun, tidak ada kasus yang diketahui tentang akun pengguna yang ditangguhkan karena alasan ini. Gunakan dengan risiko Anda sendiri.\\n\\nMetrolist hanya akan mengekstrak token Anda, dan yang lainnya disimpan secara lokal.</string>\n    <string name=\"dismiss\">Singkirkan</string>\n    <string name=\"preview\">Pratinjau</string>\n    <string name=\"enable_discord_rpc\">Aktifkan Rich Presence</string>\n    <string name=\"use_login_for_browse\">Masuk untuk menjelajahi konten</string>\n    <string name=\"use_login_for_browse_desc\">Hal ini dapat memengaruhi konten yang Anda lihat dan misalnya menampilkan album khusus premium jika Anda masuk dengan akun Premium</string>\n    <string name=\"action_login\">Masuk</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-it/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">Locale</string>\n    <string name=\"remote_history\">Remoto</string>\n    <string name=\"charts\">Classifiche</string>\n    <string name=\"back_button_desc\">Indietro</string>\n    <string name=\"album_cover_desc\">Cover dell\\'album</string>\n    <string name=\"top_music_videos\">I migliori video musicali</string>\n    <string name=\"trending\">Di tendenza</string>\n    <string name=\"weeks\">Settimane</string>\n    <string name=\"months\">Mesi</string>\n    <string name=\"years\">Anni</string>\n    <string name=\"continuous\">Continuo</string>\n    <string name=\"liked\">Piaciuti</string>\n    <string name=\"offline\">Scaricati</string>\n    <string name=\"my_top\">I miei Top</string>\n    <string name=\"cached_playlist\">In cache</string>\n    <string name=\"sync_playlist\">Sincronizza playlist</string>\n    <string name=\"sync_disabled\">Sincronizzazione disabilitata</string>\n    <string name=\"allows_for_sync_witch_youtube\">Nota: questo attiva la sincronizzazione con YouTube Music. Tale azione NON è modificabile successivamente.</string>\n    <string name=\"remove_from_cache\">Rimuovi dalla cache</string>\n    <string name=\"copy_link\">Copia link</string>\n    <string name=\"select\">Seleziona tutti</string>\n    <string name=\"like_all\">Mi piace tutto</string>\n    <string name=\"dislike_all\">Togli Mi piace a tutto</string>\n    <string name=\"sort_by_last_updated\">Data di aggiornamento</string>\n    <string name=\"link_copied\">Link copiato negli appunti</string>\n    <string name=\"lyrics\">Testo della canzone</string>\n    <string name=\"already_in_playlist\">Già nella playlist:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d volta</item>\n        <item quantity=\"many\">%d volte</item>\n        <item quantity=\"other\">%d volte</item>\n    </plurals>\n    <string name=\"similar_content\">Contenuto simile</string>\n    <string name=\"player_background_style\">Sfondo del lettore</string>\n    <string name=\"follow_theme\">Segui il tema</string>\n    <string name=\"gradient\">Gradiente</string>\n    <string name=\"player_background_blur\">Sfuma</string>\n    <string name=\"player_buttons_style\">Colori dei pulsanti del riproduttore</string>\n    <string name=\"default_style\">Predefiniti</string>\n    <string name=\"enable_swipe_thumbnail\">Attiva la possibilità di cambiare brano scorrendo sulla copertina</string>\n    <string name=\"swipe_song_to_add\">Scorri il brano verso sinistra per aggiungerlo alla coda o verso destra per riprodurlo come successivo</string>\n    <string name=\"lyrics_click_change\">Cambia i testi con il tocco</string>\n    <string name=\"slim\">Sottile</string>\n    <string name=\"slim_navbar\">Barra inferiore di navigazione sottile</string>\n    <string name=\"auto_playlists\">Playlist automatiche</string>\n    <string name=\"show_liked_playlist\">Mostra la playlist dei brani piaciuti</string>\n    <string name=\"show_downloaded_playlist\">Mostra la playlist dei brani scaricati</string>\n    <string name=\"show_top_playlist\">Mostra la playlist dei migliori brani</string>\n    <string name=\"show_cached_playlist\">Mostra la playlist dei brani in cache</string>\n    <string name=\"advanced_login\">Accedi con il token</string>\n    <string name=\"token_hidden\">Tocca per mostrare il token</string>\n    <string name=\"token_shown\">Tocca ancora per copiarlo o modificarlo</string>\n    <string name=\"token_adv_login_description\">Questo è un metodo AVANZATO per fare l\\'accesso a YouTube Music. Come alternativa al portale web, puoi inserire o aggiornare direttamente il tuo token di accesso qui. Per esempio, questo può velocizzare l\\'accesso su più dispositivi. Tieni presente che qualsiasi formato di token non valido che l\\'app non riesce ad analizzare non sarà accettato</string>\n    <string name=\"general\">Generale</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"default_lib_chips\">Cambia chip predefinito della libreria</string>\n    <string name=\"set_quick_picks\">Imposta le scelte rapide</string>\n    <string name=\"last_song_listened\">Basato sull\\'ultimo brano ascoltato</string>\n    <string name=\"app_language\">Lingua dell\\'app</string>\n    <string name=\"enable_similar_content\">Attiva Contenuto simile</string>\n    <string name=\"similar_content_desc\">Automaticamente aggiunge altri brani simili quando la fine della coda viene raggiunta</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"clear_song_cache_dialog\">Sei sicuro di voler eliminare tutti i brani in cache?</string>\n    <string name=\"clear_downloads_dialog\">Sei sicuro di voler eliminare tutti i brani scaricati?</string>\n    <string name=\"not_logged_in_youtube\">Disconnesso da YouTube</string>\n    <string name=\"default_links\">Apri link supportati</string>\n    <string name=\"open_app_settings_error\">Impossibile aprire le impostazioni dell\\'app</string>\n    <string name=\"release_notes\">Note di pubblicazione</string>\n    <string name=\"all_time\">Da sempre</string>\n    <string name=\"past_24_hours\">Ultime 24 ore</string>\n    <string name=\"past_week\">Ultima settimana</string>\n    <string name=\"past_month\">Ultimo mese</string>\n    <string name=\"past_year\">Ultimo anno</string>\n    <string name=\"top_length\">Lunghezza della mia Top list</string>\n    <string name=\"history_duration\">Durata della cronologia</string>\n    <string name=\"information\">Informazioni</string>\n    <string name=\"description\">Descrizione</string>\n    <string name=\"views\">Visualizzazioni</string>\n    <string name=\"likes\">Piaciuti</string>\n    <string name=\"dislikes\">Non mi piace</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">1 secondo</item>\n        <item quantity=\"many\">%d secondi</item>\n        <item quantity=\"other\">%d secondi</item>\n    </plurals>\n    <string name=\"generating_image\">Generazione dell\\'immagine</string>\n    <string name=\"please_wait\">Prego attendere</string>\n    <string name=\"cancel\">Annulla</string>\n    <string name=\"share_lyrics\">Condividi testo</string>\n    <string name=\"share_as_text\">Condividi come testo</string>\n    <string name=\"share_as_image\">Condividi come immagine</string>\n    <string name=\"max_selection_limit\">Limite massimo di selezione</string>\n    <string name=\"share_selected\">Condividi selezionati</string>\n    <string name=\"customize_colors\">Personalizza i colori</string>\n    <string name=\"text_color\">Colore del testo</string>\n    <string name=\"secondary_text_color\">Colore secondario del testo</string>\n    <string name=\"background_color\">Colore dello sfondo</string>\n    <string name=\"auto_download_on_like\">Scarica automaticamente quando si mette Mi piace</string>\n    <string name=\"auto_download_on_like_desc\">Scarica automaticamente i brani quando si mette Mi piace</string>\n    <string name=\"lyrics_auto_scroll\">Scorri il testo automaticamente</string>\n    <string name=\"import_csv\">Importa una playlist in formato \\\"csv\\\"</string>\n    <string name=\"playlist_add_local_to_synced_note\">Nota: non è supportata l\\'importazione di brani locali in una playlist sincronizzata o remota. Qualsiasi altra combinazione è valida</string>\n    <string name=\"import_online\">Importa una playlist in formato \\\"m3u\\\"</string>\n    <string name=\"more_content\">Altri contenuti</string>\n    <string name=\"lyrics_romanize_japanese\">Romanizza testo in giapponese</string>\n    <string name=\"lyrics_romanize_korean\">Romanizza testo in coreano</string>\n    <string name=\"yt_sync\">Sincronizza automaticamente con l\\'account</string>\n    <string name=\"new_player_design\">Nuovo design del lettore</string>\n    <string name=\"disable\">Disabilita</string>\n    <string name=\"clear_image_cache_dialog\">Sei sicuro di voler cancellare tutte le immagini in cache?</string>\n    <string name=\"swipe_sensitivity\">Sensibilità del gesto per cambiare brano nel mini lettore</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"subscribe\">Iscriviti</string>\n    <string name=\"subscribed\">Iscritto</string>\n    <string name=\"new_mini_player_design\">Nuovo design del mini lettore</string>\n    <string name=\"now_playing\">In riproduzione</string>\n    <string name=\"seek_forward_dynamic\">%1$d secondi avanti</string>\n    <string name=\"seek_backward_dynamic\">%1$d secondi indietro</string>\n    <string name=\"seek_seconds_addup\">Ricerca progressiva</string>\n    <string name=\"seek_seconds_addup_description\">Se attivato, aggiunge 5 secondi in più per ogni salto</string>\n    <string name=\"disable_load_more_when_repeat_all\">Disabilita il caricamento di altri brani quando si ripetono tutti i brani</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Non caricare automaticamente altri brani e contenuti simili quando la modalità Ripeti tutto è attiva</string>\n    <string name=\"close\">Chiudi</string>\n    <string name=\"hide_player_thumbnail\">Nascondi miniatura del lettore</string>\n    <string name=\"hide_player_thumbnail_desc\">Sostituisci la copertina dell\\'album con il logo dell\\'app nel lettore</string>\n    <string name=\"settings_section_ui\">Interfaccia</string>\n    <string name=\"settings_section_privacy\">Privacy e Sicurezza</string>\n    <string name=\"settings_section_player_content\">Lettore e Contenuto</string>\n    <string name=\"settings_section_storage\">Archiviazione e dati</string>\n    <string name=\"settings_section_system\">Sistema e informazioni</string>\n    <string name=\"starting_radio\">Avvio radio in corso</string>\n    <string name=\"config_proxy\">Configura proxy</string>\n    <string name=\"proxy_username\">Nome utente proxy</string>\n    <string name=\"proxy_password\">Password proxy</string>\n    <string name=\"enable_authentication\">Attiva autentificazione</string>\n    <string name=\"lyrics_romanization_cyrillic\">Cirillico</string>\n    <string name=\"lyrics_romanize_title\">Romanizzazione</string>\n    <string name=\"lyrics_romanization\">Romanizzazione dei testi</string>\n    <string name=\"lyrics_romanize_russian\">Romanizza testo in russo</string>\n    <string name=\"lyrics_romanize_ukrainian\">Romanizza testo in ucraino</string>\n    <string name=\"lyrics_romanize_belarusian\">Romanizza testo in bielorusso</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Romanizza testo in kirghiso</string>\n    <string name=\"lyrics_romanize_serbian\">Romanizza testo in serbo</string>\n    <string name=\"lyrics_romanize_bulgarian\">Romanizza testo in bulgaro</string>\n    <string name=\"line_by_line_option_title\">SPERIMENTALE: Determina la lingua linea per linea</string>\n    <string name=\"line_by_line_option_desc\">La lingua in cirillico verrà determinata linea per linea e non per tutto il brano.</string>\n    <string name=\"line_by_line_dialog_title\">Sei sicuro?</string>\n    <string name=\"romanize_current_track\">Romanizza attuale brano</string>\n    <string name=\"line_by_line_dialog_desc\">Questa è una funzione sperimentale.\\n\\nPer impostazione predefinita, la lingua è determinata dall\\'intera canzone ma con questa opzione attiva, sarà invece determinata linea per riga. Questo permetterà a canzoni multilingua di essere romanizzate MA la lingua potrebbe non essere sempre corretta (ad esempio se c\\'è un testo in ucraino che non contiene alcuna lettera specifica ucraina, potrebbe essere romanizzato come russo).\\n\\nSe non ne avete necessità, si consiglia di mantenere questa opzione disattivata.</string>\n    <string name=\"edit_playlist_cover\">Modifica la copertina della playlist</string>\n    <string name=\"edit_playlist_cover_note\">Nota: il tuo account deve essere collegato a un numero di telefono ed essere verificato su YouTube Music per modificare la copertina della playlist.</string>\n    <string name=\"edit_playlist_cover_note_wait\">Dopo aver selezionato un\\'immagine, si deve attendere un momento affinché compaia la nuova immagine della tua playlist.</string>\n    <string name=\"choose_from_library\">Scegli dalla galleria</string>\n    <string name=\"remove_custom_image\">Rimuovi immagine personalizzata</string>\n    <string name=\"audio_offload\">Abilita trasferimento</string>\n    <string name=\"audio_offload_description\">Utilizzare il percorso audio di scarico per la riproduzione audio. Disabilitare questo può aumentare l\\'utilizzo dell\\'energia ma può essere utile se si verificano problemi con la riproduzione audio o la post elaborazione</string>\n    <string name=\"filter_uploaded\">Caricato</string>\n    <string name=\"uploaded_playlist\">Caricato</string>\n    <string name=\"show_uploaded_playlist\">Mostra la playlist dei brani caricati</string>\n    <string name=\"discord_use_details\">Usa i dettagli al posto dello stato</string>\n    <string name=\"discord_use_details_description\">Mostra il titolo della canzone prominente al posto dei nomi degli artisti</string>\n    <string name=\"updater\">Aggiornamenti</string>\n    <string name=\"check_for_updates\">Controlla automaticamente se ci sono aggiornamenti</string>\n    <string name=\"update_notifications\">Attiva le notifiche sugli aggiornamenti</string>\n    <string name=\"update_available_title\">Un aggiornamento è disponibile</string>\n    <string name=\"update_channel_name\">Aggiornamenti dell\\'app</string>\n    <string name=\"update_channel_desc\">Notifiche sulle nuove versioni</string>\n    <string name=\"lyrics_romanize_macedonian\">Romanizza testo in macedone</string>\n    <string name=\"enable_scrobbling\">Attiva scrobbling</string>\n    <string name=\"lastfm_now_playing\">Invia l\\'ascolto attuale</string>\n    <string name=\"scrobbling_configuration\">Configurazione dello scrobbling</string>\n    <string name=\"scrobble_min_track_duration\">Invia brani più lunghi di</string>\n    <string name=\"scrobble_delay_percent\">Percentuale del ritardo sull\\'invio</string>\n    <string name=\"scrobble_delay_minutes\">Minuti di ritardo sull\\'invio</string>\n    <string name=\"integrations\">Integrazioni</string>\n    <string name=\"username\">Nome utente</string>\n    <string name=\"password\">Password</string>\n    <string name=\"lastfm_integration\">Integrazione con Last.fm</string>\n    <string name=\"swipe_song_to_remove\">Scorri il brano per rimuoverlo dalla playlist</string>\n    <string name=\"last_fm_send_likes\">Invia Mi piace/Non mi piace</string>\n    <string name=\"last_fm_send_likes_description\">Ama/non amare brani su Last.fm quando (non) vengono apprezzati su Metrolist</string>\n    <string name=\"lyrics_romanize_chinese\">Romanizza testo in cinese</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"google_cast_description\">Attiva l\\'invio dell\\'audio a Chromecast e altri dispositivi</string>\n    <string name=\"hide_video_songs\">Nascondi brani video</string>\n    <string name=\"primary_color_style\">Colore primario</string>\n    <string name=\"auto_scroll\">Risincronizza</string>\n    <string name=\"details_desc\">Visualizza le informazioni del brano</string>\n    <string name=\"edit_desc\">Cambia il titolo o l\\'artista</string>\n    <string name=\"start_radio_desc\">Crea una stazione in base a questo brano</string>\n    <string name=\"play_next_desc\">Aggiungi in cima alla coda</string>\n    <string name=\"add_to_queue_desc\">Aggiungi in fondo alla coda</string>\n    <string name=\"add_to_library_desc\">Salva nella tua libreria</string>\n    <string name=\"download_desc\">Rendi disponibile per l\\'ascolto offline</string>\n    <string name=\"add_to_playlist_desc\">Aggiungi a una delle tue playlist</string>\n    <string name=\"refetch_desc\">Ottieni gli ultimi metadata da YouTube Music</string>\n    <string name=\"share_desc\">Condividi un collegamento con questo elemento</string>\n    <string name=\"delete_desc\">Rimuovi definitivamente questo elemento</string>\n    <string name=\"advanced_desc\">Cambia il tempo e la tonalità del brano</string>\n    <string name=\"equalizer_desc\">Regola l\\'equalizzatore audio</string>\n    <string name=\"enable_dynamic_icon\">Attiva l\\'icona dinamica</string>\n    <string name=\"mini_player\">Mini lettore</string>\n    <string name=\"pure_black_mini_player\">Mini lettore nero puro</string>\n    <string name=\"cache_size_warning_title\">Aspetta!</string>\n    <string name=\"cache_size_warning_message\">Hai scelto un limite alla dimensione della cache più piccola di quella che l\\'app sta usando attualmente (%1$s). Se continui, l\\'app può rimuovere %2$s di cache per arrivare al nuovo limite. Procedere comunque?</string>\n    <string name=\"cache_size_warning_confirm\">Continua</string>\n    <string name=\"tertiary_color_style\">Colore terziario</string>\n    <string name=\"logging_in\">Connessione in corso…</string>\n    <string name=\"download_in_progress_desc\">Lo scaricamento è in corso</string>\n    <string name=\"download_playlist_desc\">Scarica tutti i brani per l\\'ascolto offline</string>\n    <string name=\"remove_download_playlist_desc\">Rimuovi tutti i brani scaricati da questa playlist</string>\n    <string name=\"share_playlist_desc\">Condividi questa playlist con altri</string>\n    <string name=\"delete_playlist_desc\">Rimuovi definitivamente questa playlist</string>\n    <string name=\"sync_playlist_desc\">Sincronizza la playlist con YouTube Music</string>\n    <string name=\"enable_better_lyrics\">Attiva il fornitore di testi Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">Testi sincronizzati alla sillaba per ogni brano. Per karaoke</string>\n    <string name=\"shuffle_playlist_first\">Riproduzione casuale della playlist/album all\\'inizio</string>\n    <string name=\"shuffle_playlist_first_desc\">Durante la riproduzione casuale, riproduci prima tutte le canzoni della playlist/album originale e poi i contenuti simili</string>\n    <string name=\"lyrics_animation_style\">Stile di animazione parola per parola</string>\n    <string name=\"none\">Nessuno</string>\n    <string name=\"fade\">Dissolvenza</string>\n    <string name=\"glow\">Bagliore</string>\n    <string name=\"slide\">Diapositiva</string>\n    <string name=\"karaoke\">Karaoke</string>\n    <string name=\"apple_music_style\">Apple Music</string>\n    <string name=\"lyrics_text_size\">Dimensioni dei testi</string>\n    <string name=\"lyrics_line_spacing\">Interlinea dei testi</string>\n    <string name=\"wrapped_top_5_albums_title\">I tuoi migliori 5 album</string>\n    <string name=\"wrapped_album_listening_time\">Hai ascoltato questo album per %d minuti</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d minuti</string>\n    <string name=\"wrapped_no_data\">Nessun dato</string>\n    <string name=\"wrapped_top_5_artists_title\">I tuoi migliori artisti dell\\'anno</string>\n    <string name=\"wrapped_artist_listening_time\">%d minuti</string>\n    <string name=\"wrapped_top_5_songs_title\">I tuoi migliori brani di quest\\'anno</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">Copertina</string>\n    <string name=\"wrapped_top_artist_title\">Il tuo migliore artista dell\\'anno è</string>\n    <string name=\"show_wrapped_card\">Mostra la schermata Wrapped</string>\n    <string name=\"album_art_for\">Copertina dell\\'album di %s</string>\n    <string name=\"wrapped_total_albums_title\">L\\'hai ascoltato per</string>\n    <string name=\"wrapped_total_albums_subtitle\">album diversi</string>\n    <string name=\"wrapped_top_album_title\">Il tuo album preferito è</string>\n    <string name=\"wrapped_playlist_ready\">La tua playlist personale è pronta</string>\n    <string name=\"wrapped_top_artist_image_content_description\">Immagine del tuo artista preferito</string>\n    <string name=\"wrapped_top_artist_listening_time\">Li hai ascoltati per %d minuti</string>\n    <string name=\"wrapped_top_song_title\">La tua canzone più ascoltata è</string>\n    <string name=\"wrapped_top_song_listening_time\">Hai ascoltato per %d minuti</string>\n    <string name=\"wrapped_total_artists_title\">Hai ascoltato</string>\n    <string name=\"wrapped_total_artists_subtitle\">artisti diversi</string>\n    <string name=\"wrapped_total_songs_title\">Hai ascoltato</string>\n    <string name=\"wrapped_total_songs_subtitle\">canzoni diverse</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_intro_subtitle\">è l\\'ora di vedere cosa hai ascoltato</string>\n    <string name=\"wrapped_intro_button\">cominciamo!</string>\n    <string name=\"wrapped_logo_content_description\">Logo di Metrolist</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"wrapped_ready_title\">IL TUO WRAPPED È PRONTO!</string>\n    <string name=\"wrapped_ready_subtitle\">È l\\'ora di vedere cosa hai amato quest\\'anno.</string>\n    <string name=\"wrapped_thank_you\">Grazie per aver ascoltato</string>\n    <string name=\"wrapped_special_thanks\">Un ringraziamento speciale a MO Agami per aver creato Metrolist</string>\n    <string name=\"wrapped_close\">Chiudi wrapped</string>\n    <string name=\"wrapped_playlist_title\">Il tuo Wrapped %s</string>\n    <string name=\"wrapped_create_playlist\">Crea playlist</string>\n    <string name=\"wrapped_playlist_saved\">Playlist salvata</string>\n    <string name=\"casting_to\">Trasmissione a %s in corso</string>\n    <string name=\"progress_percent\">Progresso %s%%</string>\n    <string name=\"listening_to_metrolist\">Ascoltando Metrolist</string>\n    <string name=\"open\">Apri</string>\n    <string name=\"failed_to_create_image\">Impossibile creare l\\'immagine: %s</string>\n    <string name=\"copied_title\">Titolo copiato</string>\n    <string name=\"copied_artist\">Nome dell\\'artista copiato</string>\n    <string name=\"error_playing\">Errore di riproduzione</string>\n    <string name=\"failed_to_parse_proxy\">Impossibile eseguire il parsing dell\\'URL proxy.</string>\n    <string name=\"lyrics_glow_effect\">Abilita l\\'effetto luminoso per i testi</string>\n    <string name=\"lyrics_glow_effect_desc\">Aggiungi un\\'animazione luminosa e un effetto di rimbalzo ai testi attivi</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"one\">Interfaccia Equalizzatore</item>\n        <item quantity=\"many\">%d profilo</item>\n        <item quantity=\"other\">%d profili</item>\n    </plurals>\n    <string name=\"equalizer_header\">Equalizzatore</string>\n    <string name=\"no_profiles\">Nessun profilo di equalizzazione</string>\n    <string name=\"import_profile\">Importa profilo</string>\n    <string name=\"eq_disabled\">Disabilitato</string>\n    <plurals name=\"band_count\">\n        <item quantity=\"one\">%d banda</item>\n        <item quantity=\"many\">%d bande</item>\n        <item quantity=\"other\">%d bande</item>\n    </plurals>\n    <string name=\"delete_profile_desc\">Elimina profilo</string>\n    <string name=\"delete_profile_confirmation\">Sei sicuro di voler eliminare %1$s? Questa azione non può essere annullata.</string>\n    <string name=\"error_file_read\">Impossibile leggere il file</string>\n    <string name=\"error_file_open\">Impossibile aprire il file: %1$s</string>\n    <string name=\"import_error_title\">Errore di importazione</string>\n    <string name=\"wavy\">Ondulato</string>\n    <string name=\"pause_music_when_media_is_muted\">Metti la musica in pausa quando il media è mutato</string>\n    <string name=\"enable_simpmusic_desc\">Testi automaticamente trovati da Musixmatch e YouTube Transcript</string>\n    <string name=\"enable_simpmusic\">Attiva SimpMusic Lyrics</string>\n    <string name=\"system_equalizer\">Equalizzatore di sistema</string>\n    <string name=\"album_art\">Copertina</string>\n    <string name=\"no_song_playing\">Nessun brano in riproduzione</string>\n    <string name=\"tap_to_open\">Premi per aprire Metrolist</string>\n    <string name=\"previous\">Precedente</string>\n    <string name=\"play_pause\">Play/Pausa</string>\n    <string name=\"next\">Prossimo</string>\n    <string name=\"like\">Mi piace</string>\n    <string name=\"widget_description\">Widget lettore musicale con controlli della riproduzione</string>\n    <string name=\"turntable_widget_description\">Widget musicale circolare con controlli di riproduzione e mi piace</string>\n    <string name=\"remember_shuffle_and_repeat\">Ricorda le opzioni di ripetizione e di riproduzione casuale</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Ricorda le modalità di ripetizione e di riproduzione casuale quando l\\'app viene riavviata</string>\n    <string name=\"about_artist\">Informazioni</string>\n    <string name=\"show_more\">Mostra di più</string>\n    <string name=\"show_less\">Mostra meno</string>\n    <string name=\"artist_page_settings\">Pagina dell\\'artista</string>\n    <string name=\"show_artist_description\">Mostra la descrizione dell\\'artista</string>\n    <string name=\"show_artist_subscriber_count\">Mostra il numero di iscritti</string>\n    <string name=\"show_artist_monthly_listeners\">Mostra gli ascolti mensili</string>\n    <string name=\"skip_silence_desc\">Avanza rapidamente attraverso le parti silenziose dei brani</string>\n    <string name=\"skip_silence_instant\">Salta immediatamente il silenzio</string>\n    <string name=\"skip_silence_instant_desc\">Salta in avanti durante i momenti di silenzio invece di accelerare la riproduzione</string>\n    <string name=\"lyrics_offset\">Tempismo dei testi</string>\n    <string name=\"persistent_shuffle_title\">Riproduzione casuale persistente</string>\n    <string name=\"persistent_shuffle_desc\">Mantieni la riproduzione casuale attiva quando riproduci nuovi brani o playlist</string>\n    <string name=\"error_title\">Errore</string>\n    <string name=\"error_eq_apply_failed\">Impossibile applicare il profilo EQ: %1$s</string>\n    <string name=\"error_playback_failed\">Riproduzione non riuscita</string>\n    <string name=\"crop_album_art\">Ritaglia copertina dell\\'album</string>\n    <string name=\"crop_album_art_desc\">Forza le proporzioni quadrate ritagliando le miniature dei video</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">Mantieni lo schermo acceso quando il lettore è espanso</string>\n    <string name=\"listen_together\">Ascolta insieme</string>\n    <string name=\"listen_together_server_url\">URL del server</string>\n    <string name=\"listen_together_username\">Nome utente</string>\n    <string name=\"listen_together_connected\">Connesso</string>\n    <string name=\"listen_together_reconnecting\">Riconnessione…</string>\n    <string name=\"listen_together_disconnected\">Disconnesso</string>\n    <string name=\"listen_together_connecting\">Connessione…</string>\n    <string name=\"listen_together_error\">Errore di connessione</string>\n    <string name=\"listen_together_create_room\">Crea una stanza</string>\n    <string name=\"listen_together_create_room_desc\">Crea una stanza e condividi il codice con gli amici</string>\n    <string name=\"listen_together_join_room\">Entra nella stanza</string>\n    <string name=\"listen_together_room_code\">Codice della stanza</string>\n    <string name=\"listen_together_you_are_host\">Sei l\\'ospitante</string>\n    <string name=\"listen_together_you_are_guest\">Sei un ospite</string>\n    <string name=\"listen_together_join_requests\">Richieste di ingresso</string>\n    <string name=\"listen_together_view_logs\">Guarda i log</string>\n    <string name=\"listen_together_view_logs_desc\">Debug di connessione e messaggi</string>\n    <string name=\"listen_together_logs\">Log di connessione</string>\n    <string name=\"listen_together_no_logs\">Non ci sono ancora log</string>\n    <string name=\"listen_together_description\">Ascolta musica con gli amici in contemporanea. Crea una stanza per ospitare o entra in una stanza esistente con un codice.</string>\n    <string name=\"listen_together_background_disconnect_note\">Nota: potresti perdere la connessione se crei una stanza mentre nessun brano viene riprodotto e poi passi a un\\'altra app.</string>\n    <string name=\"listen_together_not_configured\">Ascolta insieme non è configurato. Per favore, imposta l\\'URL di un server in Impostazioni → Integrazioni → Ascolta insieme.</string>\n    <string name=\"listen_together_suggestion_received\">%1$s ha richiesto %2$s</string>\n    <string name=\"listen_together_suggestion_sent\">Il suggerimento è stato inviato all\\'ospitante!</string>\n    <string name=\"listen_together_join_request_notification\">%1$s vuole entrare nella stanza</string>\n    <string name=\"listen_together_notification_channel_name\">Ascolta insieme</string>\n    <string name=\"listen_together_notification_channel_desc\">Notifiche per Ascolta insieme</string>\n    <string name=\"listen_together_room_created\">Stanza creata: %s</string>\n    <string name=\"listen_together_cannot_edit_username_in_room\">Impossibile modificare il nome utente mentre si è in una stanza</string>\n    <string name=\"waiting_for_approval\">In attesa di approvazione dell\\'ospitante</string>\n    <string name=\"invalid_room_code\">Codice stanza non valido</string>\n    <string name=\"join_request_denied\">Richiesta di ingresso negata</string>\n    <string name=\"join_existing_room\">Entra in una stanza esistente</string>\n    <string name=\"room_code\">Codice stanza</string>\n    <string name=\"leave_room\">Abbandona stanza</string>\n    <string name=\"join_room\">Entra</string>\n    <string name=\"create_room\">Crea</string>\n    <string name=\"joining_room\">Ingresso nella stanza %s…</string>\n    <string name=\"creating_room\">Creazione della stanza…</string>\n    <string name=\"connect\">Connetti</string>\n    <string name=\"disconnect\">Disconnettiti</string>\n    <string name=\"create\">Crea</string>\n    <string name=\"join\">Entra</string>\n    <string name=\"approve\">Approva</string>\n    <string name=\"reject\">Rifiuta</string>\n    <string name=\"clear\">Rimuovi</string>\n    <string name=\"copy\">Copia</string>\n    <string name=\"copied_to_clipboard\">Copiato negli appunti</string>\n    <string name=\"not_set\">Non impostato</string>\n    <string name=\"in_room\">Nella stanza</string>\n    <string name=\"pending_requests\">Richieste in sospeso</string>\n    <string name=\"pending_suggestions\">Suggerimenti in sospeso</string>\n    <string name=\"suggest_to_host\">Suggerisci all\\'ospitante</string>\n    <string name=\"kick_user\">Caccia</string>\n    <string name=\"host_label\">Ospitante</string>\n    <string name=\"you_label\">Tu</string>\n    <string name=\"connected_users\">Utenti connessi</string>\n    <string name=\"enter_username\">Inserisci nome utente</string>\n    <string name=\"error_username_empty\">Il nome utente è obbligatorio.</string>\n    <string name=\"resync\">Risincronizza</string>\n    <string name=\"hosting_room\">Stanza ospitante</string>\n    <string name=\"mute\">Silenzia</string>\n    <string name=\"unmute\">Riattiva audio</string>\n    <string name=\"crash_title\">Applicazione bloccata</string>\n    <string name=\"crash_description\">Si è verificato un errore inaspettato. Si prega di condividere il rapporto per aiutarci a risolvere il problema.</string>\n    <string name=\"crash_share_logs\">Condividi i Log</string>\n    <string name=\"crash_share_title\">Condividi rapporto di errore</string>\n    <string name=\"crash_report_subject\">Rapporto di Errore Metrolist</string>\n    <string name=\"crash_close\">Chiudi</string>\n    <string name=\"crash_no_log\">Nessun log di errore disponibile</string>\n    <string name=\"palette_dynamic\">Dinamico</string>\n    <string name=\"palette_crimson\">Cremisi</string>\n    <string name=\"palette_rose\">Rosa</string>\n    <string name=\"palette_purple\">Viola</string>\n    <string name=\"palette_deep_purple\">Viola Intenso</string>\n    <string name=\"palette_indigo\">Indaco</string>\n    <string name=\"palette_blue\">Blu</string>\n    <string name=\"palette_sky_blue\">Blu Cielo</string>\n    <string name=\"palette_cyan\">Ciano</string>\n    <string name=\"palette_teal\">Verde petrolio</string>\n    <string name=\"palette_green\">Verde</string>\n    <string name=\"palette_light_green\">Verde Chiaro</string>\n    <string name=\"palette_lime\">Lime</string>\n    <string name=\"palette_yellow\">Giallo</string>\n    <string name=\"palette_amber\">Ambra</string>\n    <string name=\"palette_orange\">Arancio</string>\n    <string name=\"palette_deep_orange\">Arancio Profondo</string>\n    <string name=\"palette_brown\">Marrone</string>\n    <string name=\"palette_grey\">Grigio</string>\n    <string name=\"palette_blue_grey\">Blu Grigio</string>\n    <string name=\"cd_back\">Retro</string>\n    <string name=\"cd_pure_black_mode\">Modalità Nero puro</string>\n    <string name=\"cd_light_mode\">Modalità chiara</string>\n    <string name=\"cd_dark_mode\">Modalità scura</string>\n    <string name=\"cd_palette_item\">Tavolozza %1$s</string>\n    <string name=\"listen_together_choose_server\">Scegli un server</string>\n    <string name=\"listen_together_custom_server\">Server personalizzato</string>\n    <string name=\"listen_together_use_custom_server\">Usa un server personalizzato</string>\n    <string name=\"listen_together_auto_approval_joins\">Approva automaticamente le richieste di adesione</string>\n    <string name=\"listen_together_auto_approval_joins_desc\">Approva automaticamente le richieste di adesione invece di esaminarle manualmente</string>\n    <string name=\"listen_together_sync_volume\">Sincronizza il volume dell\\'ospitante</string>\n    <string name=\"listen_together_sync_volume_desc\">Gli ospiti seguono il livello del volume dell\\'ospite</string>\n    <string name=\"copy_code\">Copia codice</string>\n    <string name=\"kick_user_desc\">Rimuovi questa persona dalla sessione</string>\n    <string name=\"permanently_kick_user\">Blocca Permanentemente</string>\n    <string name=\"permanently_kick_user_desc\">Blocca le richieste di iscrizione di questa persona e nascondi i suoi suggerimenti</string>\n    <string name=\"transfer_ownership\">Trasferisci Proprietà</string>\n    <string name=\"transfer_ownership_desc\">Rendi questa persona l\\'ospite della stanza</string>\n    <string name=\"manage_user\">Gestisci Utente</string>\n    <string name=\"listen_together_blocked_users\">Utenti bloccati</string>\n    <string name=\"listen_together_blocked_users_count\">%d utente(i ) bloccati</string>\n    <string name=\"listen_together_no_blocked_users\">Nessun utente bloccato</string>\n    <string name=\"unblock\">Sbloccare</string>\n    <string name=\"user_blocked_by_host\">Utente bloccato dall\\'host</string>\n    <string name=\"cd_system_mode\">Modalità di sistema</string>\n    <string name=\"not_playing\">Nessun brano in riproduzione</string>\n    <string name=\"tap_to_play\">Premi per aprire Metrolist</string>\n    <string name=\"widget_music_player\">Riproduttore Musicale</string>\n    <string name=\"widget_turntable\">Giradischi</string>\n    <string name=\"together\">Insieme</string>\n    <string name=\"enter_room_code\">Inserisci codice stanza</string>\n    <string name=\"listen_together_settings_desc\">Configura server, nome utente e altro</string>\n    <string name=\"ai_lyrics_translation\">Traduzione Testi con AI</string>\n    <string name=\"ai_translating_lyrics\">Traduzione del testo...</string>\n    <string name=\"ai_lyrics_translated\">Testo tradotto</string>\n    <string name=\"ai_provider\">Fornitore</string>\n    <string name=\"ai_base_url\">URL Base</string>\n    <string name=\"ai_api_key\">Chiave API</string>\n    <string name=\"ai_model\">Modello</string>\n    <string name=\"ai_translation_mode\">Modalità di Traduzione</string>\n    <string name=\"ai_target_language\">Lingua di destinazione</string>\n    <string name=\"ai_setup_guide\">Credenziali API</string>\n    <string name=\"ai_translation_literal\">Traduzione</string>\n    <string name=\"ai_translation_transcribed\">Trascrizione</string>\n    <string name=\"ai_api_key_required\">Chiave API necessaria</string>\n    <string name=\"ai_error_language_required\">La lingua di destinazione è necessaria</string>\n    <string name=\"ai_error_unexpected\">Risultato di traduzione inaspettato</string>\n    <string name=\"ai_error_unknown\">Si è verificato un errore sconosciuto</string>\n    <string name=\"ai_error_translation_failed\">Traduzione fallita</string>\n    <string name=\"ai_error_api_key_required\">È necessaria una chiave API</string>\n    <string name=\"ai_error_no_lyrics\">Nessun testo da tradurre</string>\n    <string name=\"ai_error_lyrics_empty\">Il testo è vuoto</string>\n    <string name=\"play_all\">Riproduci tutto</string>\n    <string name=\"recognize_music\">Riconoscimento Musica</string>\n    <string name=\"artist_name_column\">Colonna Nome Artista</string>\n    <string name=\"map_csv_columns\">Mappa colonne CSV</string>\n    <string name=\"column_label\">Col %d</string>\n    <string name=\"try_again\">Riprova</string>\n    <string name=\"recognition_history\">Cronologia dei Riconoscimenti</string>\n    <string name=\"song_title_column\">Colonna Titolo del Brano</string>\n    <string name=\"recently_converted\">Convertito di recente</string>\n    <string name=\"importing_csv\">Importazione di CSV</string>\n    <string name=\"youtube_url_column\">Colonna URL di YouTube (Facoltativo)</string>\n    <string name=\"re_listen\">Riascolta</string>\n    <string name=\"clear_recognition_history_confirm\">Sei sicuro di voler cancellare tutta la cronologia dei riconoscimenti?</string>\n    <string name=\"no_match_found\">Nessuna corrispondenza trovata</string>\n    <string name=\"delete_from_history\">Elimina dalla cronologia</string>\n    <string name=\"processing\">Elaborazione…</string>\n    <string name=\"clear_recognition_history\">Cancella cronologia dei riconoscimenti</string>\n    <string name=\"recognition_error\">Errore di riconoscimento</string>\n    <string name=\"enable_high_refresh_rate_desc\">Forza il display a funzionare alla frequenza di aggiornamento più alta supportata (ad esempio 120 Hz)</string>\n    <string name=\"first_row_is_header\">La prima riga è l\\'intestazione</string>\n    <string name=\"tap_to_recognize\">Tocca per riconoscere</string>\n    <string name=\"enable_high_refresh_rate\">Abilita frequenza di aggiornamento elevata</string>\n    <string name=\"play_on_app\">Riproduci su Metrolist</string>\n    <string name=\"listening\">In ascolto…</string>\n    <string name=\"continue_action\">Continua</string>\n    <string name=\"enable\">Attiva</string>\n    <string name=\"crossfade\">Dissolvenza incrociata</string>\n    <string name=\"crossfade_desc\">Dissolvenza incrociata tra i brani</string>\n    <string name=\"crossfade_duration\">Durata della dissolvenza incrociata</string>\n    <string name=\"crossfade_gapless\">Disabilita per album senza interruzioni</string>\n    <string name=\"crossfade_gapless_desc\">Non eseguire la dissolvenza incrociata se l\\'album è senza pause</string>\n    <string name=\"crossfade_beta_title\">Funzionalità Beta</string>\n    <string name=\"crossfade_beta_message\">La dissolvenza incrociata è una nuova funzionalità e potrebbe contenere bug. Se riscontri problemi, segnalali.\\n\\nQuesta funzionalità disabilita lo scaricamento dell\\'audio a causa di limitazioni tecniche.</string>\n    <string name=\"audio_offload_disabled_by_crossfade\">Disabilitato perché Crossfade è attivo</string>\n    <string name=\"hide_youtube_shorts\">Nascondi gli Shorts di YouTube</string>\n    <string name=\"listen_together_in_top_bar\">Ascolta Insieme nella barra in alto</string>\n    <string name=\"listen_together_in_top_bar_desc\">Mostra Ascolta insieme nella barra dell\\'app in alto anziché nella barra di navigazione</string>\n    <string name=\"ai_translation_literal_desc\">Traduci il significato nella lingua di destinazione</string>\n    <string name=\"ai_translation_transcribed_desc\">Converti la pronuncia nella lingua di destinazione</string>\n    <string name=\"ai_provider_help\">Ottieni chiavi API</string>\n    <string name=\"ai_provider_openrouter_help\">Visita https://openrouter.ai per i modelli gratuiti e a pagamento</string>\n    <string name=\"ai_provider_openai_help\">Visita https://platform.openai.com/api-keys</string>\n    <string name=\"ai_provider_claude_help\">Visita https://console.anthropic.com/settings/keys</string>\n    <string name=\"ai_provider_gemini_help\">Visita https://aistudio.google.com/apikey</string>\n    <string name=\"ai_provider_perplexity_help\">Visita https://perplexity.ai/settings/api</string>\n    <string name=\"ai_provider_xai_help\">Visita https://console.x.ai</string>\n    <string name=\"ai_provider_deepl_help\">Visita https://deepl.com/pro-api per le chiavi gratuite e a pagamento</string>\n    <string name=\"ai_deepl_formality\">Formalità</string>\n    <string name=\"ai_deepl_formality_default\">Predefinito</string>\n    <string name=\"ai_deepl_formality_more\">Più formale</string>\n    <string name=\"ai_deepl_formality_less\">Meno formale</string>\n    <string name=\"discord_status\">Stato</string>\n    <string name=\"discord_status_online\">Online</string>\n    <string name=\"discord_status_idle\">Inattivo</string>\n    <string name=\"discord_status_dnd\">Non disturbare</string>\n    <string name=\"discord_buttons\">Pulsanti</string>\n    <string name=\"discord_button_1\">Pulsante 1</string>\n    <string name=\"discord_button_2\">Pulsante 2</string>\n    <string name=\"login_successful\">Accesso eseguito con successo!</string>\n    <string name=\"discord_activity_type\">Tipo di attività</string>\n    <string name=\"prevent_duplicate_tracks_in_queue\">Previeni tracce duplicate in coda</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">Quando una traccia viene aggiunta nella coda, rimuovila dalle posizioni precedenti se è già presente</string>\n    <string name=\"discord_advanced_mode_description\">Mostra opzioni di personalizzazione aggiuntive per Rich Presence</string>\n    <string name=\"discord_advanced_mode\">Modalità avanzata</string>\n    <string name=\"discord_activity_name_description\">Nome personalizzato per l\\'attività (lascia vuoto per predefinito)</string>\n    <string name=\"discord_activity_name\">Nome dell\\'attività</string>\n    <string name=\"discord_competing_metrolist\">Competendo su Metrolist</string>\n    <string name=\"discord_watching_metrolist\">Guardando video su Metrolist</string>\n    <string name=\"discord_playing_metrolist\">Riproducendo musica su Metrolist</string>\n    <string name=\"discord_connect_description\">Accedi con Discord per condividere ciò che stai ascoltando</string>\n    <string name=\"discord_presence\">Presenza</string>\n    <string name=\"discord_rpc_preview\">Anteprima della Rich Presence</string>\n    <string name=\"discord_button_text_variables\">Variabili: {song_name} per il nome del brano, {artist_name} per il nome dell\\'artista, {album_name} per il nome dell\\'album</string>\n    <string name=\"discord_activity_competing\">Competere</string>\n    <string name=\"discord_activity_watching\">Guardando</string>\n    <string name=\"discord_activity_listening\">Ascoltando</string>\n    <string name=\"discord_activity_playing\">Riproducendo</string>\n    <string name=\"discord_information_warning\">Questa funzione utilizza la libreria KizzyRPC per connettersi al gateway di Discord e impostare il tuo stato di Rich Presence. Sebbene non si siano verificate sospensioni di account note a causa di un utilizzo simile, questo metodo non è ufficialmente supportato da Discord e potrebbe essere considerato una violazione dei Termini di Servizio. Il tuo token viene estratto localmente e non viene mai inviato a server di terze parti. Procedi a tua discrezione.</string>\n    <string name=\"player_background_solid\">Solido</string>\n    <string name=\"resume_on_bluetooth_connect\">Riprendi dalla connessione Bluetooth</string>\n    <string name=\"lyrics_romanize_hindi\">Romanizza testo in Hindi</string>\n    <string name=\"lyrics_romanize_punjabi\">Romanizza testo in Punjabi</string>\n    <string name=\"lyrics_romanize_as_main\">Mostra i testi romanizzati come principali</string>\n    <string name=\"display_density\">Densità dello schermo</string>\n    <string name=\"restart\">Riavvia</string>\n    <string name=\"restart_required\">Il riavvio è necessario</string>\n    <string name=\"density_restart_message\">Il cambio della densità dello schermo si applicherà dopo aver riavviato l\\'app. Vuoi riavviarla ora?</string>\n    <string name=\"enable_lrclib_desc\">Database di testi sincronizzati gestito dalla comunità</string>\n    <string name=\"enable_kugou_desc\">Ottiene i testi da KuGou, una piattaforma cinese popolare di musica</string>\n    <string name=\"youtube_music_lyrics_note\">NOTA: i testi tratti da YouTube Music saranno mostrati automaticamente quando altri testi non sono disponibili. I testi di YouTube Music solitamente non sono sincronizzati.</string>\n    <string name=\"enable_lyricsplus\">Attiva LyricsPlus</string>\n    <string name=\"enable_lyricsplus_desc\">Testi sincronizzati da più fonti</string>\n    <string name=\"lyrics_provider_selection\">Selezione del fornitore</string>\n    <string name=\"lyrics_provider_selection_desc\">Scegli quali fornitori di testi utilizzare</string>\n    <string name=\"lyrics_provider_priority\">Priorità dei fornitori di testi</string>\n    <string name=\"check_for_updates_button\">Controlla aggiornamenti</string>\n    <string name=\"hide_changelog\">Nascondi le novità</string>\n    <string name=\"view_changelog\">Mostra le novità</string>\n    <string name=\"failed_to_check_updates\">Errore nel controllare gli aggiornamenti: %s</string>\n    <string name=\"set_as_default\">Imposta come predefinito</string>\n    <string name=\"sleep_timer_default_set\">Spegnimento automatico impostato come predefinito a %d min</string>\n    <string name=\"found_in_settings_content\">Trovato in Impostazioni &gt; Contenuti</string>\n    <string name=\"plays\">riproduzioni</string>\n    <string name=\"error_episode_save\">Errore nel salvare l\\'episodio</string>\n    <string name=\"error_episode_remove\">Errore nel rimuovere l\\'episodio</string>\n    <string name=\"error_podcast_subscribe\">Errore nel seguire il podcast</string>\n    <string name=\"lyrics_provider_priority_desc\">Trascina per riordinare i fornitori per preferenza. Alta posizione = alta priorità.</string>\n    <string name=\"changelog\">Novità</string>\n    <string name=\"changelog_empty\">Nessuna novità disponibile</string>\n    <string name=\"github_releases_url\">https://github.com/MetrolistGroup/Metrolist/releases</string>\n    <string name=\"list_bullet\">•</string>\n    <string name=\"view_on_github\">Mostra su GitHub</string>\n    <string name=\"current_version\">Versione attuale</string>\n    <string name=\"version_format\">Versione: %s</string>\n    <string name=\"update_settings\">Impostazioni di aggiornamento</string>\n    <string name=\"check_for_updates_title\">Controlla aggiornamenti</string>\n    <string name=\"checking_for_updates\">Controllo di aggiornamenti…</string>\n    <string name=\"latest_version_format\">Ultima: %s</string>\n    <string name=\"error_podcast_unsubscribe\">Impossibile annullare l\\'iscrizione al podcast</string>\n    <string name=\"listen_together_auto_approval_suggestions\">Approvazione automatica dei brani suggeriti</string>\n    <string name=\"listen_together_auto_approval_suggestions_desc\">Approva automaticamente e inserisci in coda le richieste di brani da parte degli ospiti</string>\n    <string name=\"importing_playlist\">Importazione Playlist</string>\n    <string name=\"randomize_home_order\">Randomizza l\\'ordine della Schermata Iniziale</string>\n    <string name=\"randomize_home_order_desc\">Riordina in modo casuale le sezioni della schermata iniziale con priorità ponderate</string>\n    <string name=\"daily_discover_sounds_like\">Sembra %1$s</string>\n    <string name=\"daily_discover_because_you_listen_to\">Perché ascolti %1$s</string>\n    <string name=\"daily_discover_similar_to\">Simile a %1$s</string>\n    <string name=\"daily_discover_based_on\">Basato su %1$s</string>\n    <string name=\"daily_discover_for_fans_of\">Per i fan di %1$s</string>\n    <string name=\"from_the_community\">Dalla comunità</string>\n    <string name=\"logout_dialog_title\">Vuoi mantenere i dati della libreria?</string>\n    <string name=\"logout_dialog_message\">Vuoi conservare le tue playlist e i dati della libreria? I brani scaricati verranno conservati comunque.</string>\n    <string name=\"logout_keep\">Mantieni</string>\n    <string name=\"logout_clear\">Pulisci</string>\n    <string name=\"credits_lead_developer\">Sviluppatore principale</string>\n    <string name=\"credits_collaborator\">Collaboratore</string>\n    <string name=\"credits_collaborators_section\">Collaboratori</string>\n    <string name=\"credits_license_name\">Licenza GNU General Public License v3.0</string>\n    <string name=\"credits_license_desc\">Software gratuito e open source. Puoi usarlo, studiarlo, condividerlo e migliorarlo.</string>\n    <string name=\"credits_discord\">Server di Discord</string>\n    <string name=\"credits_telegram\">Canale di Telegram</string>\n    <string name=\"credits_website\">Sito Web</string>\n    <string name=\"credits_instagram\">Instagram</string>\n    <string name=\"credits_github\">GitHub</string>\n    <string name=\"credits_view_repo\">Visualizza Archivio</string>\n    <string name=\"app_version_info\">%1$s • %2$s</string>\n    <string name=\"like_what_i_do\">Ti piace quello che faccio?</string>\n    <string name=\"buy_mo_a_coffee\">Offrimi un caffè</string>\n    <string name=\"community_and_info\">Comunità e Informazioni</string>\n    <string name=\"metrolist\">METROLIST</string>\n    <string name=\"wanna_play_favorite_song\">Vuoi riprodurre il loro brano preferito?</string>\n    <string name=\"yeah\">Sì</string>\n    <string name=\"stands_with_palestine\">Questo progetto sta dalla parte della Palestina 🇵🇸</string>\n    <string name=\"filter_podcasts\">Podcast</string>\n    <string name=\"view_podcast\">Mostra podcast</string>\n    <string name=\"podcast_channels\">Canali Podcast</string>\n    <string name=\"latest_episodes\">Ultimi Episodi</string>\n    <string name=\"new_episodes\">Nuovi Episodi</string>\n    <string name=\"episodes_for_later\">Episodi per dopo</string>\n    <string name=\"save_episode_for_later\">Salva per dopo</string>\n    <string name=\"save_episode_for_later_desc\">Aggiungi alla playlist Episodi per Dopo</string>\n    <string name=\"remove_episode_from_saved\">Rimuovi dai salvati</string>\n    <string name=\"subscribe_to_podcast\">Salva podcast nella libreria</string>\n    <plurals name=\"n_episode\">\n        <item quantity=\"one\">%d episodio</item>\n        <item quantity=\"many\">%d episodi</item>\n        <item quantity=\"other\">%d episodi</item>\n    </plurals>\n    <string name=\"restore_confirm_title\">Vuoi ripristinare il backup?</string>\n    <string name=\"restore_confirm_message\">Ciò ripristinerà i dati dell\\'app dal backup.</string>\n    <string name=\"restore_account_warning\">Sarà necessario accedere nuovamente dopo il ripristino. Verrà disconnesso il seguente account:</string>\n    <string name=\"restore\">Ripristinare</string>\n    <string name=\"checking_previous_account\">Verifica account precedente…</string>\n    <string name=\"no_account_found\">Nessun account trovato</string>\n    <string name=\"widget_recognizer_name\">Riconoscitore Musicale</string>\n    <string name=\"widget_recognizer_description\">Identifica i brani che ti circondano direttamente dalla schermata iniziale</string>\n    <string name=\"widget_recognizer_tap_to_search\">Tocca per identificare il brano</string>\n    <string name=\"widget_recognizer_listening\">In ascolto…</string>\n    <string name=\"widget_recognizer_processing\">Identificazione…</string>\n    <string name=\"widget_recognizer_no_match\">Nessuna corrispondenza trovata. Prova di nuovo</string>\n    <string name=\"widget_recognizer_error\">Riconoscimento fallito</string>\n    <string name=\"widget_recognizer_error_generic\">Si è verificato un errore. Prova di nuovo</string>\n    <string name=\"widget_recognizer_unknown_song\">Brano sconosciuto</string>\n    <string name=\"widget_recognizer_unknown_artist\">Artista sconosciuto</string>\n    <string name=\"widget_recognizer_mic_desc\">Identificare brano</string>\n    <string name=\"widget_recognizer_channel_name\">Riconoscimento Musicale</string>\n    <string name=\"widget_recognizer_channel_desc\">Mostra una notifica mentre stai identificando un brano dal widget</string>\n    <string name=\"widget_recognizer_notification_text\">Registrazione dell\\'audio per identificare il brano…</string>\n    <string name=\"filter_episodes\">Episodi</string>\n    <string name=\"filter_channels\">Canali</string>\n    <string name=\"auto_playlist\">Playlist automatica</string>\n    <string name=\"downloaded_episodes\">Episodi scaricati</string>\n    <string name=\"no_subscribed_channels\">Nessun canale sottoscritto</string>\n    <string name=\"no_downloaded_episodes\">Nessun episodio scaricato</string>\n    <plurals name=\"n_channel\">\n        <item quantity=\"one\">%d canale</item>\n        <item quantity=\"many\">%d canali</item>\n        <item quantity=\"other\">%d canali</item>\n    </plurals>\n    <string name=\"speed_dial\">Selezione rapida</string>\n    <string name=\"pin_to_speed_dial\">Fissa nella Selezione rapida</string>\n    <string name=\"unpin_from_speed_dial\">Sblocca dalla Composizione rapida</string>\n    <string name=\"your_shows\">I Tuoi Spettacoli</string>\n    <string name=\"view_channel\">Vedi canale</string>\n    <string name=\"filter_profiles\">Profili</string>\n    <string name=\"sleep_timer_repeat\">Ripeti</string>\n    <string name=\"sleep_timer_daily\">Giornaliero</string>\n    <string name=\"sleep_timer_weekdays\">Da Lunedì a Venerdì</string>\n    <string name=\"sleep_timer_weekdays_weekends\">Giorni feriali / Fine settimana</string>\n    <string name=\"sleep_timer_weekends\">Fine settimana (Sab–Dom)</string>\n    <string name=\"sleep_timer_custom\">Personalizzato</string>\n    <string name=\"sleep_timer_start_time\">Orario di inizio</string>\n    <string name=\"sleep_timer_end_time\">Orario di fine</string>\n    <string name=\"sleep_timer_monday\">Lunedì</string>\n    <string name=\"sleep_timer_tuesday\">Martedì</string>\n    <string name=\"sleep_timer_wednesday\">Mercoledì</string>\n    <string name=\"sleep_timer_thursday\">Giovedì</string>\n    <string name=\"sleep_timer_friday\">Venerdì</string>\n    <string name=\"sleep_timer_saturday\">Sabato</string>\n    <string name=\"sleep_timer_sunday\">Domenica</string>\n    <string name=\"sleep_timer_stop_after_current_song\">Interrompi al termine del brano corrente quando il timer scade</string>\n    <string name=\"sleep_timer_fade_out\">Dissolvenza nell\\'ultimo minuto</string>\n    <string name=\"upload_songs\">Carica brani</string>\n    <string name=\"uploading\">Caricamento…</string>\n    <string name=\"upload_progress\">%1$d di %2$d</string>\n    <string name=\"upload_complete\">Caricamento completato</string>\n    <string name=\"upload_failed\">Caricamento non riuscito</string>\n    <string name=\"upload_file_too_large\">File troppo grande (max 300MB)</string>\n    <string name=\"upload_unsupported_format\">Formato non supportato. Usa mp3, m4a, wma, flac o ogg</string>\n    <string name=\"delete_uploaded_song\">Elimina brano caricato</string>\n    <string name=\"delete_uploaded_song_confirm\">Sei sicuro di voler eliminare questo brano caricato? Questa azione non può essere annullata.</string>\n    <string name=\"delete_uploaded_song_success\">Brano caricato eliminato</string>\n    <string name=\"delete_uploaded_song_failed\">Impossibile eliminare il brano caricato</string>\n    <string name=\"delete_uploaded_songs\">Elimina brani caricati</string>\n    <string name=\"delete_uploaded_songs_confirm\">Sei sicuro di voler eliminare %1$d brani caricati? Questa azione non può essere annullata.</string>\n    <string name=\"deleted_n_songs\">Elimina %1$d brani</string>\n    <string name=\"deleting\">Eliminazione…</string>\n    <string name=\"enable_automatic_sleeptimer\">Attiva timer per lo spegnimento automatico</string>\n    <string name=\"sleeptimer_description\">Attiva lo spegnimento automatico con un valore temporale personalizzato</string>\n    <string name=\"sleep_timer_repeat_description\">Imposta um giorno e un orario personalizzati per quando lo spegnimento automatico si deve attivare</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-it/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Home</string>\n    <string name=\"songs\">Brani</string>\n    <string name=\"artists\">Artisti</string>\n    <string name=\"albums\">Album</string>\n    <string name=\"playlists\">Playlist</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d selezionato</item>\n        <item quantity=\"many\">%d selezionati</item>\n        <item quantity=\"other\">%d selezionati</item>\n    </plurals>\n    <string name=\"history\">Cronologia</string>\n    <string name=\"stats\">Statistiche</string>\n    <string name=\"mood_and_genres\">Umori e generi</string>\n    <string name=\"account\">Account</string>\n    <string name=\"quick_picks\">Scelte rapide</string>\n    <string name=\"quick_picks_empty\">Ascolta alcuni brani per generare le tue scelte rapide</string>\n    <string name=\"new_release_albums\">Nuovi album in uscita</string>\n    <string name=\"today\">Oggi</string>\n    <string name=\"yesterday\">Ieri</string>\n    <string name=\"this_week\">Questa settimana</string>\n    <string name=\"last_week\">Settimana scorsa</string>\n    <string name=\"most_played_songs\">Brani più ascoltati</string>\n    <string name=\"most_played_artists\">Artisti più ascoltati</string>\n    <string name=\"most_played_albums\">Album più ascoltati</string>\n    <string name=\"search\">Cerca</string>\n    <string name=\"search_yt_music\">Cerca su YouTube Music…</string>\n    <string name=\"search_library\">Cerca nella libreria…</string>\n    <string name=\"filter_library\">Libreria</string>\n    <string name=\"filter_liked\">Piaciuti</string>\n    <string name=\"filter_downloaded\">Scaricati</string>\n    <string name=\"filter_all\">Tutto</string>\n    <string name=\"filter_songs\">Brani</string>\n    <string name=\"filter_videos\">Video</string>\n    <string name=\"filter_albums\">Album</string>\n    <string name=\"filter_artists\">Artisti</string>\n    <string name=\"filter_playlists\">Playlist</string>\n    <string name=\"filter_community_playlists\">Playlist della comunità</string>\n    <string name=\"filter_featured_playlists\">Playlist in evidenza</string>\n    <string name=\"filter_bookmarked\">Aggiunto ai segnalibri</string>\n    <string name=\"no_results_found\">Nessun risultato trovato</string>\n    <string name=\"from_your_library\">Dalla tua libreria</string>\n    <string name=\"liked_songs\">Brani piaciuti</string>\n    <string name=\"downloaded_songs\">Brani scaricati</string>\n    <string name=\"playlist_is_empty\">La playlist è vuota</string>\n    <string name=\"retry\">Riprova</string>\n    <string name=\"radio\">Radio</string>\n    <string name=\"shuffle\">Casuale</string>\n    <string name=\"reset\">Ricomincia</string>\n    <string name=\"details\">Dettagli</string>\n    <string name=\"edit\">Modifica</string>\n    <string name=\"start_radio\">Avvia radio</string>\n    <string name=\"play\">Riproduci</string>\n    <string name=\"play_next\">Riproduci come successiva</string>\n    <string name=\"add_to_queue\">Metti in coda</string>\n    <string name=\"add_to_library\">Aggiungi a libreria</string>\n    <string name=\"remove_from_library\">Rimuovi da libreria</string>\n    <string name=\"action_download\">Scarica</string>\n    <string name=\"downloading\">In scaricamento</string>\n    <string name=\"remove_download\">Rimuovi download</string>\n    <string name=\"import_playlist\">Importa playlist</string>\n    <string name=\"add_to_playlist\">Aggiungi a playlist</string>\n    <string name=\"view_artist\">Mostra artista</string>\n    <string name=\"view_album\">Mostra album</string>\n    <string name=\"refetch\">Aggiorna</string>\n    <string name=\"share\">Condividi</string>\n    <string name=\"delete\">Elimina</string>\n    <string name=\"remove_from_history\">Rimuovi da cronologia</string>\n    <string name=\"search_online\">Cerca online</string>\n    <string name=\"action_sync\">Sincronizza</string>\n    <string name=\"advanced\">Avanzate</string>\n    <string name=\"sort_by_create_date\">Data di aggiunta</string>\n    <string name=\"sort_by_name\">Nome</string>\n    <string name=\"sort_by_artist\">Artista</string>\n    <string name=\"sort_by_year\">Anno</string>\n    <string name=\"sort_by_song_count\">Numero brani</string>\n    <string name=\"sort_by_length\">Durata</string>\n    <string name=\"sort_by_play_time\">Numero di riproduzioni</string>\n    <string name=\"sort_by_custom\">Ordine personalizzato</string>\n    <string name=\"media_id\">ID media</string>\n    <string name=\"mime_type\">Tipo MIME</string>\n    <string name=\"codecs\">Codec</string>\n    <string name=\"bitrate\">Bitrate</string>\n    <string name=\"sample_rate\">Frequenza di campionamento</string>\n    <string name=\"loudness\">Rumorosità</string>\n    <string name=\"volume\">Volume</string>\n    <string name=\"file_size\">Dimensioni</string>\n    <string name=\"unknown\">Sconosciuto</string>\n    <string name=\"copied\">Copiato negli appunti</string>\n    <string name=\"edit_lyrics\">Modifica testo</string>\n    <string name=\"search_lyrics\">Cerca testo</string>\n    <string name=\"edit_song\">Modifica brano</string>\n    <string name=\"song_title\">Titolo del brano</string>\n    <string name=\"song_artists\">Artisti del brano</string>\n    <string name=\"error_song_title_empty\">Il titolo del brano non può essere vuoto.</string>\n    <string name=\"error_song_artist_empty\">L\\'artista del brano non può essere vuoto.</string>\n    <string name=\"save\">Salva</string>\n    <string name=\"choose_playlist\">Scegli una playlist</string>\n    <string name=\"edit_playlist\">Modifica playlist</string>\n    <string name=\"create_playlist\">Crea playlist</string>\n    <string name=\"playlist_name\">Nome della playlist</string>\n    <string name=\"error_playlist_name_empty\">Il nome della playlist non può essere vuoto.</string>\n    <string name=\"edit_artist\">Modifica artista</string>\n    <string name=\"artist_name\">Nome dell\\'artista</string>\n    <string name=\"error_artist_name_empty\">Il nome dell\\'artista non può essere vuoto.</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d brano</item>\n        <item quantity=\"many\">%d brani</item>\n        <item quantity=\"other\">%d brani</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d artista</item>\n        <item quantity=\"many\">%d artisti</item>\n        <item quantity=\"other\">%d artisti</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d album</item>\n        <item quantity=\"many\">%d albums</item>\n        <item quantity=\"other\">%d album</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d playlist</item>\n        <item quantity=\"many\">%d playlist</item>\n        <item quantity=\"other\">%d playlist</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d settimana</item>\n        <item quantity=\"many\">%d settimane</item>\n        <item quantity=\"other\">%d settimane</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d mese</item>\n        <item quantity=\"many\">%d mesi</item>\n        <item quantity=\"other\">%d mesi</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d anno</item>\n        <item quantity=\"many\">%d anni</item>\n        <item quantity=\"other\">%d anni</item>\n    </plurals>\n    <string name=\"playlist_imported\">Playlist importata</string>\n    <string name=\"removed_song_from_playlist\">Rimosso \\\"%s\\\" da playlist</string>\n    <string name=\"playlist_synced\">Playlist sincronizzata</string>\n    <string name=\"undo\">Annulla</string>\n    <string name=\"lyrics_not_found\">Testo non trovato</string>\n    <string name=\"sleep_timer\">Timer per il sonno</string>\n    <string name=\"end_of_song\">Fine del brano</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">1 minuto</item>\n        <item quantity=\"many\">%d minuti</item>\n        <item quantity=\"other\">%d minuti</item>\n    </plurals>\n    <string name=\"error_no_stream\">Stream non disponibile</string>\n    <string name=\"error_no_internet\">Nessuna connessione di rete</string>\n    <string name=\"error_timeout\">Tempo scaduto</string>\n    <string name=\"error_unknown\">Errore sconosciuto</string>\n    <string name=\"action_like\">Mi piace</string>\n    <string name=\"action_remove_like\">Rimuovi mi piace</string>\n    <string name=\"action_shuffle_on\">Riproduzione casuale attivata</string>\n    <string name=\"action_shuffle_off\">Riproduzione casuale disattivata</string>\n    <string name=\"repeat_mode_off\">Modalità ripetizione disattivata</string>\n    <string name=\"repeat_mode_one\">Ripeti brano corrente</string>\n    <string name=\"repeat_mode_all\">Ripeti coda</string>\n    <string name=\"queue_all_songs\">Tutti i brani</string>\n    <string name=\"queue_searched_songs\">Brani cercati</string>\n    <string name=\"music_player\">Riproduttore</string>\n    <string name=\"settings\">Impostazioni</string>\n    <string name=\"appearance\">Aspetto</string>\n    <string name=\"enable_dynamic_theme\">Abilita il tema dinamico</string>\n    <string name=\"dark_theme\">Tema scuro</string>\n    <string name=\"dark_theme_on\">Attivato</string>\n    <string name=\"dark_theme_off\">Disattivato</string>\n    <string name=\"dark_theme_follow_system\">Segui sistema</string>\n    <string name=\"pure_black\">Nero</string>\n    <string name=\"default_open_tab\">Scheda principale predefinita</string>\n    <string name=\"customize_navigation_tabs\">Personalizza le schede di navigazione</string>\n    <string name=\"player_text_alignment\">Allineamento del testo del riproduttore</string>\n    <string name=\"lyrics_text_position\">Posizione del testo dei brani</string>\n    <string name=\"sided\">Laterale</string>\n    <string name=\"left\">Sinistra</string>\n    <string name=\"center\">Centro</string>\n    <string name=\"right\">Destra</string>\n    <string name=\"content\">Contenuti</string>\n    <string name=\"login\">Accesso</string>\n    <string name=\"content_language\">Lingua predefinita dei contenuti</string>\n    <string name=\"content_country\">Paese predefinito dei contenuti</string>\n    <string name=\"system_default\">Predefinito di sistema</string>\n    <string name=\"enable_proxy\">Attiva proxy</string>\n    <string name=\"proxy_type\">Tipologia del proxy</string>\n    <string name=\"proxy_url\">URL proxy</string>\n    <string name=\"restart_to_take_effect\">Riavvia l\\'app per applicare le modifiche</string>\n    <string name=\"player_and_audio\">Riproduttore e audio</string>\n    <string name=\"audio_quality\">Qualità dell\\'audio</string>\n    <string name=\"audio_quality_auto\">Automatica</string>\n    <string name=\"audio_quality_high\">Alta</string>\n    <string name=\"audio_quality_low\">Bassa</string>\n    <string name=\"persistent_queue\">Coda persistente</string>\n    <string name=\"skip_silence\">Salta il silenzio</string>\n    <string name=\"audio_normalization\">Normalizzazione dell\\'audio</string>\n    <string name=\"equalizer\">Equalizzatore</string>\n    <string name=\"storage\">Archiviazione</string>\n    <string name=\"cache\">Cache</string>\n    <string name=\"image_cache\">Cache delle immagini</string>\n    <string name=\"song_cache\">Cache dei brani</string>\n    <string name=\"max_cache_size\">Dimensione massima della cache</string>\n    <string name=\"unlimited\">Illimitata</string>\n    <string name=\"clear_all_downloads\">Cancella tutti i download</string>\n    <string name=\"max_image_cache_size\">Grandezza massima della cache delle immagini</string>\n    <string name=\"clear_image_cache\">Pulisci la cache delle immagini</string>\n    <string name=\"max_song_cache_size\">Grandezza massima della cache dei brani</string>\n    <string name=\"clear_song_cache\">Pulisci la cache dei brani</string>\n    <string name=\"size_used\">%s usati</string>\n    <string name=\"privacy\">Privacy</string>\n    <string name=\"pause_listen_history\">Sospendi la cronologia degli ascolti</string>\n    <string name=\"clear_listen_history\">Cancella la cronologia degli ascolti</string>\n    <string name=\"clear_listen_history_confirm\">Sei sicuro di voler cancellare la cronologia degli ascolti?</string>\n    <string name=\"pause_search_history\">Sospendi la cronologia delle ricerche</string>\n    <string name=\"clear_search_history\">Pulisci la cronologia delle ricerche</string>\n    <string name=\"clear_search_history_confirm\">Sei sicuro di voler cancellare la cronologia delle ricerche?</string>\n    <string name=\"enable_kugou\">Attiva il fornitore di testi KuGou</string>\n    <string name=\"backup_restore\">Backup e ripristino</string>\n    <string name=\"action_backup\">Fai un backup</string>\n    <string name=\"action_restore\">Ripristina</string>\n    <string name=\"imported_playlist\">Playlist importata</string>\n    <string name=\"backup_create_success\">Backup creato con successo</string>\n    <string name=\"backup_create_failed\">Impossibile eseguire il backup</string>\n    <string name=\"restore_failed\">Impossibile eseguire il ripristino dal backup</string>\n    <string name=\"about\">Informazioni</string>\n    <string name=\"app_version\">Versione dell\\'app</string>\n    <string name=\"new_version_available\">Nuova versione disponibile</string>\n    <string name=\"translation_models\">Modelli di traduzione</string>\n    <string name=\"clear_translation_models\">Cancella i modelli di traduzione</string>\n    <string name=\"squiggly\">Ondulato</string>\n    <string name=\"action_remove_like_all\">Rimuovi tutti i mi piace</string>\n    <string name=\"library_album_empty\">Gli album della libreria verranno visualizzati qui</string>\n    <string name=\"library_playlist_empty\">Le tue playlist verranno visualizzate qui</string>\n    <string name=\"remove_from_queue\">Rimuovi dalla coda</string>\n    <string name=\"player_slider_style\">Stile slider del player</string>\n    <string name=\"keep_listening\">Continua ad ascoltare</string>\n    <string name=\"your_youtube_playlists\">Le tue playlist di YouTube</string>\n    <string name=\"duplicates_description_multiple\">La canzone %d è già nella tua playlist</string>\n    <string name=\"duplicates_description_single\">La canzone è già nella tua playlist</string>\n    <string name=\"delete_playlist_confirm\">Vuoi davvero rimuovere la playlist %s?</string>\n    <string name=\"library_song_empty\">I brani della libreria verranno visualizzati qui</string>\n    <string name=\"remove_all_from_library\">Rimuovi tutto da libreria</string>\n    <string name=\"duplicates\">Duplicati</string>\n    <string name=\"default_\">Default</string>\n    <string name=\"other_versions\">Altre versioni</string>\n    <string name=\"tempo_and_pitch\">Tempo e Tono</string>\n    <string name=\"add_all_to_library\">Aggiungi tutto a libreria</string>\n    <string name=\"remove_from_playlist\">Rimuovi da playlist</string>\n    <string name=\"theme\">Tema</string>\n    <string name=\"player\">Lettore</string>\n    <string name=\"forgotten_favorites\">Preferiti dimenticati</string>\n    <string name=\"similar_to\">Simile a</string>\n    <string name=\"add_anyway\">Aggiungere comunque</string>\n    <string name=\"remove_download_playlist_confirm\">Vuoi davvero rimuovere tutti i brani della playlist \\\"%s\\\" dalla memoria dei Brani Scaricati?</string>\n    <string name=\"skip_duplicates\">Salta duplicati</string>\n    <string name=\"library_artist_empty\">Gli artisti della libreria verranno visualizzati qui</string>\n    <string name=\"action_like_all\">Mi piace a tutto</string>\n    <string name=\"not_logged_in\">Non autenticato</string>\n    <string name=\"small\">Piccolo</string>\n    <string name=\"big\">Grande</string>\n    <string name=\"misc\">Altro</string>\n    <string name=\"queue\">Coda</string>\n    <string name=\"persistent_queue_desc\">Ripristina l\\'ultima coda all\\'apertura dell\\'app</string>\n    <string name=\"auto_load_more\">Carica automaticamente più canzoni</string>\n    <string name=\"auto_skip_next_on_error\">Salta alla prossima canzone quando si verifica un errore</string>\n    <string name=\"auto_load_more_desc\">Aggiungi automaticamente canzoni quando si raggiunge la fine della coda, se possibile</string>\n    <string name=\"enable_lrclib\">Attiva il fornitore di testi LcrLib</string>\n    <string name=\"grid_cell_size\">Dimensione delle celle della griglia</string>\n    <string name=\"auto_skip_next_on_error_desc\">Garantisci la tua esperienza di riproduzione continua</string>\n    <string name=\"stop_music_on_task_clear\">Interrompi la musica quando l\\'attività è interrotta</string>\n    <string name=\"listen_history\">Cronologia ascolti</string>\n    <string name=\"search_history\">Cronologia ricerche</string>\n    <string name=\"disable_screenshot\">Disabilita screenshot</string>\n    <string name=\"disable_screenshot_desc\">Quando questa opzione è attiva, gli screenshot e la visualizzazione dell\\'app nei Recenti sono disabilitati.</string>\n    <string name=\"hide_explicit\">Nascondi contenuti espliciti</string>\n    <string name=\"discord_integration\">Integrazione Discord</string>\n    <string name=\"discord_information\">Metrolist usa la libreria KizzyRPC per impostare lo stato del tuo account Discord. Ciò comporta l\\'uso della connessione Discord Gateway, che può essere considerata una violazione dei TOS di Discord. Tuttavia, non ci sono casi noti di account utente sospesi per questo motivo. Usalo a tuo rischio e pericolo.\\n\\nMetrolist estrarrà solo il tuo token e tutto il resto verrà archiviato localmente.</string>\n    <string name=\"options\">Opzioni</string>\n    <string name=\"use_login_for_browse\">Usa il login per navigare nei contenuti</string>\n    <string name=\"preview\">Anteprima</string>\n    <string name=\"login_failed\">Login fallito</string>\n    <string name=\"action_logout\">Esci</string>\n    <string name=\"use_login_for_browse_desc\">Ciò può influenzare il contenuto che vedi e ad esempio mostra album solo premium se hai effettuato l\\'accesso con un account Premium</string>\n    <string name=\"enable_discord_rpc\">Abilita Rich Presence</string>\n    <string name=\"dismiss\">Ignorare</string>\n    <string name=\"action_login\">Accedi</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-iw/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"weeks\">שבועות</string>\n    <string name=\"months\">חודשים</string>\n    <string name=\"background_color\">צבע רקע</string>\n    <string name=\"local_history\">מקומי</string>\n    <string name=\"remote_history\">מרוחק</string>\n    <string name=\"charts\">טבלאות</string>\n    <string name=\"back_button_desc\">אחורה</string>\n    <string name=\"album_cover_desc\">כיסוי אלבום</string>\n    <string name=\"top_music_videos\">סרטוני מוזיקה מובילים</string>\n    <string name=\"trending\">טרנדינג</string>\n    <string name=\"years\">שנים</string>\n    <string name=\"continuous\">מתמשך</string>\n    <string name=\"liked\">אהוב</string>\n    <string name=\"offline\">הורד</string>\n    <string name=\"my_top\">הנשמעים ביותר שלי</string>\n    <string name=\"cached_playlist\">נשמר</string>\n    <string name=\"sync_playlist\">סנכרן את הפלייליסט</string>\n    <string name=\"sync_disabled\">סינכרון מכובה</string>\n    <string name=\"allows_for_sync_witch_youtube\">הערה: זה מאפשר סנכרון עם YouTube Music. לא ניתן לשנות זאת מאוחר יותר.</string>\n    <string name=\"generating_image\">יצירת תמונה</string>\n    <string name=\"please_wait\">אנא המתן</string>\n    <string name=\"cancel\">ביטול</string>\n    <string name=\"share_lyrics\">שתף את מילות שיר</string>\n    <string name=\"share_as_text\">שתף כטקסט</string>\n    <string name=\"share_as_image\">שתף כתמונה</string>\n    <string name=\"max_selection_limit\">מגבלת בחירה מקסימלית</string>\n    <string name=\"share_selected\">שתף את הנבחרים</string>\n    <string name=\"customize_colors\">התאמה אישית של צבעים</string>\n    <string name=\"text_color\">צבע טקסט</string>\n    <string name=\"secondary_text_color\">צבע טקסט משני</string>\n    <string name=\"sync_playlist_desc\">סנכרון רשימת ההשמעה עם YouTube Music</string>\n    <string name=\"description\">תיאור</string>\n    <string name=\"lyrics\">מילות השיר</string>\n    <string name=\"close\">סגור</string>\n    <string name=\"next\">הבא</string>\n    <string name=\"delete_playlist_desc\">מחיקת רשימת ההשמעה הזו באופן סופי</string>\n    <string name=\"copy_link\">העתקת הקישור</string>\n    <string name=\"select\">בחירת הכל</string>\n    <string name=\"like_all\">לייק להכל</string>\n    <string name=\"dislike_all\">דיסלייק להכל</string>\n    <string name=\"sort_by_last_updated\">מחיקת רשימת ההשמעה הזו סופית</string>\n    <string name=\"link_copied\">הקישור הועתק ללוח</string>\n    <string name=\"starting_radio\">הפעלת הרדיו</string>\n    <string name=\"now_playing\">מתנגן כעת</string>\n    <string name=\"hide_player_thumbnail\">הסתרת צלמית הנגן</string>\n    <string name=\"hide_player_thumbnail_desc\">החלפת תמונת האלבום בסמל היישום בנגן</string>\n    <string name=\"crop_album_art\">חיתוך תמונת האלבום</string>\n    <string name=\"crop_album_art_desc\">כפיית יחס אורך-רוחב ריבועי ע\\\"י חיתוך צלמיות הוידאו</string>\n    <string name=\"already_in_playlist\">כבר נכללים ברשימת ההשמעה:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">פעם אחת</item>\n        <item quantity=\"two\">%d פעמים</item>\n        <item quantity=\"other\">%d פעמים</item>\n    </plurals>\n    <string name=\"seek_forward_dynamic\">+%1$d שניות קדימה</string>\n    <string name=\"seek_backward_dynamic\">-%1$d שניות אחורה</string>\n    <string name=\"seek_seconds_addup\">סריקה מתקדמת</string>\n    <string name=\"seek_seconds_addup_description\">אם מופעלת, מוסיפה עד 5 שניות נוספות בהדרגה בכל דילוג של הסריקה</string>\n    <string name=\"default_style\">ברירת-המחדל</string>\n    <string name=\"primary_color_style\">צבע ראשי</string>\n    <string name=\"tertiary_color_style\">צבע שלישוני</string>\n    <string name=\"wavy\">גלי</string>\n    <string name=\"enable_swipe_thumbnail\">הפעלת החלקה למעבר שיר</string>\n    <string name=\"swipe_song_to_add\">החלקה על שיר לשמאל להוספתו לתור או לימין להשמעתו בהמשך</string>\n    <string name=\"swipe_song_to_remove\">החלקה על שיר להסרתו מרשימת ההשמעה</string>\n    <string name=\"lyrics_click_change\">שינוי מילות השיר בנגיעה</string>\n    <string name=\"lyrics_auto_scroll\">גלילה אוטומטית של מילות השיר</string>\n    <string name=\"lyrics_glow_effect\">הפעלת אפקט זוהר למילות השיר</string>\n    <string name=\"lyrics_glow_effect_desc\">הוספת הנפשות זוהר וקפיצה למילות השיר הפעילות</string>\n    <string name=\"enable_better_lyrics\">הפעלת מילות שיר משופרות</string>\n    <string name=\"enable_better_lyrics_desc\">שימוש בספק מילות שירים משופר לסנכרון מילה-למילה של מילות השיר</string>\n    <string name=\"enable_simpmusic\">הפעלת מילות שיר SimpMusic</string>\n    <string name=\"enable_simpmusic_desc\">שימוש בספק מילות-שיר SimpMusic למילות שיר מסונכרנות</string>\n    <string name=\"similar_content\">תוכן דומה</string>\n    <string name=\"player_background_style\">סגנון הרקע של הנגן</string>\n    <string name=\"player_background_solid\">מלא</string>\n    <string name=\"follow_theme\">עפ\\\"י ערכת הנושא</string>\n    <string name=\"gradient\">הדרגה</string>\n    <string name=\"new_player_design\">עיצוב נגן חדש</string>\n    <string name=\"new_mini_player_design\">עיצוב נגן זעיר חדש</string>\n    <string name=\"player_background_blur\">טשטוש</string>\n    <string name=\"player_buttons_style\">צבעי פקדי הנגן</string>\n    <string name=\"auto_scroll\">סנכרון מחדש</string>\n    <string name=\"slim\">צר</string>\n    <string name=\"slim_navbar\">סרגל ניווט תחתון צר</string>\n    <string name=\"auto_playlists\">רשימות השמעה אוטומטיות</string>\n    <string name=\"show_liked_playlist\">הצגת רשימת השמעה אהובה</string>\n    <string name=\"show_downloaded_playlist\">הצגת רשימת השמעה שהורדה</string>\n    <string name=\"show_top_playlist\">הצגת רשימת השמעה מובילה</string>\n    <string name=\"show_cached_playlist\">הצגת רשימץ השמעה שבמטמון</string>\n    <string name=\"show_uploaded_playlist\">הצגת רשימת השמעה שהועלתה</string>\n    <string name=\"shuffle_playlist_first\">רשימת השמעה/אלבום תחילה בנגינה בסדר אקראי</string>\n    <string name=\"shuffle_playlist_first_desc\">בעת השמעה בסדר אקראי, השמעה תחילה של כל השירים מרשימת ההשמעה/האלבום המקוריים, ואח\\\"כ תוכן דומה</string>\n    <string name=\"prevent_duplicate_tracks_in_queue\">מניעת רצועות כפולות בתור</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">בעת הוספת רצועה לתור ההשמעה, הסרתה ממקומה הקודם אם היא כבר בתור</string>\n    <string name=\"uploaded_playlist\">הועלו</string>\n    <string name=\"filter_uploaded\">הועלו</string>\n    <string name=\"enable\">הפעלה</string>\n    <string name=\"remove_from_cache\">הסרה מהמטמון</string>\n    <string name=\"about_artist\">אודות</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-iw/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"songs\">שירים</string>\n    <string name=\"history\">היסטוריה</string>\n    <string name=\"home\">בית</string>\n    <string name=\"artists\">אמנים</string>\n    <string name=\"albums\">אלבומים</string>\n    <string name=\"playlists\">פלייליסטים</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d נבחר</item>\n        <item quantity=\"two\">%d נבחרו</item>\n        <item quantity=\"other\">%d נבחרו</item>\n    </plurals>\n    <string name=\"stats\">סטטיסטיקות</string>\n    <string name=\"mood_and_genres\">מצב רוח וז\\'אנרים</string>\n    <string name=\"account\">חשבון</string>\n    <string name=\"quick_picks\">בחירות מהירות</string>\n    <string name=\"quick_picks_empty\">האזינו לשירים כדי ליצור את הבחירות המהירות שלכם</string>\n    <string name=\"forgotten_favorites\">מועדפים שנשכחו</string>\n    <string name=\"keep_listening\">המשך האזנה</string>\n    <string name=\"your_youtube_playlists\">פלייליסטים ביוטיוב שלך</string>\n    <string name=\"similar_to\">דומה ל</string>\n    <string name=\"new_release_albums\">אלבומים חדשים</string>\n    <string name=\"today\">היום</string>\n    <string name=\"yesterday\">אתמול</string>\n    <string name=\"this_week\">השבוע</string>\n    <string name=\"last_week\">בשבוע האחרון</string>\n    <string name=\"most_played_songs\">השירים הכי מושמעים</string>\n    <string name=\"most_played_artists\">האמנים הכי מושמעים</string>\n    <string name=\"most_played_albums\">אלבומים הכי מושמעים</string>\n    <string name=\"search\">חיפוש</string>\n    <string name=\"search_yt_music\">חפש ביוטיוב מוזיקה…</string>\n    <string name=\"search_library\">חפש בספרייה…</string>\n    <string name=\"filter_library\">ספרייה</string>\n    <string name=\"filter_liked\">אהבתי</string>\n    <string name=\"filter_downloaded\">ירדו</string>\n    <string name=\"filter_all\">הכל</string>\n    <string name=\"filter_songs\">שירים</string>\n    <string name=\"filter_videos\">סרטונים</string>\n    <string name=\"filter_albums\">אלבומים</string>\n    <string name=\"filter_artists\">אמנים</string>\n    <string name=\"filter_playlists\">פלייליסטים</string>\n    <string name=\"filter_community_playlists\">פלייליסטים קהילתיים</string>\n    <string name=\"filter_featured_playlists\">פלייליסטים נבחרים</string>\n    <string name=\"filter_bookmarked\">סומן כמועדף</string>\n    <string name=\"no_results_found\">לא נמצאו תוצאות</string>\n    <string name=\"library_song_empty\">שירי הספרייה יופיעו כאן</string>\n    <string name=\"library_artist_empty\">אמני הספרייה יופיעו כאן</string>\n    <string name=\"library_album_empty\">אלבומי הספרייה יופיעו כאן</string>\n    <string name=\"library_playlist_empty\">הפלייליסטים שלך יופיעו כאן</string>\n    <string name=\"from_your_library\">מהספרייה שלך</string>\n    <string name=\"other_versions\">גירסאות אחרות</string>\n    <string name=\"liked_songs\">שירים שאהבתי</string>\n    <string name=\"downloaded_songs\">שירים שהורדתי</string>\n    <string name=\"playlist_is_empty\">הפלייליסט ריק</string>\n    <string name=\"remove_download_playlist_confirm\">האם אתה באמת רוצה להסיר את כל שירי הפלייליסט \\\"%s\\\" מאחסון השירים שהורדו?</string>\n    <string name=\"delete_playlist_confirm\">האם אתה באמת רוצה למחוק את רשימת ההשמעה \\\"%s\\\"?</string>\n    <string name=\"retry\">נסה שוב</string>\n    <string name=\"radio\">רדיו</string>\n    <string name=\"shuffle\">ערבוב</string>\n    <string name=\"reset\">איפוס</string>\n    <string name=\"details\">פרטים</string>\n    <string name=\"edit\">עריכה</string>\n    <string name=\"start_radio\">התחל רדיו</string>\n    <string name=\"play\">נגן</string>\n    <string name=\"play_next\">הפעל הבא</string>\n    <string name=\"add_to_queue\">הוסף לתור</string>\n    <string name=\"add_to_library\">הוסף לספרייה</string>\n    <string name=\"add_all_to_library\">הוסף הכל לספרייה</string>\n    <string name=\"remove_from_library\">הסר מהספרייה</string>\n    <string name=\"remove_all_from_library\">הסר הכל מהספרייה</string>\n    <string name=\"action_download\">הורד</string>\n    <string name=\"downloading\">מוריד</string>\n    <string name=\"remove_download\">הסר הורדה</string>\n    <string name=\"import_playlist\">ייבוא פלייליסט</string>\n    <string name=\"add_to_playlist\">הוסף לפלייליסט</string>\n    <string name=\"view_artist\">הצג אמן</string>\n    <string name=\"view_album\">הצג אלבום</string>\n    <string name=\"refetch\">אחזור</string>\n    <string name=\"share\">שיתוף</string>\n    <string name=\"delete\">מחיקה</string>\n    <string name=\"remove_from_history\">הסר מההיסטוריה</string>\n    <string name=\"remove_from_playlist\">הסר מהפלייליסט</string>\n    <string name=\"remove_from_queue\">הסר מהתור</string>\n    <string name=\"search_online\">חיפוש באינטרנט</string>\n    <string name=\"action_sync\">סינכרון</string>\n    <string name=\"advanced\">מתקדם</string>\n    <string name=\"tempo_and_pitch\">קצב וגובה צליל</string>\n    <string name=\"sort_by_create_date\">תאריך הוספה</string>\n    <string name=\"sort_by_name\">שם</string>\n    <string name=\"sort_by_artist\">אמן</string>\n    <string name=\"sort_by_year\">שנה</string>\n    <string name=\"sort_by_song_count\">ספירת שירים</string>\n    <string name=\"sort_by_length\">משך</string>\n    <string name=\"sort_by_play_time\">זמן ניגון</string>\n    <string name=\"sort_by_custom\">הזמנה בהתאמה אישית</string>\n    <string name=\"media_id\">מזהה מדיה</string>\n    <string name=\"mime_type\">סוג MIME</string>\n    <string name=\"codecs\">קודקים</string>\n    <string name=\"bitrate\">קצב סיביות</string>\n    <string name=\"sample_rate\">קצב דגימה</string>\n    <string name=\"loudness\">עוצמת קול</string>\n    <string name=\"volume\">עוצמת שמע</string>\n    <string name=\"file_size\">גודל קובץ</string>\n    <string name=\"unknown\">לא ידוע</string>\n    <string name=\"copied\">הועתק ללוח</string>\n    <string name=\"edit_lyrics\">עריכת מילות שיר</string>\n    <string name=\"search_lyrics\">חיפוש מילות שיר</string>\n    <string name=\"edit_song\">ערוך שיר</string>\n    <string name=\"song_title\">כותרת שיר</string>\n    <string name=\"song_artists\">אמני שיר</string>\n    <string name=\"error_song_title_empty\">שם השיר לא יכול להיות ריק.</string>\n    <string name=\"error_song_artist_empty\">לא ניתן להשאיר את שדה אמן השיר ריק.</string>\n    <string name=\"save\">שמור</string>\n    <string name=\"choose_playlist\">בחר פלייליסט</string>\n    <string name=\"edit_playlist\">ערוך פלייליסט</string>\n    <string name=\"create_playlist\">צור פלייליסט</string>\n    <string name=\"playlist_name\">שם פלייליסט</string>\n    <string name=\"error_playlist_name_empty\">שם הפלייליסט לא יכול להיות ריק.</string>\n    <string name=\"edit_artist\">ערוך אמן</string>\n    <string name=\"artist_name\">שם אמן</string>\n    <string name=\"error_artist_name_empty\">שם האמן לא יכול להיות ריק.</string>\n    <string name=\"duplicates\">כפילויות</string>\n    <string name=\"skip_duplicates\">דילוג על כפילויות</string>\n    <string name=\"add_anyway\">הוסף בכל זאת</string>\n    <string name=\"duplicates_description_single\">השיר כבר נמצא ברשימת ההשמעה שלך</string>\n    <string name=\"duplicates_description_multiple\">%d שירים כבר נמצאים ברשימת ההשמעה שלך</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">שיר %d</item>\n        <item quantity=\"two\">%d שירים</item>\n        <item quantity=\"other\">%d שירים</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">אמן %d</item>\n        <item quantity=\"two\">%d אמנים</item>\n        <item quantity=\"other\">%d אמנים</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">אלבום %d</item>\n        <item quantity=\"two\">%d אלבומים</item>\n        <item quantity=\"other\">%d אלבומים</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">פלייליסט %d</item>\n        <item quantity=\"two\">%d פלייליסטים</item>\n        <item quantity=\"other\">%d פלייליסטים</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">שבוע %d</item>\n        <item quantity=\"two\">%d שבועות</item>\n        <item quantity=\"other\">%d שבועות</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">חודש %d</item>\n        <item quantity=\"two\">%d חודשים</item>\n        <item quantity=\"other\">%d חודשים</item>\n    </plurals>\n    <string name=\"end_of_song\">סוף השיר</string>\n    <string name=\"action_like_all\">‌</string>\n    <string name=\"queue_all_songs\">כל השירים</string>\n    <string name=\"music_player\">‪שירים שחיפשת</string>\n    <string name=\"pure_black\">שחור</string>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">שנה %d</item>\n        <item quantity=\"two\">%d שנים</item>\n        <item quantity=\"other\">%d שנים</item>\n    </plurals>\n    <string name=\"playlist_imported\">פלייליסט יובא</string>\n    <string name=\"removed_song_from_playlist\">\\\"%s\\\" הוסר מרשימת ההשמעה</string>\n    <string name=\"playlist_synced\">פלייסליט סונכרן</string>\n    <string name=\"undo\">בטל</string>\n    <string name=\"lyrics_not_found\">מילות השיר לא נמצאו</string>\n    <string name=\"sleep_timer\">טיימר שינה</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">דקה 1</item>\n        <item quantity=\"two\">%d דקות</item>\n        <item quantity=\"other\">%d דקות</item>\n    </plurals>\n    <string name=\"error_no_stream\">אין סטרים זמין</string>\n    <string name=\"error_no_internet\">אין חיבור אינטרנט</string>\n    <string name=\"error_timeout\">פסק זמן</string>\n    <string name=\"error_unknown\">שגיאה לא ידועה</string>\n    <string name=\"action_like\">לייק</string>\n    <string name=\"action_remove_like\">הסר לייק</string>\n    <string name=\"action_remove_like_all\">הסר את כל הלייקים</string>\n    <string name=\"action_shuffle_on\">ערבוב מופעל</string>\n    <string name=\"action_shuffle_off\">ערבוב כבוי</string>\n    <string name=\"repeat_mode_off\">מצב חזרה כבוי</string>\n    <string name=\"repeat_mode_one\">חזרה על השיר הנוכחי</string>\n    <string name=\"repeat_mode_all\">חזרה על התור</string>\n    <string name=\"queue_searched_songs\">שירים שחיפשת</string>\n    <string name=\"settings\">הגדרות</string>\n    <string name=\"appearance\">נראות</string>\n    <string name=\"theme\">עיצוב</string>\n    <string name=\"enable_dynamic_theme\">הפעלת עיצוב דינמית</string>\n    <string name=\"dark_theme\">עיצוב כהה</string>\n    <string name=\"dark_theme_on\">פועל</string>\n    <string name=\"dark_theme_off\">כבוי</string>\n    <string name=\"dark_theme_follow_system\">כמו המערכת</string>\n    <string name=\"customize_navigation_tabs\">התאמה אישית של כרטיסיות ניווט</string>\n    <string name=\"player\">נגן</string>\n    <string name=\"sided\">צדדי</string>\n    <string name=\"left\">שמאל</string>\n    <string name=\"center\">מרכז</string>\n    <string name=\"right\">ימין</string>\n    <string name=\"player_slider_style\">‎סגנון מחוון הנגן</string>\n    <string name=\"default_\">ברירת מחדל</string>\n    <string name=\"player_text_alignment\">יישור טקסט של הנגן</string>\n    <string name=\"lyrics_text_position\">מיקום מילות השיר</string>\n    <string name=\"squiggly\">מתפתל</string>\n    <string name=\"misc\">שונות</string>\n    <string name=\"default_open_tab\">‌כרטיסיית פתיחה כברירת מחדל</string>\n    <string name=\"grid_cell_size\">גודל תא הרשת</string>\n    <string name=\"small\">קטן</string>\n    <string name=\"big\">גדול</string>\n    <string name=\"content\">תוכן</string>\n    <string name=\"action_logout\">התנתקות</string>\n    <string name=\"action_login\">התחברות</string>\n    <string name=\"login\">התחברות</string>\n    <string name=\"not_logged_in\">לא מחובר</string>\n    <string name=\"login_failed\">ההתחברות נכשלה</string>\n    <string name=\"content_language\">שפת תוכן ברירת מחדל</string>\n    <string name=\"content_country\">מדינת תוכן ברירת מחדל</string>\n    <string name=\"system_default\">ברירת מחדל מערכת</string>\n    <string name=\"enable_proxy\">הפעלת פרוקסי</string>\n    <string name=\"proxy_type\">סוג פרוקסי</string>\n    <string name=\"proxy_url\">כתובת פרוקסי</string>\n    <string name=\"restart_to_take_effect\">הפעל מחדש כדי שיכנס לתוקף</string>\n    <string name=\"player_and_audio\">נגן ושמע</string>\n    <string name=\"audio_quality\">איכות שמע</string>\n    <string name=\"audio_quality_auto\">אוטומטי</string>\n    <string name=\"audio_quality_high\">גבוה</string>\n    <string name=\"audio_quality_low\">נמוך</string>\n    <string name=\"queue\">תור</string>\n    <string name=\"persistent_queue\">תור קבוע</string>\n    <string name=\"persistent_queue_desc\">שחזר את התור האחרון שלך כאשר האפליקציה נדלקת</string>\n    <string name=\"auto_load_more\">טעינה אוטומטית של שירים נוספים</string>\n    <string name=\"auto_load_more_desc\">הוסף אוטומטית שירים נוספים כאשר מגיעים לסוף התור, אם אפשר</string>\n    <string name=\"skip_silence\">דילוג על שקט</string>\n    <string name=\"audio_normalization\">נרמול שמע</string>\n    <string name=\"auto_skip_next_on_error\">דילוג אוטומטי לשיר הבא כאשר מתרחשת שגיאה</string>\n    <string name=\"auto_skip_next_on_error_desc\">הבטיחו את חוויית ההשמעה הרציפה שלכם</string>\n    <string name=\"stop_music_on_task_clear\">עצירת מוזיקה בעת ניקוי המשימה</string>\n    <string name=\"equalizer\">אקלוייזר</string>\n    <string name=\"storage\">אחסון</string>\n    <string name=\"cache\">מטמון</string>\n    <string name=\"image_cache\">מטמון תמונה</string>\n    <string name=\"song_cache\">מטמון שירים</string>\n    <string name=\"max_cache_size\">גודל מטמון מקסימלי</string>\n    <string name=\"unlimited\">ללא הגבלה</string>\n    <string name=\"clear_all_downloads\">נקה את כל ההורדות</string>\n    <string name=\"max_image_cache_size\">גודל מטמון מקסימלי של תמונה</string>\n    <string name=\"clear_image_cache\">נקה את מטמון התמונה</string>\n    <string name=\"max_song_cache_size\">גודל מטמון מקסימלי של שירים</string>\n    <string name=\"clear_song_cache\">נקה את מטמון השירים</string>\n    <string name=\"size_used\">%s בשימוש</string>\n    <string name=\"privacy\">פרטיות</string>\n    <string name=\"listen_history\">היסטוריית האזנה</string>\n    <string name=\"pause_listen_history\">השהיית היסטוריית ההאזנה</string>\n    <string name=\"clear_listen_history\">נקה את היסטוריית ההאזנה</string>\n    <string name=\"clear_listen_history_confirm\">האם אתה בטוח שברצונך לנקות את כל היסטוריית ההאזנה?</string>\n    <string name=\"search_history\">היסטוריית חיפוש</string>\n    <string name=\"pause_search_history\">השהיית היסטוריית החיפוש</string>\n    <string name=\"clear_search_history\">…נקה את היסטוריית החיפוש</string>\n    <string name=\"clear_search_history_confirm\">האם אתה בטוח שברצונך למחוק את כל היסטוריית החיפוש?</string>\n    <string name=\"use_login_for_browse\">השתמש בכניסה כדי לגלוש בתוכן</string>\n    <string name=\"use_login_for_browse_desc\">זה יכול להשפיע על התוכן שתראו, לדוגמה, יציג אלבומים למשתמשי פרימיום בלבד אם אתם מחוברים לחשבון פרימיום</string>\n    <string name=\"disable_screenshot\">השבתת צילום מסך</string>\n    <string name=\"disable_screenshot_desc\">כאשר אפשרות זו מופעלת, צילומי מסך ותצוגת האפליקציה תחת \\'אחרונים\\' מושבתות.</string>\n    <string name=\"enable_lrclib\">הפעל את ספק המילים של LrcLib</string>\n    <string name=\"enable_kugou\">הפעלת ספק מילות השיר KuGou</string>\n    <string name=\"hide_explicit\">הסתר תוכן בוטה</string>\n    <string name=\"backup_restore\">גיבוי ושחזור</string>\n    <string name=\"action_backup\">גיבוי</string>\n    <string name=\"action_restore\">שחזור</string>\n    <string name=\"imported_playlist\">רשימת השמעה מיובאת</string>\n    <string name=\"backup_create_success\">גיבוי נוצר בהצלחה</string>\n    <string name=\"backup_create_failed\">לא ניתן היה ליצור גיבוי</string>\n    <string name=\"restore_failed\">שחזור הגיבוי נכשל</string>\n    <string name=\"discord_integration\">שילוב דיסקורד</string>\n    <string name=\"discord_information\">Metrolist משתמשת בספריית KizzyRPC כדי לקבוע את סטטוס חשבון הדיסקורד שלך. זה כרוך בשימוש בחיבור Discord Gateway, דבר שעשוי להיחשב כהפרה של תנאי השימוש של Discord. עם זאת, לא ידוע על מקרים של השעיית חשבונות משתמש מסיבה זו. השימוש על אחריותך בלבד.\\n\\nMetrolist יחלץ רק את הטוקן שלך, וכל השאר מאוחסן באופן מקומי.</string>\n    <string name=\"dismiss\">התעלמות</string>\n    <string name=\"options\">אפשרויות</string>\n    <string name=\"preview\">תצוגה מקדימה</string>\n    <string name=\"enable_discord_rpc\">הפעלת נוכחות עשירה</string>\n    <string name=\"about\">אודות</string>\n    <string name=\"app_version\">גירסת אפליקציה</string>\n    <string name=\"new_version_available\">גירסה חדשה זמינה</string>\n    <string name=\"translation_models\">מודלים של תרגום</string>\n    <string name=\"clear_translation_models\">ניקוי מודלי תרגום</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-ja/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">ローカル</string>\n    <string name=\"remote_history\">リモート</string>\n    <string name=\"charts\">チャート</string>\n    <string name=\"back_button_desc\">戻る</string>\n    <string name=\"album_cover_desc\">アルバムカバー</string>\n    <string name=\"trending\">急上昇</string>\n    <string name=\"weeks\">週</string>\n    <string name=\"months\">月</string>\n    <string name=\"years\">年</string>\n    <string name=\"continuous\">全期間</string>\n    <string name=\"liked\">高評価した曲</string>\n    <string name=\"offline\">ダウンロード済み</string>\n    <string name=\"top_music_videos\">人気のミュージックビデオ</string>\n    <string name=\"my_top\">マイトップ</string>\n    <string name=\"cached_playlist\">キャッシュ済み</string>\n    <string name=\"sync_playlist\">プレイリストを同期</string>\n    <string name=\"sync_disabled\">同期は無効です</string>\n    <string name=\"generating_image\">画像を生成中です</string>\n    <string name=\"please_wait\">お待ちください</string>\n    <string name=\"cancel\">キャンセル</string>\n    <string name=\"share_lyrics\">歌詞を共有</string>\n    <string name=\"customize_colors\">カラーをカスタマイズ</string>\n    <string name=\"text_color\">テキストカラー</string>\n    <string name=\"secondary_text_color\">セカンダリテキストカラー</string>\n    <string name=\"background_color\">バックグラウンドカラー</string>\n    <string name=\"remove_from_cache\">キャッシュから削除</string>\n    <string name=\"copy_link\">リンクをコピー</string>\n    <string name=\"select\">すべて選択</string>\n    <string name=\"like_all\">すべて高評価</string>\n    <string name=\"link_copied\">リンクをクリップボードにコピーしました</string>\n    <string name=\"lyrics\">歌詞</string>\n    <string name=\"dislike_all\">すべて低評価</string>\n    <string name=\"share_as_text\">テキストとして共有</string>\n    <string name=\"share_as_image\">画像として共有</string>\n    <string name=\"max_selection_limit\">選択の最大制限</string>\n    <string name=\"share_selected\">選択した項目を共有</string>\n    <string name=\"already_in_playlist\">この曲はすでにプレイリストに追加されています</string>\n    <string name=\"lyrics_click_change\">歌詞をタップして該当の箇所から再生</string>\n    <string name=\"slim\">スリム</string>\n    <string name=\"similar_content\">似た曲</string>\n    <string name=\"show_cached_playlist\">キャッシュした曲のプレイリストを表示</string>\n    <string name=\"auto_playlists\">自動プレイリスト</string>\n    <string name=\"show_liked_playlist\">高評価した曲のプレイリストを表示</string>\n    <string name=\"show_downloaded_playlist\">ダウンロードした曲のプレイリストを表示</string>\n    <string name=\"show_top_playlist\">マイトップのプレイリストを表示</string>\n    <string name=\"advanced_login\">トークンを使用してログイン</string>\n    <string name=\"token_hidden\">トークンを表示</string>\n    <string name=\"token_shown\">トークンを編集</string>\n    <string name=\"general\">一般</string>\n    <string name=\"proxy\">プロキシ</string>\n    <string name=\"app_language\">言語</string>\n    <string name=\"slim_navbar\">コンパクトな下部ナビゲーションバー</string>\n    <string name=\"enable_similar_content\">似た曲を再生</string>\n    <string name=\"follow_theme\">テーマに従う</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"other\">%d秒</item>\n    </plurals>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"release_notes\">リリースノート</string>\n    <string name=\"player_background_style\">プレーヤーの背景</string>\n    <string name=\"player_background_blur\">ぼかし</string>\n    <string name=\"player_buttons_style\">プレーヤーボタンの色</string>\n    <string name=\"information\">情報</string>\n    <string name=\"default_style\">デフォルト</string>\n    <string name=\"enable_swipe_thumbnail\">左右にスワイプして曲を変更</string>\n    <string name=\"dislikes\">低評価数</string>\n    <string name=\"auto_download_on_like\">高評価した曲を自動でダウンロード</string>\n    <string name=\"description\">説明</string>\n    <string name=\"not_logged_in_youtube\">YouTubeにログインしてください</string>\n    <string name=\"default_links\">対応のリンクを開く</string>\n    <string name=\"views\">視聴回数</string>\n    <string name=\"likes\">高評価数</string>\n    <string name=\"allows_for_sync_witch_youtube\">YouTube Musicと同期します。後から変更できません。</string>\n    <string name=\"sort_by_last_updated\">更新日</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"other\">%d回</item>\n    </plurals>\n    <string name=\"gradient\">グラデーション</string>\n    <string name=\"new_player_design\">新しいプレーヤーのデザイン</string>\n    <string name=\"new_mini_player_design\">新しいミニプレーヤーのデザイン</string>\n    <string name=\"swipe_song_to_add\">曲を右にスワイプして次に再生、左にスワイプしてキューに追加</string>\n    <string name=\"lyrics_auto_scroll\">歌詞を自動でスクロール</string>\n    <string name=\"lyrics_romanize_japanese\">日本語の歌詞にローマ字を追加</string>\n    <string name=\"lyrics_romanize_korean\">韓国語の歌詞にローマ字を追加</string>\n    <string name=\"token_adv_login_description\">高度なログイン方法です。ウェブポータルの代わりに、ログイントークンを直接入力・更新でき、複数のデバイスでのログインを高速化できます。無効な形式のトークンは使用できません</string>\n    <string name=\"yt_sync\">アカウントと自動で同期</string>\n    <string name=\"more_content\">その他のコンテンツ</string>\n    <string name=\"default_lib_chips\">デフォルトのライブラリタブを変更</string>\n    <string name=\"set_quick_picks\">ホームタブのおすすめの曲の基準</string>\n    <string name=\"last_song_listened\">最後に聴いた曲</string>\n    <string name=\"similar_content_desc\">キューの最後まで再生したあと、自動で似た曲を追加します</string>\n    <string name=\"import_online\">M3U形式のプレイリストをインポート</string>\n    <string name=\"import_csv\">CSV形式のプレイリストをインポート</string>\n    <string name=\"playlist_add_local_to_synced_note\">同期/リモートプレイリストへのローカル曲の追加はサポートしていません</string>\n    <string name=\"auto_download_on_like_desc\">ハートをつけた曲を自動でダウンロードします</string>\n    <string name=\"swipe_sensitivity\">ミニプレーヤーのスワイプ感度</string>\n    <string name=\"clear_song_cache_dialog\">キャッシュした曲をすべて削除しますか？</string>\n    <string name=\"clear_image_cache_dialog\">画像のキャッシュをすべて削除しますか？</string>\n    <string name=\"clear_downloads_dialog\">ダウンロードした曲をすべて削除しますか？</string>\n    <string name=\"disable\">オフ</string>\n    <string name=\"open_app_settings_error\">アプリの設定を開けませんでした</string>\n    <string name=\"all_time\">全期間</string>\n    <string name=\"past_24_hours\">過去24時間</string>\n    <string name=\"past_week\">過去1週間</string>\n    <string name=\"past_month\">過去1ヶ月間</string>\n    <string name=\"past_year\">過去1年間</string>\n    <string name=\"top_length\">マイトップリストの曲数</string>\n    <string name=\"history_duration\">再生履歴の期間</string>\n    <string name=\"subscribe\">チャンネル登録</string>\n    <string name=\"subscribed\">チャンネル登録済み</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"now_playing\">再生中</string>\n    <string name=\"close\">閉じる</string>\n    <string name=\"hide_player_thumbnail\">プレーヤーにサムネイルを表示しない</string>\n    <string name=\"hide_player_thumbnail_desc\">再生画面に表示されるアルバムアートをアプリアイコンに置き換えます</string>\n    <string name=\"seek_forward_dynamic\">%1$d秒進む</string>\n    <string name=\"seek_backward_dynamic\">%1$d秒戻る</string>\n    <string name=\"seek_seconds_addup\">シーク時間を段階的に増やす</string>\n    <string name=\"seek_seconds_addup_description\">連続してシーク操作を行うごとに５秒ずつ加算します</string>\n    <string name=\"disable_load_more_when_repeat_all\">全曲リピート再生しているときは追加の曲を自動で読み込まない</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">全曲リピート再生しているときは似た曲の追加を行いません</string>\n    <string name=\"uploaded_playlist\">アップロード済み</string>\n    <string name=\"filter_uploaded\">アップロード済み</string>\n    <string name=\"starting_radio\">ラジオを開始中</string>\n    <string name=\"swipe_song_to_remove\">曲をスワイプしてプレイリストから削除</string>\n    <string name=\"show_uploaded_playlist\">アップロードした曲のプレイリストを表示</string>\n    <string name=\"edit_playlist_cover\">カバー画像を変更</string>\n    <string name=\"edit_playlist_cover_note\">プレイリストのカバー画像を変更するには、アカウントが電話番号に紐付けられ、YouTube Musicで確認済みである必要があります。</string>\n    <string name=\"edit_playlist_cover_note_wait\">新しいカバー画像が反映するまでに時間がかかる場合があります。</string>\n    <string name=\"choose_from_library\">写真から選択</string>\n    <string name=\"remove_custom_image\">変更した画像を削除</string>\n    <string name=\"config_proxy\">プロキシ設定</string>\n    <string name=\"proxy_username\">プロキシのユーザー名</string>\n    <string name=\"proxy_password\">プロキシのパスワード</string>\n    <string name=\"enable_authentication\">プロキシ認証</string>\n    <string name=\"discord_use_details\">曲名を強調して表示</string>\n    <string name=\"discord_use_details_description\">アーティスト名より曲名を目立つように表示します</string>\n    <string name=\"lyrics_romanization_cyrillic\">キリル文字</string>\n    <string name=\"lyrics_romanize_title\">ローマ字</string>\n    <string name=\"lyrics_romanization\">歌詞にローマ字を追加</string>\n    <string name=\"lyrics_romanize_russian\">ロシア語の歌詞にローマ字を追加</string>\n    <string name=\"lyrics_romanize_ukrainian\">ウクライナ語の歌詞にローマ字を追加</string>\n    <string name=\"lyrics_romanize_belarusian\">ベラルーシ語の歌詞にローマ字を追加</string>\n    <string name=\"lyrics_romanize_kyrgyz\">キルギス語の歌詞にローマ字を追加</string>\n    <string name=\"lyrics_romanize_serbian\">セルビア語の歌詞にローマ字を追加</string>\n    <string name=\"lyrics_romanize_bulgarian\">ブルガリア語の歌詞にローマ字を追加</string>\n    <string name=\"line_by_line_option_title\">行ごとに言語を判別(実験的)</string>\n    <string name=\"line_by_line_option_desc\">キリル文字の言語は、曲全体ではなく行ごとに判別されます。</string>\n    <string name=\"line_by_line_dialog_title\">確認</string>\n    <string name=\"line_by_line_dialog_desc\">この機能は実験的で、動作が不安定になる場合があります。\\n通常、言語は曲全体から判別されますが、この設定をオンにすると行ごとに判別されます。複数の言語が混在する曲に対応できますが、判別結果が常に正しいとは限りません（例：ウクライナ語特有の文字が含まれていない歌詞は、ロシア語として判別される場合があります）。\\n問題がない場合は、この設定はオフにすることをおすすめします。</string>\n    <string name=\"romanize_current_track\">歌詞にローマ字を追加</string>\n    <string name=\"settings_section_ui\">インターフェース</string>\n    <string name=\"settings_section_privacy\">プライバシーとセキュリティ</string>\n    <string name=\"settings_section_player_content\">プレーヤーとコンテンツ</string>\n    <string name=\"settings_section_storage\">ストレージとデータ</string>\n    <string name=\"settings_section_system\">システムと概要</string>\n    <string name=\"updater\">アップデーター</string>\n    <string name=\"check_for_updates\">自動でアップデートを確認する</string>\n    <string name=\"update_notifications\">新しいバージョンがある場合は通知を受け取る</string>\n    <string name=\"update_available_title\">アップデートが利用可能です</string>\n    <string name=\"update_channel_name\">アプリをアップデート</string>\n    <string name=\"update_channel_desc\">新しいバージョンの通知</string>\n    <string name=\"audio_offload\">オフロード再生</string>\n    <string name=\"audio_offload_description\">音声の再生にオフロードのオーディオパスを使用します。この設定をオフにすると消費電力が増加する可能性がありますが、再生や音声の処理に関する問題の改善に役立つ場合があります</string>\n    <string name=\"lyrics_romanize_macedonian\">マケドニア語の歌詞にローマ字を追加</string>\n    <string name=\"integrations\">外部のサービスと連携</string>\n    <string name=\"username\">ユーザー名</string>\n    <string name=\"password\">パスワード</string>\n    <string name=\"lastfm_integration\">Last.fmと連携</string>\n    <string name=\"enable_scrobbling\">再生履歴を送信</string>\n    <string name=\"lastfm_now_playing\">再生中の曲を反映する</string>\n    <string name=\"scrobbling_configuration\">再生履歴の設定</string>\n    <string name=\"scrobble_min_track_duration\">送信する最小の再生時間</string>\n    <string name=\"scrobble_delay_percent\">送信する再生の割合</string>\n    <string name=\"scrobble_delay_minutes\">送信までの時間</string>\n    <string name=\"about_artist\">概要</string>\n    <string name=\"show_more\">続きを読む</string>\n    <string name=\"show_less\">折りたたむ</string>\n    <string name=\"artist_page_settings\">アーティストページ</string>\n    <string name=\"show_artist_description\">アーティストの説明を表示</string>\n    <string name=\"show_artist_subscriber_count\">チャンネル登録者数を表示</string>\n    <string name=\"show_artist_monthly_listeners\">月間視聴者数を表示</string>\n    <string name=\"download_playlist_desc\">すべての曲をオフライン再生用にダウンロードします</string>\n    <string name=\"remove_download_playlist_desc\">このプレイリストからダウンロードしたすべての曲を削除します</string>\n    <string name=\"download_in_progress_desc\">ダウンロードが進行中です</string>\n    <string name=\"share_playlist_desc\">このプレイリストを他の人と共有します</string>\n    <string name=\"delete_playlist_desc\">このプレイリストを完全に削除します</string>\n    <string name=\"sync_playlist_desc\">このプレイリストをYouTube Musicと同期します</string>\n    <string name=\"crop_album_art\">アルバムアートを切り抜く</string>\n    <string name=\"crop_album_art_desc\">動画のサムネイルを切り抜いて正方形の比率にします</string>\n    <string name=\"primary_color_style\">メインカラー</string>\n    <string name=\"tertiary_color_style\">アクセントカラー</string>\n    <string name=\"wavy\">波</string>\n    <string name=\"lyrics_glow_effect\">歌詞を発光させる</string>\n    <string name=\"lyrics_glow_effect_desc\">再生中の歌詞を発光させて強調します</string>\n    <string name=\"enable_better_lyrics\">歌詞の提供元にBetter Lyricsを使用</string>\n    <string name=\"enable_better_lyrics_desc\">単語ごとに同期した歌詞の表示にBetter Lyricsを使用します</string>\n    <string name=\"enable_simpmusic\">歌詞の提供元にSimpMusic Lyricsを使用</string>\n    <string name=\"enable_simpmusic_desc\">単語ごとに同期した歌詞の表示にSimpMusic Lyricsを使用します</string>\n    <string name=\"auto_scroll\">同期</string>\n    <string name=\"shuffle_playlist_first_desc\">シャッフル再生時、プレイリストやアルバム内の曲をすべて再生したあとに似た曲を再生します</string>\n    <string name=\"shuffle_playlist_first\">プレイリスト/アルバムの曲を優先</string>\n    <string name=\"show_wrapped_card\">Wrappedカードを表示</string>\n    <string name=\"skip_silence_instant\">無音の部分をスキップ</string>\n    <string name=\"skip_silence_instant_desc\">無音の部分は早送りせずに自動でスキップします</string>\n    <string name=\"lyrics_romanize_chinese\">中国語の歌詞にローマ字を追加</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"logging_in\">ログインしています…</string>\n    <string name=\"hide_video_songs\">動画の曲を非表示</string>\n    <string name=\"details_desc\">この曲の詳細を表示します</string>\n    <string name=\"mini_player\">ミニプレーヤー</string>\n    <string name=\"equalizer_header\">イコライザー</string>\n    <string name=\"persistent_shuffle_title\">シャッフル再生を保持</string>\n    <string name=\"persistent_shuffle_desc\">新しい曲やプレイリストを再生してもシャッフル再生状態を維持します</string>\n    <string name=\"remember_shuffle_and_repeat\">シャッフル/リピート再生を保持</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">アプリを再起動したあともシャッフル/リピート再生を維持します</string>\n    <string name=\"pause_music_when_media_is_muted\">ミュート時に再生を停止</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">プレーヤーを展開中は画面をオンに保つ</string>\n    <string name=\"lyrics_offset\">歌詞のタイミングを調整</string>\n    <string name=\"google_cast_description\">Chromecastなどのキャスト対応のデバイスに音声をキャストできます</string>\n    <string name=\"last_fm_send_likes\">高評価/低評価を反映する</string>\n    <string name=\"last_fm_send_likes_description\">Metrolistで曲に高評価/低評価をつけると、Last.fmでもラブ/アンラブされます</string>\n    <string name=\"edit_desc\">タイトルやアーティスト名を変更します</string>\n    <string name=\"start_radio_desc\">この曲をもとに似た曲を再生します</string>\n    <string name=\"play_next_desc\">キューの先頭に追加します</string>\n    <string name=\"add_to_queue_desc\">キューの最後に追加します</string>\n    <string name=\"add_to_library_desc\">ライブラリに保存します</string>\n    <string name=\"download_desc\">オフライン再生用に保存します</string>\n    <string name=\"add_to_playlist_desc\">プレイリストに追加します</string>\n    <string name=\"refetch_desc\">YouTube Musicから最新のメタデータを取得します</string>\n    <string name=\"share_desc\">このコンテンツのリンクを共有します</string>\n    <string name=\"delete_desc\">このコンテンツを完全に削除します</string>\n    <string name=\"advanced_desc\">曲の再生速度と音程を変更します</string>\n    <string name=\"equalizer_desc\">オーディオイコライザーを調整します</string>\n    <string name=\"enable_dynamic_icon\">動的アイコン</string>\n    <string name=\"pure_black_mini_player\">ピュアブラックのミニプレーヤー</string>\n    <string name=\"listen_together\">Listen Together</string>\n    <string name=\"play_pause\">再生/停止</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"apple_music_style\">Apple Music</string>\n    <string name=\"karaoke\">カラオケ</string>\n    <string name=\"none\">なし</string>\n    <string name=\"cache_size_warning_confirm\">続行</string>\n    <string name=\"fade\">フェード</string>\n    <string name=\"glow\">発光</string>\n    <string name=\"slide\">スライド</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">アルバムアート</string>\n    <string name=\"wrapped_playlist_saved\">プレイリストを保存しました</string>\n    <string name=\"system_equalizer\">システムイコライザー</string>\n    <string name=\"eq_disabled\">無効</string>\n    <string name=\"error_title\">エラー</string>\n    <string name=\"open\">開く</string>\n    <string name=\"copied_title\">タイトルをコピーしました</string>\n    <string name=\"copied_artist\">アーティスト名をコピーしました</string>\n    <string name=\"album_art\">アルバムアート</string>\n    <string name=\"next\">次へ</string>\n    <string name=\"like\">高評価</string>\n    <string name=\"not_playing\">再生していません</string>\n    <string name=\"widget_music_player\">音楽プレーヤー</string>\n    <string name=\"listen_together_server_url\">サーバーURL</string>\n    <string name=\"listen_together_choose_server\">サーバーを選択</string>\n    <string name=\"listen_together_custom_server\">カスタムサーバー</string>\n    <string name=\"listen_together_use_custom_server\">カスタムサーバーを使用</string>\n    <string name=\"listen_together_username\">ユーザー名</string>\n    <string name=\"listen_together_connected\">接続済み</string>\n    <string name=\"listen_together_reconnecting\">再接続中…</string>\n    <string name=\"listen_together_disconnected\">切断中</string>\n    <string name=\"listen_together_connecting\">接続中…</string>\n    <string name=\"listen_together_error\">接続エラー</string>\n    <string name=\"listen_together_create_room\">ルームを作成</string>\n    <string name=\"listen_together_join_room\">ルームに参加</string>\n    <string name=\"listen_together_room_code\">ルームコード</string>\n    <string name=\"mute\">ミュート</string>\n    <string name=\"unmute\">ミュート解除</string>\n    <string name=\"listen_together_view_logs\">ログ</string>\n    <string name=\"listen_together_notification_channel_name\">Listen Together</string>\n    <string name=\"invalid_room_code\">無効なルームコードです</string>\n    <string name=\"room_code\">ルームコード</string>\n    <string name=\"leave_room\">ルームから退出</string>\n    <string name=\"join_room\">参加</string>\n    <string name=\"create_room\">作成</string>\n    <string name=\"connect\">接続</string>\n    <string name=\"disconnect\">切断</string>\n    <string name=\"create\">作成</string>\n    <string name=\"join\">参加</string>\n    <string name=\"copy\">コピー</string>\n    <string name=\"copied_to_clipboard\">クリップボードにコピーしました</string>\n    <string name=\"kick_user\">キック</string>\n    <string name=\"host_label\">ホスト</string>\n    <string name=\"you_label\">あなた</string>\n    <string name=\"enter_username\">ユーザー名を入力</string>\n    <string name=\"error_username_empty\">ユーザー名が必要です。</string>\n    <string name=\"resync\">同期</string>\n    <string name=\"copy_code\">コードをコピー</string>\n    <string name=\"permanently_kick_user\">完全にブロック</string>\n    <string name=\"manage_user\">ユーザー管理</string>\n    <string name=\"listen_together_blocked_users\">ブロックしたユーザー</string>\n    <string name=\"listen_together_no_blocked_users\">誰もブロックしていません</string>\n    <string name=\"unblock\">ブロックを解除</string>\n    <string name=\"crash_title\">アプリがクラッシュしました</string>\n    <string name=\"crash_share_logs\">クラッシュログを共有</string>\n    <string name=\"crash_share_title\">クラッシュレポートを共有</string>\n    <string name=\"crash_report_subject\">Metrolistのクラッシュレポート</string>\n    <string name=\"crash_close\">閉じる</string>\n    <string name=\"crash_no_log\">クラッシュログがありません</string>\n    <string name=\"palette_dynamic\">ダイナミック</string>\n    <string name=\"palette_crimson\">クリムゾン</string>\n    <string name=\"palette_rose\">ローズ</string>\n    <string name=\"palette_purple\">紫</string>\n    <string name=\"palette_deep_purple\">ディープパープル</string>\n    <string name=\"palette_indigo\">インディゴ</string>\n    <string name=\"palette_blue\">青</string>\n    <string name=\"palette_sky_blue\">スカイブルー</string>\n    <string name=\"palette_cyan\">シアン</string>\n    <string name=\"palette_teal\">ティール</string>\n    <string name=\"palette_green\">緑</string>\n    <string name=\"palette_light_green\">ライトグリーン</string>\n    <string name=\"palette_lime\">ライム</string>\n    <string name=\"palette_yellow\">黄色</string>\n    <string name=\"palette_amber\">アンバー</string>\n    <string name=\"palette_orange\">オレンジ</string>\n    <string name=\"palette_deep_orange\">ディープオレンジ</string>\n    <string name=\"palette_brown\">ブラウン</string>\n    <string name=\"palette_grey\">グレー</string>\n    <string name=\"palette_blue_grey\">ブルーグレー</string>\n    <string name=\"cd_back\">戻る</string>\n    <string name=\"cd_pure_black_mode\">ピュアブラックモード</string>\n    <string name=\"cd_light_mode\">ライトモード</string>\n    <string name=\"cd_dark_mode\">ダークモード</string>\n    <string name=\"cd_system_mode\">システムに従う</string>\n    <string name=\"skip_silence_desc\">曲の無音の部分を早送りします</string>\n    <string name=\"lyrics_animation_style\">単語ごとのアニメーションスタイル</string>\n    <string name=\"lyrics_text_size\">歌詞のテキストサイズ</string>\n    <string name=\"lyrics_line_spacing\">歌詞の行間隔</string>\n    <string name=\"wrapped_no_data\">データがありません</string>\n    <string name=\"wrapped_create_playlist\">プレイリストを作成</string>\n    <string name=\"no_profiles\">イコライザープロファイルがありません</string>\n    <string name=\"import_profile\">プロファイルをインポート</string>\n    <string name=\"delete_profile_desc\">プロファイルを削除</string>\n    <string name=\"import_error_title\">インポートエラー</string>\n    <string name=\"widget_turntable\">ターンテーブル</string>\n    <string name=\"widget_description\">再生コントロール付きのウィジェット</string>\n    <string name=\"turntable_widget_description\">再生と高評価の操作ができる円形のウィジェット</string>\n    <string name=\"listen_together_create_room_desc\">ルームを作成し、コードを友達と共有します</string>\n    <string name=\"listen_together_description\">友達とリアルタイムで音楽を一緒に聴くことができます。ルームを作成してホストするか、コードを使って既存のルームに参加できます。</string>\n    <string name=\"listen_together_background_disconnect_note\">音楽を再生していない状態でルームを作成し、その後ほかのアプリに切り替えると、切断される場合があります。</string>\n    <string name=\"join_request_denied\">参加リクエストが拒否されました</string>\n    <string name=\"join_existing_room\">既存のルームに参加</string>\n    <string name=\"creating_room\">ルームを作成中…</string>\n    <string name=\"cache_size_warning_title\">ちょっと待って！</string>\n    <string name=\"cache_size_warning_message\">現在アプリが使用している容量（%1$s）より小さいキャッシュサイズ上限を選択しています。新しい上限に合わせるため、一部の%2$sが削除されることがあります。続行しますか？</string>\n    <string name=\"album_art_for\">%sのアルバムアート</string>\n    <string name=\"wrapped_total_albums_title\">これまでに再生した</string>\n    <string name=\"wrapped_total_albums_subtitle\">ユニークなアルバム数</string>\n    <string name=\"wrapped_top_album_title\">あなたのトップアルバムは</string>\n    <string name=\"wrapped_playlist_ready\">あなた専用のプレイリストが完成しました</string>\n    <string name=\"wrapped_top_5_albums_title\">あなたのトップ5アルバム</string>\n    <string name=\"wrapped_album_listening_time\">このアルバムを合計%d分再生しました</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d分</string>\n    <string name=\"wrapped_top_5_artists_title\">今年のトップアーティスト</string>\n    <string name=\"wrapped_artist_listening_time\">%d分</string>\n    <string name=\"wrapped_top_5_songs_title\">今年のトップソング</string>\n    <string name=\"wrapped_top_artist_title\">今年のトップアーティストは</string>\n    <string name=\"wrapped_top_artist_image_content_description\">トップアーティスト画像</string>\n    <string name=\"wrapped_top_artist_listening_time\">合計%d分再生しました</string>\n    <string name=\"wrapped_top_song_title\">最も再生した曲は</string>\n    <string name=\"wrapped_top_song_listening_time\">合計%d分再生しました</string>\n    <string name=\"wrapped_total_artists_title\">再生した</string>\n    <string name=\"wrapped_total_artists_subtitle\">ユニークなアーティスト数</string>\n    <string name=\"wrapped_total_songs_title\">再生した</string>\n    <string name=\"wrapped_total_songs_subtitle\">ユニークな曲数</string>\n    <string name=\"wrapped_intro_subtitle\">これまでに聴いてきた音楽を振り返りましょう</string>\n    <string name=\"wrapped_intro_button\">さあ、始めよう！</string>\n    <string name=\"wrapped_logo_content_description\">Metrolistロゴ</string>\n    <string name=\"wrapped_ready_title\">あなたのWrappedが完成しました！</string>\n    <string name=\"wrapped_ready_subtitle\">今年あなたが夢中になった音楽を見てみましょう。</string>\n    <string name=\"wrapped_thank_you\">聴いてくれてありがとう</string>\n    <string name=\"wrapped_special_thanks\">Metrolistを制作したMO Agamyに特別な感謝を</string>\n    <string name=\"wrapped_close\">Wrappedを閉じる</string>\n    <string name=\"wrapped_playlist_title\">あなたの%sWrapped</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"other\">%dプロファイル</item>\n    </plurals>\n    <plurals name=\"band_count\">\n        <item quantity=\"other\">%dバンド</item>\n    </plurals>\n    <string name=\"delete_profile_confirmation\">%1$sを削除しますか？この操作は元に戻せません。</string>\n    <string name=\"error_file_read\">ファイルを読み取れませんでした</string>\n    <string name=\"error_file_open\">ファイルを開けません：%1$s</string>\n    <string name=\"error_eq_apply_failed\">イコライザープロファイルを適用できません：%1$s</string>\n    <string name=\"casting_to\">%sにキャスト中</string>\n    <string name=\"progress_percent\">進行状況：%s%%</string>\n    <string name=\"listening_to_metrolist\">Metrolistで再生中</string>\n    <string name=\"failed_to_create_image\">画像の作成に失敗しました：%s</string>\n    <string name=\"error_playing\">再生中にエラーが発生しました</string>\n    <string name=\"failed_to_parse_proxy\">プロキシURLを解析できません。</string>\n    <string name=\"error_playback_failed\">再生に失敗しました</string>\n    <string name=\"no_song_playing\">再生中の曲はありません</string>\n    <string name=\"tap_to_open\">タップしてMetrolistを開く</string>\n    <string name=\"previous\">前へ</string>\n    <string name=\"tap_to_play\">タップしてMetrolistを開く</string>\n    <string name=\"listen_together_you_are_host\">あなたはホストです</string>\n    <string name=\"listen_together_you_are_guest\">あなたはゲストです</string>\n    <string name=\"listen_together_join_requests\">参加リクエスト</string>\n    <string name=\"listen_together_view_logs_desc\">接続とメッセージのデバッグ</string>\n    <string name=\"listen_together_logs\">接続ログ</string>\n    <string name=\"listen_together_no_logs\">ログはありません</string>\n    <string name=\"listen_together_auto_approval_joins\">参加リクエストを自動で承認</string>\n    <string name=\"listen_together_auto_approval_joins_desc\">参加リクエストを手動で確認せず、自動的に承認します</string>\n    <string name=\"listen_together_sync_volume\">ホストの音量と同期</string>\n    <string name=\"cd_palette_item\">%1$sパレット</string>\n    <string name=\"listen_together_not_configured\">Listen Togetherが設定されていません。”設定” ▶ ”連携” ▶ ”Listen Together” でサーバーURLを設定してください。</string>\n    <string name=\"listen_together_join_request_notification\">%1$sがルームへの参加を希望しています</string>\n    <string name=\"kick_user_desc\">このユーザーをセッションから削除します</string>\n    <string name=\"listen_together_blocked_users_count\">%d人ブロックしています</string>\n    <string name=\"transfer_ownership\">所有権を譲渡</string>\n    <string name=\"listen_together_cannot_edit_username_in_room\">ルームに参加中はユーザー名を変更できません</string>\n    <string name=\"clear\">削除</string>\n    <string name=\"listen_together_notification_channel_desc\">Listen Togetherのイベント通知</string>\n    <string name=\"joining_room\">ルーム %sに参加中…</string>\n    <string name=\"crash_description\">予期しないエラーが発生しました。問題の解決に役立てるため、クラッシュレポートを共有してください。</string>\n    <string name=\"in_room\">ルームに参加中</string>\n    <string name=\"listen_together_suggestion_sent\">ホストに提案を送信しました！</string>\n    <string name=\"waiting_for_approval\">ホストの承認を待っています</string>\n    <string name=\"permanently_kick_user_desc\">このユーザーの参加リクエストをブロックし、提案を非表示にします</string>\n    <string name=\"listen_together_sync_volume_desc\">ゲストの音量はホストの音量に同期されます</string>\n    <string name=\"enable_high_refresh_rate_desc\">ディスプレイを対応している中で最も高いリフレッシュレートで強制的に動作させます</string>\n    <string name=\"suggest_to_host\">ホストに提案</string>\n    <string name=\"pending_suggestions\">保留中の提案</string>\n    <string name=\"listen_together_settings_desc\">サーバーやユーザー名などを設定します</string>\n    <string name=\"pending_requests\">保留中のリクエスト</string>\n    <string name=\"not_set\">未設定</string>\n    <string name=\"hosting_room\">ルームをホスト中</string>\n    <string name=\"enable_high_refresh_rate\">高リフレッシュレート</string>\n    <string name=\"connected_users\">接続中のユーザー</string>\n    <string name=\"user_blocked_by_host\">ホストによってユーザーがブロックされました</string>\n    <string name=\"listen_together_suggestion_received\">%1$sが%2$sをリクエストしました</string>\n    <string name=\"reject\">拒否</string>\n    <string name=\"enter_room_code\">ルームコードを入力</string>\n    <string name=\"approve\">承認</string>\n    <string name=\"transfer_ownership_desc\">このユーザーをルームのホストにします</string>\n    <string name=\"play_all\">すべて再生</string>\n    <string name=\"listen_together_room_created\">ルームを作成しました：%s</string>\n    <string name=\"crossfade\">クロスフェード</string>\n    <string name=\"crossfade_desc\">曲の切り替わりをなめらかにします</string>\n    <string name=\"crossfade_beta_title\">ベータ機能</string>\n    <string name=\"crossfade_beta_message\">曲のクロスフェードは新機能のため、不具合が発生する場合があります。問題があればご報告してください。\\n\\n技術的な制限により、クロスフェードを使用中はオフロード再生が無効になります。</string>\n    <string name=\"enable\">オン</string>\n    <string name=\"crossfade_duration\">クロスフェードの長さ</string>\n    <string name=\"crossfade_gapless\">曲間のない曲では無効にする</string>\n    <string name=\"crossfade_gapless_desc\">曲間のない曲ではクロスフェードしません</string>\n    <string name=\"audio_offload_disabled_by_crossfade\">クロスフェードを使用中は有効にできません</string>\n    <string name=\"together\">Together</string>\n    <string name=\"ai_lyrics_translation\">AI歌詞翻訳</string>\n    <string name=\"ai_translating_lyrics\">歌詞を翻訳中...</string>\n    <string name=\"ai_lyrics_translated\">翻訳しました</string>\n    <string name=\"ai_provider\">提供元</string>\n    <string name=\"ai_base_url\">ベースURL</string>\n    <string name=\"ai_api_key\">APIキー</string>\n    <string name=\"ai_model\">モデル</string>\n    <string name=\"ai_translation_mode\">翻訳方法</string>\n    <string name=\"ai_target_language\">翻訳先の言語</string>\n    <string name=\"ai_setup_guide\">API認証情報</string>\n    <string name=\"ai_translation_literal\">翻訳</string>\n    <string name=\"ai_translation_transcribed\">文字起こし</string>\n    <string name=\"ai_api_key_required\">APIキーが必要です</string>\n    <string name=\"ai_error_api_key_required\">APIキーを入力してください</string>\n    <string name=\"ai_error_no_lyrics\">翻訳できる歌詞がありません</string>\n    <string name=\"ai_error_lyrics_empty\">歌詞がありません</string>\n    <string name=\"ai_error_language_required\">翻訳先の言語を設定してください</string>\n    <string name=\"ai_error_unexpected\">翻訳結果が正しくありません</string>\n    <string name=\"ai_error_unknown\">不明なエラーが発生しました</string>\n    <string name=\"ai_error_translation_failed\">翻訳に失敗しました</string>\n    <string name=\"recognize_music\">音楽認識</string>\n    <string name=\"tap_to_recognize\">タップして開始</string>\n    <string name=\"listening\">聴き取り中…</string>\n    <string name=\"processing\">処理中…</string>\n    <string name=\"no_match_found\">一致する曲が見つかりませんでした</string>\n    <string name=\"recognition_error\">認識エラー</string>\n    <string name=\"try_again\">もう一度試す</string>\n    <string name=\"recognition_history\">認識履歴</string>\n    <string name=\"clear_recognition_history\">認識履歴を削除</string>\n    <string name=\"clear_recognition_history_confirm\">認識履歴をすべて削除しますか？</string>\n    <string name=\"delete_from_history\">履歴から削除</string>\n    <string name=\"re_listen\">もう一度聴く</string>\n    <string name=\"play_on_app\">Metrolistで再生</string>\n    <string name=\"map_csv_columns\">CSV列の対応設定</string>\n    <string name=\"first_row_is_header\">1行目をヘッダーとして扱う</string>\n    <string name=\"artist_name_column\">アーティスト名の列</string>\n    <string name=\"song_title_column\">曲名の列</string>\n    <string name=\"youtube_url_column\">YouTube URLの列（任意）</string>\n    <string name=\"continue_action\">続行</string>\n    <string name=\"importing_csv\">CSVをインポート中</string>\n    <string name=\"recently_converted\">最近変換した項目</string>\n    <string name=\"column_label\">列%d</string>\n    <string name=\"hide_youtube_shorts\">YouTubeショートを非表示</string>\n    <string name=\"listen_together_in_top_bar\">トップバーにListen Togetherを表示</string>\n    <string name=\"listen_together_in_top_bar_desc\">Listen Togetherをナビゲーションバーから非表示にし、トップバーに表示します</string>\n    <string name=\"prevent_duplicate_tracks_in_queue\">キュー内の曲の重複を防止</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">同じ曲がキューに重複して追加されないようにします</string>\n    <string name=\"ai_translation_literal_desc\">意味を対象の言語に翻訳</string>\n    <string name=\"ai_translation_transcribed_desc\">発音を対象の文字体系に変換</string>\n    <string name=\"ai_provider_help\">APIキーを入手</string>\n    <string name=\"ai_provider_openrouter_help\">無料および有料モデルについては、 openrouter.ai をご覧ください</string>\n    <string name=\"ai_provider_openai_help\">APIキーは、 platform.openai.com/api-keys で取得できます</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"ai_provider_claude_help\">APIキーは、 console.anthropic.com/settings/keys で取得できます</string>\n    <string name=\"ai_provider_gemini_help\">APIキーは、 aistudio.google.com/apikey で取得できます</string>\n    <string name=\"ai_provider_perplexity_help\">APIキーは、 perplexity.ai/settings/api で取得できます</string>\n    <string name=\"ai_provider_xai_help\">APIキーは、 console.x.ai で取得できます</string>\n    <string name=\"ai_provider_deepl_help\">APIキーは、 deepl.com/pro-api for free and paid keys で取得できます</string>\n    <string name=\"ai_deepl_formality\">丁寧さ</string>\n    <string name=\"ai_deepl_formality_default\">デフォルト</string>\n    <string name=\"ai_deepl_formality_more\">より丁寧</string>\n    <string name=\"ai_deepl_formality_less\">よりカジュアル</string>\n    <string name=\"discord_status\">ステータス</string>\n    <string name=\"discord_status_online\">オンライン</string>\n    <string name=\"discord_status_idle\">退席中</string>\n    <string name=\"discord_status_dnd\">取り込み中</string>\n    <string name=\"discord_buttons\">ボタン</string>\n    <string name=\"discord_button_1\">ボタン 1</string>\n    <string name=\"discord_button_2\">ボタン 2</string>\n    <string name=\"login_successful\">ログインに成功しました！</string>\n    <string name=\"discord_information_warning\">この機能は、KizzyRPCライブラリを使用してDiscordのゲートウェイに接続し、再生状況を表示します。同様の使用方法によるアカウント停止の報告はありませんが、この方法はDiscordによって公式にサポートされておらず、利用規約違反とみなされる場合があります。トークンはデバイス内でのみ取得され、第三者のサーバーに送信されることはありません。自己責任で使用してください。</string>\n    <string name=\"discord_activity_type\">アクティビティの種類</string>\n    <string name=\"discord_activity_playing\">プレイ中</string>\n    <string name=\"discord_activity_listening\">再生中</string>\n    <string name=\"discord_activity_watching\">視聴中</string>\n    <string name=\"discord_activity_competing\">大会に参加中</string>\n    <string name=\"discord_button_text_variables\">利用可能な変数：{song_name}, {artist_name}, {album_name}</string>\n    <string name=\"discord_rpc_preview\">再生状況のプレビュー</string>\n    <string name=\"discord_presence\">プレゼンス</string>\n    <string name=\"discord_connect_description\">Discordにサインインして、再生中の内容を共有しよう</string>\n    <string name=\"discord_playing_metrolist\">Metrolistで再生中</string>\n    <string name=\"discord_watching_metrolist\">Metrolistで視聴中</string>\n    <string name=\"discord_competing_metrolist\">Metrolistで大会に参加中</string>\n    <string name=\"discord_activity_name\">アクティビティ名</string>\n    <string name=\"discord_activity_name_description\">アクティビティのカスタム名（空欄の場合はデフォルトを使用）</string>\n    <string name=\"discord_advanced_mode\">詳細設定</string>\n    <string name=\"discord_advanced_mode_description\">再生状況の追加のカスタマイズオプションを表示します</string>\n    <string name=\"player_background_solid\">単色</string>\n    <string name=\"resume_on_bluetooth_connect\">Bluetooth接続時に再生を再開</string>\n    <string name=\"lyrics_romanize_hindi\">ヒンディー語の歌詞にローマ字を追加</string>\n    <string name=\"lyrics_romanize_punjabi\">パンジャブ語の歌詞にローマ字を追加</string>\n    <string name=\"lyrics_romanize_as_main\">ローマ字の歌詞をメインに表示</string>\n    <string name=\"display_density\">表示サイズ</string>\n    <string name=\"restart\">再起動</string>\n    <string name=\"restart_required\">再起動が必要です</string>\n    <string name=\"density_restart_message\">表示サイズの変更は、アプリを再起動すると反映されます。再起動しますか？</string>\n    <string name=\"speed_dial\">クイック再生</string>\n    <string name=\"pin_to_speed_dial\">クイック再生にピン留め</string>\n    <string name=\"unpin_from_speed_dial\">クイック再生のピン留めを解除</string>\n    <string name=\"randomize_home_order\">ホーム画面の表示順をランダムにする</string>\n    <string name=\"randomize_home_order_desc\">ホーム画面の各項目をランダムに並び替えます</string>\n    <string name=\"daily_discover_sounds_like\">%1$sに似たおすすめ</string>\n    <string name=\"daily_discover_because_you_listen_to\">%1$sをよく聴いているため</string>\n    <string name=\"daily_discover_similar_to\">%1$sに似たおすすめ</string>\n    <string name=\"daily_discover_based_on\">%1$sをもとにしたおすすめ</string>\n    <string name=\"daily_discover_for_fans_of\">%1$sのファンにおすすめ</string>\n    <string name=\"from_the_community\">コミュニティのおすすめ</string>\n    <string name=\"enable_lrclib_desc\">コミュニティ主導の歌詞データベースです</string>\n    <string name=\"enable_kugou_desc\">中国最大のオンライン音楽サービスです</string>\n    <string name=\"youtube_music_lyrics_note\">他の歌詞が利用できない場合は、YouTube Musicの歌詞が自動的に表示されます。YouTube Musicの歌詞は通常、再生に合わせて表示されません。</string>\n    <string name=\"enable_lyricsplus\">歌詞の提供元にLyricsPlusを使用</string>\n    <string name=\"enable_lyricsplus_desc\">複数のソースの歌詞を提供する提供元です</string>\n    <string name=\"lyrics_provider_selection\">歌詞の提供元</string>\n    <string name=\"lyrics_provider_selection_desc\">使用する歌詞の提供元を選択します</string>\n    <string name=\"lyrics_provider_priority\">歌詞の提供元の優先順位</string>\n    <string name=\"lyrics_provider_priority_desc\">ドラッグして提供元を並び替えます。上にあるほど優先順位が高くなります。</string>\n    <string name=\"changelog\">変更履歴</string>\n    <string name=\"changelog_empty\">変更履歴はありません</string>\n    <string name=\"github_releases_url\">https://github.com/MetrolistGroup/Metrolist/releases</string>\n    <string name=\"list_bullet\">•</string>\n    <string name=\"view_on_github\">GitHubを開く</string>\n    <string name=\"current_version\">バージョン</string>\n    <string name=\"version_format\">バージョン: %s</string>\n    <string name=\"update_settings\">アップデート設定</string>\n    <string name=\"check_for_updates_title\">アップデート</string>\n    <string name=\"checking_for_updates\">アップデートを確認中…</string>\n    <string name=\"latest_version_format\">最新: %s</string>\n    <string name=\"check_for_updates_button\">アップデートを確認</string>\n    <string name=\"hide_changelog\">変更履歴を非表示</string>\n    <string name=\"view_changelog\">変更履歴を表示</string>\n    <string name=\"failed_to_check_updates\">アップデートの確認に失敗しました: %s</string>\n    <string name=\"set_as_default\">デフォルトに設定</string>\n    <string name=\"sleep_timer_default_set\">スリープタイマーのデフォルト値を%d分に設定しました</string>\n    <string name=\"found_in_settings_content\">”設定” ▶ ”コンテンツ”にあります</string>\n    <string name=\"plays\">回再生</string>\n    <string name=\"error_episode_save\">エピソードの保存に失敗しました</string>\n    <string name=\"error_episode_remove\">エピソードの削除に失敗しました</string>\n    <string name=\"error_podcast_subscribe\">ポッドキャストの登録に失敗しました</string>\n    <string name=\"error_podcast_unsubscribe\">ポッドキャストの登録解除に失敗しました</string>\n    <string name=\"listen_together_auto_approval_suggestions\">楽曲リクエストを自動で承認</string>\n    <string name=\"listen_together_auto_approval_suggestions_desc\">ゲストからの楽曲リクエストを自動で承認し、キューに追加します</string>\n    <string name=\"importing_playlist\">プレイリストをインポート中</string>\n    <string name=\"logout_dialog_title\">ライブラリデータを保持しますか？</string>\n    <string name=\"logout_dialog_message\">プレイリストとライブラリデータを保持しますか？ダウンロードした曲は、いずれの場合も保持されます。</string>\n    <string name=\"logout_keep\">保持</string>\n    <string name=\"logout_clear\">削除</string>\n    <string name=\"credits_lead_developer\">主任開発者</string>\n    <string name=\"credits_collaborator\">共同開発者</string>\n    <string name=\"credits_collaborators_section\">共同開発者</string>\n    <string name=\"credits_license_name\">GNU一般公衆ライセンス v3.0</string>\n    <string name=\"credits_license_desc\">無料でオープンソースのソフトウェアで、使用、学習、共有、改良が可能です。</string>\n    <string name=\"credits_discord\">Discordサーバー</string>\n    <string name=\"credits_telegram\">Telegramチャンネル</string>\n    <string name=\"credits_website\">ウェブサイト</string>\n    <string name=\"credits_instagram\">Instagram</string>\n    <string name=\"credits_github\">GitHub</string>\n    <string name=\"credits_view_repo\">リポジトリを表示</string>\n    <string name=\"app_version_info\">%1$s • %2$s</string>\n    <string name=\"like_what_i_do\">私の活動を気に入ってもらえましたか？</string>\n    <string name=\"buy_mo_a_coffee\">コーヒーをおごる</string>\n    <string name=\"community_and_info\">コミュニティと情報</string>\n    <string name=\"metrolist\">METROLIST</string>\n    <string name=\"wanna_play_favorite_song\">お気に入りの曲を再生しますか？</string>\n    <string name=\"yeah\">はい</string>\n    <string name=\"stands_with_palestine\">このプロジェクトはパレスチナを支持しています 🇵🇸</string>\n    <string name=\"filter_podcasts\">ポッドキャスト</string>\n    <string name=\"view_podcast\">ポッドキャストを表示</string>\n    <string name=\"podcast_channels\">ポッドキャストチャンネル</string>\n    <string name=\"latest_episodes\">最新のエピソード</string>\n    <string name=\"your_shows\">あなたの番組</string>\n    <string name=\"new_episodes\">新しいエピソード</string>\n    <string name=\"episodes_for_later\">あとで聴く</string>\n    <string name=\"save_episode_for_later\">あとで保存</string>\n    <string name=\"save_episode_for_later_desc\">”あとで聴く”プレイリストに追加</string>\n    <string name=\"remove_episode_from_saved\">保存した曲から削除</string>\n    <string name=\"subscribe_to_podcast\">ポッドキャストをライブラリに保存</string>\n    <plurals name=\"n_episode\">\n        <item quantity=\"other\">%dエピソード</item>\n    </plurals>\n    <string name=\"restore_confirm_title\">バックアップを復元しますか？</string>\n    <string name=\"restore_confirm_message\">バックアップからアプリのデータを復元します。</string>\n    <string name=\"restore_account_warning\">復元後、再度ログインが必要になります。このアカウントはサインアウトされます：</string>\n    <string name=\"restore\">復元</string>\n    <string name=\"checking_previous_account\">以前のアカウントを確認中…</string>\n    <string name=\"no_account_found\">アカウントが見つかりません</string>\n    <string name=\"widget_recognizer_name\">音楽認識</string>\n    <string name=\"widget_recognizer_description\">周囲で流れている曲を認識できるウィジェット</string>\n    <string name=\"widget_recognizer_tap_to_search\">タップして開始</string>\n    <string name=\"widget_recognizer_listening\">聴き取り中…</string>\n    <string name=\"widget_recognizer_processing\">認識中…</string>\n    <string name=\"widget_recognizer_no_match\">一致する曲が見つかりませんでした。もう一度お試しください</string>\n    <string name=\"widget_recognizer_error\">認識に失敗しました</string>\n    <string name=\"widget_recognizer_error_generic\">エラーが発生しました。もう一度お試しください</string>\n    <string name=\"widget_recognizer_unknown_song\">不明な曲</string>\n    <string name=\"widget_recognizer_unknown_artist\">不明なアーティスト</string>\n    <string name=\"widget_recognizer_mic_desc\">曲を認識します</string>\n    <string name=\"widget_recognizer_channel_name\">音楽認識</string>\n    <string name=\"widget_recognizer_channel_desc\">ウィジェットから曲を認識している間、通知を表示します</string>\n    <string name=\"widget_recognizer_notification_text\">音声を録音中…</string>\n    <string name=\"filter_episodes\">エピソード</string>\n    <string name=\"filter_channels\">チャンネル</string>\n    <string name=\"auto_playlist\">自動プレイリスト</string>\n    <string name=\"downloaded_episodes\">ダウンロードしたエピソード</string>\n    <string name=\"no_subscribed_channels\">登録しているチャンネルはありません</string>\n    <string name=\"no_downloaded_episodes\">ダウンロードしたエピソードはありません</string>\n    <plurals name=\"n_channel\">\n        <item quantity=\"other\">%dチャンネル</item>\n    </plurals>\n    <string name=\"view_channel\">チャンネルを表示</string>\n    <string name=\"filter_profiles\">プロフィール</string>\n    <string name=\"enable_automatic_sleeptimer\">スリープタイマーを自動的に開始</string>\n    <string name=\"sleeptimer_description\">指定した時刻にスリープタイマーを自動的に開始します</string>\n    <string name=\"sleep_timer_repeat_description\">スリープタイマーを自動で開始する曜日と時刻を設定します</string>\n    <string name=\"sleep_timer_repeat\">繰り返し</string>\n    <string name=\"sleep_timer_daily\">毎日</string>\n    <string name=\"sleep_timer_weekdays\">月曜日～金曜日</string>\n    <string name=\"sleep_timer_weekdays_weekends\">平日/ 週末</string>\n    <string name=\"sleep_timer_weekends\">週末（土・日）</string>\n    <string name=\"sleep_timer_custom\">カスタム</string>\n    <string name=\"sleep_timer_start_time\">開始時刻</string>\n    <string name=\"sleep_timer_end_time\">終了時刻</string>\n    <string name=\"sleep_timer_monday\">月曜日</string>\n    <string name=\"sleep_timer_tuesday\">火曜日</string>\n    <string name=\"sleep_timer_wednesday\">水曜日</string>\n    <string name=\"sleep_timer_thursday\">木曜日</string>\n    <string name=\"sleep_timer_friday\">金曜日</string>\n    <string name=\"sleep_timer_saturday\">土曜日</string>\n    <string name=\"sleep_timer_sunday\">日曜日</string>\n    <string name=\"sleep_timer_stop_after_current_song\">タイマー終了時のときの曲が終わってから停止</string>\n    <string name=\"sleep_timer_fade_out\">終了前の1分間でフェードアウト</string>\n    <string name=\"upload_songs\">曲をアップロード</string>\n    <string name=\"uploading\">アップロード中…</string>\n    <string name=\"upload_progress\">%1$d/%2$d</string>\n    <string name=\"upload_complete\">アップロードが完了しました</string>\n    <string name=\"upload_failed\">アップロードに失敗しました</string>\n    <string name=\"upload_file_too_large\">ファイルサイズが大きすぎます（最大300MB）</string>\n    <string name=\"upload_unsupported_format\">対応していない形式です。mp3、m4a、wma、flac、oggを使用してください</string>\n    <string name=\"delete_uploaded_song\">アップロードした曲を削除</string>\n    <string name=\"delete_uploaded_song_confirm\">このアップロードした曲を削除しますか？この操作は元に戻せません。</string>\n    <string name=\"delete_uploaded_song_success\">アップロードした曲を削除しました</string>\n    <string name=\"delete_uploaded_song_failed\">アップロードした曲の削除に失敗しました</string>\n    <string name=\"delete_uploaded_songs\">アップロードした曲を削除</string>\n    <string name=\"delete_uploaded_songs_confirm\">%1$d件のアップロードした曲を削除しますか？この操作は元に戻せません。</string>\n    <string name=\"deleted_n_songs\">%1$d件の曲を削除しました</string>\n    <string name=\"deleting\">削除中…</string>\n    <string name=\"export_playlist\">プレイリストをエクスポート</string>\n    <string name=\"export_as_csv\">CSV形式でエクスポート</string>\n    <string name=\"export_as_m3u\">M3U形式でエクスポート</string>\n    <string name=\"export_success\">プレイリストを正常にエクスポートしました</string>\n    <string name=\"export_failed\">プレイリストのエクスポートに失敗しました</string>\n    <string name=\"export_option_share\">共有</string>\n    <string name=\"export_option_save\">ドキュメントに保存</string>\n    <string name=\"qs_tile_music_recognizer\">音楽認識</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-ja/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">ホーム</string>\n    <string name=\"songs\">曲</string>\n    <string name=\"artists\">アーティスト</string>\n    <string name=\"albums\">アルバム</string>\n    <string name=\"playlists\">再生リスト</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"other\">%d 個を選択済み</item>\n    </plurals>\n    <string name=\"history\">履歴</string>\n    <string name=\"stats\">統計</string>\n    <string name=\"mood_and_genres\">ムードとジャンル</string>\n    <string name=\"account\">アカウント</string>\n    <string name=\"quick_picks\">おすすめ</string>\n    <string name=\"quick_picks_empty\">何曲か再生するとおすすめを生成します</string>\n    <string name=\"new_release_albums\">新作アルバム</string>\n    <string name=\"forgotten_favorites\">忘れられているお気に入り</string>\n    <string name=\"keep_listening\">視聴を維持</string>\n    <string name=\"your_youtube_playlists\">あなたのYouTubeの再生リスト</string>\n    <string name=\"similar_to\">これに似ている</string>\n    <string name=\"today\">今日</string>\n    <string name=\"yesterday\">昨日</string>\n    <string name=\"this_week\">今週</string>\n    <string name=\"last_week\">先週</string>\n    <string name=\"most_played_songs\">最も聴いた曲</string>\n    <string name=\"most_played_artists\">最も聴いたアーティスト</string>\n    <string name=\"most_played_albums\">最も聴いたアルバム</string>\n    <string name=\"search\">検索</string>\n    <string name=\"search_yt_music\">YouTube Music を検索…</string>\n    <string name=\"search_library\">ライブラリを検索…</string>\n    <string name=\"filter_library\">ライブラリ</string>\n    <string name=\"filter_liked\">いいね済み</string>\n    <string name=\"filter_downloaded\">ダウンロード済み</string>\n    <string name=\"filter_all\">すべて</string>\n    <string name=\"filter_songs\">曲</string>\n    <string name=\"filter_videos\">動画</string>\n    <string name=\"filter_albums\">アルバム</string>\n    <string name=\"filter_artists\">アーティスト</string>\n    <string name=\"filter_playlists\">再生リスト</string>\n    <string name=\"filter_community_playlists\">コミュニティの再生リスト</string>\n    <string name=\"filter_featured_playlists\">おすすめの再生リスト</string>\n    <string name=\"filter_bookmarked\">ブックマーク済み</string>\n    <string name=\"no_results_found\">見つかりませんでした</string>\n    <string name=\"library_song_empty\">曲のライブラリはここに表示されます</string>\n    <string name=\"library_artist_empty\">アーティストのライブラリはここに表示されます</string>\n    <string name=\"library_album_empty\">アルバムのライブラリはここに表示されます</string>\n    <string name=\"library_playlist_empty\">あなたの再生リストはここに表示されます</string>\n    <string name=\"from_your_library\">ライブラリから</string>\n    <string name=\"other_versions\">別のバージョン</string>\n    <string name=\"liked_songs\">いいねした曲</string>\n    <string name=\"downloaded_songs\">ダウンロードした曲</string>\n    <string name=\"playlist_is_empty\">再生リストが空です</string>\n    <string name=\"remove_download_playlist_confirm\">｢%s｣の再生リスト上の全曲をダウンロード済みから削除しますか？</string>\n    <string name=\"delete_playlist_confirm\">｢%s｣の再生リストを削除してもよろしいですか？</string>\n    <string name=\"retry\">再試行</string>\n    <string name=\"radio\">ラジオ</string>\n    <string name=\"shuffle\">シャッフル</string>\n    <string name=\"reset\">リセット</string>\n    <string name=\"details\">詳細</string>\n    <string name=\"edit\">編集</string>\n    <string name=\"start_radio\">ラジオを再生</string>\n    <string name=\"play\">再生</string>\n    <string name=\"play_next\">次に再生</string>\n    <string name=\"add_to_queue\">キューに追加</string>\n    <string name=\"add_to_library\">ライブラリに追加</string>\n    <string name=\"add_all_to_library\">すべてライブラリに追加</string>\n    <string name=\"remove_from_library\">ライブラリから削除</string>\n    <string name=\"remove_all_from_library\">すべてライブラリから削除</string>\n    <string name=\"action_download\">ダウンロード</string>\n    <string name=\"downloading\">ダウンロード中</string>\n    <string name=\"remove_download\">ダウンロードを削除</string>\n    <string name=\"import_playlist\">再生リストをインポート</string>\n    <string name=\"add_to_playlist\">再生リストに追加</string>\n    <string name=\"view_artist\">アーティストを表示</string>\n    <string name=\"view_album\">アルバムを表示</string>\n    <string name=\"refetch\">再取得</string>\n    <string name=\"share\">共有</string>\n    <string name=\"delete\">削除</string>\n    <string name=\"remove_from_history\">履歴から削除</string>\n    <string name=\"remove_from_playlist\">再生リストから削除</string>\n    <string name=\"remove_from_queue\">キューから削除</string>\n    <string name=\"search_online\">オンラインで検索</string>\n    <string name=\"action_sync\">同期</string>\n    <string name=\"advanced\">高度</string>\n    <string name=\"tempo_and_pitch\">速度とピッチ</string>\n    <string name=\"sort_by_create_date\">追加日時</string>\n    <string name=\"sort_by_name\">曲名</string>\n    <string name=\"sort_by_artist\">アーティスト</string>\n    <string name=\"sort_by_year\">リリース年</string>\n    <string name=\"sort_by_song_count\">曲数</string>\n    <string name=\"sort_by_length\">長さ</string>\n    <string name=\"sort_by_play_time\">再生時間</string>\n    <string name=\"sort_by_custom\">カスタム</string>\n    <string name=\"media_id\">メディア ID</string>\n    <string name=\"mime_type\">MIME タイプ</string>\n    <string name=\"codecs\">コーデック</string>\n    <string name=\"bitrate\">ビットレート</string>\n    <string name=\"sample_rate\">サンプルレート</string>\n    <string name=\"loudness\">ラウドネス</string>\n    <string name=\"volume\">音量</string>\n    <string name=\"file_size\">ファイルサイズ</string>\n    <string name=\"unknown\">不明</string>\n    <string name=\"copied\">クリップボードにコピー</string>\n    <string name=\"edit_lyrics\">歌詞を編集</string>\n    <string name=\"search_lyrics\">歌詞を検索</string>\n    <string name=\"edit_song\">曲を編集</string>\n    <string name=\"song_title\">曲名</string>\n    <string name=\"song_artists\">曲のアーティスト</string>\n    <string name=\"error_song_title_empty\">曲名は空白にできません。</string>\n    <string name=\"error_song_artist_empty\">曲のアーティストは空白にできません。</string>\n    <string name=\"save\">保存</string>\n    <string name=\"choose_playlist\">再生リストを選択</string>\n    <string name=\"edit_playlist\">再生リストを編集</string>\n    <string name=\"create_playlist\">再生リストを作成</string>\n    <string name=\"playlist_name\">再生リストの名前</string>\n    <string name=\"error_playlist_name_empty\">再生リストの名前は空白にできません。</string>\n    <string name=\"edit_artist\">アーティストを編集</string>\n    <string name=\"artist_name\">アーティスト名</string>\n    <string name=\"error_artist_name_empty\">アーティスト名は空白にできません。</string>\n    <string name=\"duplicates\">重複</string>\n    <string name=\"skip_duplicates\">重複をスキップ</string>\n    <string name=\"add_anyway\">気にせず追加</string>\n    <string name=\"duplicates_description_single\">曲はすでに再生リストにあります</string>\n    <string name=\"duplicates_description_multiple\">%d の曲はすでに再生リストにあります</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"other\">%d 曲</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"other\">%d 件のアーティスト</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"other\">%d 個のアルバム</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"other\">%d 件の再生リスト</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"other\">%d 週間</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"other\">%d か月</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"other\">%d 年</item>\n    </plurals>\n    <string name=\"playlist_imported\">再生リストをインポートしました</string>\n    <string name=\"removed_song_from_playlist\">再生リストから「%s」を削除しました</string>\n    <string name=\"playlist_synced\">再生リストが同期されました</string>\n    <string name=\"undo\">元に戻す</string>\n    <string name=\"lyrics_not_found\">歌詞が見つかりません</string>\n    <string name=\"sleep_timer\">スリープタイマー</string>\n    <string name=\"end_of_song\">曲の終わり</string>\n    <plurals name=\"minute\">\n        <item quantity=\"other\">%d 分</item>\n    </plurals>\n    <string name=\"error_no_stream\">ストリームが利用できません</string>\n    <string name=\"error_no_internet\">ネットワーク接続がありません</string>\n    <string name=\"error_timeout\">タイムアウトしました</string>\n    <string name=\"error_unknown\">不明なエラー</string>\n    <string name=\"action_like\">いいね</string>\n    <string name=\"action_like_all\">すべていいね</string>\n    <string name=\"action_remove_like\">いいねを削除</string>\n    <string name=\"action_remove_like_all\">すべてのいいねを削除</string>\n    <string name=\"action_shuffle_on\">シャッフル ON</string>\n    <string name=\"action_shuffle_off\">シャッフル OFF</string>\n    <string name=\"repeat_mode_off\">リピートモード OFF</string>\n    <string name=\"repeat_mode_one\">現在の曲をリピート</string>\n    <string name=\"repeat_mode_all\">キューをリピート</string>\n    <string name=\"queue_all_songs\">すべての曲</string>\n    <string name=\"queue_searched_songs\">検索した曲</string>\n    <string name=\"music_player\">音楽プレーヤー</string>\n    <string name=\"settings\">設定</string>\n    <string name=\"appearance\">外観</string>\n    <string name=\"theme\">テーマ</string>\n    <string name=\"enable_dynamic_theme\">ダイナミックテーマを有効化</string>\n    <string name=\"dark_theme\">ダークテーマ</string>\n    <string name=\"dark_theme_on\">オン</string>\n    <string name=\"dark_theme_off\">オフ</string>\n    <string name=\"dark_theme_follow_system\">システムに従う</string>\n    <string name=\"pure_black\">ピュアブラック</string>\n    <string name=\"customize_navigation_tabs\">ナビゲーションタブのカスタマイズ</string>\n    <string name=\"player\">プレーヤー</string>\n    <string name=\"player_text_alignment\">プレーヤーの文字揃え</string>\n    <string name=\"lyrics_text_position\">歌詞テキストの位置</string>\n    <string name=\"sided\">端</string>\n    <string name=\"left\">左</string>\n    <string name=\"center\">中央</string>\n    <string name=\"right\">右</string>\n    <string name=\"player_slider_style\">プレーヤーのスライダーの形</string>\n    <string name=\"default_\">標準</string>\n    <string name=\"squiggly\">くねくね</string>\n    <string name=\"misc\">ほか</string>\n    <string name=\"default_open_tab\">起動時に開くタブ</string>\n    <string name=\"grid_cell_size\">格子のマス目の大きさ</string>\n    <string name=\"small\">小</string>\n    <string name=\"big\">大</string>\n    <string name=\"content\">コンテンツ</string>\n    <string name=\"login\">ログイン</string>\n    <string name=\"not_logged_in\">ログインしていません</string>\n    <string name=\"content_language\">コンテンツの既定の言語</string>\n    <string name=\"content_country\">コンテンツの既定の国</string>\n    <string name=\"system_default\">システムに従う</string>\n    <string name=\"enable_proxy\">プロキシを有効化</string>\n    <string name=\"proxy_type\">プロキシの種類</string>\n    <string name=\"proxy_url\">プロキシの URL</string>\n    <string name=\"restart_to_take_effect\">適用するには再起動してください</string>\n    <string name=\"player_and_audio\">プレーヤーと音声</string>\n    <string name=\"audio_quality\">音質</string>\n    <string name=\"audio_quality_auto\">自動</string>\n    <string name=\"audio_quality_high\">高</string>\n    <string name=\"audio_quality_low\">低</string>\n    <string name=\"queue\">再生キュー</string>\n    <string name=\"persistent_queue\">再生キューを保持</string>\n    <string name=\"persistent_queue_desc\">アプリ起動時に前回のキューを復元</string>\n    <string name=\"auto_load_more\">追加の曲を自動で読み込む</string>\n    <string name=\"auto_load_more_desc\">キューの最後まで再生した時、可能なら自動で曲を追加</string>\n    <string name=\"skip_silence\">無音部分をスキップ</string>\n    <string name=\"audio_normalization\">音声の正規化</string>\n    <string name=\"auto_skip_next_on_error\">エラー発生時に自動で次の曲を再生</string>\n    <string name=\"auto_skip_next_on_error_desc\">継続的な再生体験を維持します</string>\n    <string name=\"stop_music_on_task_clear\">タスクを削除したら音楽を停止</string>\n    <string name=\"equalizer\">イコライザー</string>\n    <string name=\"storage\">保存領域</string>\n    <string name=\"cache\">キャッシュ</string>\n    <string name=\"image_cache\">画像のキャッシュ</string>\n    <string name=\"song_cache\">曲のキャッシュ</string>\n    <string name=\"max_cache_size\">最大キャッシュサイズ</string>\n    <string name=\"unlimited\">無制限</string>\n    <string name=\"clear_all_downloads\">すべてのダウンロードを消去</string>\n    <string name=\"max_image_cache_size\">画像の最大キャッシュサイズ</string>\n    <string name=\"clear_image_cache\">画像のキャッシュを消去</string>\n    <string name=\"max_song_cache_size\">曲の最大キャッシュサイズ</string>\n    <string name=\"clear_song_cache\">曲のキャッシュを消去</string>\n    <string name=\"size_used\">%s 使用中</string>\n    <string name=\"privacy\">プライバシー</string>\n    <string name=\"listen_history\">再生履歴</string>\n    <string name=\"pause_listen_history\">再生履歴を一時停止</string>\n    <string name=\"clear_listen_history\">再生履歴を消去</string>\n    <string name=\"clear_listen_history_confirm\">すべての再生履歴を消去しますか？</string>\n    <string name=\"search_history\">検索履歴</string>\n    <string name=\"pause_search_history\">検索履歴の記録を一時停止</string>\n    <string name=\"clear_search_history\">検索履歴を消去</string>\n    <string name=\"clear_search_history_confirm\">すべての検索履歴を消去しますか？</string>\n    <string name=\"disable_screenshot\">スクリーンショットを無効にする</string>\n    <string name=\"disable_screenshot_desc\">スクリーンショットと最近使用したアプリの表示を無効にします。</string>\n    <string name=\"enable_lrclib\">歌詞の提供元 LrcLib を使用</string>\n    <string name=\"enable_kugou\">歌詞の提供元 KuGou を使用</string>\n    <string name=\"hide_explicit\">露骨な内容のコンテンツを非表示</string>\n    <string name=\"backup_restore\">バックアップと復元</string>\n    <string name=\"action_backup\">バックアップ</string>\n    <string name=\"action_restore\">復元</string>\n    <string name=\"imported_playlist\">インポートした再生リスト</string>\n    <string name=\"backup_create_success\">バックアップの作成に成功しました</string>\n    <string name=\"backup_create_failed\">バックアップを作成できませんでした</string>\n    <string name=\"restore_failed\">バックアップの復元に失敗しました</string>\n    <string name=\"discord_integration\">Discord 統合</string>\n    <string name=\"discord_information\">Metrolist は KizzyRPC ライブラリを使い、Discord アカウントのステータスを設定します。これは Discord ゲートウェイ接続を使うので、Discord の利用規約に違反する可能性があります。しかし、この理由でユーザーのアカウントが停止された例は確認されていません。自己責任でご利用ください。\\n\\nMetrolist はトークンを抽出するだけです。それ以外はすべて端末内に保存されます。</string>\n    <string name=\"dismiss\">非表示</string>\n    <string name=\"options\">オプション</string>\n    <string name=\"preview\">プレビュー</string>\n    <string name=\"login_failed\">ログイン失敗</string>\n    <string name=\"action_logout\">ログアウト</string>\n    <string name=\"enable_discord_rpc\">リッチプレゼンスを使用</string>\n    <string name=\"about\">アプリについて</string>\n    <string name=\"app_version\">アプリのバージョン</string>\n    <string name=\"new_version_available\">最新版あり</string>\n    <string name=\"translation_models\">翻訳モデル</string>\n    <string name=\"clear_translation_models\">翻訳モデルを消去</string>\n    <string name=\"use_login_for_browse_desc\">例えば、Premiumのアカウントでログインした時に、Premium限定のアルバムが表示されるようになります</string>\n    <string name=\"use_login_for_browse\">表示されるコンテンツをアカウントと結び付ける</string>\n    <string name=\"action_login\">ログイン</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-km/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">លើឧបករណ៍</string>\n    <string name=\"charts\">តារាង</string>\n    <string name=\"back_button_desc\">ត្រឡប់ក្រោយ</string>\n    <string name=\"album_cover_desc\">គម្របអាល់ប៊ុម</string>\n    <string name=\"top_music_videos\">វីដេអូចម្រៀងពេញនិយម</string>\n    <string name=\"trending\">កំពុងពេញនិយម</string>\n    <string name=\"weeks\">សប្តាហ៍</string>\n    <string name=\"months\">ខែ</string>\n    <string name=\"years\">ឆ្នាំ</string>\n    <string name=\"continuous\">បន្តជាប់គ្នា</string>\n    <string name=\"liked\">ចូលចិត្ត</string>\n    <string name=\"offline\">បានទាញយក</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-ko/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">로컬</string>\n    <string name=\"remote_history\">원격</string>\n    <string name=\"charts\">차트</string>\n    <string name=\"back_button_desc\">뒤로</string>\n    <string name=\"album_cover_desc\">앨범 커버</string>\n    <string name=\"top_music_videos\">인기 뮤직 비디오</string>\n    <string name=\"trending\">트렌드</string>\n    <string name=\"weeks\">주</string>\n    <string name=\"months\">달</string>\n    <string name=\"years\">년</string>\n    <string name=\"generating_image\">이미지 생성 중</string>\n    <string name=\"cancel\">취소</string>\n    <string name=\"show_more\">더보기</string>\n    <string name=\"show_less\">간략히</string>\n    <string name=\"copy_link\">링크 복사</string>\n    <string name=\"close\">닫기</string>\n    <string name=\"lyrics\">가사</string>\n    <string name=\"link_copied\">링크를 클립보드에 복사했습니다</string>\n    <string name=\"seek_forward_dynamic\">+%1$d초 앞으로</string>\n    <string name=\"seek_backward_dynamic\">+%1$d초 뒤로</string>\n    <string name=\"new_player_design\">새로운 플레이어 디자인</string>\n    <string name=\"new_mini_player_design\">새로운 미니 플레이어 디자인</string>\n    <string name=\"app_language\">앱 언어</string>\n    <string name=\"not_logged_in_youtube\">YouTube에 로그인되어 있지 않습니다</string>\n    <string name=\"views\">조회수</string>\n    <string name=\"likes\">좋아요</string>\n    <string name=\"subscribe\">구독</string>\n    <string name=\"subscribed\">구독중</string>\n    <string name=\"username\">아이디</string>\n    <string name=\"password\">비밀번호</string>\n    <string name=\"logging_in\">로그인 중…</string>\n    <string name=\"mini_player\">미니 플레이어</string>\n    <string name=\"cache_size_warning_title\">잠깐!</string>\n    <string name=\"cache_size_warning_confirm\">다음</string>\n    <string name=\"wrapped_top_album_title\">당신의 최애 앨범은</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">앨범 아트</string>\n    <string name=\"wrapped_top_artist_title\">올해 당신의 최애 아티스트는</string>\n    <string name=\"wrapped_intro_title\">메트로리스트</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"equalizer_header\">이퀄라이저</string>\n    <string name=\"no_profiles\">이퀄라이저 프로필 없음</string>\n    <string name=\"import_profile\">프로필 가져오기</string>\n    <string name=\"system_equalizer\">시스템 이퀄라이저</string>\n    <string name=\"delete_profile_desc\">프로필 삭제</string>\n    <string name=\"copied_title\">제목 복사됨</string>\n    <string name=\"copied_artist\">아티스트 복사됨</string>\n    <string name=\"album_art\">앨범 아트</string>\n    <string name=\"play_pause\">재생/일시중지</string>\n    <string name=\"next\">다음</string>\n    <string name=\"like\">좋아요</string>\n    <string name=\"liked\">좋아요 표시 됨</string>\n    <string name=\"share_lyrics\">가사 공유</string>\n    <string name=\"cached_playlist\">캐시된 재생목록</string>\n    <string name=\"open\">열기</string>\n    <string name=\"apple_music_style\">Apple Music</string>\n    <string name=\"lyrics_text_size\">가사 글씨 크기</string>\n    <string name=\"lyrics_line_spacing\">가사 줄간격</string>\n    <string name=\"karaoke\">노래방</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-ko/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">홈</string>\n    <string name=\"songs\">노래</string>\n    <string name=\"artists\">아티스트</string>\n    <string name=\"albums\">앨범</string>\n    <string name=\"playlists\">재생목록</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"other\">%d 선택됨</item>\n    </plurals>\n    <string name=\"history\">기록</string>\n    <string name=\"stats\">통계</string>\n    <string name=\"mood_and_genres\">분위기 및 장르</string>\n    <string name=\"account\">계정</string>\n    <string name=\"quick_picks\">빠른 선곡</string>\n    <string name=\"quick_picks_empty\">빠른 선곡을 생성하기 위해 몇 곡을 들어보세요</string>\n    <string name=\"new_release_albums\">최신 앨범</string>\n    <string name=\"today\">오늘</string>\n    <string name=\"yesterday\">어제</string>\n    <string name=\"this_week\">이번 주</string>\n    <string name=\"last_week\">저번 주</string>\n    <string name=\"most_played_songs\">가장 많이 재생한 음악</string>\n    <string name=\"most_played_artists\">가장 많이 재생한 아티스트</string>\n    <string name=\"most_played_albums\">가장 많이 재생한 앨범</string>\n    <string name=\"search\">검색</string>\n    <string name=\"search_yt_music\">YouTube Music 검색…</string>\n    <string name=\"search_library\">보관함 검색…</string>\n    <string name=\"filter_library\">보관함</string>\n    <string name=\"filter_liked\">좋아요 표시 됨</string>\n    <string name=\"filter_downloaded\">다운로드 됨</string>\n    <string name=\"filter_all\">모두</string>\n    <string name=\"filter_songs\">노래</string>\n    <string name=\"filter_videos\">비디오</string>\n    <string name=\"filter_albums\">앨범</string>\n    <string name=\"filter_artists\">아티스트</string>\n    <string name=\"filter_playlists\">재생목록</string>\n    <string name=\"filter_community_playlists\">커뮤니티 재생목록</string>\n    <string name=\"filter_featured_playlists\">추천 재생목록</string>\n    <string name=\"filter_bookmarked\">북마크 됨</string>\n    <string name=\"no_results_found\">검색 결과가 없음</string>\n    <string name=\"from_your_library\">보관함에서</string>\n    <string name=\"liked_songs\">좋아요 표시한 노래</string>\n    <string name=\"downloaded_songs\">다운로드 한 노래</string>\n    <string name=\"playlist_is_empty\">재생목록이 비어있습니다</string>\n    <string name=\"retry\">재시도</string>\n    <string name=\"radio\">라디오</string>\n    <string name=\"shuffle\">셔플</string>\n    <string name=\"reset\">초기화</string>\n    <string name=\"details\">세부 정보</string>\n    <string name=\"edit\">수정</string>\n    <string name=\"start_radio\">라디오 시작</string>\n    <string name=\"play\">재생</string>\n    <string name=\"play_next\">다음 노래 재생</string>\n    <string name=\"add_to_queue\">목록에 추가</string>\n    <string name=\"add_to_library\">보관함에 추가</string>\n    <string name=\"remove_from_library\">보관함에서 삭제</string>\n    <string name=\"action_download\">다운로드</string>\n    <string name=\"downloading\">다운로드 중</string>\n    <string name=\"remove_download\">다운로드 제거</string>\n    <string name=\"import_playlist\">재생목록 불러오기</string>\n    <string name=\"add_to_playlist\">재생목록에 추가</string>\n    <string name=\"view_artist\">아티스트 보기</string>\n    <string name=\"view_album\">앨범 보기</string>\n    <string name=\"refetch\">새로고침</string>\n    <string name=\"share\">공유</string>\n    <string name=\"delete\">삭제</string>\n    <string name=\"remove_from_history\">기록에서 제거</string>\n    <string name=\"search_online\">온라인 검색</string>\n    <string name=\"action_sync\">동기화</string>\n    <string name=\"advanced\">고급</string>\n    <string name=\"sort_by_create_date\">추가된 날짜</string>\n    <string name=\"sort_by_name\">이름</string>\n    <string name=\"sort_by_artist\">아티스트</string>\n    <string name=\"sort_by_year\">년도</string>\n    <string name=\"sort_by_song_count\">곡 개수</string>\n    <string name=\"sort_by_length\">길이</string>\n    <string name=\"sort_by_play_time\">재생 시간</string>\n    <string name=\"sort_by_custom\">맞춤 정렬</string>\n    <string name=\"media_id\">미디어 ID</string>\n    <string name=\"mime_type\">MIME 타입</string>\n    <string name=\"codecs\">코덱</string>\n    <string name=\"bitrate\">비트레이트</string>\n    <string name=\"sample_rate\">샘플레이트</string>\n    <string name=\"loudness\">라우드니스</string>\n    <string name=\"volume\">음량</string>\n    <string name=\"file_size\">파일 크기</string>\n    <string name=\"unknown\">알 수 없음</string>\n    <string name=\"copied\">클립보드에 복사됨</string>\n    <string name=\"edit_lyrics\">가사 편집</string>\n    <string name=\"search_lyrics\">가사 검색</string>\n    <string name=\"edit_song\">노래 편집</string>\n    <string name=\"song_title\">노래 제목</string>\n    <string name=\"song_artists\">노래 아티스트</string>\n    <string name=\"error_song_title_empty\">노래 제목은 비워둘 수 없습니다.</string>\n    <string name=\"error_song_artist_empty\">음악 아티스트는 비워둘 수 없습니다.</string>\n    <string name=\"save\">저장</string>\n    <string name=\"choose_playlist\">재생목록 선택</string>\n    <string name=\"edit_playlist\">재생목록 편집</string>\n    <string name=\"create_playlist\">재생목록 만들기</string>\n    <string name=\"playlist_name\">재생목록 이름</string>\n    <string name=\"error_playlist_name_empty\">재생목록 이름은 비워둘 수 없습니다.</string>\n    <string name=\"edit_artist\">아티스트 편집</string>\n    <string name=\"artist_name\">아티스트 이름</string>\n    <string name=\"error_artist_name_empty\">아티스트 이름은 비워둘 수 없습니다.</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"other\">%d곡</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"other\">%d 아티스트</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"other\">%d 앨범</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"other\">%d 재생목록</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"other\">%d주</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"other\">%d개월</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"other\">%d년</item>\n    </plurals>\n    <string name=\"playlist_imported\">재생목록을 가져왔습니다</string>\n    <string name=\"removed_song_from_playlist\">재생목록에서 \\\"%s\\\"을(를) 삭제했습니다</string>\n    <string name=\"playlist_synced\">재생목록 동기화됨</string>\n    <string name=\"undo\">되돌리기</string>\n    <string name=\"lyrics_not_found\">가사를 찾을 수 없음</string>\n    <string name=\"sleep_timer\">수면 타이머</string>\n    <string name=\"end_of_song\">노래 끝</string>\n    <plurals name=\"minute\">\n        <item quantity=\"other\">%d분</item>\n    </plurals>\n    <string name=\"error_no_stream\">스트림을 찾을 수 없음</string>\n    <string name=\"error_no_internet\">네트워크 연결 없음</string>\n    <string name=\"error_timeout\">타임아웃</string>\n    <string name=\"error_unknown\">알 수 없는 오류</string>\n    <string name=\"action_like\">좋아요</string>\n    <string name=\"action_remove_like\">좋아요 취소</string>\n    <string name=\"action_shuffle_on\">셔플 활성화</string>\n    <string name=\"action_shuffle_off\">셔플 비활성화</string>\n    <string name=\"repeat_mode_off\">반복 해제</string>\n    <string name=\"repeat_mode_one\">현재 곡 반복</string>\n    <string name=\"repeat_mode_all\">대기열 반복</string>\n    <string name=\"queue_all_songs\">모든 노래</string>\n    <string name=\"queue_searched_songs\">검색된 노래</string>\n    <string name=\"music_player\">음악 플레이어</string>\n    <string name=\"settings\">설정</string>\n    <string name=\"appearance\">모양</string>\n    <string name=\"enable_dynamic_theme\">동적 테마 활성화</string>\n    <string name=\"dark_theme\">다크 테마</string>\n    <string name=\"dark_theme_on\">켬</string>\n    <string name=\"dark_theme_off\">끔</string>\n    <string name=\"dark_theme_follow_system\">시스템</string>\n    <string name=\"pure_black\">퓨어 블랙</string>\n    <string name=\"default_open_tab\">시작 시 열리는 탭</string>\n    <string name=\"customize_navigation_tabs\">내비게이션 탭 사용자 정의</string>\n    <string name=\"lyrics_text_position\">가사 위치</string>\n    <string name=\"left\">왼쪽</string>\n    <string name=\"center\">중앙</string>\n    <string name=\"right\">오른쪽</string>\n    <string name=\"content\">콘텐츠</string>\n    <string name=\"login\">로그인</string>\n    <string name=\"content_language\">기본 콘텐츠 언어</string>\n    <string name=\"content_country\">기본 콘텐츠 국가</string>\n    <string name=\"system_default\">시스템 기본값</string>\n    <string name=\"enable_proxy\">프록시 활성화</string>\n    <string name=\"proxy_type\">프록시 타입</string>\n    <string name=\"proxy_url\">프록시 URL</string>\n    <string name=\"restart_to_take_effect\">변경사항을 적용하려면 다시 시작하세요</string>\n    <string name=\"player_and_audio\">플레이어 및 오디오</string>\n    <string name=\"audio_quality\">오디오 품질</string>\n    <string name=\"audio_quality_auto\">자동</string>\n    <string name=\"audio_quality_high\">높음</string>\n    <string name=\"audio_quality_low\">낮음</string>\n    <string name=\"persistent_queue\">대기열 유지</string>\n    <string name=\"skip_silence\">무음 건너뛰기</string>\n    <string name=\"audio_normalization\">오디오 음량 평준화</string>\n    <string name=\"equalizer\">이퀄라이저</string>\n    <string name=\"storage\">저장공간</string>\n    <string name=\"cache\">캐시</string>\n    <string name=\"image_cache\">이미지 캐시</string>\n    <string name=\"song_cache\">노래 캐시</string>\n    <string name=\"max_cache_size\">캐시 최대 크기</string>\n    <string name=\"unlimited\">무제한</string>\n    <string name=\"clear_all_downloads\">다운로드 한 곡 모두 지우기</string>\n    <string name=\"max_image_cache_size\">이미지 캐시 최대 크기</string>\n    <string name=\"clear_image_cache\">이미지 캐시 지우기</string>\n    <string name=\"max_song_cache_size\">노래 캐시 최대 크기</string>\n    <string name=\"clear_song_cache\">노래 캐시 지우기</string>\n    <string name=\"size_used\">%s 사용됨</string>\n    <string name=\"privacy\">프라이버시</string>\n    <string name=\"pause_listen_history\">재생 기록 일시 중지</string>\n    <string name=\"clear_listen_history\">재생 기록 지우기</string>\n    <string name=\"clear_listen_history_confirm\">모든 재생 기록을 지우시겠습니까?</string>\n    <string name=\"pause_search_history\">검색 기록 일시 중지</string>\n    <string name=\"clear_search_history\">검색 기록 지우기</string>\n    <string name=\"clear_search_history_confirm\">모든 검색 기록을 지우시겠습니까?</string>\n    <string name=\"enable_kugou\">KuGou 가사 제공자 활성화</string>\n    <string name=\"backup_restore\">백업 및 복구</string>\n    <string name=\"action_backup\">백업</string>\n    <string name=\"action_restore\">복구</string>\n    <string name=\"imported_playlist\">가져온 재생목록</string>\n    <string name=\"backup_create_success\">백업이 성공적으로 생성되었습니다</string>\n    <string name=\"backup_create_failed\">백업을 생성할 수 없습니다</string>\n    <string name=\"restore_failed\">백업을 복구하지 못했습니다</string>\n    <string name=\"about\">정보</string>\n    <string name=\"app_version\">앱 버전</string>\n    <string name=\"new_version_available\">새 버전을 사용할 수 있습니다</string>\n    <string name=\"translation_models\">번역 모델</string>\n    <string name=\"clear_translation_models\">번역 모델 지우기</string>\n    <string name=\"your_youtube_playlists\">YouTube 재생목록</string>\n    <string name=\"remove_from_playlist\">재생목록에서 제거</string>\n    <string name=\"remove_from_queue\">대기열에서 제거</string>\n    <string name=\"keep_listening\">계속 듣기</string>\n    <string name=\"remove_all_from_library\">보관함에서 모두 제거</string>\n    <string name=\"library_artist_empty\">보관함 아티스트가 여기에 표시됩니다</string>\n    <string name=\"player_slider_style\">플레이어 슬라이더 스타일</string>\n    <string name=\"persistent_queue_desc\">앱이 시작될 때 마지막 대기열 복원</string>\n    <string name=\"misc\">기타</string>\n    <string name=\"delete_playlist_confirm\">\\\"%s\\\" 재생목록을 삭제하시겠습니까?</string>\n    <string name=\"library_song_empty\">보관함 노래가 여기에 표시됩니다</string>\n    <string name=\"library_album_empty\">보관함 앨범이 여기에 표시됩니다</string>\n    <string name=\"library_playlist_empty\">재생목록이 여기에 표시됩니다</string>\n    <string name=\"remove_download_playlist_confirm\">다운로드한 노래 저장소에서 \\\"%s\\\" 재생 목록의 모든 노래를 정말로 제거하시겠습니까?</string>\n    <string name=\"add_all_to_library\">보관함에 모두추가</string>\n    <string name=\"tempo_and_pitch\">템포와 피치</string>\n    <string name=\"duplicates\">중복</string>\n    <string name=\"skip_duplicates\">중복 건너뛰기</string>\n    <string name=\"add_anyway\">무시하고 추가</string>\n    <string name=\"duplicates_description_single\">음악이 이미 재생목록에 있습니다</string>\n    <string name=\"duplicates_description_multiple\">%d곡이 이미 재생목록에 있습니다</string>\n    <string name=\"theme\">테마</string>\n    <string name=\"player_text_alignment\">플레이어 텍스트 맞춤</string>\n    <string name=\"player\">플레이어</string>\n    <string name=\"default_\">기본값</string>\n    <string name=\"grid_cell_size\">그리드 셀 크기</string>\n    <string name=\"small\">작게</string>\n    <string name=\"big\">크게</string>\n    <string name=\"not_logged_in\">로그인되지 않음</string>\n    <string name=\"queue\">대기열</string>\n    <string name=\"auto_load_more\">자동으로 더 많은 노래 불러오기</string>\n    <string name=\"auto_skip_next_on_error\">오류 발생 시 다음 곡으로 자동 건너뛰기</string>\n    <string name=\"disable_screenshot\">스크린샷 비활성화</string>\n    <string name=\"auto_load_more_desc\">가능하다면 대기열 끝에 도달하면 자동으로 노래를 더 추가합니다</string>\n    <string name=\"listen_history\">다시 듣기</string>\n    <string name=\"enable_lrclib\">LrcLib 가사 제공자 활성화</string>\n    <string name=\"hide_explicit\">선정적인 내용 숨기기</string>\n    <string name=\"preview\">미리보기</string>\n    <string name=\"login_failed\">로그인 실패</string>\n    <string name=\"discord_integration\">Discord 통합</string>\n    <string name=\"discord_information\">Metrolist은 KizzyRPC 라이브러리를 사용하여 Discord 계정의 상태를 설정합니다. 여기에는 Discord Gateway 연결을 사용하는 것이 포함되며, 이는 Discord의 TOS 위반으로 간주될 수 있습니다. 그러나 이러한 이유로 사용자 계정이 정지된 사례는 알려진 바가 없습니다. 사용에 따른 책임은 본인에게 있습니다. \\n \\nMetrolist은 토큰만 추출하며 그 밖의 모든 내용은 로컬에 저장됩니다.</string>\n    <string name=\"options\">옵션</string>\n    <string name=\"action_logout\">로그아웃</string>\n    <string name=\"action_like_all\">모두 좋아요</string>\n    <string name=\"action_remove_like_all\">좋아요 모두 취소</string>\n    <string name=\"squiggly\">구불구불</string>\n    <string name=\"sided\">측면</string>\n    <string name=\"forgotten_favorites\">잊고 있던 좋은 음악</string>\n    <string name=\"other_versions\">다른 버전</string>\n    <string name=\"dismiss\">다시 보지 않기</string>\n    <string name=\"search_history\">검색 내역</string>\n    <string name=\"stop_music_on_task_clear\">앱 종료시 음악 중지</string>\n    <string name=\"use_login_for_browse_desc\">표시되는 콘텐츠에 영향을 끼칠 수 있으며, 예를 들어 Premium 계정으로 로그인했다면 Premium 한정 앨범이 표시됩니다</string>\n    <string name=\"use_login_for_browse\">로그인한 계정으로 콘텐츠 탐색</string>\n    <string name=\"similar_to\">아래 아티스트와 유사한 음악 추천</string>\n    <string name=\"auto_skip_next_on_error_desc\">끊김 없는 재생 경험 보장</string>\n    <string name=\"disable_screenshot_desc\">이 설정을 켜면, 스크린샷이 비활성화되고 최근 앱에서 보이는 앱 화면이 숨겨집니다.</string>\n    <string name=\"enable_discord_rpc\">활동 상태 공유</string>\n    <string name=\"action_login\">로그인</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-lt/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"years\">Metai</string>\n    <string name=\"cancel\">Atšaukti</string>\n    <string name=\"liked\">Mėgiami</string>\n    <string name=\"offline\">Atsisiuntimai</string>\n    <string name=\"my_top\">Mano top</string>\n    <string name=\"cached_playlist\">Talpykloje</string>\n    <string name=\"uploaded_playlist\">Įkėlimai</string>\n    <string name=\"text_color\">Teksto spalva</string>\n    <string name=\"secondary_text_color\">Antrinė teksto spalva</string>\n    <string name=\"background_color\">Fono spalva</string>\n    <string name=\"remove_from_cache\">Pašalinti iš talpyklos</string>\n    <string name=\"copy_link\">Kopijuoti nuorodą</string>\n    <string name=\"select\">Pasirinkti viską</string>\n    <string name=\"like_all\">Pridėti viską prie mėgiamų</string>\n    <string name=\"charts\">Topai</string>\n    <string name=\"back_button_desc\">Grįžti</string>\n    <string name=\"album_cover_desc\">Albumo viršelis</string>\n    <string name=\"top_music_videos\">Populiariausi muzikiniai vaizdo klipai</string>\n    <string name=\"weeks\">Savaitės</string>\n    <string name=\"months\">Mėnesiai</string>\n    <string name=\"continuous\">Tęstinis</string>\n    <string name=\"remote_history\">Nuotolinė istorija</string>\n    <string name=\"local_history\">Vietinė istorija</string>\n    <string name=\"trending\">Tendencijos</string>\n    <string name=\"filter_uploaded\">Įkėlimai</string>\n    <string name=\"sync_playlist\">Sinchronizuoti grojaraštį</string>\n    <string name=\"sync_disabled\">Sinchronizavimas išjungtas</string>\n    <string name=\"allows_for_sync_witch_youtube\">Pastaba: Tai leidžia sinchronizuoti su YouTube Muzika. To NEGALIMA pakeisti vėliau.</string>\n    <string name=\"generating_image\">Generuojamas paveikslėlis</string>\n    <string name=\"please_wait\">Prašome palaukti</string>\n    <string name=\"share_lyrics\">Dalintis dainos žodžiais</string>\n    <string name=\"share_as_text\">Dalintis tekstu</string>\n    <string name=\"share_as_image\">Dalintis paveikslėliu</string>\n    <string name=\"max_selection_limit\">Maksimalus pasirinkimų skaičius</string>\n    <string name=\"share_selected\">Dalintis pasirinktais</string>\n    <string name=\"customize_colors\">Spalvų nustatymai</string>\n    <string name=\"dislike_all\">Pridėti viską prie nemėgiamų</string>\n    <string name=\"sort_by_last_updated\">Atnaujinimo data</string>\n    <string name=\"link_copied\">Nuoroda nukopijuota į iškarpinę</string>\n    <string name=\"starting_radio\">Paleidžiamas radijas</string>\n    <string name=\"now_playing\">Dabar groja</string>\n    <string name=\"lyrics\">Dainos žodžiai</string>\n    <string name=\"close\">Uždaryti</string>\n    <string name=\"hide_player_thumbnail\">Slėpti grotuvo miniatiūrą</string>\n    <string name=\"hide_player_thumbnail_desc\">Grotuve pakeisti albumo paveikslėlį programėlės logotipu</string>\n    <string name=\"already_in_playlist\">Jau grojaraštyje:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d kartą</item>\n        <item quantity=\"few\">%d kartus</item>\n        <item quantity=\"many\">%d kartų</item>\n        <item quantity=\"other\">%d kartų</item>\n    </plurals>\n    <string name=\"seek_forward_dynamic\">+%1$d sekundžių į priekį</string>\n    <string name=\"seek_backward_dynamic\">-%1$d sekundžių atgal</string>\n    <string name=\"seek_seconds_addup\">Progresyvus sekimas</string>\n    <string name=\"seek_seconds_addup_description\">Jei įjungta, prie kiekvieno praleidimo palaipsniui prideda po 5 sekundes</string>\n    <string name=\"similar_content\">Panašus turinys</string>\n    <string name=\"player_background_style\">Grotuvo fono išvaizda</string>\n    <string name=\"follow_theme\">Pritaikyti prie temos</string>\n    <string name=\"gradient\">Gradientas</string>\n    <string name=\"new_player_design\">Nauja grotuvo išvaizda</string>\n    <string name=\"new_mini_player_design\">Nauja mini grotuvo išvaizda</string>\n    <string name=\"player_background_blur\">Suliejimas</string>\n    <string name=\"player_buttons_style\">Grotuvo mygtukų spalvos</string>\n    <string name=\"default_style\">Numatytoji</string>\n    <string name=\"enable_swipe_thumbnail\">Įjungti dainos keitimą braukiant</string>\n    <string name=\"swipe_song_to_add\">Braukti į kairę, kad pridėti dainą į eilę, arba į dešinę, kad ji būtų sekanti eilėje</string>\n    <string name=\"lyrics_click_change\">Paspaudus pakeisti dainos žodžius</string>\n    <string name=\"lyrics_auto_scroll\">Automatinis dainos žodžių slinkimas</string>\n    <string name=\"slim\">Siauras</string>\n    <string name=\"slim_navbar\">Siauras apatinis navigacijos meniu</string>\n    <string name=\"auto_playlists\">Automatiniai grojaraščiai</string>\n    <string name=\"show_liked_playlist\">Rodyti grojaraštį \\\"Mėgiami\\\"</string>\n    <string name=\"show_downloaded_playlist\">Rodyti grojaraštį \\\"Atsisiuntimai\\\"</string>\n    <string name=\"show_top_playlist\">Rodyti grojaraštį \\\"Populiariausi\\\"</string>\n    <string name=\"show_cached_playlist\">Rodyti grojaraštį \\\"Saugykloje\\\"</string>\n    <string name=\"show_uploaded_playlist\">Rodyti grojaraštį \\\"Įkėlimai\\\"</string>\n    <string name=\"advanced_login\">Prisijungti naudojant žetoną</string>\n    <string name=\"token_hidden\">Prilieskite kad matytumėte žetoną</string>\n    <string name=\"token_shown\">Prilieskite dar kartą, kad kopijuoti ar redaguoti</string>\n    <string name=\"enable_simpmusic\">Įjungti SimpMusic dainos tekstus</string>\n    <string name=\"enable_simpmusic_desc\">Naudoti SimpMusic dainų tekstų tiekėją sinchronizuotiems dainų tekstams</string>\n    <string name=\"auto_scroll\">Sinchronizuoti</string>\n    <string name=\"show_less\">Rodyti mažiau</string>\n    <string name=\"artist_page_settings\">Kūrėjo puslapis</string>\n    <string name=\"show_artist_description\">Rodyti atlikėjo aprašymą</string>\n    <string name=\"show_artist_subscriber_count\">Rodyti prenumeratorių skaičių</string>\n    <string name=\"show_artist_monthly_listeners\">Rodyti klausytojų skaičių per mėnesį</string>\n    <string name=\"download_playlist_desc\">Atsisiųsti visas dainas klausymui neprisijungus prie ryšio</string>\n    <string name=\"remove_download_playlist_desc\">Ištrinti visas atsiųstas dainas iš šio grojaraščio</string>\n    <string name=\"download_in_progress_desc\">Atsiunčiama</string>\n    <string name=\"share_playlist_desc\">Dalintis grojaraščiu su kitais</string>\n    <string name=\"delete_playlist_desc\">Panaikinti grojaraštį visam laikui</string>\n    <string name=\"sync_playlist_desc\">Sinchronizuoti grojaraštį su YouTube Muzika</string>\n    <string name=\"like\">Pažymėti, kad patinka</string>\n    <string name=\"update_available_title\">Galimas atnaujinimas</string>\n    <string name=\"update_channel_name\">Programėlės naujinimai</string>\n    <string name=\"turntable_widget_description\">Greita prieiga prie labiausiai grojamo kūrinio</string>\n    <string name=\"primary_color_style\">Pirminė spalva</string>\n    <string name=\"tertiary_color_style\">Tretinė spalva</string>\n    <string name=\"wavy\">Banguojantis</string>\n    <string name=\"remove_custom_image\">Panaikinti pasirinktiną nuotrauką</string>\n    <string name=\"general\">Bendra</string>\n    <string name=\"default_lib_chips\">Pakeisti numatytąją bibliotekos žetoną</string>\n    <string name=\"set_quick_picks\">Nustatyti greituosius pasirinkimus</string>\n    <string name=\"last_song_listened\">Remiantis paskutine klausyta daina</string>\n    <string name=\"app_language\">Programėlės kalba</string>\n    <string name=\"config_proxy\">Konfigūruoti proxy</string>\n    <string name=\"proxy_username\">Proxy vartotojo vardas</string>\n    <string name=\"proxy_password\">Proxy slaptažodis</string>\n    <string name=\"enable_authentication\">Įjungti autentifikavimą</string>\n    <string name=\"eq_disabled\">Išjungtas</string>\n    <string name=\"check_for_updates\">Automatiškai tikrinti, ar yra atnaujinimų</string>\n    <string name=\"updater\">Atnaujinimas</string>\n    <string name=\"username\">Vartotojo vardas</string>\n    <string name=\"password\">Slaptažodis</string>\n    <string name=\"lastfm_integration\">Last.fm integracija</string>\n    <string name=\"enable_scrobbling\">Įjungti skroblavimą</string>\n    <string name=\"romanize_current_track\">Romanizuoti dabartinį kūrinį</string>\n    <string name=\"lyrics_offset\">Teksto poslinkis</string>\n    <string name=\"settings_section_ui\">Sąsaja</string>\n    <string name=\"settings_section_privacy\">Privatumas ir Saugumas</string>\n    <string name=\"settings_section_player_content\">Grotuvas ir Turinys</string>\n    <string name=\"settings_section_storage\">Talpykla ir Duomenys</string>\n    <string name=\"settings_section_system\">Sistema ir Apie</string>\n    <string name=\"update_notifications\">Įjungti pranešimus apie atnaujinimus</string>\n    <string name=\"update_channel_desc\">Pranešimai apie naujas versijas</string>\n    <string name=\"audio_offload\">Įjungti perkėlimą</string>\n    <string name=\"audio_offload_description\">Naudoti kitą garso atkūrimo metodą. Išjungiant šį nustatymą gali padidėti baterijos sąnaudos, tačiau gali būti naudinga, jei patiriate problemų su garso atkūrimu arba garso apdorojimu</string>\n    <string name=\"google_cast\">„Google Cast”</string>\n    <string name=\"google_cast_description\">Įjungti garso perdavimą „Chromecast” ir kitiems perdavimą palaikantiems įrenginiams</string>\n    <string name=\"lyrics_romanize_macedonian\">Romanizuoti makedonų kalbos tekstus</string>\n    <string name=\"integrations\">Integracijos</string>\n    <string name=\"views\">Peržiūros</string>\n    <string name=\"likes\">Mėgsta</string>\n    <string name=\"dislikes\">Nemėgsta</string>\n    <string name=\"subscribe\">Prenumeruoti</string>\n    <string name=\"subscribed\">Prenumeruota</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">%d sekundė</item>\n        <item quantity=\"few\">%d sekundės</item>\n        <item quantity=\"many\">%d sekundžių</item>\n        <item quantity=\"other\">%d sekundžių</item>\n    </plurals>\n    <string name=\"disable_load_more_when_repeat_all\">Išjungti papildomų dainų įkėlimą, kai įjungtas visko kartojimas</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Neįkelti automatiškai daugiau dainų ir panašaus turinio, kai visko kartojimo režimas yra įjungtas</string>\n    <string name=\"pause_music_when_media_is_muted\">Sustabdyti muziką, kai medijos garsas yra nutildytas</string>\n    <string name=\"lyrics_romanization_cyrillic\">Kirilica</string>\n    <string name=\"lyrics_romanize_title\">Romanizacija</string>\n    <string name=\"lyrics_romanization\">Dainų tekstų romanizacija</string>\n    <string name=\"lyrics_romanize_japanese\">Romanizuoti japonų kalbos dainų tekstus</string>\n    <string name=\"lyrics_romanize_korean\">Romanizuoti korėjiečių kalbos dainų tekstus</string>\n    <string name=\"lyrics_romanize_chinese\">Romanizuoti kinų kalbos dainų tesktus</string>\n    <string name=\"hide_video_songs\">Paslėpti muzikos klipus</string>\n    <string name=\"details_desc\">Žiūrėti dainos informaciją</string>\n    <string name=\"edit_desc\">Pakeisti pavadinimą arba kūrėją</string>\n    <string name=\"start_radio_desc\">Sukurti stotį remiantis šiuo elementu</string>\n    <string name=\"play_next_desc\">Pridėti į eilės priekį</string>\n    <string name=\"add_to_queue_desc\">Pridėtį į eilės galą</string>\n    <string name=\"apple_music_style\">„Apple Music”</string>\n    <string name=\"add_to_library_desc\">Išsaugoti į biblioteką</string>\n    <string name=\"download_desc\">Padaryti pasiekiamą atkūrimui neprisijungus</string>\n    <string name=\"add_to_playlist_desc\">Pridėtį į grojaraštį</string>\n    <string name=\"refetch_desc\">Gauti naujausią informaciją iš „YouTube Muzika”</string>\n    <string name=\"share_desc\">Dalintis nuoroda šiam elementui</string>\n    <string name=\"delete_desc\">Panaikinti šį elementą visam laikui</string>\n    <string name=\"advanced_desc\">Pakeisti dainos tempą ir aukštį</string>\n    <string name=\"equalizer_desc\">Koreguoti garso ekvalaizerį</string>\n    <string name=\"enable_dynamic_icon\">Įjungti prisitaikančią ikonėlę</string>\n    <string name=\"mini_player\">Mini-grotuvas</string>\n    <string name=\"pure_black_mini_player\">Juodas mini-grotuvas</string>\n    <string name=\"cache_size_warning_title\">Palaukite!</string>\n    <string name=\"cache_size_warning_message\">Jūs pasirinkote talpyklos dydžio limitą mažesnį nei programėlė dabar naudoja (%1$s). Jei tęsite, programėlė panaikins talpykloje saugojamų %2$s, kad susilygintų su nauju limitu. Tęsti vis tiek?</string>\n    <string name=\"cache_size_warning_confirm\">Tęsti</string>\n    <string name=\"lyrics_animation_style\">Žodis po žodžio animacijos stilius</string>\n    <string name=\"none\">Joks</string>\n    <string name=\"fade\">išblukti</string>\n    <string name=\"glow\">Švytėti</string>\n    <string name=\"slide\">Slinkti</string>\n    <string name=\"karaoke\">Karaoke</string>\n    <string name=\"lyrics_text_size\">Dainų teksto dydis</string>\n    <string name=\"lyrics_line_spacing\">Dainų teksto eilutės tarpas</string>\n    <string name=\"album_art_for\">%s Albumo viršelis</string>\n    <string name=\"wrapped_total_albums_title\">Jūs klausėte</string>\n    <string name=\"wrapped_total_albums_subtitle\">unikalių albumų</string>\n    <string name=\"wrapped_top_album_title\">Jūsų top albumas yra</string>\n    <string name=\"wrapped_playlist_ready\">Jūsų asmeninis grojaraštis yra paruoštas</string>\n    <string name=\"wrapped_top_5_albums_title\">Jūsų top 5 albumai</string>\n    <string name=\"wrapped_album_listening_time\">Jūs klausėte šio albumo %d minučių</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d minučių</string>\n    <string name=\"wrapped_no_data\">Nėra duomenų</string>\n    <string name=\"wrapped_top_5_artists_title\">Jūsų top metų kūrėjai</string>\n    <string name=\"wrapped_artist_listening_time\">%d minučių</string>\n    <string name=\"wrapped_top_5_songs_title\">Jūsų top metų dainos</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">Albumo viršelis</string>\n    <string name=\"wrapped_top_artist_title\">Jūsų top metų kūrėjas yra</string>\n    <string name=\"wrapped_top_artist_image_content_description\">Top kūrėjo nuotrauka</string>\n    <string name=\"wrapped_top_artist_listening_time\">Jūs jų išklausėte %d minučių</string>\n    <string name=\"wrapped_top_song_title\">Jūsų daugiausia kartų grota daina yra</string>\n    <string name=\"wrapped_top_song_listening_time\">Jūs praklausėte %d minučių</string>\n    <string name=\"wrapped_total_artists_title\">Jūs klausėtės</string>\n    <string name=\"wrapped_total_artists_subtitle\">unikalių kūrėjų</string>\n    <string name=\"wrapped_total_songs_title\">Jūs klausėtės</string>\n    <string name=\"wrapped_total_songs_subtitle\">unikalių dainų</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_intro_subtitle\">laikas pamatyti, ką jūs klausėtės</string>\n    <string name=\"wrapped_intro_button\">pirmyn!</string>\n    <string name=\"wrapped_logo_content_description\">„Metrolist” logotipas</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"wrapped_ready_title\">JŪSŲ WRAPPED YRA PARUOŠTAS!</string>\n    <string name=\"wrapped_ready_subtitle\">Metas pamatyti, kas jums patiko šiais metais.</string>\n    <string name=\"wrapped_thank_you\">Ačiū, kad klausotės</string>\n    <string name=\"wrapped_special_thanks\">Ypatingas ačiū MO Agamy, kad sukūrė „Metrolist”</string>\n    <string name=\"wrapped_close\">Uždaryti Wrapped</string>\n    <string name=\"wrapped_playlist_title\">Jūsų %s Wrapped</string>\n    <string name=\"wrapped_create_playlist\">Sukurti grojaraštį</string>\n    <string name=\"wrapped_playlist_saved\">Grojaraštis išsaugotas</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"one\">%d Profilis</item>\n        <item quantity=\"few\">%d Profiliai</item>\n        <item quantity=\"many\">%d Profilių</item>\n        <item quantity=\"other\">%d Profilių</item>\n    </plurals>\n    <string name=\"equalizer_header\">Ekvalaizeris</string>\n    <string name=\"no_profiles\">Nėra ekvalaizerio profilių</string>\n    <string name=\"import_profile\">Įkelti profilį</string>\n    <string name=\"system_equalizer\">Sistemos ekvalaizeris</string>\n    <plurals name=\"band_count\">\n        <item quantity=\"one\">%d juosta</item>\n        <item quantity=\"few\">%d juostos</item>\n        <item quantity=\"many\">%d juostų</item>\n        <item quantity=\"other\">%d juostų</item>\n    </plurals>\n    <string name=\"delete_profile_desc\">Ištrinti profilį</string>\n    <string name=\"delete_profile_confirmation\">Ar tikrai norite ištrinti %1$s? Šis veiksmas negali būti atšauktas.</string>\n    <string name=\"error_file_read\">Failas negalėjo būti perskaitytas</string>\n    <string name=\"error_file_open\">Negalėjome atidaryti failo: %1$s</string>\n    <string name=\"import_error_title\">Įkėlimo klaida</string>\n    <string name=\"error_title\">Klaida</string>\n    <string name=\"error_eq_apply_failed\">Klaida, įkeliant ekvalaizerio profilį %1$s</string>\n    <string name=\"progress_percent\">Progresas %s%%</string>\n    <string name=\"listening_to_metrolist\">Klausomasi „Metrolist”</string>\n    <string name=\"open\">Atidaryti</string>\n    <string name=\"failed_to_create_image\">Klaida, kuriant nuotrauką %s</string>\n    <string name=\"copied_title\">Nukopijuotas pavadinimas</string>\n    <string name=\"copied_artist\">Nukopijuotas kūrėjas</string>\n    <string name=\"error_playing\">Grojimo klaida</string>\n    <string name=\"failed_to_parse_proxy\">Nepavyko išanalizuoti tarpinio serverio url.</string>\n    <string name=\"error_playback_failed\">Atkūrimo klaida</string>\n    <string name=\"album_art\">Albumo viršelis</string>\n    <string name=\"no_song_playing\">Nėra grojančios dainos</string>\n    <string name=\"tap_to_open\">Spauskite, kad atidarytumėte „Metrolist”</string>\n    <string name=\"previous\">Ankstesnis</string>\n    <string name=\"play_pause\">Leisti / pristabdyti</string>\n    <string name=\"next\">Kitas</string>\n    <string name=\"widget_description\">Muzikos grotuvo valdiklis su atkūrimo valdikliais</string>\n    <string name=\"about_artist\">Apie</string>\n    <string name=\"show_more\">Rodyti daugiau</string>\n    <string name=\"crop_album_art\">Apkarpyti albumo viršelį</string>\n    <string name=\"crop_album_art_desc\">Priverstinai nustatykite kvadratinį kraštinių santykį apkirpdami vaizdo įrašų miniatiūras</string>\n    <string name=\"swipe_song_to_remove\">Braukite dainą, kad ją pašalintumėte iš grojaraščio</string>\n    <string name=\"lyrics_glow_effect\">Įjungti švytinčių dainų tekstų efektą</string>\n    <string name=\"lyrics_glow_effect_desc\">Pridėkite šviečiančią animaciją ir atšokimo efektą prie aktyvių dainų tekstų</string>\n    <string name=\"enable_better_lyrics\">Įjungti Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">Naudoti Better Lyrics tiekėją pažodžiui sinchronizuotiems dainų tekstams</string>\n    <string name=\"shuffle_playlist_first\">Pirma maišyti grojaraštį/albumą</string>\n    <string name=\"shuffle_playlist_first_desc\">Maišant, pirma groti visas dainas iš grojaraščio/albumo, ir tik tada panašų turinį</string>\n    <string name=\"show_wrapped_card\">Rodyti Wrapped kortelę</string>\n    <string name=\"skip_silence_desc\">Greitasis perėjimas per tyliąsias dainos vietas</string>\n    <string name=\"skip_silence_instant\">Staigiai praleiskite tylą</string>\n    <string name=\"skip_silence_instant_desc\">Peršokite per tylias vietas, vietoj pagreitinant atkūrimą</string>\n    <string name=\"token_adv_login_description\">Tai PAŽANGUS prisijungimo būdas. Kaip alternatyvą žiniatinklio portalui, galite tiesiogiai įvesti arba atnaujinti savo prisijungimo raktą čia. Pavyzdžiui, tai gali pagreitinti prisijungimą keliuose įrenginiuose. Atminkite, kad bet kokie neteisingi rakto formatai, kurių programa nesugeba išanalizuoti, nebus priimti</string>\n    <string name=\"yt_sync\">Automatiškai sinchronizuoti su paskyra</string>\n    <string name=\"more_content\">Daugiau turinio</string>\n    <string name=\"edit_playlist_cover\">Koreguokite grojaraščio viršelį</string>\n    <string name=\"edit_playlist_cover_note\">Pastaba: norint pakeisti grojaraščio viršelį, jūsų paskyra turi būti susieta su telefono numeriu ir patvirtinta „YouTube Muzika“.</string>\n    <string name=\"edit_playlist_cover_note_wait\">Pasirinkę paveikslėlį, palaukite, kol naujas viršelis pasirodys jūsų grojaraštyje.</string>\n    <string name=\"choose_from_library\">Pasirinkti iš bibliotekos</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"discord_use_details\">Naudoti detalesnę informaciją, vietoje būsenos</string>\n    <string name=\"discord_use_details_description\">Aiškiai matyti dainos pavadinimą, o ne atlikėjo vardą</string>\n    <string name=\"enable_similar_content\">Įjungti panašų turinį</string>\n    <string name=\"similar_content_desc\">Automatiškai pridėti daugiau panašių dainų, kai pasiekiama eilės pabaiga</string>\n    <string name=\"persistent_shuffle_title\">Nuolatinis maišymas</string>\n    <string name=\"persistent_shuffle_desc\">Paleidžiant naujas dainas ar grojaraščius, įjunkite atsitiktinę grojimą</string>\n    <string name=\"remember_shuffle_and_repeat\">Atsiminti maišymą ir kartojimą</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Perkraunant programėlę, atsiminti maišymą ir kartojimo režimą</string>\n    <string name=\"percentage_format\">%d%%</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-mfe/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">Lokal</string>\n    <string name=\"remote_history\">Dan lot aplikasyon</string>\n    <string name=\"charts\">Bann graf</string>\n    <string name=\"back_button_desc\">Rétour</string>\n    <string name=\"album_cover_desc\">Cover album</string>\n    <string name=\"top_music_videos\">Bann meyer vidéo clip</string>\n    <string name=\"trending\">Tandans</string>\n    <string name=\"weeks\">Semene</string>\n    <string name=\"months\">Mois</string>\n    <string name=\"years\">Ans</string>\n    <string name=\"continuous\">Kontinien</string>\n    <string name=\"liked\">Inn Like</string>\n    <string name=\"offline\">Inn Download</string>\n    <string name=\"my_top\">Mo top</string>\n    <string name=\"cached_playlist\">Inn cache</string>\n    <string name=\"uploaded_playlist\">Inn upload</string>\n    <string name=\"filter_uploaded\">Inn upload</string>\n    <string name=\"sync_playlist\">Senkroniz playlist</string>\n    <string name=\"sync_disabled\">Senkronizasyon dezaktivé</string>\n    <string name=\"allows_for_sync_witch_youtube\">Note: Sa paramet la permet senkronizasyon ek YouTube Music. Ou PA pu resi resanze li apre.</string>\n    <string name=\"generating_image\">Generate zimage</string>\n    <string name=\"please_wait\">Patienté silvouple</string>\n    <string name=\"cancel\">Cancel</string>\n    <string name=\"share_lyrics\">Partaz bann parole</string>\n    <string name=\"share_as_text\">Partaz antan ki text</string>\n    <string name=\"share_as_image\">Partaz antan ki zimaz</string>\n    <string name=\"max_selection_limit\">Limit maximal pu seleksyon</string>\n    <string name=\"share_selected\">Partaz seleksyon</string>\n    <string name=\"customize_colors\">Personaliz kouler</string>\n    <string name=\"text_color\">Kouler text</string>\n    <string name=\"secondary_text_color\">Kouler secondaire pu text</string>\n    <string name=\"background_color\">Kouler background</string>\n    <string name=\"remove_from_cache\">Tir dan cache</string>\n    <string name=\"copy_link\">Kopié link</string>\n    <string name=\"select\">Selekté tou</string>\n    <string name=\"like_all\">Like tou</string>\n    <string name=\"dislike_all\">Dislike tou</string>\n    <string name=\"sort_by_last_updated\">Dat miz-azour</string>\n    <string name=\"link_copied\">Link inn kopié dan clipboard</string>\n    <string name=\"starting_radio\">Demaraz radio</string>\n    <string name=\"now_playing\">Lektir en cours</string>\n    <string name=\"lyrics\">Bann paroles</string>\n    <string name=\"close\">Fermé</string>\n    <string name=\"hide_player_thumbnail\">Kasiet thumbnail lekter la</string>\n    <string name=\"hide_player_thumbnail_desc\">Ranplas art album ar logo aplikasyon dan lekter</string>\n    <string name=\"already_in_playlist\">Deza dan playlist:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d fwa</item>\n        <item quantity=\"other\">%d fwa</item>\n    </plurals>\n    <string name=\"seek_forward_dynamic\">%1$d second en avan</string>\n    <string name=\"seek_backward_dynamic\">-%1$d second en aryer</string>\n    <string name=\"seek_seconds_addup\">Avansman progresif</string>\n    <string name=\"seek_seconds_addup_description\">Si sa opsyon la aktif, sak fwa avansé ou rekilé li pu azout 5 second anplis lor sa mem aksyon ki en presedans</string>\n    <string name=\"similar_content\">Konteni similaire</string>\n    <string name=\"player_background_style\">Style lekter dan background</string>\n    <string name=\"follow_theme\">Swiv thème</string>\n    <string name=\"gradient\">Degradé</string>\n    <string name=\"new_player_design\">Nouvo design lekter</string>\n    <string name=\"new_mini_player_design\">Nouvo design mini lekter</string>\n    <string name=\"player_background_blur\">Flou</string>\n    <string name=\"player_buttons_style\">Kouler bouton dan lekter</string>\n    <string name=\"default_style\">Defaut</string>\n    <string name=\"enable_swipe_thumbnail\">Aktiv swipe pu sanz lamizik</string>\n    <string name=\"swipe_song_to_add\">Swipe dan gos pou azout dan queue ou drwat pu zwé li apre</string>\n    <string name=\"swipe_song_to_remove\">Swipe lamizk pu tir li dan playlist</string>\n    <string name=\"lyrics_click_change\">Sanz bann parole kan click</string>\n    <string name=\"lyrics_auto_scroll\">Scroll bann parole automikman</string>\n    <string name=\"slim\">Mins</string>\n    <string name=\"slim_navbar\">Bar navigasyon anba mins</string>\n    <string name=\"auto_playlists\">Playlist automatik</string>\n    <string name=\"show_liked_playlist\">Afis playlist \\\"Inn Like\\\"</string>\n    <string name=\"show_downloaded_playlist\">Afis playlist \\\"Inn Download\\\"</string>\n    <string name=\"show_top_playlist\">Afis playlist \\\"Top\\\"</string>\n    <string name=\"show_cached_playlist\">Afis playlist \\\"Inn cache\\\"</string>\n    <string name=\"show_uploaded_playlist\">Afis playlist \\\"Inn Upload\\\"</string>\n    <string name=\"advanced_login\">Login ar token</string>\n    <string name=\"token_hidden\">Click pu afis token</string>\n    <string name=\"token_shown\">Click ankor pu kopié ou edit</string>\n    <string name=\"token_adv_login_description\">Sa method la enn method login AVANCÉ. Antan ki enn alternaif a portail web, ou pu bizin met ou update ou login token direkteman. Par examp, li kapav fer login pli rapid lor plizir aparey. Noté ki app la pa pu aksepté okenn format token ki invalid ek ki li pa resi analizé</string>\n    <string name=\"yt_sync\">Senkroniz ar compte automikman</string>\n    <string name=\"more_content\">Plis konteni</string>\n    <string name=\"edit_playlist_cover\">Edit cover playlist</string>\n    <string name=\"edit_playlist_cover_note\">Note: Ou compte bizin inn link ek enn limero téléphone et vérifié lor YouTube Music pu resi sanz cover playlist.</string>\n    <string name=\"edit_playlist_cover_note_wait\">Apre ki ou inn swazir n zimaz, patiente enn moment pu nouvo cover la aparet dan ou playlist.</string>\n    <string name=\"choose_from_library\">Soizir depi library</string>\n    <string name=\"remove_custom_image\">Tir zimaz personalizé</string>\n    <string name=\"general\">Zeneral</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"download_playlist_desc\">Download tou sante pu zwe offline</string>\n    <string name=\"remove_download_playlist_desc\">Tir tou sante inn download depi sa playlist la</string>\n    <string name=\"download_in_progress_desc\">Pe Download</string>\n    <string name=\"share_playlist_desc\">Partaz sa playlist la ek lezot</string>\n    <string name=\"delete_playlist_desc\">Retir sa playlist la net</string>\n    <string name=\"sync_playlist_desc\">Senkroniz playlist ek Youtube Music</string>\n    <string name=\"primary_color_style\">Kouler primaire</string>\n    <string name=\"tertiary_color_style\">Kouler tertiaire</string>\n    <string name=\"lyrics_glow_effect\">Aktiv glowing effect pu bann paroles</string>\n    <string name=\"lyrics_glow_effect_desc\">Azout animation glowing ek effet bounce lor bann paroles aktif</string>\n    <string name=\"enable_better_lyrics\">Aktiv Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">Servi Better Lyrics pu paroles senkronize mot par mot</string>\n    <string name=\"auto_scroll\">Re-senkronize</string>\n    <string name=\"shuffle_playlist_first\">Zwe playlist/album en aleatoire en premie</string>\n    <string name=\"shuffle_playlist_first_desc\">Kan en aleatoire, zwe tou sante depi playlist/album orizinal en premie, apre zwe bann cki similaire</string>\n    <string name=\"show_wrapped_card\">Montre Résumé</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-ml/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <string name=\"home\">ഹോം</string>\n    <string name=\"songs\">പാട്ടുകൾ</string>\n    <string name=\"artists\">കലാകാരന്മാർ</string>\n    <string name=\"albums\">ആൽബങ്ങൾ</string>\n    <string name=\"playlists\">പ്ലേലിസ്റ്റുകൾ</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d തിരഞ്ഞെടുത്തു</item>\n        <item quantity=\"other\">%d തിരഞ്ഞെടുത്തു</item>\n    </plurals>\n    <string name=\"search\">തിരയുക</string>\n    <string name=\"search_yt_music\">യൂട്യൂബ് സംഗീതം തിരയുക…</string>\n    <string name=\"search_library\">ലൈബ്രറിയിൽ തിരയുക…</string>\n    <string name=\"filter_all\">എല്ലാം</string>\n    <string name=\"filter_songs\">പാട്ടുകൾ</string>\n    <string name=\"filter_videos\">വീഡിയോകൾ</string>\n    <string name=\"filter_albums\">ആൽബങ്ങൾ</string>\n    <string name=\"filter_artists\">ആർട്ടിസ്റ്റുകൾ</string>\n    <string name=\"filter_playlists\">പ്ലേലിസ്റ്റുകൾ</string>\n    <string name=\"filter_community_playlists\">കമ്മ്യൂണിറ്റി പ്ലേലിസ്റ്റുകൾ</string>\n    <string name=\"filter_featured_playlists\">തിരഞ്ഞെടുത്ത പ്ലേലിസ്റ്റുകൾ</string>\n    <string name=\"from_your_library\">നിങ്ങളുടെ ലൈബ്രറിയിൽ നിന്ന്</string>\n    <string name=\"liked_songs\">ഇഷ്ടപ്പെട്ട പാട്ടുകൾ</string>\n    <string name=\"downloaded_songs\">ഡൗൺലോഡ് ചെയ്‌ത പാട്ടുകൾ</string>\n    <string name=\"retry\">വീണ്ടും ശ്രമിക്കുക</string>\n    <string name=\"radio\">റേഡിയോ</string>\n    <string name=\"shuffle\">ഷഫിൾ</string>\n    <string name=\"edit\">എഡിറ്റ് ചെയ്യുക</string>\n    <string name=\"start_radio\">റേഡിയോ ആരംഭിക്കുക</string>\n    <string name=\"play\">പ്ലേ ചെയ്യുക</string>\n    <string name=\"play_next\">അടുത്തത് പ്ലേ ചെയ്യുക</string>\n    <string name=\"add_to_queue\">ക്യൂവിൽ ചേർക്കുക</string>\n    <string name=\"add_to_library\">ലൈബ്രറിയിലേക്ക് ചേർക്കുക</string>\n    <string name=\"action_download\">ഡൗൺലോഡ്</string>\n    <string name=\"remove_download\">ഡൗൺലോഡ് നീക്കം ചെയ്യുക</string>\n    <string name=\"import_playlist\">പ്ലേലിസ്റ്റ് ഇറക്കുമതി ചെയ്യുക</string>\n    <string name=\"add_to_playlist\">പ്ലേലിസ്റ്റിൽ ആഡ് ചെയ്യുക</string>\n    <string name=\"view_artist\">കലാകാരനെ കാണുക</string>\n    <string name=\"view_album\">ആൽബം കാണുക</string>\n    <string name=\"refetch\">വീണ്ടെടുക്കുക</string>\n    <string name=\"share\">പങ്കിടുക</string>\n    <string name=\"delete\">ഡിലീറ്റ്</string>\n    <string name=\"sort_by_create_date\">ചേർത്ത തീയതി</string>\n    <string name=\"sort_by_name\">പേര്</string>\n    <string name=\"sort_by_artist\">ആർട്ടിസ്റ്റ്</string>\n    <string name=\"sort_by_year\">വർഷം</string>\n    <string name=\"sort_by_song_count\">പാട്ടുകളുടെ എണ്ണം</string>\n    <string name=\"sort_by_length\">ദൈർഘ്യം</string>\n    <string name=\"copied\">്ലിപ്പ്ബോർഡിലേക്ക് പകർത്തി</string>\n    <string name=\"edit_song\">ഗാനം എഡിറ്റ് ചെയ്യുക</string>\n    <string name=\"song_title\">പാട്ടിന്റെ പേര്</string>\n    <string name=\"song_artists\">പാട്ടുകാരൻ</string>\n    <string name=\"error_song_title_empty\">ഗാനത്തിന്റെ പേര് ശൂന്യമാക്കാൻ കഴിയില്ല.</string>\n    <string name=\"error_song_artist_empty\">പാട്ടുകാരൻ ശൂന്യമായിരിക്കാൻ കഴിയില്ല.</string>\n    <string name=\"save\">സേവ്</string>\n    <string name=\"choose_playlist\">പ്ലേലിസ്റ്റ് തിരഞ്ഞെടുക്കുക</string>\n    <string name=\"edit_playlist\">പ്ലേലിസ്റ്റ് എഡിറ്റ് ചെയ്യുക</string>\n    <string name=\"create_playlist\">പ്ലേലിസ്റ്റ് സൃഷ്ടിക്കുക</string>\n    <string name=\"playlist_name\">പ്ലേലിസ്റ്റ് പേര്</string>\n    <string name=\"error_playlist_name_empty\">പ്ലേലിസ്റ്റിന്റെ പേര് ശൂന്യമാക്കാൻ കഴിയില്ല.</string>\n    <string name=\"edit_artist\">എഡിറ്റ് ആർട്ടിസ്റ്റ്</string>\n    <string name=\"artist_name\">കലാകാരന്റെ പേര്</string>\n    <string name=\"error_artist_name_empty\">കലാകാരന്റെ പേര് ശൂന്യമാക്കാൻ കഴിയില്ല.</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d പാട്ട്</item>\n        <item quantity=\"other\">%d പാട്ടുകൾ</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d ആർട്ടിസ്റ്റ്</item>\n        <item quantity=\"other\">%d ആർട്ടിസ്റ്റുകൾ</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d ആൽബം</item>\n        <item quantity=\"other\">%d ആൽബങ്ങൾ</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d പ്ലേലിസ്റ്റ്</item>\n        <item quantity=\"other\">%d പ്ലേലിസ്റ്റുകൾ</item>\n    </plurals>\n    <string name=\"playlist_imported\">പ്ലേലിസ്റ്റ് ഇറക്കുമതി ചെയ്തു</string>\n    <string name=\"music_player\">മ്യൂസിക് പ്ലെയർ</string>\n    <string name=\"settings\">ക്രമീകരണങ്ങൾ</string>\n    <string name=\"appearance\">രൂപഭംഗി</string>\n    <string name=\"dark_theme\">ഡാർക്ക് തീം</string>\n    <string name=\"dark_theme_on\">ഓൺ</string>\n    <string name=\"dark_theme_off\">ഓഫ്</string>\n    <string name=\"dark_theme_follow_system\">സിസ്റ്റം പിന്തുടരുക</string>\n    <string name=\"default_open_tab\">സ്ഥിര ഓപ്പൺ ടാബ്</string>\n    <string name=\"lyrics_text_position\">വരികളുടെ സ്ഥാനം</string>\n    <string name=\"left\">ഇടത്</string>\n    <string name=\"center\">നടുക്ക്</string>\n    <string name=\"right\">വലത്</string>\n    <string name=\"content\">കന്റെന്റ്</string>\n    <string name=\"content_language\">സ്ഥിര കന്റെന്റ് ഭാഷ</string>\n    <string name=\"content_country\">സ്ഥിര കന്റെന്റ് രാജ്യം</string>\n    <string name=\"system_default\">സിസ്റ്റം സ്ഥിരസ്ഥിതി</string>\n    <string name=\"enable_proxy\">പ്രോക്സി പ്രവർത്തനക്ഷമമാക്കുക</string>\n    <string name=\"proxy_type\">പ്രോക്സി തരം</string>\n    <string name=\"proxy_url\">പ്രോക്സി URL</string>\n    <string name=\"restart_to_take_effect\">പ്രാബല്യത്തിൽ വരാൻ പുനരാരംഭിക്കുക</string>\n    <string name=\"player_and_audio\">പ്ലെയറും ഓഡിയോയും</string>\n    <string name=\"audio_quality\">ഓഡിയോ നിലവാരം</string>\n    <string name=\"audio_quality_auto\">Auto</string>\n    <string name=\"audio_quality_high\">കൂടി</string>\n    <string name=\"audio_quality_low\">കുറഞ്ഞ്</string>\n    <string name=\"persistent_queue\">പെർസിസ്റ്റന്റ് കീ</string>\n    <string name=\"equalizer\">ഇക്വലൈസർ</string>\n    <string name=\"unlimited\">പരിധിയില്ലാത്ത</string>\n    <string name=\"clear_all_downloads\">ഡൗൺലോഡുകൾ എല്ലാം നീക്കം ചെയ്യുക</string>\n    <string name=\"privacy\">സ്വകാര്യത</string>\n    <string name=\"pause_search_history\">തിരയൽ ചരിത്രം താൽക്കാലികമായി നിർത്തുക</string>\n    <string name=\"clear_search_history\">തിരയൽ ചരിത്രം മായ്‌ക്കുക</string>\n    <string name=\"clear_search_history_confirm\">എല്ലാ തിരയൽ ചരിത്രവും മായ്‌ക്കണമെന്ന് ഉറപ്പാണോ?</string>\n    <string name=\"backup_restore\">ബാക്കപ്പും വീണ്ടെടുക്കലും</string>\n    <string name=\"action_backup\">ബാക്കപ്പ്</string>\n    <string name=\"action_restore\">വീണ്ടെടുക്കൽ</string>\n    <string name=\"backup_create_success\">ബാക്കപ്പ് സൃഷ്‌ടിച്ചു</string>\n    <string name=\"backup_create_failed\">ബാക്കപ്പ് സൃഷ്ടിക്കാൻ കഴിഞ്ഞില്ല</string>\n    <string name=\"restore_failed\">ബാക്കപ്പ് പുനഃസ്ഥാപിക്കാൻ കഴിഞ്ഞില്ല</string>\n    <string name=\"about\">കുറിച്ച്</string>\n    <string name=\"app_version\">അപ്ലിക്കേഷൻ പതിപ്പ്</string>\n    <string name=\"use_login_for_browse_desc\">ഇത് നിങ്ങൾ കാണുന്ന ഉള്ളടക്കത്തെ സ്വാധീനിക്കും, ഉദാഹരണത്തിന് നിങ്ങൾ ഒരു പ്രീമിയം അക്കൗണ്ട് ഉപയോഗിച്ച് ലോഗിൻ ചെയ്തിട്ടുണ്ടെങ്കിൽ പ്രീമിയം-മാത്രം ആൽബങ്ങൾ കാണിക്കുന്നു</string>\n    <string name=\"forgotten_favorites\">മറന്നുപോയ പ്രിയപ്പെട്ടവ</string>\n    <string name=\"keep_listening\">കേള്‍ക്കുന്നത് തുടരൂ</string>\n    <string name=\"your_youtube_playlists\">നിങ്ങളുടെ യൂട്യൂബ് പ്ലേലിസ്റ്റുകൾ</string>\n    <string name=\"action_remove_like_all\">Like എല്ലാം നീക്കം ചെയ്യുക</string>\n    <string name=\"player\">മ്യൂസിക് പ്ലെയർ</string>\n    <string name=\"auto_load_more_desc\">സാധ്യമെങ്കിൽ, ക്യൂവിന്റെ അവസാനം എത്തുമ്പോൾ കൂടുതൽ പാട്ടുകൾ യാന്ത്രികമായി ചേർക്കുക</string>\n    <string name=\"search_history\">തിരയൽ ചരിത്രം</string>\n    <string name=\"skip_duplicates\">പകര്‍പ്പുകള്‍ ഒഴിവാക്കുക</string>\n    <string name=\"options\">ഓപ്ഷനുകൾ</string>\n    <string name=\"disable_screenshot_desc\">ഈ ഓപ്ഷൻ ഓണായിരിക്കുമ്പോൾ, സ്ക്രീൻഷോട്ടുകളും സമീപകാലങ്ങളിലെ ആപ്പിന്റെ കാഴ്ചയും പ്രവർത്തനരഹിതമാകും.</string>\n    <string name=\"default_\">സ്ഥിരസ്ഥിതി</string>\n    <string name=\"delete_playlist_confirm\">\\\"%s\\\" എന്ന പ്ലേലിസ്റ്റ് ഡിലീറ്റ് ചെയ്യാന്‍ നിങ്ങൾ ശരിക്കും ആഗ്രഹിക്കുന്നുണ്ടോ?</string>\n    <string name=\"persistent_queue_desc\">ആപ്പ് പുനരാരംഭിക്കുമ്പോൾ നിങ്ങളുടെ അവസാന ക്യൂ പുനഃസ്ഥാപിക്കുക</string>\n    <string name=\"library_song_empty\">ലൈബ്രറി ഗാനങ്ങൾ ഇവിടെ കാണിക്കും</string>\n    <string name=\"library_artist_empty\">ലൈബ്രറി ആർട്ടിസ്റ്റുകൾ ഇവിടെ കാണിക്കും</string>\n    <string name=\"library_album_empty\">ലൈബ്രറി ആൽബങ്ങൾ ഇവിടെ കാണിക്കും</string>\n    <string name=\"library_playlist_empty\">നിങ്ങളുടെ പ്ലേലിസ്റ്റുകൾ ഇവിടെ കാണിക്കും</string>\n    <string name=\"other_versions\">മറ്റ് പതിപ്പുകൾ</string>\n    <string name=\"remove_download_playlist_confirm\">ഡൗൺലോഡ് ചെയ്ത പാട്ടുകളുടെ സംഭരണത്തിൽ നിന്ന് എല്ലാ \\\"%s\\\" പ്ലേലിസ്റ്റ് ഗാനങ്ങളും ഡിലീറ്റ് ചെയ്യണോ?</string>\n    <string name=\"add_all_to_library\">എല്ലാം ലൈബ്രറിയിലേക്ക് ചേർക്കുക</string>\n    <string name=\"remove_all_from_library\">ലൈബ്രറിയിൽ നിന്ന് എല്ലാം നീക്കം ചെയ്യുക</string>\n    <string name=\"remove_from_playlist\">പ്ലേലിസ്റ്റിൽ നിന്ന് നീക്കം ചെയ്യുക</string>\n    <string name=\"remove_from_queue\">ക്യൂവിൽ നിന്ന് നീക്കം ചെയ്യുക</string>\n    <string name=\"tempo_and_pitch\">ടെമ്പോയും പിച്ചും</string>\n    <string name=\"duplicates\">പകര്‍പ്പുകള്‍</string>\n    <string name=\"add_anyway\">എന്തായാലും ചേര്‍ക്കുക</string>\n    <string name=\"duplicates_description_single\">ആ പാട്ട് നിങ്ങളുടെ പ്ലേലിസ്റ്റിൽ ഉണ്ട്</string>\n    <string name=\"duplicates_description_multiple\">%d പാട്ടുകള്‍ നിങ്ങളുടെ പ്ലേലിസ്റ്റിൽ ഇതിനകം ഉണ്ട്</string>\n    <string name=\"action_like_all\">എല്ലാം like ചെയ്യുക</string>\n    <string name=\"theme\">തീം</string>\n    <string name=\"player_text_alignment\">പ്ലെയർ ടെക്സ്റ്റ് വിന്യാസം</string>\n    <string name=\"sided\">വശത്തേക്ക്</string>\n    <string name=\"player_slider_style\">പ്ലെയർ സ്ലൈഡർ ശൈലി</string>\n    <string name=\"squiggly\">ഞെരുങ്ങി</string>\n    <string name=\"misc\">പലവക</string>\n    <string name=\"grid_cell_size\">ഗ്രിഡ് സെൽ വലുപ്പം</string>\n    <string name=\"small\">ചെറുത്</string>\n    <string name=\"big\">വലുത്</string>\n    <string name=\"not_logged_in\">Login ചെയ്തിട്ടില്ല</string>\n    <string name=\"queue\">ക്യൂ</string>\n    <string name=\"auto_load_more\">കൂടുതൽ പാട്ടുകൾ സ്വയമേവ ലോഡ് ചെയ്യുക</string>\n    <string name=\"auto_skip_next_on_error\">പിശക് സംഭവിക്കുമ്പോൾ അടുത്ത പാട്ടിലേക്ക് യാന്ത്രികമായി പോകുക</string>\n    <string name=\"auto_skip_next_on_error_desc\">തുടർച്ചയായ പ്ലേബാക്ക് അനുഭവം ഉറപ്പാക്കുക</string>\n    <string name=\"stop_music_on_task_clear\">ടാസ്‌ക് ക്ലിയറാകുമ്പോൾ സംഗീതം നിർത്തുക</string>\n    <string name=\"disable_screenshot\">സ്ക്രീൻഷോട്ട് പ്രവർത്തനരഹിതമാക്കുക</string>\n    <string name=\"hide_explicit\">അശ്ലീല ഉള്ളടക്കം മറയ്ക്കുക</string>\n    <string name=\"dismiss\">പിരിച്ചുവിടുക</string>\n    <string name=\"preview\">പ്രിവ്യൂ</string>\n    <string name=\"login_failed\">Login പരാജയപ്പെട്ടു</string>\n    <string name=\"action_logout\">Logout</string>\n    <string name=\"discord_information\">നിങ്ങളുടെ Discord അക്കൗണ്ടിന്റെ സ്റ്റാറ്റസ് സജ്ജീകരിക്കാൻ Metrolist KizzyRPC ലൈബ്രറി ഉപയോഗിക്കുന്നു. ഇതിൽ Discord ഗേറ്റ്‌വേ കണക്ഷൻ ഉപയോഗിക്കുന്നത് ഉൾപ്പെടുന്നു, ഇത് Discord-ന്റെ TOS-ന്റെ ലംഘനമായി കണക്കാക്കാം. എന്നിരുന്നാലും, ഈ കാരണത്താൽ ഉപയോക്തൃ അക്കൗണ്ടുകൾ താൽക്കാലികമായി നിർത്തിവച്ചതായി അറിയപ്പെടുന്ന കേസുകളൊന്നുമില്ല. നിങ്ങളുടെ സ്വന്തം ഉത്തരവാദിത്തത്തിൽ ഉപയോഗിക്കുക.\\n\\nMetrolist നിങ്ങളുടെ ടോക്കൺ മാത്രമേ എക്‌സ്‌ട്രാക്‌റ്റ് ചെയ്യുകയുള്ളൂ, മറ്റെല്ലാം പ്രാദേശികമായി സംഭരിക്കപ്പെടും.</string>\n    <string name=\"history\">ചരിത്രം</string>\n    <string name=\"stats\">സ്റ്റാറ്റസ്</string>\n    <string name=\"mood_and_genres\">സംഗീത വിഭാഗങ്ങൾ</string>\n    <string name=\"account\">അക്കൗണ്ട്</string>\n    <string name=\"quick_picks\">തിരഞ്ഞെടുത്തവ</string>\n    <string name=\"quick_picks_empty\">ക്വിക്ക് പിക്‌സ് ജനറേറ്റ് ചെയ്യാൻ വേണ്ടി പാട്ടുകൾ കേൾക്കുക</string>\n    <string name=\"similar_to\">സമാനമായത്</string>\n    <string name=\"new_release_albums\">പുതുതായി റിലീസ് ആയ ആൽബങ്ങൾ</string>\n    <string name=\"today\">ഇന്ന്</string>\n    <string name=\"yesterday\">ഇന്നലെ</string>\n    <string name=\"this_week\">ഈ ആഴ്ച</string>\n    <string name=\"last_week\">കഴിഞ്ഞ ആഴ്ച</string>\n    <string name=\"most_played_songs\">കൂടുതൽ കേട്ട പാട്ടുകൾ</string>\n    <string name=\"most_played_artists\">കൂടുതൽ കേട്ട കലാകാരൻമാർ</string>\n    <string name=\"most_played_albums\">കൂടുതൽ കേട്ട ആൽബങ്ങൾ</string>\n    <string name=\"filter_library\">ലൈബ്രറി</string>\n    <string name=\"filter_liked\">ഇഷ്ടപ്പെട്ടത്</string>\n    <string name=\"filter_downloaded\">ഡൌൺലോഡ് ചെയ്തത്</string>\n    <string name=\"filter_bookmarked\">മാർക്ക് ചെയ്തവ</string>\n    <string name=\"no_results_found\">ഒന്നും കണ്ടെത്താനായില്ല</string>\n    <string name=\"playlist_is_empty\">ഇതിൽ ഒന്നുമില്ല</string>\n    <string name=\"reset\">റീസെറ്റ്</string>\n    <string name=\"details\">വിശദാംശങ്ങൾ</string>\n    <string name=\"remove_from_library\">ലൈബ്രറിയിൽ നിന്ന് കളയുക</string>\n    <string name=\"remove_from_history\">ചരിത്രത്തിൽ നിന്ന് കളയുക</string>\n    <string name=\"downloading\">ഡൌൺലോഡ് ആകുന്നു</string>\n    <string name=\"search_online\">ഓൺലൈനിൽ തിരയുക</string>\n    <string name=\"action_sync\">സിങ്ക് ചെയ്യുക</string>\n    <string name=\"advanced\">കൂടുതൽ കാര്യങ്ങൾ</string>\n    <string name=\"sort_by_play_time\">കളി സമയം</string>\n    <string name=\"sort_by_custom\">ഇഷ്ടാനുസൃത ക്രമം</string>\n    <string name=\"media_id\">മീഡിയ ഐഡി</string>\n    <string name=\"mime_type\">MIME തരം</string>\n    <string name=\"codecs\">കോഡക്സ്</string>\n    <string name=\"bitrate\">ബിറ്റ് റേറ്റ്</string>\n    <string name=\"sample_rate\">സാമ്പിൾ റേറ്റ്</string>\n    <string name=\"loudness\">ശബ്ദ തീവ്രത</string>\n    <string name=\"volume\">ശബ്ദം</string>\n    <string name=\"file_size\">ഫയലിന്റെ വലിപ്പം</string>\n    <string name=\"unknown\">അറിയാത്തവ</string>\n    <string name=\"edit_lyrics\">വരികൾ മാറ്റം വരുത്തുക</string>\n    <string name=\"search_lyrics\">വരികൾ തിരയുക</string>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d ആഴ്ച</item>\n        <item quantity=\"other\">%d ആഴ്ചകൾ</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d മാസം</item>\n        <item quantity=\"other\">%d മാസങ്ങൾ</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d വർഷം</item>\n        <item quantity=\"other\">%d വർഷങ്ങൾ</item>\n    </plurals>\n    <string name=\"removed_song_from_playlist\">പ്ലേയ്‌ലിസ്റ്റിൽ നിന്ന് \\\"%s\\\" കളയുക</string>\n    <string name=\"playlist_synced\">പ്ലേലിസ്റ്റ് സിങ്ക് ചെയ്തു</string>\n    <string name=\"undo\">പഴയപടിയാക്കുക</string>\n    <string name=\"lyrics_not_found\">വരികൾ കണ്ടുപിടിക്കാനായില്ല</string>\n    <string name=\"sleep_timer\">ഉറക്കസമയം</string>\n    <string name=\"end_of_song\">പാട്ടിന്റെ അവസാനം</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">1 മിനിറ്റ്</item>\n        <item quantity=\"other\">%d മിനിറ്റുകൾ</item>\n    </plurals>\n    <string name=\"error_no_stream\">ഒരു സ്ട്രീമും ലഭ്യമല്ല</string>\n    <string name=\"error_no_internet\">നെറ്റ്‌വർക്ക് കണക്ഷൻ ഇല്ല</string>\n    <string name=\"error_timeout\">ടൈം ഔട്ട്</string>\n    <string name=\"error_unknown\">എന്തോ പ്രെശ്നം പറ്റി</string>\n    <string name=\"action_like\">ലൈക്ക്</string>\n    <string name=\"action_remove_like\">ലൈക്ക് കളയുക</string>\n    <string name=\"action_shuffle_on\">ഷഫിൾ ഓൺ</string>\n    <string name=\"action_shuffle_off\">ഷഫിൾ ഓഫ്</string>\n    <string name=\"repeat_mode_off\">റിപീറ്റ് മോഡ് ഓഫ്</string>\n    <string name=\"repeat_mode_one\">ഈ പാട്ട് റിപീറ്റ് ചെയ്യുക</string>\n    <string name=\"repeat_mode_all\">ക്യൂ റിപീറ്റ് ചെയ്യുക</string>\n    <string name=\"queue_all_songs\">എല്ലാ പാട്ടുകളും</string>\n    <string name=\"queue_searched_songs\">തിരഞ്ഞ പാട്ടുകൾ</string>\n    <string name=\"enable_dynamic_theme\">ഡൈനാമിക് തീം ഓൺ ആക്കുക</string>\n    <string name=\"pure_black\">ശുദ്ധ കറുപ്പ്</string>\n    <string name=\"customize_navigation_tabs\">നാവിഗേഷൻ ടാബിൽ മാറ്റം വരുത്തുക</string>\n    <string name=\"action_login\">ലോഗ് ഇൻ</string>\n    <string name=\"login\">ലോഗിൻ</string>\n    <string name=\"skip_silence\">നിശബ്ദത ഒഴിവാക്കുക</string>\n    <string name=\"audio_normalization\">ശബ്ദ സാധാരണവൽക്കരണം</string>\n    <string name=\"storage\">സ്റ്റോറേജ്</string>\n    <string name=\"cache\">കാച്ഛ്</string>\n    <string name=\"image_cache\">ചിത്രത്തിന്റെ കാച്ഛ്</string>\n    <string name=\"song_cache\">പാട്ടിന്റെ കാച്ഛ്</string>\n    <string name=\"max_cache_size\">പരമാവധി കാച്ഛ് വലുപ്പം</string>\n    <string name=\"max_image_cache_size\">പരമാവധി ചിത്രത്തിന്റെ കാച്ഛ് വലുപ്പം</string>\n    <string name=\"clear_image_cache\">ചിത്രത്തിന്റെ കാച്ഛ് കളയുക</string>\n    <string name=\"max_song_cache_size\">പരമാവധി പാട്ടിന്റെ കാച്ഛ് വലുപ്പം</string>\n    <string name=\"clear_song_cache\">പാട്ടിന്റെ കാച്ഛ് കളയുക</string>\n    <string name=\"size_used\">%s ഉപയോഗിച്ചു</string>\n    <string name=\"listen_history\">കേട്ടതിന്റെ ചരിത്രം</string>\n    <string name=\"pause_listen_history\">കേട്ടതിന്റെ ചരിത്രം നിർത്തുക</string>\n    <string name=\"clear_listen_history\">കേട്ടതിന്റെ ചരിത്രം കളയുക</string>\n    <string name=\"clear_listen_history_confirm\">എല്ലാ കേട്ട ചരിത്രവും കളയണമെന്ന് നിങ്ങൾക്ക് ഉറപ്പാണോ?</string>\n    <string name=\"use_login_for_browse\">കണ്ടെന്റ് കാണുവാൻ ലോഗിൻ ചെയ്യുക</string>\n    <string name=\"enable_lrclib\">LrcLib വരികൾ ഓൺ ആക്കുക</string>\n    <string name=\"enable_kugou\">KuGou വരികൾ ഓൺ ആക്കുക</string>\n    <string name=\"imported_playlist\">കൊണ്ടുവന്ന പ്ലേലിസ്റ്റ്</string>\n    <string name=\"discord_integration\">Discord യോജിപ്പിക്കുക</string>\n    <string name=\"enable_discord_rpc\">റിച്ഛ് പ്രെസെൻസ് ഓൺ ആക്കുക</string>\n    <string name=\"new_version_available\">പുതിയ വേർഷൻ ലഭ്യമാണ്</string>\n    <string name=\"translation_models\">വിവർത്തന പദ്ധതികൾ</string>\n    <string name=\"clear_translation_models\">വിവർത്തന പദ്ധതികൾ കളയുക</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-ms/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">Lokal</string>\n    <string name=\"remote_history\">Jauh</string>\n    <string name=\"charts\">Carta</string>\n    <string name=\"back_button_desc\">Kembali</string>\n    <string name=\"album_cover_desc\">Muka album</string>\n    <string name=\"top_music_videos\">Video muzik teratas</string>\n    <string name=\"trending\">Trending</string>\n    <string name=\"weeks\">Mingguan</string>\n    <string name=\"months\">Bulanan</string>\n    <string name=\"years\">Tahunan</string>\n    <string name=\"continuous\">Bersambungan</string>\n    <string name=\"liked\">Disukai</string>\n    <string name=\"offline\">Dimuat turun</string>\n    <string name=\"my_top\">Teratas saya</string>\n    <string name=\"cached_playlist\">Simpanan sementara</string>\n    <string name=\"uploaded_playlist\">Dimuat naik</string>\n    <string name=\"filter_uploaded\">Dimuat naik</string>\n    <string name=\"sync_playlist\">Segerak senarai main</string>\n    <string name=\"sync_disabled\">Senarai main ditutup</string>\n    <string name=\"allows_for_sync_witch_youtube\">Nota: Ia membolehkan penyegerakkan dengan YouTube Music. Ia TIDAK boleh ditukar kelak.</string>\n    <string name=\"generating_image\">Menjana imej</string>\n    <string name=\"please_wait\">Sila tunggu</string>\n    <string name=\"cancel\">Batal</string>\n    <string name=\"share_lyrics\">Kongsi lirik</string>\n    <string name=\"share_as_text\">Kongsi sebagai teks</string>\n    <string name=\"share_as_image\">Kongsi sebagai imej</string>\n    <string name=\"max_selection_limit\">Had pilihan maksimum</string>\n    <string name=\"share_selected\">Kongsi yang dipilih</string>\n    <string name=\"customize_colors\">Sesuaikan warna</string>\n    <string name=\"text_color\">Warna teks</string>\n    <string name=\"secondary_text_color\">Warna kedua teks</string>\n    <string name=\"background_color\">Warna latar</string>\n    <string name=\"remove_from_cache\">Dibuang dari simpanan sementara</string>\n    <string name=\"download_playlist_desc\">Muat turun semua lagu untuk dimainkan di luar talian</string>\n    <string name=\"remove_download_playlist_desc\">Buang semua lagu yang dimuat turun dari senarai main</string>\n    <string name=\"download_in_progress_desc\">Memuat turun</string>\n    <string name=\"share_playlist_desc\">Kongsi senarai main dengan yang lain</string>\n    <string name=\"delete_playlist_desc\">Buang senarai main selamanya</string>\n    <string name=\"sync_playlist_desc\">Segerakkan senarai main dengan YouTube Music</string>\n    <string name=\"copy_link\">Salin pautan</string>\n    <string name=\"select\">Pilih semua</string>\n    <string name=\"like_all\">Suka semua</string>\n    <string name=\"dislike_all\">Nyahsuka semua</string>\n    <string name=\"sort_by_last_updated\">Tarikh dikemaskini</string>\n    <string name=\"link_copied\">Pautan disalin ke clipboard</string>\n    <string name=\"starting_radio\">Memulakan radio</string>\n    <string name=\"now_playing\">Dimainkan Sekarang</string>\n    <string name=\"lyrics\">Lirik</string>\n    <string name=\"close\">Tutup</string>\n    <string name=\"hide_player_thumbnail\">Sorok Muka Pemain</string>\n    <string name=\"hide_player_thumbnail_desc\">Tukar rajah album dengan logo aplikasi pada pemain</string>\n    <string name=\"already_in_playlist\">Yang ada di dalam senarai main:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"other\">%d kali</item>\n    </plurals>\n    <string name=\"seek_forward_dynamic\">%1$d saat ke depan</string>\n    <string name=\"seek_backward_dynamic\">%1$d saat ke belakang</string>\n    <string name=\"seek_seconds_addup\">Langkau secara progresif</string>\n    <string name=\"seek_seconds_addup_description\">Jika dibuka, 5 saat tambahan akan ditambah pada setiap langkauan progresif (progressive seek)</string>\n    <string name=\"similar_content\">Kandungan seerti</string>\n    <string name=\"player_background_style\">Rupa latar pemain</string>\n    <string name=\"follow_theme\">Ikut tema</string>\n    <string name=\"gradient\">Kecerunan</string>\n    <string name=\"new_player_design\">Reka bentuk pemain baru</string>\n    <string name=\"new_mini_player_design\">Reka bentuk pemain mini baharu</string>\n    <string name=\"player_background_blur\">Kabur</string>\n    <string name=\"player_buttons_style\">Warna butang pemain</string>\n    <string name=\"default_style\">Asal</string>\n    <string name=\"primary_color_style\">Warna utama</string>\n    <string name=\"tertiary_color_style\">Warna tertiari</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-nb-rNO/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"cancel\">Avbryt</string>\n    <string name=\"share_as_text\">Del som tekst</string>\n    <string name=\"share_as_image\">Del som bilde</string>\n    <string name=\"link_copied\">Lenke kopiert til utklippstavlen</string>\n    <string name=\"token_hidden\">Klikk for å vise tokenen</string>\n    <string name=\"enable_swipe_thumbnail\">Sveip for å bytte sanger</string>\n    <string name=\"token_adv_login_description\">Dette er en AVANSERT innloggingsmetode. Som alternativ til nettportalen, kan du direkte legge inn eller oppdatere innloggingstokenen din her. Dette kan f.eks. gjøre innlogging på flere enheter raskere. NB: Ugyldige tokenformater som appen ikke lykkes til i å tolke, vil ikke godtas</string>\n    <string name=\"lyrics\">Sangtekster</string>\n    <string name=\"allows_for_sync_witch_youtube\">NB: Dette tillater synkronisering med YouTube Music. Dette kan ikke endres senere.</string>\n    <string name=\"share_lyrics\">Del sangtekster</string>\n    <string name=\"similar_content\">Lignende innhold</string>\n    <string name=\"local_history\">Lokal</string>\n    <string name=\"sync_disabled\">Synkronisering deaktivert</string>\n    <string name=\"remote_history\">Fjern</string>\n    <string name=\"charts\">Hitlister</string>\n    <string name=\"back_button_desc\">Tilbake</string>\n    <string name=\"album_cover_desc\">Albumomslag</string>\n    <string name=\"top_music_videos\">Topp musikkvideoer</string>\n    <string name=\"trending\">Trendende</string>\n    <string name=\"weeks\">Uker</string>\n    <string name=\"months\">Måneder</string>\n    <string name=\"years\">År</string>\n    <string name=\"continuous\">Kontinuerlig</string>\n    <string name=\"offline\">Nedlastet</string>\n    <string name=\"liked\">Likt</string>\n    <string name=\"my_top\">Mine topp</string>\n    <string name=\"cached_playlist\">Hurtiglagret</string>\n    <string name=\"sync_playlist\">Synkroniser spilleliste</string>\n    <string name=\"generating_image\">Skaper bilde</string>\n    <string name=\"please_wait\">Vennligst vent</string>\n    <string name=\"max_selection_limit\">Øvre grense for utvalg</string>\n    <string name=\"share_selected\">Del valgte</string>\n    <string name=\"customize_colors\">Tilpass farger</string>\n    <string name=\"text_color\">Tekstfarge</string>\n    <string name=\"secondary_text_color\">Sekundær tekstfarge</string>\n    <string name=\"background_color\">Bakgrunnsfarge</string>\n    <string name=\"remove_from_cache\">Fjern fra hurtiglagring</string>\n    <string name=\"copy_link\">Kopier lenke</string>\n    <string name=\"select\">Velg alt</string>\n    <string name=\"like_all\">Lik alt</string>\n    <string name=\"dislike_all\">Mislik alt</string>\n    <string name=\"sort_by_last_updated\">Dato oppdatert</string>\n    <string name=\"already_in_playlist\">Allerede i spilleliste:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d gang</item>\n        <item quantity=\"other\">%d ganger</item>\n    </plurals>\n    <string name=\"player_background_style\">Bakgrunnsstil for avspilleren</string>\n    <string name=\"follow_theme\">Følg tema</string>\n    <string name=\"gradient\">Gradient</string>\n    <string name=\"player_background_blur\">Uskarp</string>\n    <string name=\"player_buttons_style\">Farger på avspillerknapper</string>\n    <string name=\"default_style\">Standard</string>\n    <string name=\"swipe_song_to_add\">Sveip sangen til venstre for å legge den til i køen, eller til venstre for å legge til neste</string>\n    <string name=\"lyrics_click_change\">Endre sangtekster ved klikk</string>\n    <string name=\"slim\">Smal</string>\n    <string name=\"slim_navbar\">Skjul etiketter på bunnfeltsnavigasjonen</string>\n    <string name=\"auto_playlists\">Automatiske spillelister</string>\n    <string name=\"show_liked_playlist\">Vis «Likt»-spilleliste</string>\n    <string name=\"show_downloaded_playlist\">Vis «Nedlastet»-spilleliste</string>\n    <string name=\"show_top_playlist\">Vis «Topp»-spilleliste</string>\n    <string name=\"show_cached_playlist\">Vis «Hurtiglagret»-spilleliste</string>\n    <string name=\"advanced_login\">Avansert innlogging (med token)</string>\n    <string name=\"token_shown\">Klikk igjen for å kopiere eller redigere tokenen</string>\n    <string name=\"general\">Alminnelig</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"default_lib_chips\">Endre standardvisning i biblioteket</string>\n    <string name=\"set_quick_picks\">Still inn hurtigvalg</string>\n    <string name=\"last_song_listened\">Basert på siste sang spilt av</string>\n    <string name=\"app_language\">Appspråk</string>\n    <string name=\"uploaded_playlist\">Opplastet</string>\n    <string name=\"filter_uploaded\">Opplastet</string>\n    <string name=\"about_artist\">Om artisten</string>\n    <string name=\"show_more\">Vis mer</string>\n    <string name=\"show_less\">Vis mindre</string>\n    <string name=\"artist_page_settings\">Artistside</string>\n    <string name=\"show_artist_description\">Vis artistbeskrivelse</string>\n    <string name=\"show_artist_subscriber_count\">Vis antall abonnementer</string>\n    <string name=\"show_artist_monthly_listeners\">Vis månedlige lyttere</string>\n    <string name=\"download_playlist_desc\">Last ned alle sanger for lytting uten nett</string>\n    <string name=\"remove_download_playlist_desc\">Fjern alle nedlastede sanger fra denne spillelisten</string>\n    <string name=\"download_in_progress_desc\">Nedlasting er underveis</string>\n    <string name=\"share_playlist_desc\">Del denne spillelisten med andre</string>\n    <string name=\"delete_playlist_desc\">Fjern denne spillelisten permanent</string>\n    <string name=\"sync_playlist_desc\">Synkroniser spilleliste med YouTube Music</string>\n    <string name=\"starting_radio\">Starter radio</string>\n    <string name=\"now_playing\">Spiller nå</string>\n    <string name=\"close\">Lukk</string>\n    <string name=\"hide_player_thumbnail\">Vis miniatyrbilde i spilleren</string>\n    <string name=\"hide_player_thumbnail_desc\">Erstatt albumcover med applogo i spilleren</string>\n    <string name=\"crop_album_art\">Beskjær albumcover</string>\n    <string name=\"crop_album_art_desc\">Tving et kvadratisk visningsforhold ved å beskjære miniatyrbilder av videoer</string>\n    <string name=\"seek_forward_dynamic\">%1$d sekunder forover</string>\n    <string name=\"seek_backward_dynamic\">-%1$d sekunder bakover</string>\n    <string name=\"seek_seconds_addup\">Progressiv spoling</string>\n    <string name=\"seek_seconds_addup_description\">Dersom aktivert legges 5 ekstra sekunder til inkrementalt for hvert hopp</string>\n    <string name=\"new_player_design\">Nytt spillerdesign</string>\n    <string name=\"new_mini_player_design\">Ny minispillerdesign</string>\n    <string name=\"primary_color_style\">Primær farge</string>\n    <string name=\"tertiary_color_style\">Tertiær farge</string>\n    <string name=\"wavy\">Bølgete</string>\n    <string name=\"swipe_song_to_remove\">Sveip sangen for å fjerne den fra spillelisten</string>\n    <string name=\"lyrics_auto_scroll\">Automatisk rullende tekst</string>\n    <string name=\"lyrics_glow_effect\">Aktiver glødende tekst-effekt</string>\n    <string name=\"lyrics_glow_effect_desc\">Legg til glødende animasjon og sprett-effekt til gjeldende sangtekst</string>\n    <string name=\"enable_better_lyrics\">Aktiver Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">Bruk Better Lyrics-tjeneren til ord-for-ord sangtekster</string>\n    <string name=\"enable_simpmusic\">Aktiver SimpMusic sangtekster</string>\n    <string name=\"enable_simpmusic_desc\">Bruk SimpMusic Lyrics-tjeneren for synkroniserte sangtekster</string>\n    <string name=\"auto_scroll\">Re-synk</string>\n    <string name=\"show_uploaded_playlist\">Vis \\\"Opplastet\\\" spilleliste</string>\n    <string name=\"shuffle_playlist_first\">Shuffle spilleliste/album først</string>\n    <string name=\"shuffle_playlist_first_desc\">Når du shuffler, spill alle sanger fra den originale spillelisten/albumet først, så lignende innhold</string>\n    <string name=\"show_wrapped_card\">Vis Wrapped-kort</string>\n    <string name=\"skip_silence_desc\">Hopp forbi stille deler av sanger</string>\n    <string name=\"skip_silence_instant\">Hopp over stillhet øyeblikkelig</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-nb-rNO/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"history\">Historie</string>\n    <string name=\"home\">Hjem</string>\n    <string name=\"songs\">Sanger</string>\n    <string name=\"albums\">Album</string>\n    <string name=\"playlists\">Spillelister</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d valgt</item>\n        <item quantity=\"other\">%d valgt</item>\n    </plurals>\n    <string name=\"stats\">Statistikk</string>\n    <string name=\"mood_and_genres\">Stemning og sjangrer</string>\n    <string name=\"quick_picks\">Hurtigvalg</string>\n    <string name=\"quick_picks_empty\">Hør på sanger for å generere hurtigvalgene dine</string>\n    <string name=\"your_youtube_playlists\">YouTube-spillelistene dine</string>\n    <string name=\"similar_to\">Ligner på</string>\n    <string name=\"new_release_albums\">Nylig slupne album</string>\n    <string name=\"today\">I dag</string>\n    <string name=\"yesterday\">I går</string>\n    <string name=\"this_week\">Denne uken</string>\n    <string name=\"last_week\">Forrige uke</string>\n    <string name=\"most_played_songs\">Mest spilte sanger</string>\n    <string name=\"most_played_artists\">Mest spilte artister</string>\n    <string name=\"most_played_albums\">Mest spilte album</string>\n    <string name=\"search\">Søk</string>\n    <string name=\"search_yt_music\">Søk gjennom YouTube Music …</string>\n    <string name=\"search_library\">Søk gjennom biblioteket …</string>\n    <string name=\"filter_library\">Bibliotek</string>\n    <string name=\"filter_liked\">Likte</string>\n    <string name=\"filter_downloaded\">Nedlastede</string>\n    <string name=\"filter_all\">Alle</string>\n    <string name=\"filter_videos\">Videoer</string>\n    <string name=\"filter_albums\">Album</string>\n    <string name=\"filter_playlists\">Spillelister</string>\n    <string name=\"filter_community_playlists\">Gemenskapsspillelister</string>\n    <string name=\"filter_featured_playlists\">Utvalgte spillelister</string>\n    <string name=\"filter_bookmarked\">Bokmerkede</string>\n    <string name=\"library_playlist_empty\">Spillelistene dine skal dukke opp her</string>\n    <string name=\"from_your_library\">Fra biblioteket ditt</string>\n    <string name=\"delete_playlist_confirm\">Vil du virkelig fjerne spillelisten «%s»?</string>\n    <string name=\"retry\">Prøv på nytt</string>\n    <string name=\"radio\">Radio</string>\n    <string name=\"shuffle\">Omstokk</string>\n    <string name=\"reset\">Tilbakestill</string>\n    <string name=\"details\">Detaljer</string>\n    <string name=\"edit\">Rediger</string>\n    <string name=\"play\">Spill</string>\n    <string name=\"add_all_to_library\">Legg til alle i biblioteket</string>\n    <string name=\"remove_from_library\">Fjern fra biblioteket</string>\n    <string name=\"remove_all_from_library\">Fjern alle fra biblioteket</string>\n    <string name=\"action_download\">Last ned</string>\n    <string name=\"downloading\">Laster ned</string>\n    <string name=\"remove_download\">Fjern nedlasting</string>\n    <string name=\"import_playlist\">Importer spilleliste</string>\n    <string name=\"view_artist\">Vis artist</string>\n    <string name=\"view_album\">Vis album</string>\n    <string name=\"refetch\">Hent inn på ny</string>\n    <string name=\"share\">Del</string>\n    <string name=\"delete\">Fjern</string>\n    <string name=\"remove_from_history\">Fjern fra historikk</string>\n    <string name=\"remove_from_playlist\">Fjern fra spilleliste</string>\n    <string name=\"remove_from_queue\">Fjern fra køen</string>\n    <string name=\"search_online\">Søk på nettet</string>\n    <string name=\"action_sync\">Synkroniser</string>\n    <string name=\"advanced\">Avansert</string>\n    <string name=\"tempo_and_pitch\">Tempo og tonehøyde</string>\n    <string name=\"sort_by_create_date\">Dato tillagt</string>\n    <string name=\"sort_by_name\">Navn</string>\n    <string name=\"sort_by_artist\">Artist</string>\n    <string name=\"sort_by_year\">År</string>\n    <string name=\"sort_by_song_count\">Sangantall</string>\n    <string name=\"sort_by_length\">Lengde</string>\n    <string name=\"media_id\">Media-ID</string>\n    <string name=\"mime_type\">MIME-type</string>\n    <string name=\"codecs\">Kodeker</string>\n    <string name=\"bitrate\">Bitrate</string>\n    <string name=\"sample_rate\">Samplerate</string>\n    <string name=\"choose_playlist\">Velg spilleliste</string>\n    <string name=\"edit_playlist\">Rediger spilleliste</string>\n    <string name=\"create_playlist\">Opprett spilleliste</string>\n    <string name=\"edit_artist\">Rediger artist</string>\n    <string name=\"artist_name\">Artistnavn</string>\n    <string name=\"error_artist_name_empty\">Artistnavnet kan ikke være tomt.</string>\n    <string name=\"add_anyway\">Legg til allikevel</string>\n    <string name=\"duplicates_description_single\">Sangen er allerede i spillelisten</string>\n    <string name=\"duplicates_description_multiple\">%d sanger er allerede i spillelisten</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d sang</item>\n        <item quantity=\"other\">%d sanger</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d artist</item>\n        <item quantity=\"other\">%d artister</item>\n    </plurals>\n    <string name=\"playlist_imported\">Spilleliste importert</string>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d spilleliste</item>\n        <item quantity=\"other\">%d spillelister</item>\n    </plurals>\n    <string name=\"removed_song_from_playlist\">Fjernet «%s» fra spilleliste</string>\n    <string name=\"playlist_synced\">Spilleliste synkronisert</string>\n    <string name=\"undo\">Angre</string>\n    <string name=\"lyrics_not_found\">Sangtekster funnet ikke</string>\n    <string name=\"sleep_timer\">Søvntidsur</string>\n    <string name=\"error_no_stream\">Ingen strømmer tilgjengelig</string>\n    <string name=\"error_no_internet\">Ingen nettverktilkobling</string>\n    <string name=\"error_timeout\">Tidsavbrudd</string>\n    <string name=\"action_like\">Lik</string>\n    <string name=\"action_remove_like\">Fjern like</string>\n    <string name=\"action_remove_like_all\">Fjern alle liker</string>\n    <string name=\"repeat_mode_one\">Gjentar én sang</string>\n    <string name=\"repeat_mode_all\">Gjentar køen</string>\n    <string name=\"queue_all_songs\">Alle sanger</string>\n    <string name=\"queue_searched_songs\">Søkte sanger</string>\n    <string name=\"music_player\">Musikkspiller</string>\n    <string name=\"settings\">Innstillinger</string>\n    <string name=\"appearance\">Utseende</string>\n    <string name=\"dark_theme\">Mørk</string>\n    <string name=\"dark_theme_follow_system\">Følg systemet</string>\n    <string name=\"pure_black\">Ren svart</string>\n    <string name=\"customize_navigation_tabs\">Tilpass navigasjonsfaner</string>\n    <string name=\"player\">Spiller</string>\n    <string name=\"player_text_alignment\">Spiller-tekstjustering</string>\n    <string name=\"sided\">På siden</string>\n    <string name=\"center\">Senter</string>\n    <string name=\"right\">Høyre</string>\n    <string name=\"default_\">Forvalg</string>\n    <string name=\"misc\">Ymse</string>\n    <string name=\"default_open_tab\">Forvalgsfane</string>\n    <string name=\"grid_cell_size\">Rutenettscellestørrelse</string>\n    <string name=\"small\">Liten</string>\n    <string name=\"content\">Innhold</string>\n    <string name=\"login\">Logg inn</string>\n    <string name=\"not_logged_in\">Ikke innlogget</string>\n    <string name=\"content_language\">Forvalginnholdspråk</string>\n    <string name=\"system_default\">Systemforvalg</string>\n    <string name=\"enable_proxy\">Slå på mellomtjener</string>\n    <string name=\"proxy_type\">Type</string>\n    <string name=\"proxy_url\">Nettadresse</string>\n    <string name=\"restart_to_take_effect\">Start på ny for iføre endringer</string>\n    <string name=\"player_and_audio\">Spiller og lyd</string>\n    <string name=\"audio_quality\">Lydkvalitet</string>\n    <string name=\"audio_quality_auto\">Automatisk</string>\n    <string name=\"audio_quality_high\">Høy</string>\n    <string name=\"queue\">Kø</string>\n    <string name=\"persistent_queue\">Vedvarende kø</string>\n    <string name=\"persistent_queue_desc\">Gjenopprett køen din når programmet starter</string>\n    <string name=\"auto_load_more\">Last inn flere sanger automatisk</string>\n    <string name=\"auto_load_more_desc\">Legg til flere sanger automatisk når køens slutt er nådd, om mulig</string>\n    <string name=\"skip_silence\">Hopp over stillhet</string>\n    <string name=\"auto_skip_next_on_error_desc\">Sørger for at avspillingen er fortløpende</string>\n    <string name=\"stop_music_on_task_clear\">Stopp musikk når programmet dras bort</string>\n    <string name=\"equalizer\">Tonekontroll</string>\n    <string name=\"cache\">Hurtiglager</string>\n    <string name=\"image_cache\">Bildehurtiglager</string>\n    <string name=\"max_cache_size\">Maksimal hurtiglagringsstørrelse</string>\n    <string name=\"size_used\">%s brukt</string>\n    <string name=\"privacy\">Personvern</string>\n    <string name=\"listen_history\">Lyttehistorikk</string>\n    <string name=\"clear_search_history\">Fjern søkehistorikk</string>\n    <string name=\"disable_screenshot\">Skru av skjermavbildninger</string>\n    <string name=\"disable_screenshot_desc\">Når dette er aktivert, kan skjermavbildninger ikke utføres i programmet, og «Nylig» i programmet er avslått</string>\n    <string name=\"enable_lrclib\">Slå på LrcLib-sangteksttilbyder</string>\n    <string name=\"enable_kugou\">Slå på KuGou-sangteksttilbyder</string>\n    <string name=\"action_backup\">Sikkerhetskopier</string>\n    <string name=\"discord_integration\">Discord-integrasjon</string>\n    <string name=\"dismiss\">Avvis</string>\n    <string name=\"options\">Innstillinger</string>\n    <string name=\"login_failed\">Innlogging mislyktes</string>\n    <string name=\"enable_discord_rpc\">Slå på rik tilstedeværelse</string>\n    <string name=\"app_version\">Programversjon</string>\n    <string name=\"new_version_available\">Ny versjon tilgjengelig</string>\n    <string name=\"translation_models\">Oversettingsmodeller</string>\n    <string name=\"clear_translation_models\">Fjern oversettingsmodeller</string>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d uke</item>\n        <item quantity=\"other\">%d uker</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d måned</item>\n        <item quantity=\"other\">%d måneder</item>\n    </plurals>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">1 minutt</item>\n        <item quantity=\"other\">%d minutt</item>\n    </plurals>\n    <string name=\"artists\">Artister</string>\n    <string name=\"account\">Konto</string>\n    <string name=\"no_results_found\">Ingen treff funnet</string>\n    <string name=\"library_song_empty\">Bibliotekssanger dukker opp her</string>\n    <string name=\"library_artist_empty\">Biblioteksartister dukker opp her</string>\n    <string name=\"forgotten_favorites\">Glemte favoritter</string>\n    <string name=\"keep_listening\">Fortsett å lytte</string>\n    <string name=\"filter_songs\">Sanger</string>\n    <string name=\"filter_artists\">Artister</string>\n    <string name=\"library_album_empty\">Biblioteksalbum dukker opp her</string>\n    <string name=\"other_versions\">Andre utgaver</string>\n    <string name=\"liked_songs\">Likte sanger</string>\n    <string name=\"downloaded_songs\">Nedlastede sanger</string>\n    <string name=\"playlist_is_empty\">Spillelisten er tom</string>\n    <string name=\"remove_download_playlist_confirm\">Vil du virkelig fjerne alle «%s» spillelistesanger fra lagringen av nedlastede sanger?</string>\n    <string name=\"start_radio\">Start radio</string>\n    <string name=\"add_to_queue\">Legg til i køen</string>\n    <string name=\"add_to_library\">Legg til i biblioteket</string>\n    <string name=\"play_next\">Spill neste</string>\n    <string name=\"add_to_playlist\">Legg til i spilleliste</string>\n    <string name=\"sort_by_play_time\">Spilletid</string>\n    <string name=\"sort_by_custom\">Egendefinert rekkefølge</string>\n    <string name=\"loudness\">Lydintensitet</string>\n    <string name=\"volume\">Lydstyrke</string>\n    <string name=\"unknown\">Ukjent</string>\n    <string name=\"edit_lyrics\">Rediger sangtekster</string>\n    <string name=\"file_size\">Filstørrelse</string>\n    <string name=\"copied\">Kopiert til utklippstavlen</string>\n    <string name=\"search_lyrics\">Let etter sangtekster</string>\n    <string name=\"edit_song\">Rediger sang</string>\n    <string name=\"song_artists\">Sangartister</string>\n    <string name=\"song_title\">Sangtittel</string>\n    <string name=\"playlist_name\">Spillelistenavn</string>\n    <string name=\"error_playlist_name_empty\">Spillelistenavnet kan ikke være tomt.</string>\n    <string name=\"error_song_title_empty\">Sangtittelen kan ikke være tom.</string>\n    <string name=\"skip_duplicates\">Hopp over duplikater</string>\n    <string name=\"error_song_artist_empty\">Sangartistene kan ikke være tom.</string>\n    <string name=\"save\">Lagre</string>\n    <string name=\"duplicates\">Duplikater</string>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d album</item>\n        <item quantity=\"other\">%d album</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d år</item>\n        <item quantity=\"other\">%d år</item>\n    </plurals>\n    <string name=\"end_of_song\">Slutten av sangen</string>\n    <string name=\"error_unknown\">Ukjent feil</string>\n    <string name=\"action_like_all\">Lik alle</string>\n    <string name=\"enable_dynamic_theme\">Slå på dynamisk drakt</string>\n    <string name=\"action_shuffle_on\">Omstokking på</string>\n    <string name=\"action_shuffle_off\">Omstokking av</string>\n    <string name=\"repeat_mode_off\">Gjentagelse av</string>\n    <string name=\"theme\">Drakt</string>\n    <string name=\"dark_theme_on\">På</string>\n    <string name=\"dark_theme_off\">Av</string>\n    <string name=\"lyrics_text_position\">Sangtekstjustering</string>\n    <string name=\"left\">Venstre</string>\n    <string name=\"player_slider_style\">Spiller-glidebryterstil</string>\n    <string name=\"squiggly\">Snirklete</string>\n    <string name=\"big\">Stor</string>\n    <string name=\"content_country\">Forvalginnholdsland</string>\n    <string name=\"audio_quality_low\">Lav</string>\n    <string name=\"auto_skip_next_on_error\">Hopp til neste sang automatisk når en feil oppstår</string>\n    <string name=\"audio_normalization\">Lydnormalisering</string>\n    <string name=\"song_cache\">Sanghurtiglagring</string>\n    <string name=\"storage\">Lagring</string>\n    <string name=\"unlimited\">Ubegrenset</string>\n    <string name=\"clear_listen_history\">Fjern lyttehistorikk</string>\n    <string name=\"clear_listen_history_confirm\">Fjern lyttehistorikk?</string>\n    <string name=\"search_history\">Søkehistorikk</string>\n    <string name=\"clear_all_downloads\">Fjern alle nedlastinger</string>\n    <string name=\"max_image_cache_size\">Maksimal bildehurtiglagringsstørrelse</string>\n    <string name=\"pause_search_history\">Ikke lagre søkehistorikk</string>\n    <string name=\"clear_image_cache\">Fjern bildehurtiglager</string>\n    <string name=\"max_song_cache_size\">Maksimal sanghurtiglagerstørrelse</string>\n    <string name=\"pause_listen_history\">Ikke lagre lyttehistorikk</string>\n    <string name=\"clear_search_history_confirm\">Fjern søkehistorikken din?</string>\n    <string name=\"clear_song_cache\">Fjern sanghurtiglager</string>\n    <string name=\"hide_explicit\">Skjul eksplisitt innhold</string>\n    <string name=\"backup_restore\">Sikkerhetskopiering og gjenoppretting</string>\n    <string name=\"backup_create_success\">Sikkerhetskopi opprettet</string>\n    <string name=\"backup_create_failed\">Kunne ikke opprette sikkerhetskopi</string>\n    <string name=\"restore_failed\">Kunne ikke utføre gjenoppretting</string>\n    <string name=\"action_restore\">Gjenopprett</string>\n    <string name=\"imported_playlist\">Spilleliste importert</string>\n    <string name=\"action_logout\">Logg ut</string>\n    <string name=\"discord_information\">Metrolist bruker KizzyRPC for å stille inn statusen på Discord-kontoen din. Dette innebærer bruk ave Discord Gateway-portneren, som kan anses som et brudd på Discords tjenestevilkår. Det finnes dog ingen kjente tilfelle av suspenderte brukerkonter på denne grunnen. Bruk på egen risiko. \\n \\nMetrolist henter bare inn symbolet ditt, og alt annet blir lagret lokalt.</string>\n    <string name=\"preview\">Forhåndsvisning</string>\n    <string name=\"about\">Om</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-night/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Widget colors for dark mode -->\n    <color name=\"widget_text_primary\">#E6E1E5</color>\n    <color name=\"widget_text_secondary\">#CAC4D0</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-night/widget_colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Widget colors fallback for Android < 12 (API 31) - Dark Mode -->\n    <!-- Material 3 Primary Container (Dark) -->\n    <color name=\"widget_primary_container\">#4F378B</color>\n    <color name=\"widget_on_primary_container\">#EADDFF</color>\n\n    <!-- Material 3 Tertiary Container (Dark) - For like button -->\n    <color name=\"widget_tertiary_container\">#633B48</color>\n    <color name=\"widget_on_tertiary_container\">#FFD8E4</color>\n\n    <!-- Play button colors (Low style) -->\n    <color name=\"widget_play_button_low_bg\">@color/widget_primary_container</color>\n    <color name=\"widget_play_button_low_icon\">@color/widget_on_primary_container</color>\n\n    <!-- Music Recognizer Widget colors (Dark) -->\n    <!-- Active mic button background – lighter accent for dark theme -->\n    <color name=\"widget_mic_active_bg\">#9A82DB</color>\n    <!-- Pulse ring colors (opacity variants for outward ripple animation) -->\n    <color name=\"widget_mic_pulse_ring\">#99EADDFF</color>\n    <color name=\"widget_mic_pulse_ring_mid\">#66EADDFF</color>\n    <color name=\"widget_mic_pulse_ring_low\">#33EADDFF</color>\n    <color name=\"widget_mic_pulse_ring_fade\">#1AEADDFF</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-night-v31/widget_colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Widget colors for Android 12+ using Material 3 dynamic colors - Dark Mode -->\n    <!-- Material 3 Primary Container (Dark) -->\n    <color name=\"widget_primary_container\">@android:color/system_accent1_700</color>\n    <color name=\"widget_on_primary_container\">@android:color/system_accent1_100</color>\n\n    <!-- Material 3 Tertiary Container (Dark) - For like button -->\n    <color name=\"widget_tertiary_container\">@android:color/system_accent3_700</color>\n    <color name=\"widget_on_tertiary_container\">@android:color/system_accent3_100</color>\n\n    <!-- Play button colors (Low style) -->\n    <color name=\"widget_play_button_low_bg\">@color/widget_primary_container</color>\n    <color name=\"widget_play_button_low_icon\">@color/widget_on_primary_container</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-nl/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"charts\">Hitlijsten</string>\n    <string name=\"back_button_desc\">Terug</string>\n    <string name=\"album_cover_desc\">Albumhoes</string>\n    <string name=\"trending\">Trending</string>\n    <string name=\"weeks\">Weken</string>\n    <string name=\"months\">Maanden</string>\n    <string name=\"years\">Jaren</string>\n    <string name=\"continuous\">Voortdurend</string>\n    <string name=\"liked\">Nummers die je leuk vindt</string>\n    <string name=\"offline\">Gedownload</string>\n    <string name=\"my_top\">Meest geluisterd</string>\n    <string name=\"cached_playlist\">Gecached</string>\n    <string name=\"sync_playlist\">Playlist synchroniseren</string>\n    <string name=\"sync_disabled\">Synchroniseren uitgeschakeld</string>\n    <string name=\"allows_for_sync_witch_youtube\">Opmerking: Dit activeert synchronisatie met YouTube Music. Dit kan later NIET worden gewijzigd.</string>\n    <string name=\"generating_image\">Afbeelding genereren</string>\n    <string name=\"please_wait\">Een ogenblik geduld</string>\n    <string name=\"cancel\">Annuleren</string>\n    <string name=\"share_lyrics\">Songtekst delen</string>\n    <string name=\"share_as_text\">Delen als tekst</string>\n    <string name=\"share_as_image\">Delen als afbeelding</string>\n    <string name=\"max_selection_limit\">Max. selectie limiet</string>\n    <string name=\"share_selected\">Deel geselecteerden</string>\n    <string name=\"customize_colors\">Kleuren personaliseren</string>\n    <string name=\"text_color\">Tekstkleur</string>\n    <string name=\"secondary_text_color\">Secundaire tekstkleur</string>\n    <string name=\"background_color\">Achtergrondkleur</string>\n    <string name=\"top_music_videos\">Beste muziek videos</string>\n    <string name=\"select\">Alles selecteren</string>\n    <string name=\"like_all\">Alles leuk vinden</string>\n    <string name=\"dislike_all\">Alles niet leuk vinden</string>\n    <string name=\"sort_by_last_updated\">Datum gewijzigd</string>\n    <string name=\"link_copied\">Link gekopieerd naar het klembord</string>\n    <string name=\"lyrics\">Songtekst</string>\n    <string name=\"already_in_playlist\">Al in afspeellijst:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d keer</item>\n        <item quantity=\"other\">%d keren</item>\n    </plurals>\n    <string name=\"player_background_style\">Speler achtergrond stijl</string>\n    <string name=\"follow_theme\">Volg thema</string>\n    <string name=\"gradient\">Gradient</string>\n    <string name=\"default_style\">Standaard</string>\n    <string name=\"advanced_login\">Login met token</string>\n    <string name=\"token_hidden\">Klik om token te tonen</string>\n    <string name=\"token_shown\">Klik opnieuw om te kopiëren of te wijzigen</string>\n    <string name=\"general\">Algemeen</string>\n    <string name=\"proxy\">Proxy</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">1 seconde</item>\n        <item quantity=\"other\">%d seconden</item>\n    </plurals>\n    <string name=\"local_history\">Lokaal</string>\n    <string name=\"remote_history\">Afstandsbediening</string>\n    <string name=\"uploaded_playlist\">Geüpload</string>\n    <string name=\"filter_uploaded\">Geüpload</string>\n    <string name=\"remove_from_cache\">Verwijder van cache</string>\n    <string name=\"copy_link\">Kopieer link</string>\n    <string name=\"starting_radio\">Radio starten</string>\n    <string name=\"close\">Afsluiten</string>\n    <string name=\"similar_content\">Gelijkaardige inhoud</string>\n    <string name=\"player_background_blur\">Vervagen</string>\n    <string name=\"player_buttons_style\">Kleuren spelerknoppen</string>\n    <string name=\"enable_swipe_thumbnail\">Vegen om lied te veranderen</string>\n    <string name=\"swipe_song_to_add\">Naar links vegen om lied in de afspeellijst te zetten of naar rechts om als volgende te spelen</string>\n    <string name=\"lyrics_click_change\">Naar tekst springen bij klikken</string>\n    <string name=\"slim\">Dun</string>\n    <string name=\"slim_navbar\">Compacte hoofdnavigatiebalk</string>\n    <string name=\"auto_playlists\">Automatische playlists</string>\n    <string name=\"show_liked_playlist\">\\\"Nummers die je leuk vindt\\\" tonen</string>\n    <string name=\"show_downloaded_playlist\">\\\"Gedownload\\\" tonen</string>\n    <string name=\"show_top_playlist\">\\\"Meest geluisterd\\\" tonen</string>\n    <string name=\"show_cached_playlist\">\\\"Gecached\\\" tonen</string>\n    <string name=\"last_song_listened\">Gebaseerd op laatst geluisterd nummer</string>\n    <string name=\"app_language\">App taal</string>\n    <string name=\"enable_similar_content\">Vergelijkbare muziek aanzetten</string>\n    <string name=\"similar_content_desc\">Automatisch vergelijkbare nummers toevoegen wanneer het einde van de afspeellijst is bereikt</string>\n    <string name=\"auto_download_on_like\">Automatisch downloaden bij het liken</string>\n    <string name=\"clear_song_cache_dialog\">Weet je zeker dat je alle gecached nummers wilt wissen?</string>\n    <string name=\"clear_downloads_dialog\">Weet je zeker dat je alle downloads wilt wissen?</string>\n    <string name=\"not_logged_in_youtube\">Niet op YouTube ingelogd</string>\n    <string name=\"default_links\">Open ondersteunde links</string>\n    <string name=\"open_app_settings_error\">Kon app instellingen niet openen</string>\n    <string name=\"release_notes\">Uitgaven informatie</string>\n    <string name=\"past_24_hours\">Afgelopen 24 uur</string>\n    <string name=\"past_week\">Afgelopen week</string>\n    <string name=\"past_month\">Afgelopen maand</string>\n    <string name=\"past_year\">Afgelopen jaar</string>\n    <string name=\"top_length\">Meest geluisterd lengte</string>\n    <string name=\"history_duration\">Geschiedenis duur</string>\n    <string name=\"information\">Informatie</string>\n    <string name=\"description\">Omschrijving</string>\n    <string name=\"views\">Weergaven</string>\n    <string name=\"lyrics_auto_scroll\">Automatisch songtekst scrollen</string>\n    <string name=\"lyrics_romanize_japanese\">Japans romaniseren</string>\n    <string name=\"lyrics_romanize_korean\">Koreaans romaniseren</string>\n    <string name=\"yt_sync\">Account automatisch synchroniseren</string>\n    <string name=\"more_content\">Meer inhoud</string>\n    <string name=\"new_player_design\">Nieuw speler ontwerp</string>\n    <string name=\"swipe_sensitivity\">Mini speler swipe gevoeligheid</string>\n    <string name=\"clear_image_cache_dialog\">Weet je zeker dat je gecached afbeeldingen wilt wissen?</string>\n    <string name=\"disable\">Uitschakelen</string>\n    <string name=\"subscribe\">Abonneer</string>\n    <string name=\"subscribed\">Geabonneerd</string>\n    <string name=\"new_mini_player_design\">Nieuw mini-speler ontwerp</string>\n    <string name=\"now_playing\">Nu afspelen</string>\n    <string name=\"seek_forward_dynamic\">+%1$d seconden vooruit</string>\n    <string name=\"seek_backward_dynamic\">-%1$d seconden achteruit</string>\n    <string name=\"hide_player_thumbnail_desc\">Vervang album afbeelding met de app logo in de speler</string>\n    <string name=\"settings_section_privacy\">Privacy &amp; Beveiliging</string>\n    <string name=\"settings_section_player_content\">Speler &amp; Inhoud</string>\n    <string name=\"settings_section_storage\">Opslag &amp; Gegevens</string>\n    <string name=\"settings_section_system\">Systeem &amp; Over</string>\n    <string name=\"config_proxy\">Proxy configureren</string>\n    <string name=\"proxy_username\">Proxy gebruikersnaam</string>\n    <string name=\"proxy_password\">Proxy wachtwoord</string>\n    <string name=\"enable_authentication\">Authenticatie aanzetten</string>\n    <string name=\"lyrics_romanize_title\">Romanisatie</string>\n    <string name=\"lyrics_romanization\">Songtekst romanizatie</string>\n    <string name=\"lyrics_romanize_russian\">Russisch romaniseren</string>\n    <string name=\"lyrics_romanize_ukrainian\">Oekraïens romaniseren</string>\n    <string name=\"lyrics_romanize_belarusian\">Wit-Russisch romaniseren</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Kirgizisch romaniseren</string>\n    <string name=\"lyrics_romanize_serbian\">Servisch romaniseren</string>\n    <string name=\"lyrics_romanize_bulgarian\">Bulgaars romaniseren</string>\n    <string name=\"line_by_line_option_title\">EXPERIMENTEEL: Taal per regel detecteren</string>\n    <string name=\"line_by_line_dialog_title\">Weet je dit zeker?</string>\n    <string name=\"romanize_current_track\">Huidig nummer romaniseren</string>\n    <string name=\"edit_playlist_cover\">Afspeellijst omslag aanpassen</string>\n    <string name=\"edit_playlist_cover_note\">Opmerking: Je account moet aan een telefoonnummer gekoppeld zijn en geverifieerd zijn op YouTube Music om de afspeellijstcover te kunnen wijzigen.</string>\n    <string name=\"edit_playlist_cover_note_wait\">Aanpassen van omslag kan enkele momenten duren.</string>\n    <string name=\"choose_from_library\">Uit bibliotheek kiezen</string>\n    <string name=\"remove_custom_image\">Verwijder aangepaste afbeelding</string>\n    <string name=\"show_uploaded_playlist\">\\\"Geupload\\\" tonen</string>\n    <string name=\"lyrics_romanize_macedonian\">Macedonisch romaniseren</string>\n    <string name=\"updater\">Updateprogramma</string>\n    <string name=\"check_for_updates\">Automatisch voor updates controleren</string>\n    <string name=\"update_notifications\">Update meldingen aanzetten</string>\n    <string name=\"update_available_title\">Update beschikbaar</string>\n    <string name=\"integrations\">Integraties</string>\n    <string name=\"username\">Gebruikersnaam</string>\n    <string name=\"password\">Wachtwoord</string>\n    <string name=\"lastfm_integration\">Last.fm Integratie</string>\n    <string name=\"enable_scrobbling\">Scrobbling aanzetten</string>\n    <string name=\"scrobbling_configuration\">Scrobbling Configuratie</string>\n    <string name=\"lyrics_romanize_chinese\">Chinees romaniseren</string>\n    <string name=\"hide_video_songs\">Video nummers verbergen</string>\n    <string name=\"primary_color_style\">Primaire kleur</string>\n    <string name=\"auto_scroll\">Synchroniseren</string>\n    <string name=\"details_desc\">Nummer informatie</string>\n    <string name=\"edit_desc\">Titel of artiest veranderen</string>\n    <string name=\"add_to_library_desc\">Opslaan in bibliotheek</string>\n    <string name=\"download_desc\">Beschikbaar maken voor offline afspelen</string>\n    <string name=\"add_to_playlist_desc\">Toevoegen aan afspeellijst</string>\n    <string name=\"refetch_desc\">Opnieuw YouTube Music metadata ophalen</string>\n    <string name=\"advanced_desc\">Nummer tempo en toonhoogte aanpassen</string>\n    <string name=\"equalizer_desc\">Audio equalizer aanpassen</string>\n    <string name=\"enable_dynamic_icon\">Dynamisch app pictogram</string>\n    <string name=\"mini_player\">Mini-speler</string>\n    <string name=\"pure_black_mini_player\">OLED mini-speler</string>\n    <string name=\"tertiary_color_style\">Tertiaire kleur</string>\n    <string name=\"logging_in\">Inloggen…</string>\n    <string name=\"remove_download_playlist_desc\">Verwijder alle gedownloaden nummers van deze afspeellijst</string>\n    <string name=\"download_in_progress_desc\">Downloaden wordt uitgevoerd</string>\n    <string name=\"share_playlist_desc\">Deel afspeellijst</string>\n    <string name=\"delete_playlist_desc\">Afspeellijst verwijderen</string>\n    <string name=\"sync_playlist_desc\">Synchroniseer afspeellijst met YouTube Music</string>\n    <string name=\"lyrics_text_size\">Songtekst lettergrootte</string>\n    <string name=\"lyrics_line_spacing\">Songtekst regel afstand</string>\n    <string name=\"show_wrapped_card\">Toon Wrapped</string>\n    <string name=\"progress_percent\">Vooruitgang %s%%</string>\n    <string name=\"copied_title\">Titel gekopieerd</string>\n    <string name=\"copied_artist\">Artiest gekopieerd</string>\n    <string name=\"lyrics_glow_effect\">Gloeiende songtekst</string>\n    <string name=\"download_playlist_desc\">Alle nummers downloaden voor offline afspelen</string>\n    <string name=\"swipe_song_to_remove\">Swipe nummer om het van de afspeellijst te verwijderen</string>\n    <string name=\"about_artist\">Over</string>\n    <string name=\"show_more\">Meer tonen</string>\n    <string name=\"show_less\">Minder tonen</string>\n    <string name=\"artist_page_settings\">Artiesten pagina</string>\n    <string name=\"show_artist_description\">Artiesten beschrijving tonen</string>\n    <string name=\"show_artist_subscriber_count\">Aantal abonnees tonen</string>\n    <string name=\"show_artist_monthly_listeners\">Maandelijkse luisteraars tonen</string>\n    <string name=\"hide_player_thumbnail\">Verberg album afbeelding in speler</string>\n    <string name=\"seek_seconds_addup\">Muziek progressief doorzoeken</string>\n    <string name=\"wavy\">Golvend</string>\n    <string name=\"seek_seconds_addup_description\">voegt 5 seconden toe bij elke skip</string>\n    <string name=\"default_lib_chips\">Verander standaard bibliotheek chip</string>\n    <string name=\"import_online\">Importeer een \\\"m3u\\\" afspeellijst</string>\n    <string name=\"import_csv\">Importeer een \\\"csv\\\" afspeellijst</string>\n    <string name=\"enable_better_lyrics\">Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">Gebruik de Better Lyrics provider voor woord-voor-woord gesynchroniseerde songtekst</string>\n    <string name=\"lyrics_romanization_cyrillic\">Cyrillisch</string>\n    <string name=\"line_by_line_option_desc\">De Cyrillische taal zal lijn voor lijn gedetecteerd worden in plaats van het hele lied.</string>\n    <string name=\"set_quick_picks\">Snelle keuzes instellen</string>\n    <string name=\"play_pause\">Afspelen/Pauzeer</string>\n    <string name=\"lyrics_animation_style\">Woord-voor-woord animatie stijl</string>\n    <string name=\"lyrics_glow_effect_desc\">Voegt gloeiende animatie en stuiter effecten toe aan actieve songtekst</string>\n    <string name=\"none\">Geen</string>\n    <string name=\"fade\">Vervagen</string>\n    <string name=\"glow\">Gloei</string>\n    <string name=\"slide\">Schuiven</string>\n    <string name=\"crop_album_art\">Albumhoes bijsnijden</string>\n    <string name=\"enable_simpmusic\">SimpMusic Lyrics aanzetten</string>\n    <string name=\"remember_shuffle_and_repeat\">Behoud de shuffle- en herhaalmodus</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Behoud de shuffle- en herhaalmodus bij het opnieuw opstarten van de app</string>\n    <string name=\"crop_album_art_desc\">Dwing een vierkante beeldverhouding door videominiaturen bij te snijden</string>\n    <string name=\"enable_simpmusic_desc\">Gebruik SimpMusic songtekst provider voor gesynchroniseerde teksten</string>\n    <string name=\"skip_silence_instant\">Stilte direct overslaan</string>\n    <string name=\"skip_silence_instant_desc\">Spoel tijdens stille momenten vooruit in plaats van het afspelen te versnellen</string>\n    <string name=\"discord_use_details_description\">Toon de titel van het nummer duidelijk in plaats van de naam van de artiest</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"playlist_add_local_to_synced_note\">Opmerking: het toevoegen van lokale nummers aan gesynchroniseerde/externe afspeellijsten wordt niet ondersteund. Alle andere combinaties zijn geldig</string>\n    <string name=\"auto_download_on_like_desc\">Download automatisch nummers die je leuk vindt</string>\n    <string name=\"all_time\">Altijd</string>\n    <string name=\"likes\">Vind-ik-leuks</string>\n    <string name=\"dislikes\">Niet leuk</string>\n    <string name=\"pause_music_when_media_is_muted\">Muziek pauzeren wanneer media gedempt is</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">Houd scherm aan als mediaspeler is uitgeklapt</string>\n    <string name=\"update_channel_desc\">Meldingen over nieuwe versies</string>\n    <string name=\"google_cast\">Google Casten</string>\n    <string name=\"play_next_desc\">Voeg toe aan het begin van je wachtrij</string>\n    <string name=\"add_to_queue_desc\">Voeg toe aan het einde van je wachtrij</string>\n    <string name=\"share_desc\">Deel een link naar dit item</string>\n    <string name=\"delete_desc\">Dit item permanent verwijderen</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-nl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <string name=\"home\">Thuis</string>\n    <string name=\"songs\">Nummers</string>\n    <string name=\"artists\">Artiesten</string>\n    <string name=\"albums\">Albums</string>\n    <string name=\"playlists\">Afspeellijsten</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d geselecteerd</item>\n        <item quantity=\"other\">%d geselecteerd</item>\n    </plurals>\n    <string name=\"history\">Geschiedenis</string>\n    <string name=\"stats\">Statistieken</string>\n    <string name=\"mood_and_genres\">Stemmingen en genres</string>\n    <string name=\"account\">Account</string>\n    <string name=\"quick_picks\">Snelle keuzes</string>\n    <string name=\"quick_picks_empty\">Beluister een aantal nummers om je snelle keuzes te genereren</string>\n    <string name=\"new_release_albums\">Nieuwe albums</string>\n    <string name=\"today\">Vandaag</string>\n    <string name=\"yesterday\">Gisteren</string>\n    <string name=\"this_week\">Deze week</string>\n    <string name=\"last_week\">Vorige week</string>\n    <string name=\"most_played_songs\">Meest afgespeelde nummers</string>\n    <string name=\"most_played_artists\">Meest afgespeelde artiesten</string>\n    <string name=\"most_played_albums\">Meest afgespeelde artiesten</string>\n    <string name=\"search\">Zoeken</string>\n    <string name=\"search_yt_music\">Zoeken via YouTube Music…</string>\n    <string name=\"search_library\">Zoeken in bibliotheek…</string>\n    <string name=\"filter_library\">Bibliotheek</string>\n    <string name=\"filter_liked\">Geliked</string>\n    <string name=\"filter_downloaded\">Gedownload</string>\n    <string name=\"filter_all\">Alles</string>\n    <string name=\"filter_songs\">Nummers</string>\n    <string name=\"filter_videos\">Video\\'s</string>\n    <string name=\"filter_albums\">Albums</string>\n    <string name=\"filter_artists\">Artiesten</string>\n    <string name=\"filter_playlists\">Afspeellijsten</string>\n    <string name=\"filter_community_playlists\">Afspeellijsten van de community</string>\n    <string name=\"filter_featured_playlists\">Voorgestelde afspeellijsten</string>\n    <string name=\"filter_bookmarked\">Gebookmarked</string>\n    <string name=\"no_results_found\">Geen resultaten gevonden</string>\n    <string name=\"from_your_library\">Verwijderen van bibliotheek</string>\n    <string name=\"liked_songs\">Favoriete nummers</string>\n    <string name=\"downloaded_songs\">Gedownloade nummers</string>\n    <string name=\"playlist_is_empty\">Afspeellijst is leeg</string>\n    <string name=\"retry\">Opnieuw</string>\n    <string name=\"radio\">Radio</string>\n    <string name=\"shuffle\">Schuifelen</string>\n    <string name=\"reset\">Resetten</string>\n    <string name=\"details\">Details</string>\n    <string name=\"edit\">Bewerken</string>\n    <string name=\"start_radio\">Radio starten</string>\n    <string name=\"play\">Afspelen</string>\n    <string name=\"play_next\">Volgende afspelen</string>\n    <string name=\"add_to_queue\">Voeg toe aan wachtrij</string>\n    <string name=\"add_to_library\">Voeg toe aan bibliotheek</string>\n    <string name=\"remove_from_library\">Verwijderen van bibliotheek</string>\n    <string name=\"action_download\">Downloaden</string>\n    <string name=\"downloading\">Wordt gedownload</string>\n    <string name=\"remove_download\">Verwijder download</string>\n    <string name=\"import_playlist\">Importeer afspeellijst</string>\n    <string name=\"add_to_playlist\">Voeg toe aan afspeellijst</string>\n    <string name=\"view_artist\">Toon artiest</string>\n    <string name=\"view_album\">Toon album</string>\n    <string name=\"refetch\">Ververs</string>\n    <string name=\"share\">Delen</string>\n    <string name=\"delete\">Verwijderen</string>\n    <string name=\"remove_from_history\">Verwijder uit geschiedenis</string>\n    <string name=\"search_online\">Zoek online</string>\n    <string name=\"action_sync\">Synchroniseer</string>\n    <string name=\"advanced\">Geavanceerd</string>\n    <string name=\"sort_by_create_date\">Datum toegevoegd</string>\n    <string name=\"sort_by_name\">Naam</string>\n    <string name=\"sort_by_artist\">Artiest</string>\n    <string name=\"sort_by_year\">Jaar</string>\n    <string name=\"sort_by_song_count\">Aantal nummers</string>\n    <string name=\"sort_by_length\">Duur</string>\n    <string name=\"sort_by_play_time\">Speeltijd</string>\n    <string name=\"sort_by_custom\">Aangepaste volgorde</string>\n    <string name=\"media_id\">Media-id</string>\n    <string name=\"mime_type\">MIME-type</string>\n    <string name=\"codecs\">Codecs</string>\n    <string name=\"bitrate\">Bitsnelheid</string>\n    <string name=\"sample_rate\">Voorbeeldfrequentie</string>\n    <string name=\"loudness\">Luidheid</string>\n    <string name=\"volume\">Volume</string>\n    <string name=\"file_size\">Bestandsgrootte</string>\n    <string name=\"unknown\">Onbekend</string>\n    <string name=\"copied\">Gekopieerd naar klembord</string>\n    <string name=\"edit_lyrics\">Bewerk songtekst</string>\n    <string name=\"search_lyrics\">Zoek naar songtekst</string>\n    <string name=\"edit_song\">Bewerk nummer</string>\n    <string name=\"song_title\">Titel nummer</string>\n    <string name=\"song_artists\">Artiesten nummer</string>\n    <string name=\"error_song_title_empty\">Titel van nummer mag niet leeg zijn.</string>\n    <string name=\"error_song_artist_empty\">Artiest kan niet leeg zijn.</string>\n    <string name=\"save\">Opslaan</string>\n    <string name=\"choose_playlist\">Kies afspeellijst</string>\n    <string name=\"edit_playlist\">Bewerk afspeellijst</string>\n    <string name=\"create_playlist\">Afspeellijst maken</string>\n    <string name=\"playlist_name\">Naam van afspeellijst</string>\n    <string name=\"error_playlist_name_empty\">Afspeellijstnaam mag niet leeg zijn.</string>\n    <string name=\"edit_artist\">Bewerk artiest</string>\n    <string name=\"artist_name\">Artiest naam</string>\n    <string name=\"error_artist_name_empty\">Artiestennaam mag niet leeg zijn.</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d nummer</item>\n        <item quantity=\"other\">%d nummers</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d artiest</item>\n        <item quantity=\"other\">%d artiesten</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d album</item>\n        <item quantity=\"other\">%d albums</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d afspeellijst</item>\n        <item quantity=\"other\">%d afspeellijsten</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d week</item>\n        <item quantity=\"other\">%d weken</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d maand</item>\n        <item quantity=\"other\">%d maanden</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d jaar</item>\n        <item quantity=\"other\">%d jaren</item>\n    </plurals>\n    <string name=\"playlist_imported\">Afspeellijst geimporteerd</string>\n    <string name=\"removed_song_from_playlist\">\\\"%s\\\" van afspeelijst verwijderd</string>\n    <string name=\"playlist_synced\">Afspeellijst gesynchroniseerd</string>\n    <string name=\"undo\">Ongedaan maken</string>\n    <string name=\"lyrics_not_found\">Songtekst niet gevonden</string>\n    <string name=\"sleep_timer\">Slaap timer</string>\n    <string name=\"end_of_song\">Einde van nummer</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">1 minuut</item>\n        <item quantity=\"other\">%d minuten</item>\n    </plurals>\n    <string name=\"error_no_stream\">Geen stream beschikbaar</string>\n    <string name=\"error_no_internet\">Geen netwerkverbinding</string>\n    <string name=\"error_timeout\">Time-out</string>\n    <string name=\"error_unknown\">Onbekende fout</string>\n    <string name=\"action_like\">Leuk vinden</string>\n    <string name=\"action_remove_like\">Verwijder like</string>\n    <string name=\"action_shuffle_on\">Schuifelen aan</string>\n    <string name=\"action_shuffle_off\">Schuifelen uit</string>\n    <string name=\"repeat_mode_off\">Herhaalmodus uit</string>\n    <string name=\"repeat_mode_one\">Huidig nummer herhalen</string>\n    <string name=\"repeat_mode_all\">Wachtrij herhalen</string>\n    <string name=\"queue_all_songs\">Alle nummers</string>\n    <string name=\"queue_searched_songs\">Opgezochte nummers</string>\n    <string name=\"music_player\">Muziekspeler</string>\n    <string name=\"settings\">Instellingen</string>\n    <string name=\"appearance\">Uiterlijk</string>\n    <string name=\"enable_dynamic_theme\">Dynamisch thema inschakelen</string>\n    <string name=\"dark_theme\">Donker thema</string>\n    <string name=\"dark_theme_on\">Aan</string>\n    <string name=\"dark_theme_off\">Uit</string>\n    <string name=\"dark_theme_follow_system\">Volg systeem</string>\n    <string name=\"pure_black\">Zuiver zwart</string>\n    <string name=\"default_open_tab\">Standaard tabblad</string>\n    <string name=\"customize_navigation_tabs\">Pas tabbladen aan</string>\n    <string name=\"lyrics_text_position\">Songtekst positie</string>\n    <string name=\"left\">Links</string>\n    <string name=\"center\">Midden</string>\n    <string name=\"right\">Rechts</string>\n    <string name=\"content\">Inhoud</string>\n    <string name=\"login\">Inloggen</string>\n    <string name=\"content_language\">Standaard inhoudstaal</string>\n    <string name=\"content_country\">Standaard inhoudsland</string>\n    <string name=\"system_default\">Systeemstandaard</string>\n    <string name=\"enable_proxy\">Proxy inschakelen</string>\n    <string name=\"proxy_type\">Proxy-type</string>\n    <string name=\"proxy_url\">Proxy-URL</string>\n    <string name=\"restart_to_take_effect\">Herstart om effect te hebben</string>\n    <string name=\"player_and_audio\">Speler en geluid</string>\n    <string name=\"audio_quality\">Geluidskwaliteit</string>\n    <string name=\"audio_quality_auto\">Automatisch</string>\n    <string name=\"audio_quality_high\">Hoog</string>\n    <string name=\"audio_quality_low\">Laag</string>\n    <string name=\"persistent_queue\">Blijvende wachtrij</string>\n    <string name=\"skip_silence\">Sla sliltes over</string>\n    <string name=\"audio_normalization\">Geluidsnormalisatie</string>\n    <string name=\"equalizer\">Egalisator</string>\n    <string name=\"storage\">Opslag</string>\n    <string name=\"cache\">Cache</string>\n    <string name=\"image_cache\">Afbeeldingen Cache</string>\n    <string name=\"song_cache\">Nummer Cache</string>\n    <string name=\"max_cache_size\">Maximale cache grootte</string>\n    <string name=\"unlimited\">Ongelimiteerd</string>\n    <string name=\"clear_all_downloads\">Wis alle downloads</string>\n    <string name=\"max_image_cache_size\">Max. grootte cache voor afbeeldingen</string>\n    <string name=\"clear_image_cache\">Wis afbeeldingen cache</string>\n    <string name=\"max_song_cache_size\">Max. grootte cache voor nummers</string>\n    <string name=\"clear_song_cache\">Wis nummer cache</string>\n    <string name=\"size_used\">%s gebruikt</string>\n    <string name=\"privacy\">Privéheid</string>\n    <string name=\"pause_listen_history\">Pauzeer luistergeschiedenis</string>\n    <string name=\"clear_listen_history\">Luistergeschiedenis wissen</string>\n    <string name=\"clear_listen_history_confirm\">Weet je zeker dat je de luistergeschiedenis wil wissen?</string>\n    <string name=\"pause_search_history\">Pauzeer zoekgeschiedenis</string>\n    <string name=\"clear_search_history\">Zoekgeschiedenis wissen</string>\n    <string name=\"clear_search_history_confirm\">Weet je zeker dat je de zoekgeschiedenis wil wissen?</string>\n    <string name=\"enable_kugou\">Schakel KuGou songtekst provider in</string>\n    <string name=\"backup_restore\">Backup en herstel</string>\n    <string name=\"action_backup\">Reservekopie</string>\n    <string name=\"action_restore\">Herstellen</string>\n    <string name=\"imported_playlist\">Afspeellijst geimporteerd</string>\n    <string name=\"backup_create_success\">Reservekopie succesvol gemaakt</string>\n    <string name=\"backup_create_failed\">Reservekopie maken mislukt</string>\n    <string name=\"restore_failed\">Reservekopie herstellen mislukt</string>\n    <string name=\"about\">Over</string>\n    <string name=\"app_version\">App versie</string>\n    <string name=\"new_version_available\">Nieuwe versie beschikbaar</string>\n    <string name=\"translation_models\">Vertaalmodellen</string>\n    <string name=\"clear_translation_models\">Verwijder vertaalmodellen</string>\n    <string name=\"enable_lrclib\">LrcLib tekstaanbieder inschakelen</string>\n    <string name=\"stop_music_on_task_clear\">Stop muziek op \\'taak wissen\\'</string>\n    <string name=\"dismiss\">Afwijzen</string>\n    <string name=\"tempo_and_pitch\">Tempo en toonhoogte</string>\n    <string name=\"auto_load_more_desc\">Voeg indien mogelijk automatisch meer nummers toe wanneer het einde van de wachtrij is bereikt</string>\n    <string name=\"forgotten_favorites\">Vergeten favorieten</string>\n    <string name=\"keep_listening\">Blijf luisteren</string>\n    <string name=\"your_youtube_playlists\">Je YouTube-afspeellijsten</string>\n    <string name=\"similar_to\">Vergelijkbaar met</string>\n    <string name=\"library_playlist_empty\">Je afspeellijsten worden hier weergegeven</string>\n    <string name=\"library_song_empty\">Bibliotheeknummers worden hier weergegeven</string>\n    <string name=\"library_artist_empty\">Bibliotheekartiesten worden hier weergegeven</string>\n    <string name=\"library_album_empty\">Bibliotheekalbums worden hier weergegeven</string>\n    <string name=\"other_versions\">Andere versies</string>\n    <string name=\"remove_download_playlist_confirm\">Wilt u echt alle nummers van de afspeellijst “%s” verwijderen uit de opslag Gedownloade nummers?</string>\n    <string name=\"delete_playlist_confirm\">Wil je echt de afspeellijst “%s” verwijderen?</string>\n    <string name=\"add_all_to_library\">Alles toevoegen aan bibliotheek</string>\n    <string name=\"remove_all_from_library\">Alles verwijderen uit bibliotheek</string>\n    <string name=\"remove_from_playlist\">Uit afspeellijst verwijderen</string>\n    <string name=\"remove_from_queue\">Uit wachtrij verwijderen</string>\n    <string name=\"duplicates\">Duplicaten</string>\n    <string name=\"skip_duplicates\">Duplicaten overslaan</string>\n    <string name=\"add_anyway\">Toch toevoegen</string>\n    <string name=\"duplicates_description_single\">Het nummer staat al in je afspeellijst</string>\n    <string name=\"duplicates_description_multiple\">%d nummers staan al in je afspeellijst</string>\n    <string name=\"action_like_all\">Alle leuk vinden</string>\n    <string name=\"action_remove_like_all\">Alle \\'vind ik leuks\\' verwijderen</string>\n    <string name=\"theme\">Thema</string>\n    <string name=\"player\">Speler</string>\n    <string name=\"player_text_alignment\">Tekstuitlijning van speler</string>\n    <string name=\"sided\">Zijdig</string>\n    <string name=\"player_slider_style\">Speler schuifregelaar-stijl</string>\n    <string name=\"default_\">Standaard</string>\n    <string name=\"squiggly\">Kronkelend</string>\n    <string name=\"misc\">Overige</string>\n    <string name=\"grid_cell_size\">Rastercelgrootte</string>\n    <string name=\"small\">Klein</string>\n    <string name=\"big\">Groot</string>\n    <string name=\"not_logged_in\">Niet ingelogd</string>\n    <string name=\"queue\">Wachtrij</string>\n    <string name=\"persistent_queue_desc\">Herstel je laatste wachtrij wanneer de app start</string>\n    <string name=\"auto_load_more\">Automatisch meer nummers laden</string>\n    <string name=\"auto_skip_next_on_error\">Automatisch overspringen naar het volgende nummer bij een fout</string>\n    <string name=\"auto_skip_next_on_error_desc\">Zorg voor een continue afspeelervaring</string>\n    <string name=\"listen_history\">Luistergeschiedenis</string>\n    <string name=\"search_history\">Zoekgeschiedenis</string>\n    <string name=\"disable_screenshot\">Schermafbeelding uitschakelen</string>\n    <string name=\"disable_screenshot_desc\">Als deze optie is ingeschakeld, zijn schermafbeeldingen en de weergave van de app in Recente bestanden uitgeschakeld.</string>\n    <string name=\"hide_explicit\">Expliciete inhoud verbergen</string>\n    <string name=\"discord_integration\">Discord integratie</string>\n    <string name=\"discord_information\">Metrolist gebruikt de KizzyRPC bibliotheek om de status van je Discord account in te stellen. Dit omvat het gebruik van de Discord Gateway verbinding, die kan worden beschouwd als een schending van de TOS van Discord. Er zijn echter geen gevallen bekend van gebruikersaccounts die om deze reden zijn geschorst. Gebruik op eigen risico.\\n\\nMetrolist zal alleen uw token uitpakken, en al het andere wordt lokaal opgeslagen.</string>\n    <string name=\"options\">Opties</string>\n    <string name=\"preview\">Voorbeeld</string>\n    <string name=\"login_failed\">Aanmelden mislukt</string>\n    <string name=\"action_logout\">Afmelden</string>\n    <string name=\"enable_discord_rpc\">Rijke aanwezigheid inschakelen</string>\n    <string name=\"use_login_for_browse_desc\">Dit kan invloed hebben op welke inhoud u ziet en laat bijvoorbeeld alleen premium albums zien als u bent ingelogd met een Premium account</string>\n    <string name=\"use_login_for_browse\">Gebruik login om inhoud te bekijken</string>\n    <string name=\"action_login\">Inloggen</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-nn/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">Eining</string>\n    <string name=\"remote_history\">Tenar</string>\n    <string name=\"charts\">Hittlister</string>\n    <string name=\"back_button_desc\">Far atter</string>\n    <string name=\"album_cover_desc\">Albumomslag</string>\n    <string name=\"top_music_videos\">Populære musikkvideoar</string>\n    <string name=\"trending\">Trendande</string>\n    <string name=\"weeks\">Veker</string>\n    <string name=\"months\">Månader</string>\n    <string name=\"years\">År</string>\n    <string name=\"continuous\">Uavbroten</string>\n    <string name=\"liked\">Lika</string>\n    <string name=\"offline\">Nedladde</string>\n    <string name=\"my_top\">Mine topp</string>\n    <string name=\"cached_playlist\">Snarlagra</string>\n    <string name=\"uploaded_playlist\">Oppladde</string>\n    <string name=\"filter_uploaded\">Oppladde</string>\n    <string name=\"sync_playlist\">Synkroniser speleliste</string>\n    <string name=\"sync_disabled\">Synkronisering deaktivert</string>\n    <string name=\"allows_for_sync_witch_youtube\">Obs! Dette tillèt synkronisering med YouTube Music og kan IKKJE brigdast seinare.</string>\n    <string name=\"generating_image\">Skapar bilete</string>\n    <string name=\"please_wait\">Venlegast vent</string>\n    <string name=\"cancel\">Bryt av</string>\n    <string name=\"share_lyrics\">Del songtekster</string>\n    <string name=\"share_as_text\">Del som tekst</string>\n    <string name=\"share_as_image\">Del som bilete</string>\n    <string name=\"max_selection_limit\">Øvre grense for utval</string>\n    <string name=\"share_selected\">Del valde</string>\n    <string name=\"customize_colors\">Tilpass leter</string>\n    <string name=\"text_color\">Tekstlet</string>\n    <string name=\"secondary_text_color\">Sekundær tekstlet</string>\n    <string name=\"background_color\">Bakgrunnslet</string>\n    <string name=\"remove_from_cache\">Tak bort frå snarlagring</string>\n    <string name=\"copy_link\">Kopier lenkje</string>\n    <string name=\"select\">Vel alle</string>\n    <string name=\"like_all\">Lik alle</string>\n    <string name=\"dislike_all\">Mislik alle</string>\n    <string name=\"sort_by_last_updated\">Dato oppdatert</string>\n    <string name=\"link_copied\">Lenkje kopiert til utklippstavla</string>\n    <string name=\"starting_radio\">Påbyrjar radio</string>\n    <string name=\"now_playing\">Spelar no</string>\n    <string name=\"lyrics\">Songtekster</string>\n    <string name=\"close\">Lat att</string>\n    <string name=\"hide_player_thumbnail\">Gøym avspelarminiatyrbilete</string>\n    <string name=\"hide_player_thumbnail_desc\">Byt ut albumkunst med appmerket i spelar</string>\n    <string name=\"already_in_playlist\">Alt i speleliste:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d gong</item>\n        <item quantity=\"other\">%d gonger</item>\n    </plurals>\n    <string name=\"seek_forward_dynamic\">%1$d sekund framover</string>\n    <string name=\"seek_backward_dynamic\">%1$d sekund attover</string>\n    <string name=\"seek_seconds_addup\">Progressiv søking</string>\n    <string name=\"seek_seconds_addup_description\">Dreg på seg 5 sekund i tillegg på kvar einskild søkingsframspoling om påslegen</string>\n    <string name=\"similar_content\">Liknande innhald</string>\n    <string name=\"player_background_style\">Bakgrunnsstil for avspelar</string>\n    <string name=\"follow_theme\">Fylg tema</string>\n    <string name=\"gradient\">Letovergang</string>\n    <string name=\"new_player_design\">Ny spelardesign</string>\n    <string name=\"new_mini_player_design\">Ny minispelardesign</string>\n    <string name=\"player_background_blur\">Uskarpleik</string>\n    <string name=\"player_buttons_style\">Let på avspelarknappar</string>\n    <string name=\"default_style\">Standard</string>\n    <string name=\"enable_swipe_thumbnail\">Sveip for å byta songar</string>\n    <string name=\"swipe_song_to_add\">Sveip songen åt venstre for å leggja han til i køen eller til høgre for å spela han nest</string>\n    <string name=\"swipe_song_to_remove\">Sveip songen for å taka han bort frå spelelista</string>\n    <string name=\"lyrics_click_change\">Brigd songtekster ved klikk</string>\n    <string name=\"lyrics_auto_scroll\">Skroll songtekster sjølvverkande</string>\n    <string name=\"slim\">Smal</string>\n    <string name=\"slim_navbar\">Smalt botnnavigeringsfelt</string>\n    <string name=\"auto_playlists\">Automatiske spelelister</string>\n    <string name=\"show_liked_playlist\">Syn «Lika»-spelelista</string>\n    <string name=\"show_downloaded_playlist\">Syn «Nedladde»-spelelista</string>\n    <string name=\"show_top_playlist\">Syn «Mine topp»-spelelista</string>\n    <string name=\"show_cached_playlist\">Syn «Snarlagra»-spelelista</string>\n    <string name=\"show_uploaded_playlist\">Syn «Oppladde»-spelelista</string>\n    <string name=\"advanced_login\">Logg inn med lykel</string>\n    <string name=\"token_hidden\">Trykk for å syna lykelen</string>\n    <string name=\"token_shown\">Trykk att for å kopiera eller brigda</string>\n    <string name=\"token_adv_login_description\">Dette er ein AVANSERT innloggingsmetode. Som alternativ til nettportalen, kan du direkte leggja inn eller oppdatera innloggingslykelen din (innloggings-«token») her. Dette kan t.d. gjera det å logga inn på fleire einingar snøggare. NB: Ugilde lykelformat som appen ikkje lukkast i å tolka, kjem ikkje til å verta godtekne</string>\n    <string name=\"yt_sync\">Automatisk synkronisering med konto</string>\n    <string name=\"more_content\">Meir innhald</string>\n    <string name=\"edit_playlist_cover\">Brigd spelelisteomslag</string>\n    <string name=\"edit_playlist_cover_note\">NB: Kontoen din lyt vera lenkt til eit telefonnummer og stadfest på YouTube Music for å brigda spelelisteomslaget.</string>\n    <string name=\"edit_playlist_cover_note_wait\">Etter at du vel eit bilete, dukkar det nye omslaget på spelelista opp om eit bel. Venlegast vent imedan dette hender.</string>\n    <string name=\"choose_from_library\">Vel frå samling</string>\n    <string name=\"remove_custom_image\">Fjern eigendefinert bilete</string>\n    <string name=\"general\">Ålment</string>\n    <string name=\"proxy\">Proxytenar</string>\n    <string name=\"default_lib_chips\">Brigd standardvisning i samlinga</string>\n    <string name=\"set_quick_picks\">Still inn snarval</string>\n    <string name=\"last_song_listened\">Basert på siste song høyrd på</string>\n    <string name=\"app_language\">Appmål</string>\n    <string name=\"config_proxy\">Konfigurer proxytenar</string>\n    <string name=\"proxy_username\">Proxytenarbrukarnamn</string>\n    <string name=\"proxy_password\">Proxytenarpassord</string>\n    <string name=\"enable_authentication\">Slå på autentisering</string>\n    <string name=\"discord_use_details\">Nøyt detaljar i staden for ein tilstand</string>\n    <string name=\"discord_use_details_description\">Syn songtittel fremst i staden for artistnamn</string>\n    <string name=\"enable_similar_content\">Slå på liknande innhald</string>\n    <string name=\"similar_content_desc\">Legg automatisk til fleire liknande songar når køen tek slutt</string>\n    <string name=\"percentage_format\">%d %%</string>\n    <string name=\"import_online\">Importer m3u-speleliste</string>\n    <string name=\"import_csv\">Importer csv-speleliste</string>\n    <string name=\"download_playlist_desc\">Lad ned alle songar for fråkopla avspeling</string>\n    <string name=\"remove_download_playlist_desc\">Tak alle nedladde songar bort frå denne spelelista</string>\n    <string name=\"download_in_progress_desc\">Nedlading er i gang</string>\n    <string name=\"share_playlist_desc\">Del denne spelelista med andre</string>\n    <string name=\"delete_playlist_desc\">Slett denne spelelista for æve</string>\n    <string name=\"sync_playlist_desc\">Synkroniser spelelista med YouTube Music</string>\n    <string name=\"primary_color_style\">Hovudlet</string>\n    <string name=\"tertiary_color_style\">Tredjelet</string>\n    <string name=\"lyrics_glow_effect\">Glødande songtekster</string>\n    <string name=\"lyrics_glow_effect_desc\">Legg glød og sprett på den noverande songteksta</string>\n    <string name=\"enable_better_lyrics\">Slå på Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">Nøyt Better Lyrics-tilbydaren for ord-etter-ord synkroniserte songtekster</string>\n    <string name=\"auto_scroll\">Attsynkroniser</string>\n    <string name=\"shuffle_playlist_first\">Blanda speleliste/album fyrst</string>\n    <string name=\"shuffle_playlist_first_desc\">Når du blandar, skal alle songar frå den opphavlege spelelista/album verta spela av fyrst, og så liknande innhald</string>\n    <string name=\"show_wrapped_card\">Syn Wrapped-kort</string>\n    <string name=\"playlist_add_local_to_synced_note\">Obs! Det er ikkje støtt å leggja lokale songar inn i synkroniserte/fjerre spelelister. Alle andre samanblandingar funkar</string>\n    <string name=\"auto_download_on_like\">Lad sjølvverkande ned ved å lika</string>\n    <string name=\"auto_download_on_like_desc\">Lad sjølvverkande ned songar når du likar dei</string>\n    <string name=\"swipe_sensitivity\">Minispelarsveipsfølsemd</string>\n    <string name=\"sensitivity_percentage\">%1$d %%</string>\n    <string name=\"clear_song_cache_dialog\">Er du viss på at du vil reinsa alle hurtiglagra songar?</string>\n    <string name=\"clear_image_cache_dialog\">Er du viss på at du vil reinsa alle hurtiglagra bilete?</string>\n    <string name=\"clear_downloads_dialog\">Er du viss på at du vil reinsa alle nedladingar?</string>\n    <string name=\"disable\">Slå av</string>\n    <string name=\"not_logged_in_youtube\">Ikkje logga på YouTube</string>\n    <string name=\"default_links\">Opna stødde lenkjer</string>\n    <string name=\"open_app_settings_error\">Klarte ikkje å opna applikasjonsinnstillingar</string>\n    <string name=\"release_notes\">Sleppsnotat</string>\n    <string name=\"all_time\">All æve</string>\n    <string name=\"past_24_hours\">Siste døger</string>\n    <string name=\"past_week\">Siste veke</string>\n    <string name=\"past_month\">Siste månad</string>\n    <string name=\"past_year\">Siste år</string>\n    <string name=\"top_length\">Lengd på Mine topp-lista</string>\n    <string name=\"history_duration\">Sogelengd</string>\n    <string name=\"information\">Opplysingar</string>\n    <string name=\"description\">Skildring</string>\n    <string name=\"views\">Syningar</string>\n    <string name=\"likes\">Likar</string>\n    <string name=\"dislikes\">Mislikar</string>\n    <string name=\"subscribe\">Abonner</string>\n    <string name=\"subscribed\">Abonnert</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">1 sekund</item>\n        <item quantity=\"other\">%d sekund</item>\n    </plurals>\n    <string name=\"disable_load_more_when_repeat_all\">Ikkje lad inn fleire når alt tvitekst</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Ikkje lad sjølvverkande inn fleire songar og liknande innhald når tvitek alt-modus er påslege</string>\n    <string name=\"lyrics_romanization_cyrillic\">Kyrillisk</string>\n    <string name=\"lyrics_romanize_title\">Romanisering</string>\n    <string name=\"lyrics_romanization\">Romanisering av songtekster</string>\n    <string name=\"lyrics_romanize_japanese\">Romaniser japanske songtekster</string>\n    <string name=\"lyrics_romanize_korean\">Romaniser koreanske songtekster</string>\n    <string name=\"lyrics_romanize_chinese\">Romaniser kinesiske songtekster</string>\n    <string name=\"lyrics_romanize_russian\">Romaniser ryske songtekster</string>\n    <string name=\"lyrics_romanize_ukrainian\">Romaniser ukrainske songtekster</string>\n    <string name=\"lyrics_romanize_belarusian\">Romaniser kviteryske songtekster</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Romaniser kirgisiske songtekster</string>\n    <string name=\"lyrics_romanize_serbian\">Romaniser serbiske songtekster</string>\n    <string name=\"lyrics_romanize_bulgarian\">Romaniser bulgarske songtekster</string>\n    <string name=\"line_by_line_option_title\">EKSPERIMENTELT: Kjenn att mål line etter line</string>\n    <string name=\"line_by_line_option_desc\">Målet som nøyter det kyrilliske alfabetet, skal kjennast att line etter line i staden for i songen som ein heilskap.</string>\n    <string name=\"line_by_line_dialog_title\">Er du viss?</string>\n    <string name=\"about_artist\">Om</string>\n    <string name=\"show_more\">Syn meir</string>\n    <string name=\"show_less\">Syn mindre</string>\n    <string name=\"artist_page_settings\">Artistside</string>\n    <string name=\"show_artist_description\">Syn artistskildring</string>\n    <string name=\"show_artist_subscriber_count\">Syn abonnementstal</string>\n    <string name=\"show_artist_monthly_listeners\">Syn månadlege lydarar</string>\n    <string name=\"wavy\">Snirklete</string>\n    <string name=\"enable_simpmusic\">Slå på songtekster frå SimpMusic</string>\n    <string name=\"enable_simpmusic_desc\">Nøyt songtekster frå SimpMusic-tilbydaren for synkroniserte songtekster</string>\n    <string name=\"skip_silence_desc\">Hoppa fram gjennom ljodlause luter av songar</string>\n    <string name=\"skip_silence_instant\">Hoppa over togn momentant</string>\n    <string name=\"skip_silence_instant_desc\">Hoppa fram under togn i songen i staden for å auka avspelingsfarta</string>\n    <string name=\"remember_shuffle_and_repeat\">Hugs blanding og tvitaking</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Hugs blandings- og tvitakingsmodus når appen vert omstarta</string>\n    <string name=\"pause_music_when_media_is_muted\">Pausa musikk når medium er dempa</string>\n    <string name=\"line_by_line_dialog_desc\">Dette er ein funksjon som ender og då kan mislukkast.\\n\\nSom standard er mål avgjort frå heile songen, men med denne funksjonen vert målet avgjort line etter line. Dette tillèt at fleirmålssongar funkar, MEN det kan hende at målet ikkje alltid er rett (t.d. om det er ei ukrainsk songtekst som ikkje inneheld bokstavar som einast er i ukrainsk, så kan det hende at ho vert romanisert som russisk).\\n\\nDersom du ikkje har lyte, råder vi til å halde denne funksjonen slegen av.</string>\n    <string name=\"romanize_current_track\">Romaniser noverande song</string>\n    <string name=\"lyrics_offset\">Songtekstsforskuving</string>\n    <string name=\"settings_section_ui\">Grensesned</string>\n    <string name=\"settings_section_privacy\">Personvern og tryggleik</string>\n    <string name=\"settings_section_player_content\">Avspelar og innhald</string>\n    <string name=\"settings_section_storage\">Lagring og data</string>\n    <string name=\"settings_section_system\">System og opplysingar</string>\n    <string name=\"updater\">Oppdaterar</string>\n    <string name=\"check_for_updates\">Sjekka etter oppdateringar sjølvverkande</string>\n    <string name=\"update_notifications\">Slå på oppdateringsvarsel</string>\n    <string name=\"update_available_title\">Oppdatering tilgjengeleg</string>\n    <string name=\"update_channel_name\">Appoppdateringar</string>\n    <string name=\"update_channel_desc\">Varsel om nye utgåver</string>\n    <string name=\"audio_offload\">Slå på avlading</string>\n    <string name=\"audio_offload_description\">Nøyte avladingsljodstigen («offload audio path») for audioavspeling. Å slå dette av kan auka straumforbruk, men kan gagna om du opplever lyte med audioavspeling eller etterhandsaming</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"google_cast_description\">Slå på det å kasta audio til Chromecast og andre Cast-kunnige einingar</string>\n    <string name=\"lyrics_romanize_macedonian\">Romaniser makedonske songtekster</string>\n    <string name=\"integrations\">Hopflettingar</string>\n    <string name=\"username\">Nøytarnamn</string>\n    <string name=\"password\">Passord</string>\n    <string name=\"lastfm_integration\">Hopfletting med Last.fm</string>\n    <string name=\"enable_scrobbling\">Slå på scrobbling</string>\n    <string name=\"lastfm_now_playing\">Send «Spelar no»</string>\n    <string name=\"last_fm_send_likes\">Send likar/mislikar</string>\n    <string name=\"last_fm_send_likes_description\">Elska/avelska songar i Last.fm når dei vert lika/mislika i Metrolist</string>\n    <string name=\"logging_in\">Loggar inn…</string>\n    <string name=\"scrobbling_configuration\">Scrobblingsoppset</string>\n    <string name=\"scrobble_min_track_duration\">Scrobbla songar lengre en</string>\n    <string name=\"scrobble_delay_percent\">Skrobblingsforseinking i prosent</string>\n    <string name=\"scrobble_delay_minutes\">Skrobblingsforseinking i minutt</string>\n    <string name=\"hide_video_songs\">Gøym videosongar</string>\n    <string name=\"details_desc\">Syn songopplysingar</string>\n    <string name=\"edit_desc\">Brigda tittel eller artist</string>\n    <string name=\"start_radio_desc\">Oppretta ein stasjon utifrå denne songen</string>\n    <string name=\"play_next_desc\">Legg til øvst i køen</string>\n    <string name=\"add_to_queue_desc\">Legg til nedst i køen</string>\n    <string name=\"add_to_library_desc\">Lagra i biblioteket ditt</string>\n    <string name=\"download_desc\">Gjer tilgjengeleg for fråkopla avspeling</string>\n    <string name=\"add_to_playlist_desc\">Legg til i ei av dine spelelister</string>\n    <string name=\"refetch_desc\">Henta dei siste metadataa frå YouTube Music</string>\n    <string name=\"share_desc\">Del ei lenkje til denne songen</string>\n    <string name=\"delete_desc\">Tak bort denne songen permanent</string>\n    <string name=\"advanced_desc\">Brigd snøggleiken og tonehøgd på songen</string>\n    <string name=\"equalizer_desc\">Juster audioutjamnaren</string>\n    <string name=\"enable_dynamic_icon\">Slå på dynamisk ikon</string>\n    <string name=\"mini_player\">Minispelar</string>\n    <string name=\"pure_black_mini_player\">Reinsvart minispelar</string>\n    <string name=\"cache_size_warning_title\">Vent no då!</string>\n    <string name=\"cache_size_warning_message\">Du har valt ei snarlagringsstorleiksgrense mindre en storleiken på snarlagringa nett no (%1$s). Dersom du held fram, kan appen taka bort somme snarlagra %2$s for å høva til den nye grensa. Hald fram likevel?</string>\n    <string name=\"cache_size_warning_confirm\">Hald fram</string>\n    <string name=\"lyrics_animation_style\">Ord-etter-ord-animeringsstil</string>\n    <string name=\"none\">Ingen</string>\n    <string name=\"fade\">Avbleiking</string>\n    <string name=\"glow\">Lysing</string>\n    <string name=\"slide\">Skliding</string>\n    <string name=\"karaoke\">Karaoke</string>\n    <string name=\"apple_music_style\">Apple Music</string>\n    <string name=\"lyrics_text_size\">Tekststorleik på songtekster</string>\n    <string name=\"lyrics_line_spacing\">Lineavstand på songtekster</string>\n    <string name=\"album_art_for\">Albumkunst for %s</string>\n    <string name=\"wrapped_total_albums_title\">Du har høyrt på</string>\n    <string name=\"wrapped_total_albums_subtitle\">ulike album</string>\n    <string name=\"wrapped_top_album_title\">Toppalbumet ditt er</string>\n    <string name=\"wrapped_playlist_ready\">Di personlege speleliste er klar</string>\n    <string name=\"wrapped_top_5_albums_title\">Dine toppalbum i år</string>\n    <string name=\"wrapped_album_listening_time\">Du har høyrt på dette albumet i %d minutt</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d minutt</string>\n    <string name=\"wrapped_no_data\">Ingen data</string>\n    <string name=\"wrapped_top_5_artists_title\">Dine toppartistar i år</string>\n    <string name=\"wrapped_artist_listening_time\">%d minutt</string>\n    <string name=\"wrapped_top_5_songs_title\">Dine toppsongar i år</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">Albumkunst</string>\n    <string name=\"wrapped_top_artist_title\">Toppartisten din i år er</string>\n    <string name=\"wrapped_top_artist_image_content_description\">Toppartistbilete</string>\n    <string name=\"wrapped_top_artist_listening_time\">Du har høyrt på hen i %d minutt</string>\n    <string name=\"wrapped_top_song_title\">Din mest spela song er</string>\n    <string name=\"wrapped_top_song_listening_time\">Du har høyrt på i %d minutt</string>\n    <string name=\"wrapped_total_artists_title\">Du høyrde på</string>\n    <string name=\"wrapped_total_artists_subtitle\">ulike artistar</string>\n    <string name=\"wrapped_total_songs_title\">Du høyrde på</string>\n    <string name=\"wrapped_total_songs_subtitle\">ulike songar</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_intro_subtitle\">Det er på tide å sjå på kva du har høyrt på i år!</string>\n    <string name=\"wrapped_intro_button\">Set i gang!</string>\n    <string name=\"wrapped_logo_content_description\">Metrolist-merket</string>\n    <string name=\"wrapped_year\">2026</string>\n    <string name=\"wrapped_ready_title\">DIN WRAPPED ER KLAR!</string>\n    <string name=\"wrapped_ready_subtitle\">På tide å sjå på kva du elska i år.</string>\n    <string name=\"wrapped_thank_you\">Takk for at du høyrde</string>\n    <string name=\"wrapped_special_thanks\">Hjarteleg takk til Mo Agamy for å skapa Metrolist</string>\n    <string name=\"wrapped_close\">Steng wrapped</string>\n    <string name=\"wrapped_playlist_title\">Din %s Wrapped</string>\n    <string name=\"wrapped_create_playlist\">Oppretta speleliste</string>\n    <string name=\"wrapped_playlist_saved\">Speleliste lagra</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"one\">%d profil</item>\n        <item quantity=\"other\">%d profilar</item>\n    </plurals>\n    <string name=\"equalizer_header\">Ljodutjemnar</string>\n    <string name=\"no_profiles\">Ingen ljodutjemnarprofilar</string>\n    <string name=\"import_profile\">Importer profil</string>\n    <string name=\"system_equalizer\">Systemljodutjemnar</string>\n    <string name=\"eq_disabled\">Slegen av</string>\n    <plurals name=\"band_count\">\n        <item quantity=\"one\">%d band</item>\n        <item quantity=\"other\">%d band</item>\n    </plurals>\n    <string name=\"delete_profile_desc\">Tak bort profil</string>\n    <string name=\"delete_profile_confirmation\">Er du viss på at du vil taka bort «%1$s»? Dette kan ikkje angrast.</string>\n    <string name=\"error_file_read\">Klarte ikkje å lesa fil</string>\n    <string name=\"error_file_open\">Klarte ikkje å opna fil: %1$s</string>\n    <string name=\"import_error_title\">Importeringslyte</string>\n    <string name=\"casting_to\">Kastar til %s</string>\n    <string name=\"progress_percent\">Framsteg %s %%</string>\n    <string name=\"listening_to_metrolist\">Høyrer på Metrolist</string>\n    <string name=\"open\">Opna</string>\n    <string name=\"failed_to_create_image\">Mislukkast i å skapa bilete: %s</string>\n    <string name=\"copied_title\">Kopierte tittel</string>\n    <string name=\"copied_artist\">Kopierte artist</string>\n    <string name=\"error_playing\">Feil under avspeling</string>\n    <string name=\"failed_to_parse_proxy\">Mislukkast i å tolka mellomtenar-URL</string>\n    <string name=\"album_art\">Albumkunst</string>\n    <string name=\"no_song_playing\">Ingen song vert spela av</string>\n    <string name=\"tap_to_open\">Trykk for å opna Metrolist</string>\n    <string name=\"previous\">Førre</string>\n    <string name=\"play_pause\">Spela av/Pausa</string>\n    <string name=\"next\">Neste</string>\n    <string name=\"like\">Lika</string>\n    <string name=\"widget_description\">Musikkavspelarwidget med avspelingskontrollar</string>\n    <string name=\"turntable_widget_description\">Snartilgang til songen du spela av nylegast</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-or/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">ସ୍ଥାନୀୟ</string>\n    <string name=\"remote_history\">ଦୂରସ୍ଥ</string>\n    <string name=\"charts\">ଚାର୍ଟ</string>\n    <string name=\"back_button_desc\">ପଛକୁ</string>\n    <string name=\"album_cover_desc\">ଆଲବମ୍ ମଲାଟ</string>\n    <string name=\"top_music_videos\">ଶ୍ରେଷ୍ଠ ସଙ୍ଗୀତ ଭିଡିଓ</string>\n    <string name=\"trending\">ସଦ୍ୟତମ ଚର୍ଚ୍ଚିତ</string>\n    <string name=\"weeks\">ସପ୍ତାହ</string>\n    <string name=\"months\">ମାସ</string>\n    <string name=\"years\">ବର୍ଷ</string>\n    <string name=\"continuous\">ଲଗାତାର</string>\n    <string name=\"liked\">ପସନ୍ଦ କରିଥିବା</string>\n    <string name=\"offline\">ଡାଉନଲୋଡ ହୋଇଛି</string>\n    <string name=\"my_top\">ମୋର ପ୍ରିୟ</string>\n    <string name=\"uploaded_playlist\">ଉପଲୋଡ ହେଇଛି</string>\n    <string name=\"filter_uploaded\">ଅପଲୋଡ଼ ହେଇଛି</string>\n    <string name=\"generating_image\">ଚିତ୍ର ପ୍ରସ୍ତୁତ ହେଉଛି</string>\n    <string name=\"please_wait\">ଅପେକ୍ଷା କରନ୍ତୁ</string>\n    <string name=\"cancel\">ବନ୍ଦ କରନ୍ତୁ</string>\n    <string name=\"enable\">ଚାଲୁ</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-pa/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <string name=\"home\">ਘਰ</string>\n    <string name=\"songs\">ਗੀਤ</string>\n    <string name=\"artists\">ਕਲਾਕਾਰ</string>\n    <string name=\"albums\">ਐਲਬਮ</string>\n    <string name=\"playlists\">ਪਲੇਲਿਸਟ</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d ਚੁਣਿਆ ਗਿਆ</item>\n        <item quantity=\"other\">%d ਚੁਣੇ ਗਏ</item>\n    </plurals>\n    <string name=\"history\">ਅਤੀਤ</string>\n    <string name=\"stats\">ਅੰਕੜੇ</string>\n    <string name=\"mood_and_genres\">ਮੂਡ ਅਤੇ ਸ਼ੈਲੀ</string>\n    <string name=\"account\">ਖਾਤਾ</string>\n    <string name=\"quick_picks\">ਉਂਗਲਾਂ ਤੇ ਚੁਣੇ ਗੀਤ</string>\n    <string name=\"quick_picks_empty\">ਉਂਗਲਾਂ ਤੇ ਚੁਣੇ ਗੀਤ ਪਾਉਣ ਲਈ ਗੀਤ ਸੁਣਨੇ ਆਰੰਭ ਕਰੋ</string>\n    <string name=\"new_release_albums\">ਨਵੀਆਂ ਜਾਰੀ ਐਲਬਮਾਂ</string>\n    <string name=\"today\">ਅੱਜ</string>\n    <string name=\"yesterday\">ਕੱਲ੍ਹ</string>\n    <string name=\"this_week\">ਇਸ ਹਫ਼ਤੇ</string>\n    <string name=\"last_week\">ਪਿਛਲੇ ਹਫ਼ਤੇ</string>\n    <string name=\"most_played_songs\">ਵੱਧ ਸੁਣੇ ਗਏ ਗੀਤ</string>\n    <string name=\"most_played_artists\">ਵਧੇਰੇ ਸੁਣੇ ਗਏ ਕਲਾਕਾਰ</string>\n    <string name=\"most_played_albums\">ਵਧੇਰੇ ਸੁਣੀਆਂ ਐਲਬਮਾਂ</string>\n    <string name=\"search\">ਖੋਜੋ</string>\n    <string name=\"search_yt_music\">ਯੂਟਿਊਬ ਮਿਊਜ਼ਿਕ ਖੋਜੋ…</string>\n    <string name=\"search_library\">ਲਾਇਬ੍ਰੇਰੀ ਖੋਜੋ…</string>\n    <string name=\"filter_library\">ਲਾਇਬ੍ਰੇਰੀ</string>\n    <string name=\"filter_liked\">ਪਸੰਦ ਕੀਤੇ</string>\n    <string name=\"filter_downloaded\">ਡਾਊਨਲੋਡ ਕੀਤੇ</string>\n    <string name=\"filter_all\">ਸਾਰੇ</string>\n    <string name=\"filter_songs\">ਗੀਤ</string>\n    <string name=\"filter_videos\">ਵੀਡੀਓ</string>\n    <string name=\"filter_albums\">ਐਲਬਮ</string>\n    <string name=\"filter_artists\">ਕਲਾਕਾਰ</string>\n    <string name=\"filter_playlists\">ਪਲੇਲਿਸਟਾਂ</string>\n    <string name=\"filter_community_playlists\">ਕਮਿਊਨਿਟੀ ਪਲੇਲਿਸਟਾਂ</string>\n    <string name=\"filter_featured_playlists\">ਪ੍ਰਦਰਸ਼ਿਤ ਪਲੇਲਿਸਟਾਂ</string>\n    <string name=\"filter_bookmarked\">ਬੁੱਕਮਾਰਕ ਕੀਤੇ</string>\n    <string name=\"no_results_found\">ਕੋਈ ਨਤੀਜੇ ਨਹੀਂ ਲੱਭੇ</string>\n    <string name=\"from_your_library\">ਤੁਹਾਡੀ ਲਾਇਬ੍ਰੇਰੀ ਤੋਂ</string>\n    <string name=\"liked_songs\">ਪਸੰਦ ਕੀਤੇ ਗੀਤ</string>\n    <string name=\"downloaded_songs\">ਡਾਊਨਲੋਡ ਕੀਤੇ ਗੀਤ</string>\n    <string name=\"playlist_is_empty\">ਪਲੇਲਿਸਟ ਖਾਲੀ ਹੈ</string>\n    <string name=\"retry\">ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ</string>\n    <string name=\"radio\">ਰੇਡੀਓ</string>\n    <string name=\"shuffle\">ਸ਼ਫਲ ਕਰੋ</string>\n    <string name=\"reset\">ਰੀਸੈੱਟ ਕਰੋ</string>\n    <string name=\"details\">ਵੇਰਵੇ</string>\n    <string name=\"edit\">ਸੰਪਾਦਿਤ ਕਰੋ</string>\n    <string name=\"start_radio\">ਰੇਡੀਓ ਸ਼ੁਰੂ ਕਰੋ</string>\n    <string name=\"play\">ਚਲਾਓ</string>\n    <string name=\"play_next\">ਅਗਲਾ ਚਲਾਓ</string>\n    <string name=\"add_to_queue\">ਕਤਾਰ ਵਿੱਚ ਜੋੜ੍ਹੋ</string>\n    <string name=\"add_to_library\">ਲਾਇਬ੍ਰੇਰੀ ਵਿੱਚ ਸ਼ਾਮਿਲ ਕਰੋ</string>\n    <string name=\"remove_from_library\">ਲਾਇਬ੍ਰੇਰੀ ਤੋਂ ਹਟਾਓ</string>\n    <string name=\"action_download\">ਡਾਊਨਲੋਡ</string>\n    <string name=\"downloading\">ਡਾਊਨਲੋਡ ਹੋ ਰਿਹਾ</string>\n    <string name=\"remove_download\">ਡਾਊਨਲੋਡ ਹਟਾਓ</string>\n    <string name=\"import_playlist\">ਪਲੇਲਿਸਟ ਅਯਾਤ ਕਰੋ</string>\n    <string name=\"add_to_playlist\">ਪਲੇਲਿਸਟ ਵਿੱਚ ਸ਼ਾਮਿਲ ਕਰੋ</string>\n    <string name=\"view_artist\">ਕਲਾਕਾਰ ਵੇਖੋ</string>\n    <string name=\"view_album\">ਐਲਬਮ ਵੇਖੋ</string>\n    <string name=\"refetch\">ਮੁੜ ਪ੍ਰਾਪਤ ਕਰੋ</string>\n    <string name=\"share\">ਸਾਂਝਾ ਕਰੋ</string>\n    <string name=\"delete\">ਮਿਟਾਓ</string>\n    <string name=\"remove_from_history\">ਅਤੀਤ ਵਿੱਚੋਂ ਹਟਾਓ</string>\n    <string name=\"search_online\">ਆਨਲਾਈਨ ਖੋਜ ਕਰੋ</string>\n    <string name=\"action_sync\">ਸਿੰਕਰੋਨਾਈਜ਼ ਕਰੋ</string>\n    <string name=\"advanced\">ਐਡਵਾਂਸਡ</string>\n    <string name=\"sort_by_create_date\">ਮਿਤੀ</string>\n    <string name=\"sort_by_name\">ਨਾਮ</string>\n    <string name=\"sort_by_artist\">ਕਲਾਕਾਰ</string>\n    <string name=\"sort_by_year\">ਸਾਲ</string>\n    <string name=\"sort_by_song_count\">ਗਿਣਤੀ</string>\n    <string name=\"sort_by_length\">ਲੰਬਾਈ</string>\n    <string name=\"sort_by_play_time\">ਚਲਾਉਣ ਦਾ ਸਮਾਂ</string>\n    <string name=\"sort_by_custom\">ਅਨੁਕੂਲਿਤ ਕ੍ਰਮ</string>\n    <string name=\"media_id\">ਮੀਡੀਆ ਆਈਡੀ</string>\n    <string name=\"mime_type\">ਮਾਈਮ ਪ੍ਰਕਾਰ</string>\n    <string name=\"codecs\">ਕੋਡੈਕਸ</string>\n    <string name=\"bitrate\">ਬਿੱਟਰੇਟ</string>\n    <string name=\"sample_rate\">ਸੈਂਪਲ ਰੇਟ</string>\n    <string name=\"loudness\">ਤੀਬਰਤਾ</string>\n    <string name=\"volume\">ਆਵਾਜ਼</string>\n    <string name=\"file_size\">ਫਾਈਲ ਆਕਾਰ</string>\n    <string name=\"unknown\">ਅਗਿਆਤ</string>\n    <string name=\"copied\">ਕਲਿੱਪਬੋਰਡ \\'ਤੇ ਕਾਪੀ ਕੀਤਾ ਗਿਆ</string>\n    <string name=\"edit_lyrics\">ਬੋਲਾਂ ਨੂੰ ਸੰਪਾਦਿਤ ਕਰੋ</string>\n    <string name=\"search_lyrics\">ਬੋਲ ਖੋਜੋ</string>\n    <string name=\"edit_song\">ਗੀਤ ਸੰਪਾਦਿਤ ਕਰੋ</string>\n    <string name=\"song_title\">ਗੀਤ ਦਾ ਸਿਰਲੇਖ</string>\n    <string name=\"song_artists\">ਗੀਤ ਕਲਾਕਾਰ</string>\n    <string name=\"error_song_title_empty\">ਗੀਤ ਦਾ ਸਿਰਲੇਖ ਖਾਲੀ ਨਹੀਂ ਹੋ ਸਕਦਾ।</string>\n    <string name=\"error_song_artist_empty\">ਗੀਤ ਕਲਾਕਾਰ ਖਾਲੀ ਨਹੀਂ ਹੋ ਸਕਦਾ।</string>\n    <string name=\"save\">ਸਾਂਭੋ</string>\n    <string name=\"choose_playlist\">ਪਲੇਲਿਸਟ ਚੁਣੋ</string>\n    <string name=\"edit_playlist\">ਪਲੇਲਿਸਟ ਸੰਪਾਦਿਤ ਕਰੋ</string>\n    <string name=\"create_playlist\">ਪਲੇਲਿਸਟ ਬਣਾਓ</string>\n    <string name=\"playlist_name\">ਪਲੇਲਿਸਟ ਦਾ ਨਾਮ</string>\n    <string name=\"error_playlist_name_empty\">ਪਲੇਲਿਸਟ ਨਾਮ ਖਾਲੀ ਨਹੀਂ ਹੋ ਸਕਦਾ।</string>\n    <string name=\"edit_artist\">ਕਲਾਕਾਰ ਸੰਪਾਦਿਤ ਕਰੋ</string>\n    <string name=\"artist_name\">ਕਲਾਕਾਰ ਦਾ ਨਾਮ</string>\n    <string name=\"error_artist_name_empty\">ਕਲਾਕਾਰ ਦਾ ਨਾਮ ਖਾਲੀ ਨਹੀਂ ਹੋ ਸਕਦਾ।</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d ਗੀਤ</item>\n        <item quantity=\"other\">%d ਗੀਤ</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d ਕਲਾਕਾਰ</item>\n        <item quantity=\"other\">%d ਕਲਾਕਾਰ</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d ਐਲਬਮ</item>\n        <item quantity=\"other\">%d ਐਲਬਮ</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d ਪਲੇਲਿਸਟ</item>\n        <item quantity=\"other\">%d ਪਲੇਲਿਸਟਾਂ</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d ਹਫ਼ਤਾ</item>\n        <item quantity=\"other\">%d ਹਫ਼ਤੇ</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d ਮਹੀਨਾ</item>\n        <item quantity=\"other\">%d ਮਹੀਨੇ</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d ਸਾਲ</item>\n        <item quantity=\"other\">%d ਸਾਲ</item>\n    </plurals>\n    <string name=\"playlist_imported\">ਪਲੇਲਿਸਟ ਅਯਾਤ ਕੀਤੀ ਗਈ</string>\n    <string name=\"removed_song_from_playlist\">ਪਲੇਲਿਸਟ ਤੋਂ \\\"%s\\\" ਹਟਾਇਆ ਗਿਆ</string>\n    <string name=\"playlist_synced\">ਪਲੇਲਿਸਟ ਸਿੰਕ ਹੋਈ</string>\n    <string name=\"undo\">ਅਣਕੀਤਾ ਕਰੋ</string>\n    <string name=\"lyrics_not_found\">ਬੋਲ ਨਹੀਂ ਮਿਲੇ</string>\n    <string name=\"sleep_timer\">ਸਲੀਪ ਟਾਈਮਰ</string>\n    <string name=\"end_of_song\">ਗੀਤ ਸਮਾਪਤ</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">%d ਮਿੰਟ</item>\n        <item quantity=\"other\">%d ਮਿੰਟ</item>\n    </plurals>\n    <string name=\"error_no_stream\">ਕੋਈ ਸਟ੍ਰੀਮ ਉਪਲਬਧ ਨਹੀਂ ਹੈ</string>\n    <string name=\"error_no_internet\">ਕੋਈ ਨੈੱਟਵਰਕ ਕਨੈਕਸ਼ਨ ਨਹੀਂ</string>\n    <string name=\"error_timeout\">ਸਮਾਂ ਸਮਾਪਤ</string>\n    <string name=\"error_unknown\">ਅਗਿਆਤ ਤਰੁੱਟੀ</string>\n    <string name=\"action_like\">ਪਸੰਦ ਕਰੋ</string>\n    <string name=\"action_remove_like\">ਪਸੰਦ ਹਟਾਓ</string>\n    <string name=\"action_shuffle_on\">ਸ਼ਫ਼ਲ ਆਨ</string>\n    <string name=\"action_shuffle_off\">ਸ਼ਫ਼ਲ ਆਫ</string>\n    <string name=\"repeat_mode_off\">ਰਿਪੀਟ ਮੋਡ ਆਫ</string>\n    <string name=\"repeat_mode_one\">ਮੌਜੂਦਾ ਗੀਤ ਦੁਹਰਾਓ</string>\n    <string name=\"repeat_mode_all\">ਕਤਾਰ ਨੂੰ ਦੁਹਰਾਓ</string>\n    <string name=\"queue_all_songs\">ਸਾਰੇ ਗੀਤ</string>\n    <string name=\"queue_searched_songs\">ਖੋਜੇ ਗਏ ਗੀਤ</string>\n    <string name=\"music_player\">ਸੰਗੀਤ ਪਲੇਅਰ</string>\n    <string name=\"settings\">ਸੈਟਿੰਗਾਂ</string>\n    <string name=\"appearance\">ਦਿੱਖ</string>\n    <string name=\"enable_dynamic_theme\">ਡਾਇਨੈਮਿਕ ਥੀਮ ਚਾਲੂ ਕਰੋ</string>\n    <string name=\"dark_theme\">ਗੂੜ੍ਹਾ ਥੀਮ</string>\n    <string name=\"dark_theme_on\">ਚਾਲੂ</string>\n    <string name=\"dark_theme_off\">ਬੰਦ</string>\n    <string name=\"dark_theme_follow_system\">ਸਿਸਟਮ ਦੀ ਪਾਲਣਾ ਕਰੋ</string>\n    <string name=\"pure_black\">ਸ਼ਾਹ ਕਾਲ੍ਹਾ</string>\n    <string name=\"default_open_tab\">ਡੀਫ਼ਾਲਟ ਟੈਬ</string>\n    <string name=\"customize_navigation_tabs\">ਨੈਵੀਗੇਸ਼ਨ ਟੈਬਾਂ ਨੂੰ ਆਪਣੀ ਪਸੰਦ ਦਾ ਢਾਲੋ</string>\n    <string name=\"lyrics_text_position\">ਬੋਲਾਂ ਦੀ ਸਥਿਤੀ</string>\n    <string name=\"left\">ਖੱਬੇ</string>\n    <string name=\"center\">ਵਿਚਕਾਰ</string>\n    <string name=\"right\">ਸੱਜੇ</string>\n    <string name=\"content\">ਸਮੱਗਰੀ</string>\n    <string name=\"login\">ਲਾਗ-ਇਨ</string>\n    <string name=\"content_language\">ਡੀਫ਼ਾਲਟ ਸਮੱਗਰੀ ਭਾਸ਼ਾ</string>\n    <string name=\"content_country\">ਡੀਫ਼ਾਲਟ ਸਮੱਗਰੀ ਦੇਸ਼</string>\n    <string name=\"system_default\">ਸਿਸਟਮ ਡੀਫ਼ਾਲਟ</string>\n    <string name=\"enable_proxy\">ਪ੍ਰੌਕਸੀ ਚਾਲੂ ਕਰੋ</string>\n    <string name=\"proxy_type\">ਪ੍ਰੌਕਸੀ ਕਿਸਮ</string>\n    <string name=\"proxy_url\">ਪ੍ਰੌਕਸੀ URL</string>\n    <string name=\"restart_to_take_effect\">ਪ੍ਰਭਾਵੀ ਹੋਣ ਲਈ ਮੁੜ-ਚਾਲੂ ਕਰੋ</string>\n    <string name=\"player_and_audio\">ਪਲੇਅਰ ਅਤੇ ਆਡੀਓ</string>\n    <string name=\"audio_quality\">ਆਡੀਓ ਕੁਆਲਿਟੀ</string>\n    <string name=\"audio_quality_auto\">ਆਟੋ</string>\n    <string name=\"audio_quality_high\">ਉੱਚ</string>\n    <string name=\"audio_quality_low\">ਘੱਟ</string>\n    <string name=\"persistent_queue\">ਨਿਰੰਤਰ ਕਤਾਰ</string>\n    <string name=\"skip_silence\">ਚੁੱਪ ਤੇ ਸਕਿੱਪ ਕਰੋ</string>\n    <string name=\"audio_normalization\">ਆਡੀਓ ਨਾਰਮਲਾਈਜ਼ੇਸ਼ਨ</string>\n    <string name=\"equalizer\">ਈਕੋਲਾਈਜ਼ਰ</string>\n    <string name=\"storage\">ਸਟੋਰੇਜ</string>\n    <string name=\"cache\">ਕੈਸ਼ੇ</string>\n    <string name=\"image_cache\">ਚਿੱਤਰ ਕੈਸ਼ੇ</string>\n    <string name=\"song_cache\">ਗੀਤ ਕੈਸ਼ੇ</string>\n    <string name=\"max_cache_size\">ਵੱਧੋ-ਵੱਧ ਕੈਸ਼ੇ ਆਕਾਰ</string>\n    <string name=\"unlimited\">ਅਸੀਮਤ</string>\n    <string name=\"clear_all_downloads\">ਸਭ ਡਾਊਨਲੋਡ ਸਾਫ਼ ਕਰੋ</string>\n    <string name=\"max_image_cache_size\">ਅਧਿਕਤਮ ਚਿੱਤਰ ਕੈਸ਼ੇ ਆਕਾਰ</string>\n    <string name=\"clear_image_cache\">ਚਿੱਤਰ ਕੈਸ਼ੇ ਸਾਫ਼ ਕਰੋ</string>\n    <string name=\"max_song_cache_size\">ਅਧਿਕਤਮ ਗੀਤ ਕੈਸ਼ੇ ਆਕਾਰ</string>\n    <string name=\"clear_song_cache\">ਗੀਤ ਕੈਸ਼ ਸਾਫ਼ ਕਰੋ</string>\n    <string name=\"size_used\">%s ਵਰਤਿਆ ਗਿਆ</string>\n    <string name=\"privacy\">ਗੋਪਨੀਯਤਾ</string>\n    <string name=\"pause_listen_history\">ਸੁਣਨ ਦਾ ਅਤੀਤ ਰੋਕੋ</string>\n    <string name=\"clear_listen_history\">ਸੁਣੇ ਗੀਤਾਂ ਦੀ ਹਿਸਟਰੀ ਮਿਟਾਓ</string>\n    <string name=\"clear_listen_history_confirm\">Are you sure to clear all listen history?</string>\n    <string name=\"pause_search_history\">ਖੋਜ ਇਤਿਹਾਸ ਨੂੰ ਰੋਕੋ</string>\n    <string name=\"clear_search_history\">ਖੋਜ ਇਤਿਹਾਸ ਸਾਫ਼ ਕਰੋ</string>\n    <string name=\"clear_search_history_confirm\">ਕੀ ਤੁਸੀਂ ਸਾਰੇ ਖੋਜ ਇਤਿਹਾਸ ਨੂੰ ਸਾਫ਼ ਕਰਨ ਲਈ ਯਕੀਨੀ ਹੋ?</string>\n    <string name=\"enable_kugou\">KuGou ਬੋਲ ਪ੍ਰਦਾਤਾ ਨੂੰ ਸਮਰੱਥ ਬਣਾਓ</string>\n    <string name=\"backup_restore\">ਬੈਕਅੱਪ ਅਤੇ ਰੀਸਟੋਰ</string>\n    <string name=\"action_backup\">ਬੈਕਅੱਪ</string>\n    <string name=\"action_restore\">ਰੀਸਟੋਰ</string>\n    <string name=\"imported_playlist\">ਇੰਪੋਰਟ ਕੀਤੀ ਪਲੇਲਿਸਟ</string>\n    <string name=\"backup_create_success\">ਬੈਕਅੱਪ ਸਫਲਤਾਪੂਰਵਕ ਬਣਾਇਆ ਗਿਆ</string>\n    <string name=\"backup_create_failed\">ਬੈਕਅੱਪ ਨਹੀਂ ਬਣਾਇਆ ਜਾ ਸਕਿਆ</string>\n    <string name=\"restore_failed\">ਬੈਕਅੱਪ ਰੀਸਟੋਰ ਕਰਨਾ ਅਸਫਲ ਰਿਹਾ</string>\n    <string name=\"about\">ਐਪ ਦੇ ਬਾਰੇ</string>\n    <string name=\"app_version\">ਐਪ ਸੰਸਕਰਣ</string>\n    <string name=\"new_version_available\">ਨਵਾਂ ਸੰਸਕਰਣ ਉਪਲੱਬਧ ਹੈ</string>\n    <string name=\"translation_models\">ਆਨੁਵਾਦ ਦੇ ਮਾਡਲ</string>\n    <string name=\"clear_translation_models\">ਅਨੁਵਾਦ ਦੇ ਮਾਡਲ ਮਿਟਾਓ</string>\n    <string name=\"forgotten_favorites\">ਭੁੱਲੇ ਹੋਏ ਮਨਪਸੰਦ ਗਾਣੇ</string>\n    <string name=\"keep_listening\">ਸੁਣਦੇ ਰਹੋ</string>\n    <string name=\"your_youtube_playlists\">ਤੁਹਾਡੀ ਯੂਟਿਊਬ ਚਾਲ ਸੂਚੀਆਂ</string>\n    <string name=\"similar_to\">ਇਹਦੇ ਵਰਗੇ</string>\n    <string name=\"library_song_empty\">ਲਾਇਬ੍ਰੇਰੀ</string>\n    <string name=\"library_artist_empty\">ਲਾਇਬ੍ਰੇਰੀ ਗਾਇਕ ਇੱਥੇ ਦਿਖਣਗੇ</string>\n    <string name=\"library_album_empty\">ਲਾਇਬ੍ਰੇਰੀ ਟੇਪਾਂ ਇੱਥੇ ਦਿਖਣ ਗੀਆਂ</string>\n    <string name=\"library_playlist_empty\">ਤੁਹਾਡੀਆਂ ਚਾਲ ਸੂਚੀਆਂ ਇੱਥੇ ਦਿਖਣ ਗਿਆਂ</string>\n    <string name=\"other_versions\">ਟੇਪ ਦ੍ਰਿਸ਼</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-pl/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"liked\">Polubione</string>\n    <string name=\"offline\">Pobrane</string>\n    <string name=\"my_top\">Mój Top</string>\n    <string name=\"select\">Zaznacz wszystko</string>\n    <string name=\"like_all\">Polub wszystko</string>\n    <string name=\"sort_by_last_updated\">Data zaktualizowania</string>\n    <string name=\"already_in_playlist\">Już na playliście:</string>\n    <string name=\"player_background_style\">Styl tła odtwarzacza</string>\n    <string name=\"follow_theme\">Zgodnie z motywem</string>\n    <string name=\"gradient\">Gradient</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"default_lib_chips\">Zmień domyślny chip biblioteki</string>\n    <string name=\"set_quick_picks\">Ustaw Szybki wybór</string>\n    <string name=\"last_song_listened\">Na podstawie poprzedniego wysłuchanego utworu</string>\n    <string name=\"all_time\">Kiedykolwiek</string>\n    <string name=\"past_24_hours\">Ostatnie 24 godziny</string>\n    <string name=\"past_week\">Ubiegły tydzień</string>\n    <string name=\"past_month\">Ubiegły miesiąc</string>\n    <string name=\"past_year\">Ubiegły rok</string>\n    <string name=\"top_length\">Długość mojej Top listy</string>\n    <string name=\"remote_history\">Zdalna</string>\n    <string name=\"back_button_desc\">Wstecz</string>\n    <string name=\"weeks\">Tygodnie</string>\n    <string name=\"months\">Miesiące</string>\n    <string name=\"years\">Lata</string>\n    <string name=\"continuous\">Ciągły</string>\n    <string name=\"cached_playlist\">W pamięci podręcznej</string>\n    <string name=\"cancel\">Anuluj</string>\n    <string name=\"album_cover_desc\">Okładka</string>\n    <string name=\"sync_playlist\">Synchronizuj playlistę</string>\n    <string name=\"player_background_blur\">Rozmycie</string>\n    <string name=\"default_style\">Domyślny</string>\n    <string name=\"general\">Ogólne</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"information\">Informacje</string>\n    <string name=\"views\">Wyświetlenia</string>\n    <string name=\"likes\">Polubienia</string>\n    <string name=\"trending\">Popularne</string>\n    <string name=\"lyrics\">Tekst</string>\n    <string name=\"local_history\">Lokalna</string>\n    <string name=\"charts\">Wykresy</string>\n    <string name=\"description\">Opis</string>\n    <string name=\"background_color\">Kolor tła</string>\n    <string name=\"show_downloaded_playlist\">Pokaż playlistę \\\"Pobrane\\\"</string>\n    <string name=\"customize_colors\">Dostosuj kolory</string>\n    <string name=\"token_adv_login_description\">Jest to ZAAWANSOWANA metoda logowania. Alternatywnie do portalu internetowego można tutaj bezpośrednio wprowadzić lub zaktualizować token logowania. Może to na przykład przyspieszyć logowanie na wielu urządzeniach. Należy pamiętać, że wszelkie nieprawidłowe formaty tokenów, których aplikacja nie przeanalizuje, nie zostaną zaakceptowane</string>\n    <string name=\"text_color\">Kolor tekstu</string>\n    <string name=\"secondary_text_color\">Drugi kolor tekstu</string>\n    <string name=\"similar_content_desc\">Automatycznie dodawaj więcej podobnych utworów po osiągnięciu końca kolejki</string>\n    <string name=\"clear_downloads_dialog\">Czy na pewno chcesz wyczyścić wszystkie pobrane pliki?</string>\n    <string name=\"top_music_videos\">Najlepsze teledyski</string>\n    <string name=\"sync_disabled\">Synchronizacja wyłączona</string>\n    <string name=\"allows_for_sync_witch_youtube\">Uwaga: Umożliwia to synchronizację z YouTube Music. Nie można tego zmienić później.</string>\n    <string name=\"generating_image\">Generowanie obrazu</string>\n    <string name=\"please_wait\">Proszę czekać</string>\n    <string name=\"share_lyrics\">Udostępnij tekst</string>\n    <string name=\"share_as_text\">Udostępnij jako tekst</string>\n    <string name=\"share_as_image\">Udostępnij jako obraz</string>\n    <string name=\"max_selection_limit\">Maksymalny limit wyboru</string>\n    <string name=\"share_selected\">Udostępnij wybrane</string>\n    <string name=\"remove_from_cache\">Usuń z pamięci podręcznej</string>\n    <string name=\"copy_link\">Skopiuj link</string>\n    <string name=\"dislike_all\">Odlajkuj wszystko</string>\n    <string name=\"similar_content\">Podobna zawartość</string>\n    <string name=\"link_copied\">Link skopiowany do schowka</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d raz</item>\n        <item quantity=\"few\">%d razy</item>\n        <item quantity=\"many\">%d razy</item>\n        <item quantity=\"other\">%d razy</item>\n    </plurals>\n    <string name=\"player_buttons_style\">Kolory przycisków odtwarzacza</string>\n    <string name=\"enable_swipe_thumbnail\">Włącz przesunięcie, aby zmienić utwór</string>\n    <string name=\"swipe_song_to_add\">Przesuń utwór w prawo, aby odtworzyć następny lub w lewo, aby dodać go do kolejki</string>\n    <string name=\"lyrics_click_change\">Zmiana tekstu po kliknięciu</string>\n    <string name=\"slim\">Wąski / Smukły</string>\n    <string name=\"slim_navbar\">Smukły pasek nawigacji</string>\n    <string name=\"auto_playlists\">Automatyczna lista odtwarzania</string>\n    <string name=\"show_liked_playlist\">Pokaż playlistę \\\"polubione\\\"</string>\n    <string name=\"show_top_playlist\">Pokaż playlistę \\\"top\\\"</string>\n    <string name=\"advanced_login\">Logowanie za pomocą tokena</string>\n    <string name=\"token_hidden\">Stuknij, aby wyświetlić token</string>\n    <string name=\"token_shown\">Naciśnij ponownie, aby skopiować lub edytować</string>\n    <string name=\"app_language\">Język aplikacji</string>\n    <string name=\"enable_similar_content\">Włącz podobną zawartość</string>\n    <string name=\"auto_download_on_like\">Automatyczne pobieranie polubionych utwórów</string>\n    <string name=\"auto_download_on_like_desc\">Automatycznie pobieraj utwory, gdy je polubisz</string>\n    <string name=\"clear_song_cache_dialog\">Czy na pewno chcesz wyczyścić wszystkie utwory z pamięci podręcznej?</string>\n    <string name=\"show_cached_playlist\">Pokaż listę odtwarzania z pamięci podręcznej</string>\n    <string name=\"not_logged_in_youtube\">Niezalogowany do YouTube</string>\n    <string name=\"default_links\">Otwórz obsługiwane linki</string>\n    <string name=\"open_app_settings_error\">Nie można otworzyć ustawień aplikacji</string>\n    <string name=\"release_notes\">Informacje o wersji</string>\n    <string name=\"history_duration\">Czas trwania historii</string>\n    <string name=\"dislikes\">Dislajki</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">%d sekunda</item>\n        <item quantity=\"few\">%d sekund</item>\n        <item quantity=\"many\">%d sekund</item>\n        <item quantity=\"other\">%d sekund</item>\n    </plurals>\n    <string name=\"lyrics_auto_scroll\">Autoprzesuwanie tekstu</string>\n    <string name=\"import_online\">Importuj listę odtwarzania \\\"m3u\\\"</string>\n    <string name=\"import_csv\">Importuj playlistę CSV</string>\n    <string name=\"playlist_add_local_to_synced_note\">Notatka: Dodawanie lokalnej muzyki do synchronizowanej/zewnętrznej listy odtworzeń nie jest wspierane. Wszelkie inne kombinacje są poprawne</string>\n    <string name=\"lyrics_romanize_japanese\">Romanizuj teksty japońskie</string>\n    <string name=\"lyrics_romanize_korean\">Romanizuj teksty koreańskie</string>\n    <string name=\"yt_sync\">Automatyczna synchronizacja z kontem</string>\n    <string name=\"more_content\">Więcej zawartości</string>\n    <string name=\"swipe_sensitivity\">Czułość przesunięcia</string>\n    <string name=\"clear_image_cache_dialog\">Czy na pewno chcesz wyczyścić wszystkie obrazy z pamięci podręcznej?</string>\n    <string name=\"new_player_design\">Nowy wygląd odtwarzacza</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"disable\">Wyłączyć</string>\n    <string name=\"new_mini_player_design\">Nowy wygląd małego odwarzacza</string>\n    <string name=\"subscribe\">Subskrybuj</string>\n    <string name=\"subscribed\">Zasubskrybowano</string>\n    <string name=\"now_playing\">Teraz odtwarzane</string>\n    <string name=\"close\">Zamknij</string>\n    <string name=\"hide_player_thumbnail\">Ukryj miniaturę odtwarzacza</string>\n    <string name=\"hide_player_thumbnail_desc\">Zastąpienie okładki albumu logo aplikacji w odtwarzaczu</string>\n    <string name=\"seek_forward_dynamic\">+%1$d sekund do przodu</string>\n    <string name=\"seek_backward_dynamic\">-%1$d sekund wstecz</string>\n    <string name=\"seek_seconds_addup\">Progresywne wyszukiwanie</string>\n    <string name=\"seek_seconds_addup_description\">Jeśli włączone, dodaje 5 dodatkowych sekund przy każdym pominięciu wyszukiwania</string>\n    <string name=\"disable_load_more_when_repeat_all\">Wyłącz ładowanie więcej po powtórzeniu wszystkich</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Nie ładuj automatycznie większej liczby utworów i podobnych treści, gdy włączony jest tryb powtarzania wszystkich utworów</string>\n    <string name=\"settings_section_ui\">Interfejs</string>\n    <string name=\"settings_section_privacy\">Prywatność i bezpieczeństwo</string>\n    <string name=\"settings_section_player_content\">Odtwarzacz i zawartość</string>\n    <string name=\"settings_section_storage\">Pamięć i dane</string>\n    <string name=\"settings_section_system\">System i informacje</string>\n    <string name=\"starting_radio\">Uruchamianie radia</string>\n    <string name=\"edit_playlist_cover\">Edytuj okładkę playlisty</string>\n    <string name=\"edit_playlist_cover_note\">Uwaga: Twoje konto musi być powiązane z numerem telefonu i zweryfikowane w YouTube Music, aby zmienić okładkę playlisty.</string>\n    <string name=\"edit_playlist_cover_note_wait\">Po wybraniu obrazu, zaczekaj chwilę, aż nowa okładka pojawi się na twojej playliście.</string>\n    <string name=\"choose_from_library\">Wybierz z biblioteki</string>\n    <string name=\"remove_custom_image\">Usuń niestandardowy obraz</string>\n    <string name=\"config_proxy\">Skonfiguruj proxy</string>\n    <string name=\"proxy_username\">Nazwa użytkownika proxy</string>\n    <string name=\"proxy_password\">Hasło proxy</string>\n    <string name=\"enable_authentication\">Włącz uwierzytelnianie</string>\n    <string name=\"lyrics_romanization_cyrillic\">Cyrylica</string>\n    <string name=\"lyrics_romanize_title\">Romanizacja</string>\n    <string name=\"lyrics_romanization\">Romanizacja tekstu</string>\n    <string name=\"lyrics_romanize_russian\">Romanizuj teksty rosyjskie</string>\n    <string name=\"lyrics_romanize_ukrainian\">Romanizuj teksty ukraińskie</string>\n    <string name=\"lyrics_romanize_belarusian\">Romanizuj teksty białoruskie</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Romanizuj teksty kirgiskie</string>\n    <string name=\"lyrics_romanize_serbian\">Romanizuj teksty serbskie</string>\n    <string name=\"lyrics_romanize_bulgarian\">Romanizuj teksty bułgarskie</string>\n    <string name=\"line_by_line_option_title\">EKSPERYMENTALNE: Wykrywanie języka linijka po linijce</string>\n    <string name=\"line_by_line_option_desc\">Język cyrylicy będzie wykrywany wiersz po wierszu, zamiast w całej piosence.</string>\n    <string name=\"line_by_line_dialog_title\">Jesteś pewien?</string>\n    <string name=\"line_by_line_dialog_desc\">Funkcja eksperymentalna.\\n\\nDomyślnie język jest określany na podstawie całego utworu, ale po włączeniu tej opcji będzie on określany wiersz po wierszu. Umożliwi to działanie utworów wielojęzycznych, ALE język może nie zawsze być poprawny (na przykład, jeśli tekst po ukraińsku nie zawiera liter charakterystycznych dla tego języka, może zostać zromanizowany na rosyjski).\\n\\nJeśli nie masz problemów, zalecamy wyłączenie tej opcji.</string>\n    <string name=\"romanize_current_track\">Romanizuj bieżący utwór</string>\n    <string name=\"audio_offload\">Włącz odciążenie</string>\n    <string name=\"audio_offload_description\">Użyj ścieżki audio do odtwarzania dźwięku. Wyłączenie tej opcji może zwiększyć zużycie energii, ale może być przydatne w przypadku problemów z odtwarzaniem dźwięku lub jego przetwarzaniem</string>\n    <string name=\"uploaded_playlist\">Przesłane</string>\n    <string name=\"filter_uploaded\">Przesłane</string>\n    <string name=\"show_uploaded_playlist\">Pokaż playlistę \\\"Przesłane\\\"</string>\n    <string name=\"discord_use_details\">Używaj detali zamiast stanu</string>\n    <string name=\"discord_use_details_description\">Pokaż wyraźnie nazwy utworów zamiast artystów</string>\n    <string name=\"updater\">Aktualizacje</string>\n    <string name=\"check_for_updates\">Automatycznie sprawdzaj dostępność aktualizacji</string>\n    <string name=\"update_notifications\">Włącz powiadomienia o aktualizacjach</string>\n    <string name=\"update_available_title\">Aktualizacja dostępna</string>\n    <string name=\"update_channel_desc\">Powiadomienia o nowych wersjach</string>\n    <string name=\"lyrics_romanize_macedonian\">Romanizuj macedońskie teksty</string>\n    <string name=\"integrations\">Integracje</string>\n    <string name=\"username\">Nazwa użytkownika</string>\n    <string name=\"password\">Hasło</string>\n    <string name=\"lastfm_integration\">Integracja Last.fm</string>\n    <string name=\"enable_scrobbling\">Włącz przewijanie</string>\n    <string name=\"lastfm_now_playing\">Wyślij Teraz Odtwarzane</string>\n    <string name=\"scrobbling_configuration\">Konfiguracja scrobblowania</string>\n    <string name=\"primary_color_style\">Kolor podstawowy</string>\n    <string name=\"tertiary_color_style\">Kolor trzeciorzędny</string>\n    <string name=\"swipe_song_to_remove\">Przesuń utwór, aby usunąć go z listy odtwarzania</string>\n    <string name=\"auto_scroll\">Ponowna synchronizacja</string>\n    <string name=\"lyrics_romanize_chinese\">Romanizuj chińskie teksty</string>\n    <string name=\"update_channel_name\">Aktualizacje aplikacji</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"google_cast_description\">Włącz przesyłanie dźwięku do Chromecasta i innych urządzeń obsługujących Cast</string>\n    <string name=\"last_fm_send_likes\">Wyślij polubienia/niepolubienia</string>\n    <string name=\"last_fm_send_likes_description\">Polub/niepolub piosenki w Last.fm, gdy są polubione/niepolubione w Metrolist</string>\n    <string name=\"logging_in\">Logowanie…</string>\n    <string name=\"hide_video_songs\">Ukryj utwory wideo</string>\n    <string name=\"details_desc\">Wyświetl informacje o utworze</string>\n    <string name=\"edit_desc\">Zmień tytuł lub artystę</string>\n    <string name=\"start_radio_desc\">Utwórz stację na podstawie tego elementu</string>\n    <string name=\"play_next_desc\">Dodaj na górę kolejki</string>\n    <string name=\"add_to_queue_desc\">Dodaj na dół kolejki</string>\n    <string name=\"add_to_library_desc\">Zapisz w swojej bibliotece</string>\n    <string name=\"download_desc\">Udostępnij do odtwarzania offline</string>\n    <string name=\"add_to_playlist_desc\">Dodaj do jednej ze swoich list odtwarzania</string>\n    <string name=\"refetch_desc\">Pobierz najnowsze metadane z YouTube Music</string>\n    <string name=\"share_desc\">Udostępnij link do tego elementu</string>\n    <string name=\"delete_desc\">Trwale usuń ten element</string>\n    <string name=\"advanced_desc\">Zmień tempo i wysokość dźwięku utworu</string>\n    <string name=\"equalizer_desc\">Dostosuj korektor dźwięku</string>\n    <string name=\"enable_dynamic_icon\">Włącz ikonę dynamiczną</string>\n    <string name=\"mini_player\">Miniodtwarzacz</string>\n    <string name=\"cache_size_warning_title\">Czekaj!</string>\n    <string name=\"cache_size_warning_message\">Wybrano limit rozmiaru pamięci podręcznej mniejszy niż ten, którego aplikacja aktualnie używa (%1$s). Jeśli będziesz kontynuować, aplikacja może usunąć część pamięci podręcznej %2$s, aby dopasować ją do nowego limitu. Czy mimo to kontynuować?</string>\n    <string name=\"cache_size_warning_confirm\">Kontynuować</string>\n    <string name=\"download_playlist_desc\">Pobierz utwory do odtwarzania w trybie offline</string>\n    <string name=\"remove_download_playlist_desc\">Usuń wszystkie pobrane utwory z tej playlisty</string>\n    <string name=\"download_in_progress_desc\">Pobieranie w trakcie</string>\n    <string name=\"share_playlist_desc\">Podziel się playlistą z innymi</string>\n    <string name=\"delete_playlist_desc\">Trwale usuń playlistę</string>\n    <string name=\"sync_playlist_desc\">Synchronizuj playlistę z YouTube Music</string>\n    <string name=\"wavy\">Fala</string>\n    <string name=\"lyrics_glow_effect\">Włącz efekt podświetlenia tekstu</string>\n    <string name=\"lyrics_glow_effect_desc\">Dodaj efekty podświetlenia i odbicia do aktywnego tekstu</string>\n    <string name=\"enable_better_lyrics\">Włącz \\\"Better Lyrics\\\"</string>\n    <string name=\"enable_better_lyrics_desc\">Tekst piosenki zsynchronizowany na poziomie sylab, idealny do karaoke</string>\n    <string name=\"shuffle_playlist_first\">Najpierw odtwórz losowo playlistę/album</string>\n    <string name=\"shuffle_playlist_first_desc\">Podczas odtwarzania losowego najpierw odtwarzaj wszystkie utwory z oryginalnej playlisty/albumu, a potem podobne utwory</string>\n    <string name=\"show_wrapped_card\">Wyświetl kartę Wrapped</string>\n    <string name=\"scrobble_min_track_duration\">Scrobbluj utwory dłuższe niż</string>\n    <string name=\"scrobble_delay_percent\">Procent opóźnienia scrobblowania</string>\n    <string name=\"scrobble_delay_minutes\">Opóźnienie scrobblowania w minutach</string>\n    <string name=\"pure_black_mini_player\">Miniodtwarzacz w czystej czerni</string>\n    <string name=\"lyrics_animation_style\">Styl animacji synchronizowanych tekstów</string>\n    <string name=\"none\">Brak</string>\n    <string name=\"fade\">Zanik</string>\n    <string name=\"glow\">Poświata</string>\n    <string name=\"slide\">Przesunięcie</string>\n    <string name=\"karaoke\">Karaoke</string>\n    <string name=\"apple_music_style\">Apple Music</string>\n    <string name=\"lyrics_text_size\">Wielkość tekstu utworów</string>\n    <string name=\"lyrics_line_spacing\">Odstępy między wierszami</string>\n    <string name=\"album_art_for\">Okładka albumu %s</string>\n    <string name=\"wrapped_total_albums_title\">Słuchałeś/aś</string>\n    <string name=\"wrapped_total_albums_subtitle\">Unikalne albumy</string>\n    <string name=\"wrapped_top_album_title\">Twój ulubiony album to</string>\n    <string name=\"wrapped_playlist_ready\">Twoja osobista lista odtwarzania jest gotowa</string>\n    <string name=\"wrapped_top_5_albums_title\">Twoje 5 ulubionych albumów</string>\n    <string name=\"wrapped_album_listening_time\">Słuchałeś/aś tego albumu przez %d minut</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d minut(-y)</string>\n    <string name=\"wrapped_no_data\">Brak danych</string>\n    <string name=\"wrapped_top_5_artists_title\">Twoi najlepsi artyści roku</string>\n    <string name=\"wrapped_artist_listening_time\">%d minut(-y)</string>\n    <string name=\"wrapped_top_5_songs_title\">Twoje ulubione utwory tego roku</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">Okładka albumu</string>\n    <string name=\"wrapped_top_artist_title\">Twoim ulubionym artystą tego roku jest</string>\n    <string name=\"wrapped_top_artist_image_content_description\">Zdjęcie najlepszego artysty</string>\n    <string name=\"wrapped_top_artist_listening_time\">Słuchałeś/aś ich przez %d minut</string>\n    <string name=\"wrapped_top_song_title\">Twój najczęściej odtwarzany utwór to</string>\n    <string name=\"wrapped_top_song_listening_time\">Słuchałeś/aś przez %d minut</string>\n    <string name=\"wrapped_total_artists_title\">Słuchałeś/aś</string>\n    <string name=\"wrapped_total_artists_subtitle\">Unikalni artyści</string>\n    <string name=\"wrapped_total_songs_title\">Słuchałeś/aś</string>\n    <string name=\"wrapped_total_songs_subtitle\">Unikalnie piosenki</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_intro_subtitle\">Czas sprawdzić, czego słuchałeś/aś</string>\n    <string name=\"wrapped_intro_button\">Zaczynamy!</string>\n    <string name=\"wrapped_logo_content_description\">Logo Metrolist</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"wrapped_ready_title\">­TWOJE WRAPPED JEST GOTOWE!</string>\n    <string name=\"wrapped_ready_subtitle\">Czas sprawdzić, co pokochałeś/aś w tym roku.</string>\n    <string name=\"wrapped_thank_you\">Dziękujemy za słuchanie</string>\n    <string name=\"wrapped_special_thanks\">Specjalne podziękowania dla MO Agamy za stworzenie Metrolist</string>\n    <string name=\"wrapped_close\">Zamknij Wrapped</string>\n    <string name=\"wrapped_playlist_title\">Twoje %s Wrapped</string>\n    <string name=\"wrapped_create_playlist\">Utwórz playlistę</string>\n    <string name=\"wrapped_playlist_saved\">Zapisano playlistę</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"one\">%d Profil</item>\n        <item quantity=\"few\">%d Profili</item>\n        <item quantity=\"many\">%d Profili</item>\n        <item quantity=\"other\">%d Profili</item>\n    </plurals>\n    <string name=\"equalizer_header\">Korektor dźwięku</string>\n    <string name=\"no_profiles\">Brak profili korektora dźwięku</string>\n    <string name=\"import_profile\">Zaimportuj profil</string>\n    <string name=\"eq_disabled\">Wyłączony</string>\n    <plurals name=\"band_count\">\n        <item quantity=\"one\">%d zespół</item>\n        <item quantity=\"few\">%d zespoły</item>\n        <item quantity=\"many\">%d zespoły</item>\n        <item quantity=\"other\">%d zespoły</item>\n    </plurals>\n    <string name=\"delete_profile_desc\">Usuń profil</string>\n    <string name=\"delete_profile_confirmation\">Czy na pewno chcesz usunąć %1$s? Tej akcji nie można cofnąć.</string>\n    <string name=\"error_file_read\">Nie można odczytać pliku</string>\n    <string name=\"error_file_open\">Nie udało się otworzyć pliku: %1$s</string>\n    <string name=\"import_error_title\">Błąd importu</string>\n    <string name=\"casting_to\">Wysyłanie do %s</string>\n    <string name=\"progress_percent\">Postęp %s%%</string>\n    <string name=\"listening_to_metrolist\">Słuchasz Metrolist</string>\n    <string name=\"open\">Otwórz</string>\n    <string name=\"failed_to_create_image\">Nie udało się utworzyć obrazu: %s</string>\n    <string name=\"copied_title\">Skopiowano Tytuł</string>\n    <string name=\"copied_artist\">Skopiowano Wykonawcę</string>\n    <string name=\"error_playing\">Błąd odtwarzania</string>\n    <string name=\"failed_to_parse_proxy\">Nie udało się przetworzyć adresu URL proxy.</string>\n    <string name=\"about_artist\">O artyście</string>\n    <string name=\"show_more\">Pokaż więcej</string>\n    <string name=\"show_less\">Pokaż mniej</string>\n    <string name=\"artist_page_settings\">Profil artysty</string>\n    <string name=\"show_artist_description\">Więcej o artyście</string>\n    <string name=\"show_artist_subscriber_count\">Pokaż liczbę obserwujących</string>\n    <string name=\"show_artist_monthly_listeners\">Miesięczna liczba słuchaczy</string>\n    <string name=\"crop_album_art\">Przytnij okładkę</string>\n    <string name=\"crop_album_art_desc\">Wymuś kwadratowe miniatury wideo</string>\n    <string name=\"enable_simpmusic\">Pokazuj teksty z SimpMusic</string>\n    <string name=\"enable_simpmusic_desc\">Tekst automatycznie pozyskany z Musixmatch i YouTube Transcript</string>\n    <string name=\"skip_silence_desc\">Pomijaj ciszę w utworach</string>\n    <string name=\"skip_silence_instant\">Natychmiast pomijaj ciszę</string>\n    <string name=\"like\">Polub</string>\n    <string name=\"widget_description\">Widżet odtwarzacza z przyciskami sterowania</string>\n    <string name=\"system_equalizer\">Korektor systemowy</string>\n    <string name=\"skip_silence_instant_desc\">Pomijaj ciszę bez zmiany tempa odtwarzania</string>\n    <string name=\"persistent_shuffle_title\">Trwałe mieszanie utworów</string>\n    <string name=\"persistent_shuffle_desc\">Zachowaj tryb mieszania po zmianie utworu lub playlisty</string>\n    <string name=\"remember_shuffle_and_repeat\">Zapamiętaj mieszanie i powtarzanie</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Zapamiętaj tryb mieszania i powtarzania po ponownym uruchomieniu aplikacji</string>\n    <string name=\"pause_music_when_media_is_muted\">Wstrzymaj po wyciszeniu dźwięku</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">Nie wygaszaj ekranu w widoku odtwarzacza</string>\n    <string name=\"lyrics_offset\">Synchronizacja tekstu</string>\n    <string name=\"error_title\">Błąd</string>\n    <string name=\"error_eq_apply_failed\">Nie udało się zastosować profilu korektora: %1$s</string>\n    <string name=\"error_playback_failed\">Nie udało się odtworzyć utworu</string>\n    <string name=\"album_art\">Okładka albumu</string>\n    <string name=\"no_song_playing\">Nic nie jest odtwarzane</string>\n    <string name=\"tap_to_open\">Dotknij, aby otworzyć Metrolist</string>\n    <string name=\"previous\">Poprzedni</string>\n    <string name=\"play_pause\">Odtwórz/Wstrzymaj</string>\n    <string name=\"next\">Następny</string>\n    <string name=\"turntable_widget_description\">Okrągły widżet ze sterowaniem odtwarzania muzyki</string>\n    <string name=\"deleting\">Usuwanie…</string>\n    <string name=\"deleted_n_songs\">Usunięto %1$d piosenek</string>\n    <string name=\"filter_profiles\">Profile</string>\n    <string name=\"filter_channels\">Kanały</string>\n    <string name=\"auto_playlist\">Automatyczna playlista</string>\n    <string name=\"downloaded_episodes\">Pobrane odcinki</string>\n    <string name=\"no_downloaded_episodes\">Brak pobranych odcinków</string>\n    <string name=\"no_subscribed_channels\">Brak zasubskrybowanych kanałów</string>\n    <plurals name=\"n_channel\">\n        <item quantity=\"one\">%d kanał</item>\n        <item quantity=\"few\">%d kanały</item>\n        <item quantity=\"many\">%d kanałów</item>\n        <item quantity=\"other\">%d kanałów</item>\n    </plurals>\n    <string name=\"restore_confirm_title\">Przywrócić kopię?</string>\n    <string name=\"enable\">Włącz</string>\n    <string name=\"player_background_solid\">Jednolity</string>\n    <string name=\"display_density\">Gęstość wyświetlania</string>\n    <string name=\"restart\">Uruchom ponownie</string>\n    <string name=\"restart_required\">Wymagane ponowne uruchomienie</string>\n    <string name=\"density_restart_message\">Zmiana gęstości wyświetlania zacznie obowiązywać po ponownym uruchomieniu aplikacji. Uruchomić ją ponownie teraz?</string>\n    <string name=\"enable_lyricsplus\">Włącz LyricsPlus</string>\n    <string name=\"enable_lyricsplus_desc\">Synchronizuje teksty piosenek z różnych zródeł</string>\n    <string name=\"lyrics_provider_selection\">Wybór dostawcy tekstów utworów</string>\n    <string name=\"lyrics_provider_selection_desc\">Wybierz, których dostawców tekstów utworów chcesz używać</string>\n    <string name=\"prevent_duplicate_tracks_in_queue\">Zapobiegaj duplikatom utworów w kolejce</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">Gdy dodajesz utwór do kolejki, usuwa jego wcześniejszą pozycję, jeśli był już w niej obecny</string>\n    <string name=\"lyrics_provider_priority_desc\">Przeciągnij, aby zmienić kolejność dostawców tekstów utworów. Wyższa pozycja -&gt; wyższy priorytet.</string>\n    <string name=\"enable_lrclib_desc\">Synchronizowana baza danych tekstów utworów tworzona przez społeczność</string>\n    <string name=\"enable_kugou_desc\">Pobiera teksty utworów z KuGou, popularnej chińskiej platformy muzycznej</string>\n    <string name=\"youtube_music_lyrics_note\">UWAGA: Teksty z YouTube Music będą wyświetlane automatycznie, gdy inne teksty nie są dostępne. Teksty z YTM zwykle nie są synchronizowane.</string>\n    <string name=\"lyrics_provider_priority\">Kolejność dostawców tekstów utworów</string>\n    <string name=\"listen_together_server_url\">Serwer URL</string>\n    <string name=\"listen_together_username\">Nazwa użytkownika</string>\n    <string name=\"listen_together_connected\">Połączono</string>\n    <string name=\"listen_together_reconnecting\">Ponowne łączenie…</string>\n    <string name=\"listen_together_disconnected\">Rozłączono</string>\n    <string name=\"listen_together_connecting\">Łączenie…</string>\n    <string name=\"listen_together_error\">Błąd połączenia</string>\n    <string name=\"listen_together_create_room\">Utwórz serwer</string>\n    <string name=\"listen_together_create_room_desc\">Utwórz serwer i podziel się kodem ze znajomymi</string>\n    <string name=\"listen_together_join_room\">Dołącz</string>\n    <string name=\"listen_together_room_code\">Kod serwera</string>\n    <string name=\"listen_together_you_are_host\">Jesteś hostem serwera</string>\n    <string name=\"listen_together_you_are_guest\">Jesteś gościem</string>\n    <string name=\"listen_together_join_requests\">Prośby o dołączenie</string>\n    <string name=\"listen_together_view_logs\">Zobacz logi</string>\n    <string name=\"listen_together_view_logs_desc\">Debug połączeń i wiadomości</string>\n    <string name=\"listen_together_logs\">Logi połączenia</string>\n    <string name=\"listen_together_no_logs\">Na razie brak logów</string>\n    <string name=\"listen_together_description\">Słuchaj muzyki razem ze znajomymi w czasie rzeczywistym. Stwórz pokój aby stać się hostem lub dołącz do istniejącego serwera za pomocą kodu.</string>\n    <string name=\"listen_together_background_disconnect_note\">Uwaga: Możesz zostać rozłączony, jeśli utworzysz serwer, gdy nie gra żadna muzyka, a następnie przełączysz się na inną aplikację.</string>\n    <string name=\"listen_together_not_configured\">\\\"Słuchaj razem\\\" nie zostało skonfigurowane. Proszę ustawić URL serwera w Ustawienia → Integracje → Słuchaj razem.</string>\n    <string name=\"listen_together_suggestion_received\">%1$s poprosił %2$s</string>\n    <string name=\"listen_together_suggestion_sent\">Sugestia została wysłana do hosta!</string>\n    <string name=\"listen_together_join_request_notification\">%1$s chce dołączyć do serwera</string>\n    <string name=\"listen_together_notification_channel_name\">Słuchaj razem</string>\n    <string name=\"changelog\">Lista zmian</string>\n    <string name=\"changelog_empty\">Brak dostępnych dzienników zmian</string>\n    <string name=\"github_releases_url\">https://github.com/MetrolistGroup/Metrolist/releases</string>\n    <string name=\"view_on_github\">Zobacz na GitHub</string>\n    <string name=\"current_version\">Obecna wersja</string>\n    <string name=\"version_format\">Wersja: %s</string>\n    <string name=\"update_settings\">Zaktualizuj ustawienia</string>\n    <string name=\"check_for_updates_title\">Sprawdź dostępność aktualizacji</string>\n    <string name=\"checking_for_updates\">Sprawdzanie dostępności aktualizacji…</string>\n    <string name=\"check_for_updates_button\">Sprawdź dostępność aktualizacji</string>\n    <string name=\"hide_changelog\">Ukryj dziennik zmian</string>\n    <string name=\"view_changelog\">Pokaż dziennik zmian</string>\n    <string name=\"failed_to_check_updates\">Nie udało się sprawdzić dostępności aktualizacji: %s</string>\n    <string name=\"set_as_default\">Ustaw jako domyślny</string>\n    <string name=\"sleep_timer_default_set\">Wyłącznik czasowy domyślnie ustawiony na %d min</string>\n    <string name=\"enable_automatic_sleeptimer\">Uruchom automatyczny wyłącznik czasowy</string>\n    <string name=\"sleep_timer_repeat\">Powtarzaj</string>\n    <string name=\"sleep_timer_daily\">Codziennie</string>\n    <string name=\"sleep_timer_weekdays\">Od poniedziałku do piątku</string>\n    <string name=\"sleep_timer_weekdays_weekends\">Dni tygodnia / Weekendy</string>\n    <string name=\"sleep_timer_weekends\">Weekendy (Sob-Niedz)</string>\n    <string name=\"sleep_timer_start_time\">Godzina rozpoczęcia</string>\n    <string name=\"sleep_timer_end_time\">Godzina zakończenia</string>\n    <string name=\"sleep_timer_monday\">Poniedziałek</string>\n    <string name=\"sleep_timer_tuesday\">Wtorek</string>\n    <string name=\"sleep_timer_wednesday\">Środa</string>\n    <string name=\"sleep_timer_thursday\">Czwartek</string>\n    <string name=\"sleep_timer_friday\">Piątek</string>\n    <string name=\"sleep_timer_saturday\">Sobota</string>\n    <string name=\"sleep_timer_sunday\">Niedziela</string>\n    <string name=\"sleep_timer_fade_out\">Stopniowe wyciszanie w ostatniej minucie</string>\n    <string name=\"resume_on_bluetooth_connect\">Wznów po podłączeniu Bluetooth</string>\n    <string name=\"crossfade\">Crossfade</string>\n    <string name=\"crossfade_desc\">Płynne przejście między utworami</string>\n    <string name=\"crossfade_duration\">Czas płynnego przejścia między utworami</string>\n    <string name=\"crossfade_gapless\">Wyłącz dla albumów bez przerw</string>\n    <string name=\"crossfade_gapless_desc\">Nie stosuj płynnego przejścia jeśli album jest bez przerw</string>\n    <string name=\"crossfade_beta_title\">Funkcja Beta</string>\n    <string name=\"crossfade_beta_message\">Crossfade to nowa funkcja i może zawierać błędy. Jeśli napotkasz jakiekolwiek problemy, prosimy o ich zgłoszenie\\n\\nTa funkcja wyłączna odciążane audio z powodu ograniczeń technicznych.</string>\n    <string name=\"lyrics_romanize_hindi\">Romanizuj tekst Hindi</string>\n    <string name=\"lyrics_romanize_punjabi\">Romanizuj teksty w pendżabskim</string>\n    <string name=\"audio_offload_disabled_by_crossfade\">Wyłączone ponieważ włączono Crossfade</string>\n    <string name=\"hide_youtube_shorts\">Ukryj YouTube Shorts</string>\n    <string name=\"export_playlist\">Eksportuj playlistę</string>\n    <string name=\"export_as_csv\">Eksportuj jako CSV</string>\n    <string name=\"export_as_m3u\">Eksportuj jako M3U</string>\n    <string name=\"export_success\">Playlista wyeksportowana pomyślnie</string>\n    <string name=\"export_failed\">Nie udało się wyeksportować playlisty</string>\n    <string name=\"export_option_share\">Udostępnij</string>\n    <string name=\"export_option_save\">Zapisz w Dokumentach</string>\n    <string name=\"list_bullet\">•</string>\n    <string name=\"latest_version_format\">Najnowsza: %s</string>\n    <string name=\"sleeptimer_description\">Automatycznie włącza wyłącznik czasowy z domyślną wartością o niestandardowym czasie</string>\n    <string name=\"sleep_timer_repeat_description\">Ustaw niestandardową godzinę, o której wyłącznik czasowy powinien się automatycznie aktywować</string>\n    <string name=\"sleep_timer_custom\">Niestandardowy</string>\n    <string name=\"sleep_timer_stop_after_current_song\">Zatrzymaj na końcu bieżącego utworu po zakończeniu odliczania czasu</string>\n    <string name=\"lyrics_romanize_as_main\">Pokaż zromanizowane teksty jako główne</string>\n    <string name=\"found_in_settings_content\">Znajdujące się w Ustawieniach &gt; Zawartość</string>\n    <string name=\"plays\">odtworzeń</string>\n    <string name=\"error_episode_save\">Nie udało się zapisać epizodu</string>\n    <string name=\"error_episode_remove\">Nie udało się usunąć epizodu</string>\n    <string name=\"error_podcast_subscribe\">Nie udało się zasubskrybować do podcastu</string>\n    <string name=\"error_podcast_unsubscribe\">Nie udało się odsubskrybować od podcastu</string>\n    <string name=\"view_channel\">Pokaż Kanał</string>\n    <string name=\"not_playing\">Brak odtwarzanego utworu</string>\n    <string name=\"tap_to_play\">Dotknij, aby otworzyć Metrolist</string>\n    <string name=\"widget_music_player\">Odtwarzacz muzyki</string>\n    <string name=\"widget_turntable\">Obrotnica</string>\n    <string name=\"widget_recognizer_name\">Rozpoznawanie Muzyki</string>\n    <string name=\"widget_recognizer_description\">Identyfikuj utwory odtwarzane dookoła ciebie bezpośrednio z ekranu głównego</string>\n    <string name=\"widget_recognizer_tap_to_search\">Dotknij, aby zidentyfikować piosenkę</string>\n    <string name=\"widget_recognizer_listening\">Słuchanie…</string>\n    <string name=\"widget_recognizer_processing\">Identyfikowanie…</string>\n    <string name=\"widget_recognizer_no_match\">Brak pasujących wyników. Spróbuj ponownie</string>\n    <string name=\"widget_recognizer_error\">Rozpoznanie nie powiodło się</string>\n    <string name=\"widget_recognizer_error_generic\">Wystąpił błąd. Spróbuj ponownie</string>\n    <string name=\"widget_recognizer_unknown_song\">Nieznana piosenka</string>\n    <string name=\"widget_recognizer_unknown_artist\">Nieznany artysta</string>\n    <string name=\"widget_recognizer_mic_desc\">Zidentyfikuj piosenkę</string>\n    <string name=\"widget_recognizer_channel_name\">Rozpoznawanie muzyki</string>\n    <string name=\"widget_recognizer_channel_desc\">Wyświetla powiadomienie podczas identyfikowania utworu z widżetu</string>\n    <string name=\"widget_recognizer_notification_text\">Nagrywanie dźwięku w celu identyfikacji utworu…</string>\n    <string name=\"together\">Razem</string>\n    <string name=\"listen_together\">Słuchanie Razem</string>\n    <string name=\"listen_together_choose_server\">Wybierz serwer</string>\n    <string name=\"listen_together_custom_server\">Niestandardowy serwer</string>\n    <string name=\"listen_together_use_custom_server\">Użyj niestandardowego serwera</string>\n    <string name=\"mute\">Wycisz</string>\n    <string name=\"unmute\">Odcisz</string>\n    <string name=\"listen_together_auto_approval_joins\">Automatycznie zatwierdzaj prośby o dołączenie</string>\n    <string name=\"listen_together_auto_approval_joins_desc\">Automatycznie zatwierdza prośby o dołączenie zamiast przeglądania ich manualnie</string>\n    <string name=\"listen_together_auto_approval_suggestions\">Automatycznie zatwierdzaj sugestie piosenek</string>\n    <string name=\"listen_together_auto_approval_suggestions_desc\">Automatyczne zatwierdzanie i dodawanie do kolejki sugestii piosenek od gości</string>\n    <string name=\"listen_together_sync_volume\">Synchronizuj głośność hosta</string>\n    <string name=\"listen_together_sync_volume_desc\">Goście podążają za poziomem głośności gospodarza</string>\n    <string name=\"listen_together_in_top_bar\">Słuchaj Razem na górnym pasku</string>\n    <string name=\"listen_together_in_top_bar_desc\">Pokaż Słuchaj Razem na górnym pasku aplikacji zamiast na pasku nawigacyjnym</string>\n    <string name=\"listen_together_notification_channel_desc\">Powiadomienia o wydarzeniach w Słuchaj Razem</string>\n    <string name=\"listen_together_room_created\">Pokój stworzony: %s</string>\n    <string name=\"listen_together_cannot_edit_username_in_room\">Nie można edytować nazwy użytkownika będąc w pokoju</string>\n    <string name=\"waiting_for_approval\">Oczekiwanie na zatwierdzenie przez hosta</string>\n    <string name=\"invalid_room_code\">Nieprawidłowy kod pokoju</string>\n    <string name=\"join_request_denied\">Prośba o dołączenie odrzucona</string>\n    <string name=\"join_existing_room\">Dołącz do istniejącego pokoju</string>\n    <string name=\"room_code\">Kod pokoju</string>\n    <string name=\"leave_room\">Opuść pokój</string>\n    <string name=\"join_room\">Dołącz</string>\n    <string name=\"create_room\">Stwórz</string>\n    <string name=\"joining_room\">Dołączanie do pokoju %s…</string>\n    <string name=\"creating_room\">Tworzenie pokoju…</string>\n    <string name=\"connect\">Połącz</string>\n    <string name=\"disconnect\">Rozłącz</string>\n    <string name=\"create\">Stwórz</string>\n    <string name=\"join\">Dołącz</string>\n    <string name=\"approve\">Zatwierdź</string>\n    <string name=\"reject\">Odrzuć</string>\n    <string name=\"clear\">Wyczyść</string>\n    <string name=\"copy\">Skopiuj</string>\n    <string name=\"copied_to_clipboard\">Skopiowano do schowka</string>\n    <string name=\"not_set\">Nie ustawiono</string>\n    <string name=\"hosting_room\">Hostowanie pokoju</string>\n    <string name=\"in_room\">W pokoju</string>\n    <string name=\"pending_requests\">Oczekujące prośby</string>\n    <string name=\"pending_suggestions\">Oczekujące sugestie</string>\n    <string name=\"suggest_to_host\">Zaproponuj hostowi</string>\n    <string name=\"kick_user\">Wyrzuć</string>\n    <string name=\"host_label\">Host</string>\n    <string name=\"you_label\">Ty</string>\n    <string name=\"connected_users\">Połączeni użytkownicy</string>\n    <string name=\"enter_username\">Wprowadź nazwę użytkownika</string>\n    <string name=\"enter_room_code\">Wprowadź kod pokoju</string>\n    <string name=\"listen_together_settings_desc\">Skonfiguruj serwer, nazwę użytkownika i inne</string>\n    <string name=\"error_username_empty\">Nazwa użytkownika jest wymagana.</string>\n    <string name=\"resync\">Resynchronizuj</string>\n    <string name=\"copy_code\">Kopiuj kod</string>\n    <string name=\"kick_user_desc\">Usuń tę osobę z sesji</string>\n    <string name=\"permanently_kick_user\">Zablokuj na stałe</string>\n    <string name=\"permanently_kick_user_desc\">Zablokuj prośby o dołączenie tej osoby i ukryj jej sugestie</string>\n    <string name=\"transfer_ownership\">Przenieś Własność</string>\n    <string name=\"transfer_ownership_desc\">Uczyń tę osobę hostem pokoju</string>\n    <string name=\"manage_user\">Zarządzaj użytkownikiem</string>\n    <string name=\"listen_together_blocked_users\">Zablokowani użytkownicy</string>\n    <string name=\"listen_together_blocked_users_count\">%d użytkowników zablokowanych</string>\n    <string name=\"listen_together_no_blocked_users\">Brak zablokowanych użytkowników</string>\n    <string name=\"unblock\">Odblokuj</string>\n    <string name=\"user_blocked_by_host\">Użytkownik zablokowany przez hosta</string>\n    <string name=\"ai_lyrics_translation\">Tłumaczenie tekstów AI</string>\n    <string name=\"ai_translating_lyrics\">Tłumaczenie tekstu...</string>\n    <string name=\"ai_lyrics_translated\">Tekst przetłumaczony</string>\n    <string name=\"ai_provider\">Dostawca</string>\n    <string name=\"ai_base_url\">Podstawowy URL</string>\n    <string name=\"ai_api_key\">Klucz API</string>\n    <string name=\"ai_model\">Model</string>\n    <string name=\"ai_translation_mode\">Tryb Tłumaczenia</string>\n    <string name=\"ai_target_language\">Język Docelowy</string>\n    <string name=\"ai_setup_guide\">Klucze API</string>\n    <string name=\"ai_translation_literal\">Tłumaczenie</string>\n    <string name=\"ai_translation_literal_desc\">Przetłumacz znaczenie na język docelowy</string>\n    <string name=\"ai_translation_transcribed\">Transkrypcja</string>\n    <string name=\"ai_translation_transcribed_desc\">Konwertuj wymowę do skryptu docelowego</string>\n    <string name=\"ai_api_key_required\">Klucz API Wymagany</string>\n    <string name=\"ai_error_api_key_required\">Klucz API jest wymagany</string>\n    <string name=\"ai_error_no_lyrics\">Brak tekstu do przetłumaczenia</string>\n    <string name=\"ai_error_lyrics_empty\">Tekst jest pusty</string>\n    <string name=\"ai_error_language_required\">Język docelowy jest wymagany</string>\n    <string name=\"ai_error_unexpected\">Nieoczekiwany wynik tłumaczenia</string>\n    <string name=\"ai_error_unknown\">Wystąpił nieznany błąd</string>\n    <string name=\"ai_error_translation_failed\">Tłumaczenie nie powiodło się</string>\n    <string name=\"ai_provider_help\">Pozyskaj Klucze API</string>\n    <string name=\"ai_provider_openrouter_help\">Odwiedź https://openrouter.ai, aby zapoznać się z modelami darmowymi i płatnymi</string>\n    <string name=\"ai_provider_openai_help\">Odwiedź https://platform.openai.com/api-keys</string>\n    <string name=\"ai_provider_claude_help\">Odwiedź https://console.anthropic.com/settings/keys</string>\n    <string name=\"ai_provider_gemini_help\">Odwiedź https://aistudio.google.com/apikey</string>\n    <string name=\"ai_provider_perplexity_help\">Odwiedź https://perplexity.ai/settings/api</string>\n    <string name=\"ai_provider_xai_help\">Odwiedź https://console.x.ai</string>\n    <string name=\"ai_provider_deepl_help\">Odwiedź https://deepl.com/pro-api, aby uzyskać bezpłatne i płatne klucze</string>\n    <string name=\"ai_deepl_formality\">Formalność</string>\n    <string name=\"ai_deepl_formality_default\">Domyślna</string>\n    <string name=\"ai_deepl_formality_more\">Bardziej Formalna</string>\n    <string name=\"ai_deepl_formality_less\">Mniej Formalna</string>\n    <string name=\"crash_title\">Aplikacja uległa awarii</string>\n    <string name=\"crash_description\">Wystąpił nieoczekiwany błąd. Udostępnij raport o awarii, aby pomóc nam rozwiązać problem.</string>\n    <string name=\"crash_share_logs\">Udostępnij dzienniki</string>\n    <string name=\"crash_share_title\">Udostępnij raport o awarii</string>\n    <string name=\"crash_report_subject\">Raport o awarii Metrolist</string>\n    <string name=\"crash_close\">Zamknij</string>\n    <string name=\"crash_no_log\">Brak dostępnego dziennika awarii</string>\n    <string name=\"palette_dynamic\">Dynamiczna</string>\n    <string name=\"palette_crimson\">Karmazynowa</string>\n    <string name=\"palette_rose\">Różowa</string>\n    <string name=\"palette_purple\">Fioletowa</string>\n    <string name=\"palette_deep_purple\">Głęboko Fioletowa</string>\n    <string name=\"palette_indigo\">Indygo</string>\n    <string name=\"palette_blue\">Niebieska</string>\n    <string name=\"palette_sky_blue\">Błękitna</string>\n    <string name=\"palette_cyan\">Cyjanowa</string>\n    <string name=\"palette_teal\">Turkusowa</string>\n    <string name=\"palette_green\">Zielona</string>\n    <string name=\"palette_light_green\">Jasno Zielona</string>\n    <string name=\"palette_lime\">Limonkowa</string>\n    <string name=\"palette_yellow\">Żółta</string>\n    <string name=\"palette_amber\">Bursztynowa</string>\n    <string name=\"palette_orange\">Pomarańczowa</string>\n    <string name=\"palette_deep_orange\">Głęboko Pomarańczowa</string>\n    <string name=\"palette_brown\">Brązowa</string>\n    <string name=\"palette_grey\">Szara</string>\n    <string name=\"palette_blue_grey\">Niebiesko-szara</string>\n    <string name=\"cd_back\">Powrót</string>\n    <string name=\"cd_pure_black_mode\">Tryb czystej czerni</string>\n    <string name=\"cd_light_mode\">Tryb jasny</string>\n    <string name=\"cd_dark_mode\">Tryb ciemny</string>\n    <string name=\"cd_system_mode\">Tryb systemowy</string>\n    <string name=\"cd_palette_item\">paleta %1$s</string>\n    <string name=\"play_all\">Odtwórz wszystkie</string>\n    <string name=\"enable_high_refresh_rate\">Włącz wysoką częstotliwość odświeżania</string>\n    <string name=\"enable_high_refresh_rate_desc\">Wymuś działanie wyświetlacza z najwyższą obsługiwaną częstotliwością odświeżania (np. 120 Hz)</string>\n    <string name=\"recognize_music\">Rozpoznawanie Muzyki</string>\n    <string name=\"tap_to_recognize\">Dotknij, aby rozpoznać</string>\n    <string name=\"listening\">Słuchanie…</string>\n    <string name=\"processing\">Procesowanie…</string>\n    <string name=\"no_match_found\">Nie znaleziono dopasowania</string>\n    <string name=\"recognition_error\">Błąd rozpoznawania</string>\n    <string name=\"try_again\">Spróbuj ponownie</string>\n    <string name=\"recognition_history\">Historia Rozpoznawania</string>\n    <string name=\"clear_recognition_history\">Wyczyść historię rozpoznawania</string>\n    <string name=\"clear_recognition_history_confirm\">Czy na pewno chcesz wyczyścić całą historię rozpoznawania?</string>\n    <string name=\"delete_from_history\">Usuń z historii</string>\n    <string name=\"re_listen\">Posłuchaj ponownie</string>\n    <string name=\"play_on_app\">Odtwórz w Metrolist</string>\n    <string name=\"qs_tile_music_recognizer\">Rozpoznawaj muzykę</string>\n    <string name=\"map_csv_columns\">Mapuj kolumny CSV</string>\n    <string name=\"first_row_is_header\">Pierwszy wiersz to nagłówek</string>\n    <string name=\"artist_name_column\">Kolumna Nazwy artysty</string>\n    <string name=\"song_title_column\">Kolumna tytułu piosenki</string>\n    <string name=\"youtube_url_column\">Kolumna adresu URL YouTube (opcjonalnie)</string>\n    <string name=\"continue_action\">Kontynuuj</string>\n    <string name=\"importing_csv\">Importowanie CSV</string>\n    <string name=\"importing_playlist\">Importowanie Playlisty</string>\n    <string name=\"recently_converted\">Ostatnio Przekonwertowane</string>\n    <string name=\"column_label\">Kol %d</string>\n    <string name=\"discord_status\">Status</string>\n    <string name=\"discord_status_online\">Online</string>\n    <string name=\"discord_status_idle\">Bezczynny</string>\n    <string name=\"discord_status_dnd\">Nie przeszkadzać</string>\n    <string name=\"discord_buttons\">Przyciski</string>\n    <string name=\"discord_button_1\">Przycisk 1</string>\n    <string name=\"discord_button_2\">Przycisk 2</string>\n    <string name=\"login_successful\">Logowanie pomyślne!</string>\n    <string name=\"discord_information_warning\">Ta funkcja wykorzystuje bibliotekę KizzyRPC do połączenia z bramą Discorda i ustawienia statusu Rich Presence. Chociaż nie odnotowano żadnych przypadków zawieszenia konta z powodu podobnego użycia, ta metoda nie jest oficjalnie obsługiwana przez Discord i może zostać uznana za naruszenie Warunków korzystania z usługi. Twój token jest pobierany lokalnie i nigdy nie jest wysyłany na serwery zewnętrzne. Postępuj według własnego uznania.</string>\n    <string name=\"discord_activity_type\">Rodzaj aktywności</string>\n    <string name=\"discord_activity_playing\">Gra</string>\n    <string name=\"discord_activity_listening\">Słucha</string>\n    <string name=\"discord_activity_watching\">Ogląda</string>\n    <string name=\"discord_activity_competing\">Konkuruje</string>\n    <string name=\"discord_button_text_variables\">Zmienne: {song_name}, {artist_name}, {album_name}</string>\n    <string name=\"discord_rpc_preview\">Podgląd Rich Presence</string>\n    <string name=\"discord_presence\">Obecność</string>\n    <string name=\"discord_connect_description\">Zaloguj się za pomocą Discorda, aby udostępnić to, czego słuchasz</string>\n    <string name=\"discord_playing_metrolist\">Gra w Metrolist</string>\n    <string name=\"discord_watching_metrolist\">Ogląda Metrolist</string>\n    <string name=\"discord_competing_metrolist\">Konkuruje w Metrolist</string>\n    <string name=\"discord_activity_name\">Nazwa aktywności</string>\n    <string name=\"discord_activity_name_description\">Niestandardowa nazwa aktywności (pozostaw puste, aby ustawić domyślną nazwę)</string>\n    <string name=\"discord_advanced_mode\">Tryb zaawansowany</string>\n    <string name=\"discord_advanced_mode_description\">Pokaż dodatkowe opcje dostosowywania dla Rich Presence</string>\n    <string name=\"speed_dial\">Szybkie wybieranie</string>\n    <string name=\"pin_to_speed_dial\">Przypnij do szybkiego wybierania</string>\n    <string name=\"unpin_from_speed_dial\">Odepnij od szybkiego wybierania</string>\n    <string name=\"randomize_home_order\">Losowa kolejność ekranu głównego</string>\n    <string name=\"randomize_home_order_desc\">Losowa zmiana kolejności sekcji ekranu głównego z uwzględnieniem priorytetów</string>\n    <string name=\"daily_discover_sounds_like\">Brzmi jak %1$s</string>\n    <string name=\"daily_discover_because_you_listen_to\">Ponieważ słuchasz %1$s</string>\n    <string name=\"daily_discover_similar_to\">Podobny do %1$s</string>\n    <string name=\"daily_discover_based_on\">Na podstawie %1$s</string>\n    <string name=\"daily_discover_for_fans_of\">Dla fanów %1$s</string>\n    <string name=\"from_the_community\">Ze społeczności</string>\n    <string name=\"logout_dialog_title\">Zachować dane biblioteki?</string>\n    <string name=\"logout_dialog_message\">Czy chcesz zachować swoje playlisty i dane z biblioteki? Pobrane utwory i tak zostaną zachowane.</string>\n    <string name=\"logout_keep\">Zachowaj</string>\n    <string name=\"logout_clear\">Wyczyść</string>\n    <string name=\"credits_lead_developer\">Główny Deweloper</string>\n    <string name=\"credits_collaborator\">Współpracownik</string>\n    <string name=\"credits_collaborators_section\">Współpracownicy</string>\n    <string name=\"credits_license_name\">GNU General Public License v3.0</string>\n    <string name=\"credits_license_desc\">Darmowe oprogramowanie o otwartym kodzie źródłowym. Możesz z niego korzystać, studiować je, udostępniać i ulepszać.</string>\n    <string name=\"credits_discord\">Serwer Discorda</string>\n    <string name=\"credits_telegram\">Kanał na Telegramie</string>\n    <string name=\"credits_website\">Strona</string>\n    <string name=\"credits_instagram\">Instagram</string>\n    <string name=\"credits_github\">GitHub</string>\n    <string name=\"credits_view_repo\">Pokaż Repozytorium</string>\n    <string name=\"app_version_info\">%1$s • %2$s</string>\n    <string name=\"like_what_i_do\">Podoba Ci się to co robię?</string>\n    <string name=\"buy_mo_a_coffee\">Kup mi kawę</string>\n    <string name=\"community_and_info\">Społeczność i informacje</string>\n    <string name=\"metrolist\">METROLIST</string>\n    <string name=\"wanna_play_favorite_song\">Chcesz zagrać ich ulubioną piosenkę?</string>\n    <string name=\"yeah\">Spoko</string>\n    <string name=\"stands_with_palestine\">Ten projekt wspiera Palestynę 🇵🇸</string>\n    <string name=\"filter_podcasts\">Podcasty</string>\n    <string name=\"view_podcast\">Pokaż podcast</string>\n    <string name=\"podcast_channels\">Kanały z podcastami</string>\n    <string name=\"latest_episodes\">Najnowsze Epizody</string>\n    <string name=\"your_shows\">Twoje Seriale</string>\n    <string name=\"new_episodes\">Nowe Epizody</string>\n    <string name=\"episodes_for_later\">Epizody na później</string>\n    <string name=\"save_episode_for_later\">Zapisz na później</string>\n    <string name=\"save_episode_for_later_desc\">Dodaj do playlisty „Odcinki na później”</string>\n    <string name=\"remove_episode_from_saved\">Usuń z zapisanych</string>\n    <string name=\"subscribe_to_podcast\">Zapisz podcast w bibliotece</string>\n    <plurals name=\"n_episode\">\n        <item quantity=\"one\">%d epizod</item>\n        <item quantity=\"few\">%d epizody</item>\n        <item quantity=\"many\">%d epizody</item>\n        <item quantity=\"other\">%d epizody</item>\n    </plurals>\n    <string name=\"filter_episodes\">Epizody</string>\n    <string name=\"restore_confirm_message\">Spowoduje to przywrócenie danych aplikacji z kopii zapasowej.</string>\n    <string name=\"restore_account_warning\">Po przywróceniu będziesz musiał się ponownie zalogować. Następujące konto zostanie wylogowane:</string>\n    <string name=\"restore\">Przywróć</string>\n    <string name=\"checking_previous_account\">Szukanie poprzedniego konta…</string>\n    <string name=\"no_account_found\">Nie znaleziono konta</string>\n    <string name=\"upload_songs\">Prześlij piosenki</string>\n    <string name=\"uploading\">Przesyłanie…</string>\n    <string name=\"upload_progress\">%1$d z %2$d</string>\n    <string name=\"upload_complete\">Przesyłanie zakończone</string>\n    <string name=\"upload_failed\">Przesyłanie nie powiodło się</string>\n    <string name=\"upload_file_too_large\">Plik za duży (maksymalnie 300MB)</string>\n    <string name=\"upload_unsupported_format\">Nieobsługiwany format. Użyj mp3, m4a, wma, flac lub ogg</string>\n    <string name=\"delete_uploaded_song\">Usuń przesłany utwór</string>\n    <string name=\"delete_uploaded_song_confirm\">Czy na pewno chcesz usunąć ten przesłany utwór? Tej czynności nie można cofnąć.</string>\n    <string name=\"delete_uploaded_song_success\">Przesłana piosenka została usunięta</string>\n    <string name=\"delete_uploaded_song_failed\">Nie udało się usunąć przesłanego utworu</string>\n    <string name=\"delete_uploaded_songs\">Usuń przesłane utwory</string>\n    <string name=\"delete_uploaded_songs_confirm\">Czy na pewno chcesz usunąć %1$d przesłanych utworów? Tej czynności nie można cofnąć.</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-pl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Główna</string>\n    <string name=\"songs\">Utwory</string>\n    <string name=\"artists\">Artyści</string>\n    <string name=\"albums\">Albumy</string>\n    <string name=\"playlists\">Playlisty</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d zaznaczony</item>\n        <item quantity=\"few\">%d zaznaczone</item>\n        <item quantity=\"many\">%d zaznaczonych</item>\n        <item quantity=\"other\">%d zaznaczonych</item>\n    </plurals>\n    <string name=\"history\">Historia</string>\n    <string name=\"stats\">Statystyki</string>\n    <string name=\"mood_and_genres\">Nastroje i gatunki</string>\n    <string name=\"account\">Konto</string>\n    <string name=\"quick_picks\">Szybki wybór</string>\n    <string name=\"quick_picks_empty\">Słuchaj utworów, aby wygenerować szybkie wybory</string>\n    <string name=\"new_release_albums\">Nowo wydane albumy</string>\n    <string name=\"today\">Dzisiaj</string>\n    <string name=\"yesterday\">Wczoraj</string>\n    <string name=\"this_week\">Ten tydzień</string>\n    <string name=\"last_week\">Ostatni tydzień</string>\n    <string name=\"most_played_songs\">Najczęściej słuchane utwory</string>\n    <string name=\"most_played_artists\">Najczęściej słuchani artyści</string>\n    <string name=\"most_played_albums\">Najczęściej słuchane albumy</string>\n    <string name=\"search\">Szukaj</string>\n    <string name=\"search_yt_music\">Szukaj w YouTube Music…</string>\n    <string name=\"search_library\">Szukaj w bibliotece…</string>\n    <string name=\"filter_library\">Biblioteka</string>\n    <string name=\"filter_liked\">Polubione</string>\n    <string name=\"filter_downloaded\">Pobrane</string>\n    <string name=\"filter_all\">Wszystko</string>\n    <string name=\"filter_songs\">Utwory</string>\n    <string name=\"filter_videos\">Filmy</string>\n    <string name=\"filter_albums\">Albumy</string>\n    <string name=\"filter_artists\">Wykonawcy</string>\n    <string name=\"filter_playlists\">Playlisty</string>\n    <string name=\"filter_community_playlists\">Playlisty społeczności</string>\n    <string name=\"filter_featured_playlists\">Polecane playlisty</string>\n    <string name=\"filter_bookmarked\">Zapisane</string>\n    <string name=\"no_results_found\">Brak wyników</string>\n    <string name=\"from_your_library\">Z Twojej biblioteki</string>\n    <string name=\"liked_songs\">Polubione utwory</string>\n    <string name=\"downloaded_songs\">Pobrane utwory</string>\n    <string name=\"playlist_is_empty\">Playlista jest pusta</string>\n    <string name=\"retry\">Ponów</string>\n    <string name=\"radio\">Radio</string>\n    <string name=\"shuffle\">Losuj</string>\n    <string name=\"reset\">Reset</string>\n    <string name=\"details\">Szczegóły</string>\n    <string name=\"edit\">Edytuj</string>\n    <string name=\"start_radio\">Włącz radio</string>\n    <string name=\"play\">Odtwórz</string>\n    <string name=\"play_next\">Odtwórz jako następny</string>\n    <string name=\"add_to_queue\">Dodaj do kolejki</string>\n    <string name=\"add_to_library\">Dodaj do biblioteki</string>\n    <string name=\"remove_from_library\">Usuń z biblioteki</string>\n    <string name=\"action_download\">Pobierz</string>\n    <string name=\"downloading\">Pobieranie</string>\n    <string name=\"remove_download\">Usuń z pobranych</string>\n    <string name=\"import_playlist\">Importuj playlistę</string>\n    <string name=\"add_to_playlist\">Dodaj do playlisty</string>\n    <string name=\"view_artist\">Pokaż artystę</string>\n    <string name=\"view_album\">Pokaż album</string>\n    <string name=\"refetch\">Odśwież</string>\n    <string name=\"share\">Udostępnij</string>\n    <string name=\"delete\">Usuń</string>\n    <string name=\"remove_from_history\">Usuń z historii</string>\n    <string name=\"search_online\">Szukaj online</string>\n    <string name=\"action_sync\">Synchronizuj</string>\n    <string name=\"advanced\">Zaawansowane</string>\n    <string name=\"sort_by_create_date\">Data dodania</string>\n    <string name=\"sort_by_name\">Nazwa</string>\n    <string name=\"sort_by_artist\">Wykonawca</string>\n    <string name=\"sort_by_year\">Rok</string>\n    <string name=\"sort_by_song_count\">Liczba utworów</string>\n    <string name=\"sort_by_length\">Długość</string>\n    <string name=\"sort_by_play_time\">Długość utworu</string>\n    <string name=\"sort_by_custom\">Niestandardowa kolejność</string>\n    <string name=\"media_id\">ID media</string>\n    <string name=\"mime_type\">Typ MIME</string>\n    <string name=\"codecs\">Kodeki</string>\n    <string name=\"bitrate\">Przepływność</string>\n    <string name=\"sample_rate\">Częstotliwość próbkowania</string>\n    <string name=\"loudness\">Natężenie</string>\n    <string name=\"volume\">Głośność</string>\n    <string name=\"file_size\">Rozmiar pliku</string>\n    <string name=\"unknown\">Nieznane</string>\n    <string name=\"copied\">Skopiowano do schowka</string>\n    <string name=\"edit_lyrics\">Edytuj tekst</string>\n    <string name=\"search_lyrics\">Szukaj tekstu</string>\n    <string name=\"edit_song\">Edytuj utwór</string>\n    <string name=\"song_title\">Tytuł utworu</string>\n    <string name=\"song_artists\">Wykonawcy utworu</string>\n    <string name=\"error_song_title_empty\">Tytuł utworu nie może być pusty.</string>\n    <string name=\"error_song_artist_empty\">Wykonawca utworu nie może być pusty.</string>\n    <string name=\"save\">Zapisz</string>\n    <string name=\"choose_playlist\">Wybierz playlistę</string>\n    <string name=\"edit_playlist\">Edytuj playlistę</string>\n    <string name=\"create_playlist\">Utwórz playlistę</string>\n    <string name=\"playlist_name\">Nazwa playlisty</string>\n    <string name=\"error_playlist_name_empty\">Nazwa playlisty nie może być pusta.</string>\n    <string name=\"edit_artist\">Edytuj artystę</string>\n    <string name=\"artist_name\">Nazwa artysty</string>\n    <string name=\"error_artist_name_empty\">Nazwa artysty nie może być pusta.</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d utwór</item>\n        <item quantity=\"few\">%d utworów</item>\n        <item quantity=\"many\">%d utworów</item>\n        <item quantity=\"other\">%d utworów</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d artysta</item>\n        <item quantity=\"few\">%d artystów</item>\n        <item quantity=\"many\">%d artystów</item>\n        <item quantity=\"other\">%d artystów</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d album</item>\n        <item quantity=\"few\">%d albumów</item>\n        <item quantity=\"many\">%d albumów</item>\n        <item quantity=\"other\">%d albumów</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d playlista</item>\n        <item quantity=\"few\">%d playlist</item>\n        <item quantity=\"many\">%d playlist</item>\n        <item quantity=\"other\">%d playlist</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d tydzień</item>\n        <item quantity=\"few\">%d tygodni</item>\n        <item quantity=\"many\">%d tygodni</item>\n        <item quantity=\"other\">%d tygodni</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d miesiąc</item>\n        <item quantity=\"few\">%d miesięcy</item>\n        <item quantity=\"many\">%d miesięcy</item>\n        <item quantity=\"other\">%d miesięcy</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d rok</item>\n        <item quantity=\"few\">%d lat</item>\n        <item quantity=\"many\">%d lat</item>\n        <item quantity=\"other\">%d lat</item>\n    </plurals>\n    <string name=\"playlist_imported\">Playlista zaimportowana</string>\n    <string name=\"removed_song_from_playlist\">Usunięto \\\"%s\\\" z playlisty</string>\n    <string name=\"playlist_synced\">Playlista zsynchronizowana</string>\n    <string name=\"undo\">Cofnij</string>\n    <string name=\"lyrics_not_found\">Nie znaleziono tekstu</string>\n    <string name=\"sleep_timer\">Wyłącznik czasowy</string>\n    <string name=\"end_of_song\">Koniec utworu</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">1 minuta</item>\n        <item quantity=\"few\">%d minut</item>\n        <item quantity=\"many\">%d minut</item>\n        <item quantity=\"other\">%d minut</item>\n    </plurals>\n    <string name=\"error_no_stream\">Brak dostępnego źródła</string>\n    <string name=\"error_no_internet\">Brak połączenia z Internetem</string>\n    <string name=\"error_timeout\">Limit czasu</string>\n    <string name=\"error_unknown\">Nieznany błąd</string>\n    <string name=\"action_like\">Polub</string>\n    <string name=\"action_remove_like\">Usuń polubienie</string>\n    <string name=\"action_shuffle_on\">Losowanie włączone</string>\n    <string name=\"action_shuffle_off\">Losowanie wyłączone</string>\n    <string name=\"repeat_mode_off\">Powtarzanie wyłączone</string>\n    <string name=\"repeat_mode_one\">Powtórz bieżący utwór</string>\n    <string name=\"repeat_mode_all\">Powtórz kolejkę</string>\n    <string name=\"queue_all_songs\">Wszystkie utwory</string>\n    <string name=\"queue_searched_songs\">Wyszukane utwory</string>\n    <string name=\"music_player\">Odtwarzacz</string>\n    <string name=\"settings\">Ustawienia</string>\n    <string name=\"appearance\">Wygląd</string>\n    <string name=\"enable_dynamic_theme\">Włącz motyw dynamiczny</string>\n    <string name=\"dark_theme\">Ciemny motyw</string>\n    <string name=\"dark_theme_on\">Włącz</string>\n    <string name=\"dark_theme_off\">Wyłącz</string>\n    <string name=\"dark_theme_follow_system\">Zgodnie z systemem</string>\n    <string name=\"pure_black\">Czysta czerń</string>\n    <string name=\"default_open_tab\">Domyślnie otwarta zakładka</string>\n    <string name=\"customize_navigation_tabs\">Modyfikuj zakładki nawigacji</string>\n    <string name=\"lyrics_text_position\">Położenie tekstu utworu</string>\n    <string name=\"left\">Z lewej</string>\n    <string name=\"center\">Na środku</string>\n    <string name=\"right\">Z prawej</string>\n    <string name=\"content\">Zawartość</string>\n    <string name=\"login\">Login</string>\n    <string name=\"content_language\">Domyślny język zawartości</string>\n    <string name=\"content_country\">Domyślny kraj zawartości</string>\n    <string name=\"system_default\">Domyślny systemu</string>\n    <string name=\"enable_proxy\">Włącz proxy</string>\n    <string name=\"proxy_type\">Typ proxy</string>\n    <string name=\"proxy_url\">URL proxy</string>\n    <string name=\"restart_to_take_effect\">Uruchom ponownie, aby zastosować zmiany</string>\n    <string name=\"player_and_audio\">Odtwarzacz i audio</string>\n    <string name=\"audio_quality\">Jakość audio</string>\n    <string name=\"audio_quality_auto\">Automatyczna</string>\n    <string name=\"audio_quality_high\">Wysoka</string>\n    <string name=\"audio_quality_low\">Niska</string>\n    <string name=\"persistent_queue\">Trwała kolejka</string>\n    <string name=\"skip_silence\">Pomiń ciszę</string>\n    <string name=\"audio_normalization\">Normalizacja audio</string>\n    <string name=\"equalizer\">Korektor</string>\n    <string name=\"storage\">Pamięć</string>\n    <string name=\"cache\">Pamięć podręczna</string>\n    <string name=\"image_cache\">Pamięć podręczna obrazów</string>\n    <string name=\"song_cache\">Pamięć podręczna utworów</string>\n    <string name=\"max_cache_size\">Maksymalny rozmiar pamięci podręcznej</string>\n    <string name=\"unlimited\">Nieskończony</string>\n    <string name=\"clear_all_downloads\">Wyczyść pobrane</string>\n    <string name=\"max_image_cache_size\">Maksymalny rozmiar pamięci podręcznej obrazów</string>\n    <string name=\"clear_image_cache\">Wyczyść pamięć podręczną obrazów</string>\n    <string name=\"max_song_cache_size\">Maksymalny rozmiar pamięci podręcznej utworów</string>\n    <string name=\"clear_song_cache\">Wyczyść pamięć podręczną utworów</string>\n    <string name=\"size_used\">%s w użyciu</string>\n    <string name=\"privacy\">Prywatność</string>\n    <string name=\"pause_listen_history\">Wstrzymaj historię odtwarzania</string>\n    <string name=\"clear_listen_history\">Wyczyść historię odtwarzania</string>\n    <string name=\"clear_listen_history_confirm\">Czy na pewno chcesz wyczyścić całą historię odtwarzania?</string>\n    <string name=\"pause_search_history\">Wstrzymaj historię wyszukiwania</string>\n    <string name=\"clear_search_history\">Wyczyść historię wyszukiwania</string>\n    <string name=\"clear_search_history_confirm\">Czy na pewno chcesz wyczyścić całą historię wyszukiwania?</string>\n    <string name=\"enable_kugou\">Pobieraj teksty utworów z KuGou</string>\n    <string name=\"backup_restore\">Kopia zapasowa i przywracanie</string>\n    <string name=\"action_backup\">Kopia zapasowa</string>\n    <string name=\"action_restore\">Przywróć</string>\n    <string name=\"imported_playlist\">Zaimportowane playlisty</string>\n    <string name=\"backup_create_success\">Kopia zapasowa utworzona pomyślnie</string>\n    <string name=\"backup_create_failed\">Nie udało się stworzyć kopii zapasowej</string>\n    <string name=\"restore_failed\">Nie udało się przywrócić kopii zapasowej</string>\n    <string name=\"about\">O aplikacji</string>\n    <string name=\"app_version\">Wersja aplikacji</string>\n    <string name=\"new_version_available\">Dostępna nowa wersja</string>\n    <string name=\"translation_models\">Modele tłumaczeń</string>\n    <string name=\"clear_translation_models\">Wyczyść modele tłumaczeń</string>\n    <string name=\"auto_skip_next_on_error_desc\">Zapewnij ciągłość odtwarzania</string>\n    <string name=\"add_all_to_library\">Dodaj wszystko do biblioteki</string>\n    <string name=\"big\">Duży</string>\n    <string name=\"theme\">Motyw</string>\n    <string name=\"queue\">Kolejka</string>\n    <string name=\"persistent_queue_desc\">Przywróć ostatnią kolejkę podczas uruchomienia aplikacji</string>\n    <string name=\"auto_load_more\">Automatycznie załaduj więcej utworów</string>\n    <string name=\"auto_load_more_desc\">Automatycznie dodaj więcej utworów, kiedy kolejka się skończy, o ile to możliwe</string>\n    <string name=\"auto_skip_next_on_error\">Automatycznie pomiń do następnego utworu, gdy wystąpi błąd</string>\n    <string name=\"stop_music_on_task_clear\">Zatrzymaj muzykę po wykonaniu zadania</string>\n    <string name=\"disable_screenshot_desc\">Gdy ta opcja jest włączona, zrzuty ekranu i widok aplikacji na ekranie Ostatnie są wyłączone.</string>\n    <string name=\"enable_lrclib\">Pobieraj teksty utworów z LrcLib</string>\n    <string name=\"small\">Mały</string>\n    <string name=\"action_like_all\">Polub wszystkie</string>\n    <string name=\"default_\">Domyślny</string>\n    <string name=\"remove_from_playlist\">Usuń z playlisty</string>\n    <string name=\"remove_all_from_library\">Usuń wszystko z biblioteki</string>\n    <string name=\"your_youtube_playlists\">Twoje playlisty YouTube</string>\n    <string name=\"enable_discord_rpc\">Włącz Rich Presence</string>\n    <string name=\"remove_from_queue\">Usuń z kolejki</string>\n    <string name=\"login_failed\">Błąd logowania</string>\n    <string name=\"similar_to\">Podobne do</string>\n    <string name=\"library_playlist_empty\">Twoje playlisty pojawią się tutaj</string>\n    <string name=\"other_versions\">Inne wersje</string>\n    <string name=\"duplicates_description_single\">Utwór jest już obecny w Twojej playliście</string>\n    <string name=\"action_remove_like_all\">Usuń wszystkie polubienia</string>\n    <string name=\"player_text_alignment\">Wyrównanie tekstu odtwarzacza</string>\n    <string name=\"sided\">Z boku</string>\n    <string name=\"squiggly\">Falisty</string>\n    <string name=\"forgotten_favorites\">Zapomniane ulubione</string>\n    <string name=\"keep_listening\">Słuchaj dalej</string>\n    <string name=\"library_song_empty\">Utwory z biblioteki pojawią się tutaj</string>\n    <string name=\"library_artist_empty\">Artyści z biblioteki pojawią się tutaj</string>\n    <string name=\"library_album_empty\">Albumy z biblioteki pojawią się tutaj</string>\n    <string name=\"remove_download_playlist_confirm\">Czy na pewno chcesz usunąć wszystkie utwory playlisty \\\"%s\\\" z Pobranych utworów?</string>\n    <string name=\"delete_playlist_confirm\">Czy na pewno chcesz usunąć playlistę \\\"%s\\\"?</string>\n    <string name=\"tempo_and_pitch\">Tempo i wysokość</string>\n    <string name=\"duplicates\">Duplikaty</string>\n    <string name=\"skip_duplicates\">Pomiń duplikaty</string>\n    <string name=\"add_anyway\">Dodaj mimo to</string>\n    <string name=\"duplicates_description_multiple\">%d utwory są już obecne w Twojej playliście</string>\n    <string name=\"player\">Odtwarzacz</string>\n    <string name=\"player_slider_style\">Styl suwaka odtwarzacza</string>\n    <string name=\"misc\">Różne</string>\n    <string name=\"grid_cell_size\">Rozmiar komórki siatki</string>\n    <string name=\"not_logged_in\">Nie zalogowano</string>\n    <string name=\"listen_history\">Słuchaj historii</string>\n    <string name=\"search_history\">Szukaj w historii</string>\n    <string name=\"disable_screenshot\">Wyłącz zrzut ekranu</string>\n    <string name=\"hide_explicit\">Ukryj wulgarne treści</string>\n    <string name=\"discord_integration\">Integracja z Discordem</string>\n    <string name=\"discord_information\">Metrolist używa biblioteki KizzyRPC do aktualizacji statusu konta Discord. Wiąże się to z korzystaniem z połączenia Discord Gateway, co może być postrzegane jako naruszenie warunków korzystania z usług Discord (TOS). Nie są jednak znane przypadki zawieszenia kont użytkowników z tego powodu. Używaj na własne ryzyko.\\n\\nMetrolist wyodrębni tylko twój token; wszystko inne jest przechowywane lokalnie.</string>\n    <string name=\"dismiss\">Odrzuć</string>\n    <string name=\"options\">Opcje</string>\n    <string name=\"preview\">Podgląd</string>\n    <string name=\"action_logout\">Wyloguj</string>\n    <string name=\"use_login_for_browse\">Zaloguj się, aby przeglądać treści</string>\n    <string name=\"use_login_for_browse_desc\">Może mieć wpływ na to, jakie treści widzisz i np. pokazuje albumy tylko premium, jeśli jesteś zalogowany na konto Premium</string>\n    <string name=\"action_login\">Zaloguj</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-pt/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">Local</string>\n    <string name=\"remote_history\">Remoto</string>\n    <string name=\"charts\">Tabelas</string>\n    <string name=\"back_button_desc\">Voltar</string>\n    <string name=\"album_cover_desc\">Capa do álbum</string>\n    <string name=\"top_music_videos\">Vídeos de música populares</string>\n    <string name=\"trending\">Em destaque</string>\n    <string name=\"weeks\">Semanas</string>\n    <string name=\"months\">Meses</string>\n    <string name=\"years\">Anos</string>\n    <string name=\"continuous\">Contínuo</string>\n    <string name=\"liked\">Favoritas</string>\n    <string name=\"offline\">Transferido</string>\n    <string name=\"my_top\">O meu top</string>\n    <string name=\"cached_playlist\">Em cache</string>\n    <string name=\"sync_playlist\">Sincronizar lista de reprodução</string>\n    <string name=\"sync_disabled\">Sincronização desativada</string>\n    <string name=\"allows_for_sync_witch_youtube\">Observação: Isto permite a sincronização com o YouTube Music. Isto NÃO pode ser alterado mais tarde.</string>\n    <string name=\"generating_image\">A gerar imagem</string>\n    <string name=\"please_wait\">Por favor, aguarde</string>\n    <string name=\"cancel\">Cancelar</string>\n    <string name=\"share_lyrics\">Partilhar letra</string>\n    <string name=\"share_as_text\">Partilhar como texto</string>\n    <string name=\"share_as_image\">Partilhar como imagem</string>\n    <string name=\"max_selection_limit\">Limite máximo de seleção</string>\n    <string name=\"share_selected\">Partilhar selecionado</string>\n    <string name=\"customize_colors\">Personalizar cores</string>\n    <string name=\"text_color\">Cor do texto</string>\n    <string name=\"secondary_text_color\">Cor secundária do texto</string>\n    <string name=\"background_color\">Cor de fundo</string>\n    <string name=\"remove_from_cache\">Remover do cache</string>\n    <string name=\"copy_link\">Copiar hiperligação</string>\n    <string name=\"select\">Selecionar tudo</string>\n    <string name=\"like_all\">Gostar de tudo</string>\n    <string name=\"dislike_all\">Remover gostar de tudo</string>\n    <string name=\"sort_by_last_updated\">Quando atualizado</string>\n    <string name=\"link_copied\">Hiperligação copiada para a área de transferência</string>\n    <string name=\"lyrics\">Letra</string>\n    <string name=\"already_in_playlist\">Já está na lista de reprodução:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d vez</item>\n        <item quantity=\"many\">%d vezes</item>\n        <item quantity=\"other\">%d vezes</item>\n    </plurals>\n    <string name=\"similar_content\">Conteúdo similar</string>\n    <string name=\"player_background_style\">Estilo do fundo do reprodutor</string>\n    <string name=\"follow_theme\">Seguir o tema</string>\n    <string name=\"gradient\">Gradiente</string>\n    <string name=\"new_player_design\">Novo desenho do reprodutor</string>\n    <string name=\"new_mini_player_design\">Novo design do mini reprodutor</string>\n    <string name=\"player_background_blur\">Desfocado</string>\n    <string name=\"player_buttons_style\">Cores dos botões do reprodutor</string>\n    <string name=\"default_style\">Predefinição</string>\n    <string name=\"enable_swipe_thumbnail\">Ativar o deslizar para trocar de música</string>\n    <string name=\"swipe_song_to_add\">Deslizar na música para esquerda para adicionar na fila ou para a direita para tocá-la em seguida</string>\n    <string name=\"lyrics_click_change\">Alterar letra ao clicar</string>\n    <string name=\"lyrics_auto_scroll\">Rolar letra automaticamente</string>\n    <string name=\"lyrics_romanize_japanese\">Romanizar letras em japonês</string>\n    <string name=\"lyrics_romanize_korean\">Romanizar letras em coreano</string>\n    <string name=\"slim\">Pequeno</string>\n    <string name=\"slim_navbar\">Barra estreita de navegação inferior</string>\n    <string name=\"auto_playlists\">Listas automáticas</string>\n    <string name=\"show_liked_playlist\">Mostrar lista de favoritos</string>\n    <string name=\"show_downloaded_playlist\">Mostrar lista de descarregados</string>\n    <string name=\"show_top_playlist\">Mostrar lista de top</string>\n    <string name=\"show_cached_playlist\">Mostrar lista de músicas em cache</string>\n    <string name=\"advanced_login\">Login com token</string>\n    <string name=\"token_hidden\">Toque para mostrar o token</string>\n    <string name=\"token_shown\">Toque novamente para copiar ou editar</string>\n    <string name=\"token_adv_login_description\">Este é um método avançado de login. Como uma alternativa ao portal web, pode digitar ou atualizar o seu token diretamente por aqui. Como exemplo, isto pode fazer com que fazer login em vários dispositivos seja mais rápido. Observa que qualquer formato inválido do token que a app não consiga interpretar não será aceitado</string>\n    <string name=\"yt_sync\">Sincronizar automaticamente com a conta</string>\n    <string name=\"more_content\">Mais conteúdo</string>\n    <string name=\"general\">Geral</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"default_lib_chips\">Alterar ecrã da biblioteca padrão</string>\n    <string name=\"set_quick_picks\">Definir escolhas rápidas</string>\n    <string name=\"last_song_listened\">Com base na última música ouvida</string>\n    <string name=\"app_language\">Idioma da app</string>\n    <string name=\"enable_similar_content\">Ativar conteúdo similar</string>\n    <string name=\"similar_content_desc\">Adicionar automaticamente mais músicas similares quando o final da fila for alcançado</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"import_online\">Importar uma lista \\\"m3u\\\"</string>\n    <string name=\"import_csv\">Importar uma lista \\\"csv\\\"</string>\n    <string name=\"playlist_add_local_to_synced_note\">Observação: Não há apoio para adicionar músicas locais em listas sincronizadas/remotas. Qualquer outra combinação é válida</string>\n    <string name=\"auto_download_on_like\">Descarregar automaticamente ao curtir</string>\n    <string name=\"auto_download_on_like_desc\">Descarregar músicas automaticamente quando as curte</string>\n    <string name=\"swipe_sensitivity\">Sensibilidade do gesto de deslizar no mini reprodutor</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"clear_song_cache_dialog\">Tem certeza que quer limpar todas as músicas em cache?</string>\n    <string name=\"clear_image_cache_dialog\">Tem certeza que quer apagar todas as imagens no cache?</string>\n    <string name=\"clear_downloads_dialog\">Tem certeza que quer limpar todas as descargas?</string>\n    <string name=\"disable\">Desativar</string>\n    <string name=\"not_logged_in_youtube\">Não conectado ao YouTube</string>\n    <string name=\"default_links\">Abrir ligações suportadas</string>\n    <string name=\"open_app_settings_error\">Não foi possível abrir as configurações da app</string>\n    <string name=\"release_notes\">Registo de mudanças</string>\n    <string name=\"all_time\">Desde sempre</string>\n    <string name=\"past_24_hours\">Últimas 24 horas</string>\n    <string name=\"past_week\">Última semana</string>\n    <string name=\"past_month\">Último mês</string>\n    <string name=\"past_year\">Último ano</string>\n    <string name=\"top_length\">Comprimento da lista Meu Top</string>\n    <string name=\"history_duration\">Duração do histórico</string>\n    <string name=\"information\">Informações</string>\n    <string name=\"description\">Descrição</string>\n    <string name=\"views\">Visualizações</string>\n    <string name=\"likes\">Favoritos</string>\n    <string name=\"dislikes\">Desgostados</string>\n    <string name=\"subscribe\">Inscrever-se</string>\n    <string name=\"subscribed\">Inscrito</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">%d segundo</item>\n        <item quantity=\"many\">%d de segundos</item>\n        <item quantity=\"other\">%d segundos</item>\n    </plurals>\n    <string name=\"close\">Fechar</string>\n    <string name=\"seek_forward_dynamic\">+%1$d segundos para a frente</string>\n    <string name=\"seek_backward_dynamic\">-%1$d segundos para trás</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Não carregar automaticamente mais músicas e conteúdo semelhante quando o modo repetir tudo está ativado</string>\n    <string name=\"now_playing\">Agora a Tocar</string>\n    <string name=\"hide_player_thumbnail\">Ocultar Miniatura do Reprodutor</string>\n    <string name=\"hide_player_thumbnail_desc\">Substituir a capa do álbum pelo logótipo da aplicação no reprodutor</string>\n    <string name=\"seek_seconds_addup\">Procura progressiva</string>\n    <string name=\"seek_seconds_addup_description\">Se ativado, adiciona 5 segundos extra incrementalmente em cada ignorar na procura</string>\n    <string name=\"disable_load_more_when_repeat_all\">Desativar carregar mais quando repetir tudo</string>\n    <string name=\"uploaded_playlist\">Carregado</string>\n    <string name=\"filter_uploaded\">Carregado</string>\n    <string name=\"download_playlist_desc\">Descarregar todas as músicas para ouvir offline</string>\n    <string name=\"remove_download_playlist_desc\">Remover todas as músicas descarregadas desta lista de reprodução</string>\n    <string name=\"download_in_progress_desc\">Descarregamento em progresso</string>\n    <string name=\"share_playlist_desc\">Partilhar esta lista de reprodução com os outros</string>\n    <string name=\"delete_playlist_desc\">Remover esta lista de reprodução permanentemente</string>\n    <string name=\"sync_playlist_desc\">Sincronizar lista de reprodução com o YouTube Music</string>\n    <string name=\"starting_radio\">A começar rádio</string>\n    <string name=\"primary_color_style\">Cor primária</string>\n    <string name=\"swipe_song_to_remove\">Arrastar música para a remover da lista de reprodução</string>\n    <string name=\"lyrics_glow_effect\">Habilitar efeito de letras brilhantes</string>\n    <string name=\"enable_better_lyrics\">Habilitar Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">Usar o provedor Better Lyrics para letras sincronizadas palavra-a-palavra</string>\n    <string name=\"auto_scroll\">Re-sincronizar</string>\n    <string name=\"show_uploaded_playlist\">Mostrar lista de reprodução \\\"Carregada\\\"</string>\n    <string name=\"edit_playlist_cover\">Editar capa da lista de reprodução</string>\n    <string name=\"edit_playlist_cover_note\">Nota: A tua conta tem de estar ligada a um número de telefone e verificada no YouTube Music para mudar a capa desta lista de reprodução.</string>\n    <string name=\"edit_playlist_cover_note_wait\">Depois de selecionar a imagem, por favor espera um momentinho para a nova capa aparecer na tua lista de reprodução.</string>\n    <string name=\"choose_from_library\">Escolher da biblioteca</string>\n    <string name=\"remove_custom_image\">Remover imagem personalizada</string>\n    <string name=\"config_proxy\">Configurar proxy</string>\n    <string name=\"proxy_username\">Nome de utilizador proxy</string>\n    <string name=\"proxy_password\">Palavra-passe proxy</string>\n    <string name=\"enable_authentication\">Habilitar autenticação</string>\n    <string name=\"discord_use_details\">Usar detalhes em vez de estado</string>\n    <string name=\"discord_use_details_description\">Mostrar titulo da música proeminentemente em vez de nome do artista</string>\n    <string name=\"lyrics_romanization_cyrillic\">Cirílica</string>\n    <string name=\"line_by_line_option_title\">EXPERIMENTAL: Detetar língua linha por linha</string>\n    <string name=\"line_by_line_dialog_title\">Tens a certeza?</string>\n    <string name=\"settings_section_ui\">Interface</string>\n    <string name=\"settings_section_privacy\">Privacidade &amp; Segurança</string>\n    <string name=\"settings_section_player_content\">Reprodutor &amp; Conteúdo</string>\n    <string name=\"settings_section_storage\">Armazenamento &amp; Dados</string>\n    <string name=\"settings_section_system\">Sistema &amp; Sobre</string>\n    <string name=\"updater\">Atualizador</string>\n    <string name=\"check_for_updates\">Procurar por atualizações automaticamente</string>\n    <string name=\"update_notifications\">Habilitar notificações de atualizações</string>\n    <string name=\"update_available_title\">Atualização Disponível</string>\n    <string name=\"update_channel_desc\">Notificações sobre novas versões</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"album_art_for\">Arte do álbum para %s</string>\n    <string name=\"wrapped_total_albums_title\">Tu ouviste</string>\n    <string name=\"wrapped_total_albums_subtitle\">álbuns únicos</string>\n    <string name=\"wrapped_top_album_title\">O teu maior álbum é</string>\n    <string name=\"wrapped_playlist_ready\">A tua lista de reprodução está pronta</string>\n    <string name=\"wrapped_top_5_albums_title\">Os teus 5 maiores álbuns</string>\n    <string name=\"wrapped_album_listening_time\">Tu ouviste este álbum durante %d minutos</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d minutos</string>\n    <string name=\"wrapped_no_data\">Sem dados</string>\n    <string name=\"wrapped_top_5_artists_title\">Os teus maiores artistas deste ano</string>\n    <string name=\"wrapped_artist_listening_time\">%d minutos</string>\n    <string name=\"wrapped_top_5_songs_title\">As tuas maiores músicas do ano</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">Capa do álbum</string>\n    <string name=\"wrapped_top_artist_title\">O teu maior artista do ano é</string>\n    <string name=\"wrapped_top_artist_image_content_description\">Imagem do maior artista</string>\n    <string name=\"wrapped_top_artist_listening_time\">Tu ouviste-os por %d minutos</string>\n    <string name=\"wrapped_top_song_title\">A tua música mais tocada é</string>\n    <string name=\"wrapped_top_song_listening_time\">Tu ouviste-a por %d minutos</string>\n    <string name=\"wrapped_total_artists_title\">Tu ouviste a</string>\n    <string name=\"wrapped_total_artists_subtitle\">artistas únicos</string>\n    <string name=\"wrapped_total_songs_title\">Tu ouviste a</string>\n    <string name=\"wrapped_total_songs_subtitle\">músicas únicas</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_intro_subtitle\">está na hora de ver o que andaste a ouvir</string>\n    <string name=\"wrapped_intro_button\">vamos lá!</string>\n    <string name=\"wrapped_logo_content_description\">Logo do Metrolist</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"wrapped_ready_title\">O TEU WRAPPED ESTÁ PRONTO!</string>\n    <string name=\"wrapped_ready_subtitle\">Está na hora de ver o que amaste este ano.</string>\n    <string name=\"wrapped_thank_you\">Obrigado por ouvires</string>\n    <string name=\"wrapped_special_thanks\">Um agradecimento a MO Agamy por criar o Metrolist</string>\n    <string name=\"wrapped_close\">Fechar wrapped</string>\n    <string name=\"wrapped_playlist_title\">O teu %s Wrapped</string>\n    <string name=\"wrapped_create_playlist\">Criar lista de reprodução</string>\n    <string name=\"tertiary_color_style\">Cor terciária</string>\n    <string name=\"shuffle_playlist_first\">Misturar lista de reprodução/album primeiro</string>\n    <string name=\"wavy\">Ondulado</string>\n    <string name=\"lyrics_glow_effect_desc\">Adicionar animação brilhante e efeito saltitante à letra ativa</string>\n    <string name=\"shuffle_playlist_first_desc\">Ao reproduzir aleatoriamente, reproduza primeiro todas as músicas da lista de reprodução/álbum original e, em seguida, conteúdo semelhante</string>\n    <string name=\"show_wrapped_card\">Mostrar cartão \\\"Wrapped\\\"</string>\n    <string name=\"lyrics_romanize_title\">Romanização</string>\n    <string name=\"lyrics_romanization\">Romanização de letras</string>\n    <string name=\"lyrics_romanize_chinese\">Romaniza letras em mandarim</string>\n    <string name=\"lyrics_romanize_russian\">Romanizar letras em russo</string>\n    <string name=\"lyrics_romanize_ukrainian\">Romanizar letras em ucraniano</string>\n    <string name=\"lyrics_romanize_belarusian\">Romanizar letras em bielorrusso</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Romanizar letras em quirguiz</string>\n    <string name=\"lyrics_romanize_serbian\">Romanizar letras em sérvio</string>\n    <string name=\"lyrics_romanize_bulgarian\">Romanizar letras em búlgaro</string>\n    <string name=\"line_by_line_option_desc\">O idioma cirílico será detetado linha por linha, em vez de toda a música.</string>\n    <string name=\"line_by_line_dialog_desc\">Esta é uma funcionalidade experimental que pode funcionar ou não. \\n\\nPor predefinição, o idioma é determinado a partir da música inteira, mas com esta opção ativada, ele será determinado linha por linha. Isso permitirá que músicas em vários idiomas funcionem, MAS o idioma pode nem sempre estar correto (por exemplo, se houver uma letra em ucraniano que não contenha letras específicas do ucraniano, ela pode ser romanizada como russo). \\n\\nSe não tiver problemas, recomenda-se manter esta opção desativada.</string>\n    <string name=\"romanize_current_track\">Romanizar a faixa atual</string>\n    <string name=\"update_channel_name\">Atualizações da aplicação</string>\n    <string name=\"audio_offload\">Habilitar descarregamento</string>\n    <string name=\"audio_offload_description\">Use o caminho de descarregamento de áudio para reprodução de áudio. Desativar essa opção pode aumentar o consumo de energia, mas pode ser útil se você tiver problemas com a reprodução de áudio ou pós-processamento</string>\n    <string name=\"google_cast_description\">Ativar a transmissão de áudio para o Chromecast e outros dispositivos compatíveis com transmissão</string>\n    <string name=\"lyrics_romanize_macedonian\">Romanizar letras em macedónio</string>\n    <string name=\"integrations\">Integrações</string>\n    <string name=\"username\">Utilizador</string>\n    <string name=\"password\">Palavra-passe</string>\n    <string name=\"lastfm_integration\">Integração com Last.fm</string>\n    <string name=\"enable_scrobbling\">Habilitar \\\"scrobbling\\\"</string>\n    <string name=\"lastfm_now_playing\">Enviar a Tocar Agora</string>\n    <string name=\"last_fm_send_likes\">Enviar Gostos/Não Gostos</string>\n    <string name=\"last_fm_send_likes_description\">Músicas curtidas/descurtidas no Last.fm quando são curtidas/descurtidas no Metrolist</string>\n    <string name=\"logging_in\">A iniciar sessão…</string>\n    <string name=\"scrobbling_configuration\">Configuração do \\\"Scrobbling\\\"</string>\n    <string name=\"scrobble_min_track_duration\">\\\"Scrobblar\\\" músicas com duração superior a</string>\n    <string name=\"scrobble_delay_percent\">Percentagem de atraso do \\\"scrobble\\\"</string>\n    <string name=\"scrobble_delay_minutes\">Atraso do \\\"scrobble\\\" em minutos</string>\n    <string name=\"hide_video_songs\">Ocultar músicas em vídeo</string>\n    <string name=\"details_desc\">Ver informações da música</string>\n    <string name=\"edit_desc\">Modificar o título ou o artista</string>\n    <string name=\"start_radio_desc\">Criar uma estação baseada neste item</string>\n    <string name=\"play_next_desc\">Adicionar ao topo da sua fila</string>\n    <string name=\"add_to_queue_desc\">Adicionar ao fim da sua fila</string>\n    <string name=\"add_to_library_desc\">Guardar na sua biblioteca</string>\n    <string name=\"download_desc\">Disponibilizar para reprodução \\\"offline\\\"</string>\n    <string name=\"add_to_playlist_desc\">Adicionar a uma das suas listas de reprodução</string>\n    <string name=\"refetch_desc\">Obter os metadados mais recentes do YouTube Music</string>\n    <string name=\"share_desc\">Partilhar um \\\"link\\\" para este item</string>\n    <string name=\"delete_desc\">Remover este item permanentemente</string>\n    <string name=\"advanced_desc\">Alterar o tempo e o tom da música</string>\n    <string name=\"equalizer_desc\">Ajustar o equalizador de áudio</string>\n    <string name=\"enable_dynamic_icon\">Habilitar o ícone dinâmico</string>\n    <string name=\"mini_player\">Mini-reprodutor</string>\n    <string name=\"pure_black_mini_player\">Mini-reprodutor preto puro</string>\n    <string name=\"cache_size_warning_title\">Aguarde!</string>\n    <string name=\"cache_size_warning_message\">Escolheu um limite de tamanho de cache menor do que o que a aplicação está a utilizar atualmente (%1$s). Se continuar, a aplicação poderá remover alguns %2$s armazenados em cache para corresponder ao novo limite. Deseja continuar mesmo assim?</string>\n    <string name=\"cache_size_warning_confirm\">Continuar</string>\n    <string name=\"lyrics_animation_style\">Estilo da animação palavra-por-palavra</string>\n    <string name=\"none\">Nenhum</string>\n    <string name=\"fade\">Desvanecer</string>\n    <string name=\"glow\">Brilho</string>\n    <string name=\"slide\">Deslizar</string>\n    <string name=\"karaoke\">Karaoke</string>\n    <string name=\"apple_music_style\">Apple Music</string>\n    <string name=\"lyrics_text_size\">Tamanho do texto das letras</string>\n    <string name=\"lyrics_line_spacing\">Espaçamento entre linhas das letras</string>\n    <string name=\"wrapped_playlist_saved\">Listas de reprodução guardadas</string>\n    <string name=\"equalizer_header\">Equalizador</string>\n    <string name=\"no_profiles\">Sem perfis de equalização</string>\n    <string name=\"import_profile\">Importar perfil</string>\n    <string name=\"eq_disabled\">Desativado</string>\n    <string name=\"delete_profile_desc\">Apagar perfil</string>\n    <string name=\"delete_profile_confirmation\">Tem a certeza de que deseja eliminar %1$s? Esta ação não pode ser desfeita.</string>\n    <string name=\"error_file_read\">Impossível ler o ficheiro</string>\n    <string name=\"error_file_open\">Falha ao abrir o ficheiro: %1$s</string>\n    <string name=\"import_error_title\">Erro de importação</string>\n    <string name=\"casting_to\">Transmitir para %s</string>\n    <string name=\"progress_percent\">Progresso %s%%</string>\n    <string name=\"listening_to_metrolist\">Ouvir no Metrolist</string>\n    <string name=\"open\">Aberto</string>\n    <string name=\"failed_to_create_image\">Falha ao criar imagem: %s</string>\n    <string name=\"copied_title\">Título copiado</string>\n    <string name=\"copied_artist\">Artista copiado</string>\n    <string name=\"error_playing\">Erro de reprodução</string>\n    <string name=\"failed_to_parse_proxy\">Falha ao processar o URL do proxy.</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"one\">%d perfil</item>\n        <item quantity=\"many\">%d perfis</item>\n        <item quantity=\"other\">%d perfis</item>\n    </plurals>\n    <plurals name=\"band_count\">\n        <item quantity=\"one\">%d banda</item>\n        <item quantity=\"many\">%d bandas</item>\n        <item quantity=\"other\">%d bandas</item>\n    </plurals>\n    <string name=\"pause_music_when_media_is_muted\">Pausar a música quando a multimédia está silenciada</string>\n    <string name=\"about_artist\">Sobre</string>\n    <string name=\"show_more\">Mostrar mais</string>\n    <string name=\"show_less\">Mostrar menos</string>\n    <string name=\"artist_page_settings\">Página do artista</string>\n    <string name=\"show_artist_description\">Mostrar descrição do artista</string>\n    <string name=\"show_artist_subscriber_count\">Mostrar contador de subscritores</string>\n    <string name=\"show_artist_monthly_listeners\">Mostrar ouvintes mensais</string>\n    <string name=\"crop_album_art\">Cortar Capa do Álbum</string>\n    <string name=\"crop_album_art_desc\">Forçar uma proporção quadrada cortando as miniaturas de vídeos</string>\n    <string name=\"enable_simpmusic\">Habilitar SimpMusic Lyrics</string>\n    <string name=\"enable_simpmusic_desc\">Usar o provedor SimpMusic Lyrics para letras sincronizadas</string>\n    <string name=\"skip_silence_instant\">Saltar instantâneamente silêncio</string>\n    <string name=\"persistent_shuffle_title\">Modo aleatório persistente</string>\n    <string name=\"persistent_shuffle_desc\">Manter o modo aleatório ligado quando começarem novas músicas ou listas de reprodução</string>\n    <string name=\"remember_shuffle_and_repeat\">Lembrar modo aleatório e repetir</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Lembrar modo aleatório e modo repetir quando a aplicação reiniciar</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">Manter ecrã ligado quando o reprodutor estiver expandido</string>\n    <string name=\"system_equalizer\">Equalizador do Sistema</string>\n    <string name=\"error_title\">Erro</string>\n    <string name=\"error_playback_failed\">Erro na reprodução</string>\n    <string name=\"album_art\">Arte do álbum</string>\n    <string name=\"no_song_playing\">Sem música a tocar</string>\n    <string name=\"tap_to_open\">Toca para abrir o Metrolist</string>\n    <string name=\"previous\">Retroceder</string>\n    <string name=\"play_pause\">Reproduzir/Pausar</string>\n    <string name=\"next\">Próximo</string>\n    <string name=\"like\">Gosto</string>\n    <string name=\"widget_description\">Widget de reprodutor de música com controlos de reprodução</string>\n    <string name=\"turntable_widget_description\">Acesso rápido para a tua faixa mais reproduzida recentemente</string>\n    <string name=\"skip_silence_instant_desc\">Saltar as partes silenciosas das músicas em vez de aumentar a velocidade</string>\n    <string name=\"skip_silence_desc\">Avançar durante as partes silenciosas das músicas</string>\n    <string name=\"enable\">Habilitar</string>\n    <string name=\"prevent_duplicate_tracks_in_queue\">Impedir faixas duplicadas em fila</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">Ao adicionar uma faixa à fila, remova-a da sua posição anterior se já estiver presente</string>\n    <string name=\"crossfade\">Transição suave</string>\n    <string name=\"crossfade_desc\">Transição suave entre músicas</string>\n    <string name=\"crossfade_duration\">Duração da transição suave</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-pt/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Início</string>\n    <string name=\"albums\">Álbuns</string>\n    <string name=\"playlists\">Listas de reprodução</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d selecionada</item>\n        <item quantity=\"many\">%d selecionadas</item>\n        <item quantity=\"other\">%d selecionadas</item>\n    </plurals>\n    <string name=\"history\">Histórico</string>\n    <string name=\"stats\">Estatísticas</string>\n    <string name=\"mood_and_genres\">Tom e Géneros</string>\n    <string name=\"account\">Conta</string>\n    <string name=\"quick_picks\">Escolhas rápidas</string>\n    <string name=\"quick_picks_empty\">Ouça músicas para gerar as escolhas rápidas</string>\n    <string name=\"forgotten_favorites\">Favoritas esquecidas</string>\n    <string name=\"keep_listening\">Continuar a ouvir</string>\n    <string name=\"your_youtube_playlists\">Listas de reprodução YouTube</string>\n    <string name=\"similar_to\">Semelhantes</string>\n    <string name=\"today\">Hoje</string>\n    <string name=\"yesterday\">Ontem</string>\n    <string name=\"most_played_albums\">Álbuns mais reproduzidos</string>\n    <string name=\"search\">Pesquisar</string>\n    <string name=\"search_yt_music\">Pesquisar no YouTube Music…</string>\n    <string name=\"filter_liked\">Gosto</string>\n    <string name=\"filter_downloaded\">Transferidas</string>\n    <string name=\"filter_all\">Tudo</string>\n    <string name=\"search_library\">Pesquisar na biblioteca…</string>\n    <string name=\"filter_library\">Biblioteca</string>\n    <string name=\"filter_songs\">Músicas</string>\n    <string name=\"filter_community_playlists\">Listas de reprodução da comunidade</string>\n    <string name=\"filter_featured_playlists\">Listas de reprodução em destaque</string>\n    <string name=\"filter_bookmarked\">Marcadas</string>\n    <string name=\"no_results_found\">Não há resultados</string>\n    <string name=\"library_playlist_empty\">As listas de reprodução aparecerão aqui</string>\n    <string name=\"library_song_empty\">As músicas da biblioteca aparecerão aqui</string>\n    <string name=\"library_artist_empty\">Os artistas da biblioteca aparecerão aqui</string>\n    <string name=\"other_versions\">Outras versões</string>\n    <string name=\"downloaded_songs\">Músicas transferidas</string>\n    <string name=\"playlist_is_empty\">A lista de reprodução está vazia</string>\n    <string name=\"remove_download_playlist_confirm\">Deseja remover do armazenamento das \\\"Músicas Transferidas\\\" todas as músicas da lista de reprodução \\\"%s\\\"?</string>\n    <string name=\"delete_playlist_confirm\">Deseja eliminar a lista de reprodução \\\"%s\\\"?</string>\n    <string name=\"retry\">Tentar novamente</string>\n    <string name=\"radio\">Rádio</string>\n    <string name=\"shuffle\">Misturar</string>\n    <string name=\"details\">Detalhes</string>\n    <string name=\"edit\">Editar</string>\n    <string name=\"start_radio\">Iniciar rádio</string>\n    <string name=\"play\">Reproduzir</string>\n    <string name=\"play_next\">Seguinte</string>\n    <string name=\"add_to_queue\">Adicionar à fila</string>\n    <string name=\"remove_all_from_library\">Remover tudo da biblioteca</string>\n    <string name=\"action_download\">Transferir</string>\n    <string name=\"downloading\">A transferir</string>\n    <string name=\"remove_download\">Remover transferidas</string>\n    <string name=\"import_playlist\">Importar lista de reprodução</string>\n    <string name=\"add_to_playlist\">Adicionar à lista de reprodução</string>\n    <string name=\"view_artist\">Ver artista</string>\n    <string name=\"view_album\">Ver álbum</string>\n    <string name=\"share\">Partilhar</string>\n    <string name=\"delete\">Eliminar</string>\n    <string name=\"remove_from_history\">Remover do histórico</string>\n    <string name=\"remove_from_playlist\">Remover da lista de reprodução</string>\n    <string name=\"search_online\">Pesquisar on-line</string>\n    <string name=\"action_sync\">Sincronizar</string>\n    <string name=\"advanced\">Avançadas</string>\n    <string name=\"tempo_and_pitch\">Tempo e Nota Tónica</string>\n    <string name=\"sort_by_create_date\">Data de adição</string>\n    <string name=\"sort_by_artist\">Artista</string>\n    <string name=\"sort_by_year\">Ano</string>\n    <string name=\"sort_by_song_count\">Número de músicas</string>\n    <string name=\"sort_by_length\">Duração</string>\n    <string name=\"sort_by_play_time\">Tempo de reprodução</string>\n    <string name=\"sort_by_custom\">Ordem personalizada</string>\n    <string name=\"media_id\">Id. de multimédia</string>\n    <string name=\"mime_type\">Tipo MIME</string>\n    <string name=\"codecs\">Codificadores</string>\n    <string name=\"bitrate\">Taxa de dados</string>\n    <string name=\"sample_rate\">Frequência</string>\n    <string name=\"volume\">Volume</string>\n    <string name=\"add_to_library\">Adicionar à biblioteca</string>\n    <string name=\"add_all_to_library\">Adicionar tudo à biblioteca</string>\n    <string name=\"remove_from_library\">Remover da biblioteca</string>\n    <string name=\"file_size\">Tamanho do ficheiro</string>\n    <string name=\"unknown\">Desconhecido</string>\n    <string name=\"copied\">Copiado para a área de transferência</string>\n    <string name=\"search_lyrics\">Pesquisar letras</string>\n    <string name=\"edit_song\">Editar música</string>\n    <string name=\"song_title\">Título</string>\n    <string name=\"error_song_title_empty\">O título não pode estar vazio.</string>\n    <string name=\"error_song_artist_empty\">O artista não pode estar vazio.</string>\n    <string name=\"save\">Guardar</string>\n    <string name=\"choose_playlist\">Escolher lista de reprodução</string>\n    <string name=\"edit_playlist\">Editar lista de reprodução</string>\n    <string name=\"create_playlist\">Criar lista de reprodução</string>\n    <string name=\"playlist_name\">Nome da lista de reprodução</string>\n    <string name=\"error_playlist_name_empty\">O nome da lista de reprodução não pode estar vazio.</string>\n    <string name=\"edit_artist\">Editar artista</string>\n    <string name=\"artist_name\">Nome do artista</string>\n    <string name=\"error_artist_name_empty\">O nome do artista não pode estar vazio.</string>\n    <string name=\"skip_duplicates\">Ignorar duplicadas</string>\n    <string name=\"duplicates\">Duplicadas</string>\n    <string name=\"duplicates_description_single\">Esta música já está na lista de reprodução</string>\n    <string name=\"add_anyway\">Adicionar na mesma</string>\n    <string name=\"duplicates_description_multiple\">%d músicas já estão na lista de reprodução</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d música</item>\n        <item quantity=\"many\">%d músicas</item>\n        <item quantity=\"other\">%d músicas</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d artista</item>\n        <item quantity=\"many\">%d artistas</item>\n        <item quantity=\"other\">%d artistas</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d álbum</item>\n        <item quantity=\"many\">%d álbuns</item>\n        <item quantity=\"other\">%d álbuns</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d lista de reprodução</item>\n        <item quantity=\"many\">%d listas de reprodução</item>\n        <item quantity=\"other\">%d listas de reprodução</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d semana</item>\n        <item quantity=\"many\">%d semanas</item>\n        <item quantity=\"other\">%d semanas</item>\n    </plurals>\n    <string name=\"queue_all_songs\">Todas as músicas</string>\n    <string name=\"queue_searched_songs\">Músicas pesquisadas</string>\n    <string name=\"music_player\">Reprodutor de Música</string>\n    <string name=\"settings\">Definições</string>\n    <string name=\"appearance\">Aparência</string>\n    <string name=\"theme\">Tema</string>\n    <string name=\"enable_dynamic_theme\">Ativar tema dinâmico</string>\n    <string name=\"dark_theme\">Tema Escuro</string>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d mês</item>\n        <item quantity=\"many\">%d meses</item>\n        <item quantity=\"other\">%d meses</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d ano</item>\n        <item quantity=\"many\">%d anos</item>\n        <item quantity=\"other\">%d anos</item>\n    </plurals>\n    <string name=\"playlist_imported\">Lista de reprodução importada</string>\n    <string name=\"removed_song_from_playlist\">\\\"%s\\\" removida da lista de reprodução</string>\n    <string name=\"playlist_synced\">Lista de reprodução sincronizada</string>\n    <string name=\"undo\">Anular</string>\n    <string name=\"lyrics_not_found\">Letra não encontrada</string>\n    <string name=\"sleep_timer\">Temporizador</string>\n    <string name=\"end_of_song\">Fim da música</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">%d minuto</item>\n        <item quantity=\"many\">%d minutos</item>\n        <item quantity=\"other\">%d minutos</item>\n    </plurals>\n    <string name=\"error_no_stream\">Não existem fluxos</string>\n    <string name=\"error_no_internet\">Sem ligação de rede</string>\n    <string name=\"error_timeout\">Caducidade</string>\n    <string name=\"error_unknown\">Erro desconhecido</string>\n    <string name=\"action_like\">Gostar</string>\n    <string name=\"action_like_all\">Gostar de tudo</string>\n    <string name=\"action_remove_like\">Remover gosto</string>\n    <string name=\"action_remove_like_all\">Remover gosto de tudo</string>\n    <string name=\"action_shuffle_on\">Baralhar ativo</string>\n    <string name=\"action_shuffle_off\">Baralhar inativo</string>\n    <string name=\"repeat_mode_off\">Modo de repetição ativo</string>\n    <string name=\"repeat_mode_one\">Repetir música atual</string>\n    <string name=\"repeat_mode_all\">Repetir fila</string>\n    <string name=\"dark_theme_off\">Inativo</string>\n    <string name=\"dark_theme_follow_system\">Tema do sistema</string>\n    <string name=\"pure_black\">Preto puro</string>\n    <string name=\"player\">Reprodutor</string>\n    <string name=\"player_text_alignment\">Alinhamento do texto do reprodutor</string>\n    <string name=\"sided\">Lado a lado</string>\n    <string name=\"left\">Esquerda</string>\n    <string name=\"center\">Centro</string>\n    <string name=\"right\">Direita</string>\n    <string name=\"player_slider_style\">Estilo do cursor do reprodutor</string>\n    <string name=\"squiggly\">Difuso</string>\n    <string name=\"default_open_tab\">Separador inicial</string>\n    <string name=\"grid_cell_size\">Tamanho da grelha</string>\n    <string name=\"small\">Pequena</string>\n    <string name=\"big\">Grande</string>\n    <string name=\"content\">Conteúdo</string>\n    <string name=\"login\">Iniciar sessão</string>\n    <string name=\"not_logged_in\">Sessão não iniciada</string>\n    <string name=\"content_language\">Idioma predefinido do conteúdo</string>\n    <string name=\"system_default\">Predefinição do sistema</string>\n    <string name=\"enable_proxy\">Ativar proxy</string>\n    <string name=\"proxy_type\">Tipo de proxy</string>\n    <string name=\"proxy_url\">URL do proxy</string>\n    <string name=\"restart_to_take_effect\">Reinicie para aplicar</string>\n    <string name=\"player_and_audio\">Reprodução e áudio</string>\n    <string name=\"pause_search_history\">Pausar histórico de pesquisa</string>\n    <string name=\"clear_search_history\">Limpar histórico de pesquisa</string>\n    <string name=\"clear_search_history_confirm\">Deseja limpar todo o histórico de pesquisa?</string>\n    <string name=\"disable_screenshot\">Desativar captura de ecrã</string>\n    <string name=\"disable_screenshot_desc\">Quando esta opção está ativada, as capturas de ecrã e a visualização da aplicação em \\\"Recentes\\\" estão desativadas.</string>\n    <string name=\"enable_lrclib\">Ativar provedor de letras LrcLib</string>\n    <string name=\"enable_kugou\">Ativar provedor de letras KuGou</string>\n    <string name=\"hide_explicit\">Ocultar conteúdo explícito</string>\n    <string name=\"backup_restore\">Cópia de segurança e restauro</string>\n    <string name=\"audio_quality_low\">Baixa</string>\n    <string name=\"queue\">Fila</string>\n    <string name=\"persistent_queue\">Fila persistente</string>\n    <string name=\"persistent_queue_desc\">Restaurar a sua última fila quando a aplicação inicia</string>\n    <string name=\"auto_load_more\">Carregar mais músicas automaticamente</string>\n    <string name=\"skip_silence\">Ignorar silêncio</string>\n    <string name=\"audio_normalization\">Normalização de som</string>\n    <string name=\"equalizer\">Equalizador</string>\n    <string name=\"cache\">Cache</string>\n    <string name=\"image_cache\">Cache de imagens</string>\n    <string name=\"song_cache\">Cache de músicas</string>\n    <string name=\"max_cache_size\">Tamanho máximo</string>\n    <string name=\"unlimited\">Ilimitado</string>\n    <string name=\"clear_all_downloads\">Limpar todas as transferências</string>\n    <string name=\"max_image_cache_size\">Tamanho máximo para cache de imagens</string>\n    <string name=\"clear_image_cache\">Limpar cache de imagens</string>\n    <string name=\"max_song_cache_size\">Tamanho máximo para cache de músicas</string>\n    <string name=\"size_used\">%s utilizado</string>\n    <string name=\"privacy\">Privacidade</string>\n    <string name=\"listen_history\">Histórico de reprodução</string>\n    <string name=\"imported_playlist\">Lista de reprodução importada</string>\n    <string name=\"backup_create_success\">Cópia de segurança criada com sucesso</string>\n    <string name=\"restore_failed\">Não foi possível restaurar a cópia de segurança</string>\n    <string name=\"discord_integration\">Integração Discord</string>\n    <string name=\"dismiss\">Ignorar</string>\n    <string name=\"login_failed\">Autenticação falhou</string>\n    <string name=\"action_logout\">Terminar sessão</string>\n    <string name=\"enable_discord_rpc\">Ativar Rich Presence</string>\n    <string name=\"about\">Sobre</string>\n    <string name=\"app_version\">Versão da Aplicação</string>\n    <string name=\"new_version_available\">Disponível nova versão</string>\n    <string name=\"translation_models\">Modelos de Tradução</string>\n    <string name=\"clear_translation_models\">Limpar modelos de tradução</string>\n    <string name=\"songs\">Músicas</string>\n    <string name=\"artists\">Artistas</string>\n    <string name=\"new_release_albums\">Novos álbuns de lançamento</string>\n    <string name=\"this_week\">Esta semana</string>\n    <string name=\"last_week\">Semana passada</string>\n    <string name=\"most_played_songs\">Músicas mais reproduzidas</string>\n    <string name=\"most_played_artists\">Artistas mais reproduzidos</string>\n    <string name=\"filter_videos\">Vídeos</string>\n    <string name=\"filter_albums\">Álbuns</string>\n    <string name=\"filter_artists\">Artistas</string>\n    <string name=\"filter_playlists\">Listas de reprodução</string>\n    <string name=\"from_your_library\">Da sua biblioteca</string>\n    <string name=\"liked_songs\">Músicas de que gosto</string>\n    <string name=\"sort_by_name\">Nome</string>\n    <string name=\"remove_from_queue\">Remover da fila</string>\n    <string name=\"reset\">Repor</string>\n    <string name=\"refetch\">Obter</string>\n    <string name=\"loudness\">Sonoridade</string>\n    <string name=\"library_album_empty\">Os álbuns da biblioteca aparecerão aqui</string>\n    <string name=\"edit_lyrics\">Editar letras</string>\n    <string name=\"song_artists\">Artistas da música</string>\n    <string name=\"dark_theme_on\">Ativo</string>\n    <string name=\"customize_navigation_tabs\">Personalizar separadores</string>\n    <string name=\"lyrics_text_position\">Posição do texto da letra</string>\n    <string name=\"default_\">Predefinição</string>\n    <string name=\"misc\">Outras</string>\n    <string name=\"content_country\">País predefinido do conteúdo</string>\n    <string name=\"action_restore\">Restaurar</string>\n    <string name=\"action_backup\">Efetuar Cópia</string>\n    <string name=\"backup_create_failed\">Não foi possível criar a cópia de segurança</string>\n    <string name=\"options\">Opções</string>\n    <string name=\"preview\">Prever</string>\n    <string name=\"audio_quality\">Qualidade do áudio</string>\n    <string name=\"audio_quality_high\">Alta</string>\n    <string name=\"audio_quality_auto\">Automática</string>\n    <string name=\"pause_listen_history\">Pausar histórico de reprodução</string>\n    <string name=\"clear_listen_history\">Limpar histórico de reprodução</string>\n    <string name=\"storage\">Armazenamento</string>\n    <string name=\"clear_song_cache\">Limpar cache de músicas</string>\n    <string name=\"auto_load_more_desc\">Se possível, adicionar mais músicas automaticamente quando é atingido o fim da fila</string>\n    <string name=\"clear_listen_history_confirm\">Deseja limpar todo o histórico de reprodução?</string>\n    <string name=\"search_history\">Histórico de pesquisas</string>\n    <string name=\"auto_skip_next_on_error_desc\">Garante um experiência contínua de reprodução</string>\n    <string name=\"auto_skip_next_on_error\">Avançar automaticamente para a música seguinte se ocorrer algum erro</string>\n    <string name=\"stop_music_on_task_clear\">Parar ao limpar a tarefa</string>\n    <string name=\"discord_information\">Metrolist utiliza a biblioteca KizzyRPC para definir o estado da sua conta Discord. Isto envolve uma ligação ao Discord Gateway, que pode ser considerada uma violação dos termos do serviço Discord. Contudo ,não há casos conhecidos de contas suspensas por este motivo. Utilize por sua conta e risco.\\n\\nMetrolist apenas irá extrair o seu token e tudo o resto é armazenado localmente.</string>\n    <string name=\"use_login_for_browse\">Inicie a sessão para ver o conteúdo de navegação</string>\n    <string name=\"use_login_for_browse_desc\">Isto influencia o conteúdo visível, e por exemplo, mostra apenas álbuns Premium, se estiver autenticado com uma conta Premium</string>\n    <string name=\"action_login\">Iniciar sessão</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-pt-rBR/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"weeks\">Semanas</string>\n    <string name=\"months\">Meses</string>\n    <string name=\"years\">Anos</string>\n    <string name=\"continuous\">Contínuo</string>\n    <string name=\"liked\">Favoritas</string>\n    <string name=\"offline\">Baixadas</string>\n    <string name=\"my_top\">Meu top</string>\n    <string name=\"select\">Selecionar tudo</string>\n    <string name=\"like_all\">Favoritar tudo</string>\n    <string name=\"sort_by_last_updated\">Quando atualizado</string>\n    <string name=\"lyrics\">Letra</string>\n    <string name=\"already_in_playlist\">Já está na playlist:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d vez</item>\n        <item quantity=\"many\">%d de vezes</item>\n        <item quantity=\"other\">%d vezes</item>\n    </plurals>\n    <string name=\"similar_content\">Conteúdo similar</string>\n    <string name=\"player_background_style\">Estilo do fundo do tocador</string>\n    <string name=\"follow_theme\">Seguir o tema</string>\n    <string name=\"gradient\">Gradiente</string>\n    <string name=\"player_background_blur\">Desfocado</string>\n    <string name=\"enable_swipe_thumbnail\">Deslizar para trocar de música</string>\n    <string name=\"lyrics_click_change\">Alterar letra ao clicar</string>\n    <string name=\"slim\">Pequeno</string>\n    <string name=\"slim_navbar\">Barra de navegação inferior pequena</string>\n    <string name=\"general\">Geral</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"default_lib_chips\">Alterar tela da biblioteca padrão</string>\n    <string name=\"set_quick_picks\">Definir escolhas rápidas</string>\n    <string name=\"last_song_listened\">Com base na última música ouvida</string>\n    <string name=\"app_language\">Idioma do app</string>\n    <string name=\"enable_similar_content\">Habilitar conteúdo similar</string>\n    <string name=\"similar_content_desc\">Adicionar automaticamente mais músicas similares quando o final da fila for alcançado</string>\n    <string name=\"default_links\">Abrir links suportados</string>\n    <string name=\"open_app_settings_error\">Não foi possível abrir as configurações do app</string>\n    <string name=\"release_notes\">Registro de mudanças</string>\n    <string name=\"all_time\">Desde sempre</string>\n    <string name=\"past_24_hours\">Últimas 24 horas</string>\n    <string name=\"past_week\">Última semana</string>\n    <string name=\"past_month\">Último mês</string>\n    <string name=\"past_year\">Último ano</string>\n    <string name=\"top_length\">Tamanho da playlist \\\"Mais Tocadas\\\"</string>\n    <string name=\"history_duration\">Duração do histórico</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">%d segundo</item>\n        <item quantity=\"many\">%d de segundos</item>\n        <item quantity=\"other\">%d segundos</item>\n    </plurals>\n    <string name=\"local_history\">Local</string>\n    <string name=\"cached_playlist\">Em cache</string>\n    <string name=\"token_adv_login_description\">Esse é um método avançado de login. Como uma alternativa ao portal web, você pode digitar ou atualizar o seu token diretamente por aqui. Como exemplo, isso pode fazer com que fazer login em vários dispositivos seja mais rápido. Observa que qualquer formato inválido do token que o app não consiga interpretar não será aceitado</string>\n    <string name=\"information\">Informações</string>\n    <string name=\"allows_for_sync_witch_youtube\">Observação: Isso permite a sincronização com o YouTube Music. Isso NÃO pode ser alterado mais tarde.</string>\n    <string name=\"swipe_song_to_add\">Deslizar na música para esquerda para adicionar na fila ou para a direita para tocá-la em seguida</string>\n    <string name=\"show_top_playlist\">Mostrar playlist \\\"Mais Tocadas\\\"</string>\n    <string name=\"show_cached_playlist\">Mostrar playlist \\\"Em cache\\\"</string>\n    <string name=\"remote_history\">Remoto</string>\n    <string name=\"charts\">Ranques</string>\n    <string name=\"back_button_desc\">Voltar</string>\n    <string name=\"album_cover_desc\">Arte do álbum</string>\n    <string name=\"top_music_videos\">Vídeos de música populares</string>\n    <string name=\"trending\">Em alta</string>\n    <string name=\"sync_disabled\">Sincronização desativada</string>\n    <string name=\"remove_from_cache\">Remover do cache</string>\n    <string name=\"copy_link\">Copiar link</string>\n    <string name=\"link_copied\">Link copiado para a área de transferência</string>\n    <string name=\"player_buttons_style\">Cores dos botões do tocador</string>\n    <string name=\"default_style\">Padrão</string>\n    <string name=\"auto_playlists\">Playlists automáticas</string>\n    <string name=\"show_downloaded_playlist\">Mostrar playlist \\\"Baixadas\\\"</string>\n    <string name=\"sync_playlist\">Sincronizar playlist</string>\n    <string name=\"dislike_all\">Desfavoritar tudo</string>\n    <string name=\"show_liked_playlist\">Mostrar playlist \\\"Curtidas\\\"</string>\n    <string name=\"advanced_login\">Login com token</string>\n    <string name=\"token_hidden\">Toque para mostrar o token</string>\n    <string name=\"token_shown\">Toque novamente para copiar ou editar</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"clear_song_cache_dialog\">Você tem certeza que quer limpar todas as músicas em cache?</string>\n    <string name=\"clear_downloads_dialog\">Você tem certeza que você quer limpar todos os downloads?</string>\n    <string name=\"not_logged_in_youtube\">Não conectado ao YouTube</string>\n    <string name=\"description\">Descrição</string>\n    <string name=\"views\">Visualizações</string>\n    <string name=\"likes\">Curtidos</string>\n    <string name=\"dislikes\">Não curti</string>\n    <string name=\"cancel\">Cancelar</string>\n    <string name=\"share_lyrics\">Compartilhar letra</string>\n    <string name=\"share_as_text\">Compartilhar como texto</string>\n    <string name=\"share_as_image\">Compartilhar como imagem</string>\n    <string name=\"max_selection_limit\">Limite máximo de seleção</string>\n    <string name=\"share_selected\">Compartilhar selecionado</string>\n    <string name=\"customize_colors\">Customizar cores</string>\n    <string name=\"text_color\">Cor do texto</string>\n    <string name=\"secondary_text_color\">Cor secundária do texto</string>\n    <string name=\"background_color\">Cor de fundo</string>\n    <string name=\"auto_download_on_like\">Baixar automaticamente ao curtir</string>\n    <string name=\"auto_download_on_like_desc\">Baixar músicas automaticamente quando você as curte</string>\n    <string name=\"please_wait\">Por favor aguarde</string>\n    <string name=\"generating_image\">Gerando imagem</string>\n    <string name=\"lyrics_auto_scroll\">Rolar letra automaticamente</string>\n    <string name=\"import_online\">Importar uma playlist \\\"m3u\\\"</string>\n    <string name=\"import_csv\">Importar uma playlist csv</string>\n    <string name=\"playlist_add_local_to_synced_note\">Observação: Não há suporte para adicionar músicas locais em playlists sincronizadas/remotas. Qualquer outra combinação é válida</string>\n    <string name=\"lyrics_romanize_japanese\">Romanizar letras em Japonês</string>\n    <string name=\"lyrics_romanize_korean\">Romanizar letras em Coreano</string>\n    <string name=\"yt_sync\">Sincronizar automaticamente com a conta</string>\n    <string name=\"more_content\">Mais conteúdo</string>\n    <string name=\"new_player_design\">Novo design do tocador</string>\n    <string name=\"swipe_sensitivity\">Sensibilidade do gesto de deslizar no mini tocador</string>\n    <string name=\"clear_image_cache_dialog\">Tem certeza que quer apagar todas as imagens no cache?</string>\n    <string name=\"disable\">Desativar</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"subscribe\">Inscrever-se</string>\n    <string name=\"subscribed\">Inscrito</string>\n    <string name=\"new_mini_player_design\">Novo design do mini tocador</string>\n    <string name=\"now_playing\">Tocando agora</string>\n    <string name=\"close\">Fechar</string>\n    <string name=\"hide_player_thumbnail\">Ocultar miniatura do tocador</string>\n    <string name=\"hide_player_thumbnail_desc\">Substituir arte do álbum com a logo do app no tocador</string>\n    <string name=\"seek_forward_dynamic\">+%1$d segundos para frente</string>\n    <string name=\"seek_backward_dynamic\">-%1$d segundos para trás</string>\n    <string name=\"starting_radio\">Iniciando a rádio</string>\n    <string name=\"seek_seconds_addup_description\">Ao ativar, adiciona acréscimos de até 5 segundos extras em cada pulo do avançar/retroceder</string>\n    <string name=\"disable_load_more_when_repeat_all\">Desativar o carregar mais ao repetir tudo</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Não carregar mais músicas automaticamente e conteúdo parecido quando o modo de repetir tudo está ativado</string>\n    <string name=\"settings_section_ui\">Interface</string>\n    <string name=\"settings_section_privacy\">Privacidade e segurança</string>\n    <string name=\"settings_section_player_content\">Tocador e conteúdo</string>\n    <string name=\"settings_section_storage\">Armazenamento e dados</string>\n    <string name=\"settings_section_system\">Sistema e informações</string>\n    <string name=\"seek_seconds_addup\">Avançar/retroceder progressivo</string>\n    <string name=\"edit_playlist_cover\">Editar capa da playlist</string>\n    <string name=\"edit_playlist_cover_note\">Observação: A sua conta deve estar vinculada a um número de telefone e verificada no YouTube Music para alterar a capa da playlist.</string>\n    <string name=\"edit_playlist_cover_note_wait\">Ao selecionar uma imagem, aguarde um momento para que a capa nova apareça na sua playlist.</string>\n    <string name=\"config_proxy\">Configurar proxy</string>\n    <string name=\"proxy_username\">Nome do usuário da proxy</string>\n    <string name=\"proxy_password\">Senha da proxy</string>\n    <string name=\"enable_authentication\">Ativar autenticação</string>\n    <string name=\"lyrics_romanization_cyrillic\">Alfabeto cirílico</string>\n    <string name=\"lyrics_romanize_title\">Romanização</string>\n    <string name=\"lyrics_romanization\">Romanização da letra</string>\n    <string name=\"lyrics_romanize_russian\">Romanizar letras em Russo</string>\n    <string name=\"lyrics_romanize_ukrainian\">Romanizar letras em Ucraniano</string>\n    <string name=\"lyrics_romanize_belarusian\">Romanizar letras em Bielorrusso</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Romanizar letras em Quirguiz</string>\n    <string name=\"lyrics_romanize_serbian\">Romanizar letras em Sérvio</string>\n    <string name=\"lyrics_romanize_bulgarian\">Romanizar letras em Búlgaro</string>\n    <string name=\"line_by_line_option_title\">EXPERIMENTAL: Detectar idioma linha por linha</string>\n    <string name=\"line_by_line_option_desc\">O idioma Cirílico será detectado linha por linha em vez da música inteira.</string>\n    <string name=\"line_by_line_dialog_title\">Tem certeza?</string>\n    <string name=\"line_by_line_dialog_desc\">Este é um recurso experimental que pode acertar ou errar.\\n\\nPor padrão, o idioma é determinado da música inteira, mas com esta opção ativada, será determinado de linha por linha. Isto permite que funcione para músicas em vários idiomas MAS o idioma pode estar incorreto (por exemplo se há uma letra em Ucraniano que não contém nenhum símbolo específico ao Ucraniano, ela pode ser romanizada como Russo).\\n\\nSe você não está tendo problemas, é recomendado manter esta opção desativada.</string>\n    <string name=\"romanize_current_track\">Romanizar faixa atual</string>\n    <string name=\"choose_from_library\">Escolher da biblioteca</string>\n    <string name=\"remove_custom_image\">Remover imagem customizada</string>\n    <string name=\"audio_offload\">Ativar descarga</string>\n    <string name=\"audio_offload_description\">Usar o caminho de descarga de áudio para a reprodução de áudio. Ao desativar isto, o consumo de energia aumenta, mas pode ser útil se ter problemas com reprodução de áudio ou pós-processamento</string>\n    <string name=\"uploaded_playlist\">Enviadas</string>\n    <string name=\"filter_uploaded\">Enviado</string>\n    <string name=\"show_uploaded_playlist\">Mostrar playlist \\\"Enviadas\\\"</string>\n    <string name=\"updater\">Atualizador</string>\n    <string name=\"check_for_updates\">Conferir se há atualizações automaticamente</string>\n    <string name=\"lyrics_romanize_macedonian\">Romanizar letras em Macedônio</string>\n    <string name=\"update_notifications\">Ativar notificações de atualizações</string>\n    <string name=\"update_available_title\">Atualização disponível</string>\n    <string name=\"update_channel_name\">Atualizações do aplicativo</string>\n    <string name=\"update_channel_desc\">Notificações sobre novas versões</string>\n    <string name=\"discord_use_details\">Usar detalhes ao invés do estado</string>\n    <string name=\"discord_use_details_description\">Mostrar o título da música destacadamente em vez do nome do artista</string>\n    <string name=\"integrations\">Integrações</string>\n    <string name=\"username\">Nome do usuário</string>\n    <string name=\"password\">Senha</string>\n    <string name=\"lastfm_integration\">Integração com o Last.fm</string>\n    <string name=\"enable_scrobbling\">Ativar scrobbling</string>\n    <string name=\"lastfm_now_playing\">Enviar reprodução atual</string>\n    <string name=\"scrobbling_configuration\">Configuração de scrobbling</string>\n    <string name=\"scrobble_min_track_duration\">Fazer scrobble de músicas mais longas que</string>\n    <string name=\"scrobble_delay_percent\">Porcentagem do atraso de Scrobble</string>\n    <string name=\"scrobble_delay_minutes\">Minutos do atraso do Scrobble</string>\n    <string name=\"swipe_song_to_remove\">Deslizar música para removê-la da playlist</string>\n    <string name=\"primary_color_style\">Cor primária</string>\n    <string name=\"tertiary_color_style\">Cor terciária</string>\n    <string name=\"auto_scroll\">Sincronizar</string>\n    <string name=\"lyrics_romanize_chinese\">Romanizar letras em Chinês</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"google_cast_description\">Habilitar transmissão de áudio para o Chromecast e outros dispositivos compatíveis</string>\n    <string name=\"last_fm_send_likes\">Enviar Likes/Deslikes</string>\n    <string name=\"last_fm_send_likes_description\">Curtir/Descurtir músicas no Last.fm quanto são Curtidas/Descurtidas no Metrolist</string>\n    <string name=\"logging_in\">Fazendo login…</string>\n    <string name=\"hide_video_songs\">Ocultar videoclipes</string>\n    <string name=\"details_desc\">Ver as informações da música</string>\n    <string name=\"edit_desc\">Mudar o título ou artista</string>\n    <string name=\"start_radio_desc\">Criar uma estação baseada neste item</string>\n    <string name=\"play_next_desc\">Adicionar ao topo da sua fila de reprodução</string>\n    <string name=\"add_to_queue_desc\">Adicionar ao fim da sua fila de reprodução</string>\n    <string name=\"add_to_library_desc\">Salvar na sua biblioteca</string>\n    <string name=\"download_desc\">Tornar disponível para reprodução offline</string>\n    <string name=\"add_to_playlist_desc\">Adicionar à uma de suas playlists</string>\n    <string name=\"refetch_desc\">Obter os metadados mais recentes do YouTube Music</string>\n    <string name=\"share_desc\">Compartilhar o link deste item</string>\n    <string name=\"delete_desc\">Remover permanentemente este item</string>\n    <string name=\"advanced_desc\">Mudar o andamento e o tom da música</string>\n    <string name=\"equalizer_desc\">Ajustar o equalizador de áudio</string>\n    <string name=\"enable_dynamic_icon\">Ativar ícone dinâmico</string>\n    <string name=\"mini_player\">Mini-reprodutor</string>\n    <string name=\"pure_black_mini_player\">Mini-tocador preto puro</string>\n    <string name=\"cache_size_warning_title\">Espere!</string>\n    <string name=\"cache_size_warning_message\">Você escolheu um limite do tamanho de cache menor do que o aplicativo está usando atualmente (%1$s). Se continuar, o aplicativo poderá remover parte do cache %2$s para corresponder ao novo limite. Continuar mesmo assim?</string>\n    <string name=\"cache_size_warning_confirm\">Continuar</string>\n    <string name=\"download_playlist_desc\">Baixar todas as músicas para reprodução offline</string>\n    <string name=\"remove_download_playlist_desc\">Remover todas as músicas baixadas desta playlist</string>\n    <string name=\"download_in_progress_desc\">Transferência está em andamento</string>\n    <string name=\"share_playlist_desc\">Compartilhe esta playlist com outras pessoas</string>\n    <string name=\"delete_playlist_desc\">Remover esta playlist permanentemente</string>\n    <string name=\"sync_playlist_desc\">Sincronizar playlist com o YouTube Music</string>\n    <string name=\"lyrics_glow_effect\">Ativar o efeito de letras brilhantes</string>\n    <string name=\"lyrics_glow_effect_desc\">Adicionar animação brilhante e efeito de salto para letras ativas</string>\n    <string name=\"enable_better_lyrics\">Ativar Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">Letras sincronizadas sílaba por sílaba para qualquer música, perfeitas para karaokê</string>\n    <string name=\"shuffle_playlist_first\">Embaralhar playlist/álbum primeiro</string>\n    <string name=\"shuffle_playlist_first_desc\">Quando no aleatório, tocar todas as músicas da playlist/álbum original primeiro, depois tocar conteúdo similar</string>\n    <string name=\"show_wrapped_card\">Mostrar cartão da retrospectiva</string>\n    <string name=\"lyrics_animation_style\">Estilo de Animação das Letras</string>\n    <string name=\"none\">Nenhum</string>\n    <string name=\"fade\">Desvanecer</string>\n    <string name=\"glow\">Brilhar</string>\n    <string name=\"slide\">Deslizar</string>\n    <string name=\"karaoke\">Karaoke</string>\n    <string name=\"apple_music_style\">Apple Music</string>\n    <string name=\"lyrics_text_size\">Tamanho do texto das letras</string>\n    <string name=\"lyrics_line_spacing\">Espaçamento de linha das letras</string>\n    <string name=\"album_art_for\">Arte de álbum para %s</string>\n    <string name=\"wrapped_total_albums_title\">Você já ouviu</string>\n    <string name=\"wrapped_total_albums_subtitle\">álbuns únicos</string>\n    <string name=\"wrapped_top_album_title\">Seu álbum preferido é</string>\n    <string name=\"wrapped_playlist_ready\">Sua playlist pessoal está pronta</string>\n    <string name=\"wrapped_top_5_albums_title\">Seus 5 álbuns mais ouvidos</string>\n    <string name=\"wrapped_album_listening_time\">Você ouviu esse album por %d minutos</string>\n    <string name=\"wrapped_no_data\">Sem informações</string>\n    <string name=\"wrapped_top_5_artists_title\">Seus artistas mais ouvidos do ano</string>\n    <string name=\"wrapped_artist_listening_time\">%d minutos</string>\n    <string name=\"wrapped_top_5_songs_title\">Suas canções mais ouvidas do ano</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">Arte do album</string>\n    <string name=\"wrapped_top_artist_title\">Seu artista mais ouvido do ano é</string>\n    <string name=\"wrapped_top_artist_image_content_description\">Melhor imagem do artista</string>\n    <string name=\"wrapped_top_artist_listening_time\">Você ouviu eles por %d minutos</string>\n    <string name=\"wrapped_top_song_title\">Sua canção mais tocada é</string>\n    <string name=\"wrapped_top_song_listening_time\">Você ouviu por %d minutos</string>\n    <string name=\"wrapped_total_artists_title\">Você ouviu</string>\n    <string name=\"wrapped_total_artists_subtitle\">Artistas únicos</string>\n    <string name=\"wrapped_total_songs_title\">Você ouviu</string>\n    <string name=\"wrapped_total_songs_subtitle\">canções únicas</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_intro_subtitle\">É hora de checar o que você tem ouvido</string>\n    <string name=\"wrapped_intro_button\">vamos lá!</string>\n    <string name=\"wrapped_logo_content_description\">Metrolist Logo</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"wrapped_ready_title\">SUA RETROSPECTIVA ESTÁ PRONTA!</string>\n    <string name=\"wrapped_ready_subtitle\">Hora de checar o que você amou esse ano.</string>\n    <string name=\"wrapped_thank_you\">Obrigado por ouvir</string>\n    <string name=\"wrapped_special_thanks\">Agradecimentos especiais ao MO Agamy por criar o Metrolist</string>\n    <string name=\"wrapped_close\">Fechar retrospectiva</string>\n    <string name=\"wrapped_playlist_title\">Sua Retrospectiva %s</string>\n    <string name=\"wrapped_create_playlist\">Criar playlist</string>\n    <string name=\"wavy\">Ondulado</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d minutos</string>\n    <string name=\"wrapped_playlist_saved\">Playlist salva</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"one\">%d Perfil</item>\n        <item quantity=\"many\">%d Perfis</item>\n        <item quantity=\"other\">%d Perfis</item>\n    </plurals>\n    <string name=\"equalizer_header\">Equalizador</string>\n    <string name=\"no_profiles\">Sem perfis de equalizador</string>\n    <string name=\"import_profile\">Importar Perfil</string>\n    <string name=\"eq_disabled\">Desativado</string>\n    <plurals name=\"band_count\">\n        <item quantity=\"one\">%d banda</item>\n        <item quantity=\"many\">%d bandas</item>\n        <item quantity=\"other\">%d bandas</item>\n    </plurals>\n    <string name=\"delete_profile_desc\">Apagar Perfil</string>\n    <string name=\"delete_profile_confirmation\">Tem certeza de que pretende apagar %1$s? Esta ação não pode ser desfeita.</string>\n    <string name=\"error_file_read\">Não foi possível ler o arquivo</string>\n    <string name=\"error_file_open\">Não foi possível abrir o arquivo: %1$s</string>\n    <string name=\"import_error_title\">Erro ao importar</string>\n    <string name=\"casting_to\">Transmitir para %s</string>\n    <string name=\"progress_percent\">Progresso %s%%</string>\n    <string name=\"listening_to_metrolist\">Ouvindo Metrolist</string>\n    <string name=\"open\">Abrir</string>\n    <string name=\"failed_to_create_image\">Falha ao criar a imagem: %s</string>\n    <string name=\"copied_title\">Título Copiado</string>\n    <string name=\"copied_artist\">Artista Copiado</string>\n    <string name=\"error_playing\">Erro ao reproduzir</string>\n    <string name=\"failed_to_parse_proxy\">Não foi possível analisar o url proxy.</string>\n    <string name=\"pause_music_when_media_is_muted\">Pausar a música quando o dispositivo estiver sem som</string>\n    <string name=\"enable_simpmusic\">Habilitar letras do SimpMusic</string>\n    <string name=\"enable_simpmusic_desc\">Usar o provedor SimpMusic Lyrics para letras sincronizadas</string>\n    <string name=\"remember_shuffle_and_repeat\">Memorizar aleatório e repetir</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Memorizar aleatório e modo de repetição ao reiniciar o aplicativo</string>\n    <string name=\"system_equalizer\">Equalizador do Sistema</string>\n    <string name=\"album_art\">Capa do Álbum</string>\n    <string name=\"no_song_playing\">Nenhuma música tocando</string>\n    <string name=\"tap_to_open\">Clique para abrir o Metrolist</string>\n    <string name=\"previous\">Anterior</string>\n    <string name=\"play_pause\">Tocar/Pausar</string>\n    <string name=\"next\">Próximo</string>\n    <string name=\"like\">Gostei</string>\n    <string name=\"widget_description\">Widget do tocador com controles de reprodução</string>\n    <string name=\"turntable_widget_description\">Widget circular de música com controles de reprodução e curtida</string>\n    <string name=\"about_artist\">Sobre</string>\n    <string name=\"show_more\">Mostrar mais</string>\n    <string name=\"show_less\">Mostrar menos</string>\n    <string name=\"artist_page_settings\">Página do artista</string>\n    <string name=\"show_artist_description\">Mostrar descrição do artista</string>\n    <string name=\"show_artist_subscriber_count\">Mostrar quantidade de inscritos</string>\n    <string name=\"show_artist_monthly_listeners\">Mostrar ouvintes mensais</string>\n    <string name=\"skip_silence_desc\">Avançar rapidamente a partes silenciosas das músicas</string>\n    <string name=\"skip_silence_instant\">Pular silêncio automaticamente</string>\n    <string name=\"skip_silence_instant_desc\">Pular trechos silenciosos em vez de acelerar a reprodução</string>\n    <string name=\"persistent_shuffle_title\">Modo aleatório persistente</string>\n    <string name=\"persistent_shuffle_desc\">Manter o modo aleatório ativado quando começar novas músicas ou playlists</string>\n    <string name=\"lyrics_offset\">Atraso da letra</string>\n    <string name=\"enable\">Habilitar</string>\n    <string name=\"crop_album_art\">Cortar Capa do Álbum</string>\n    <string name=\"crop_album_art_desc\">Forçar formato quadrado nas miniaturas dos vídeos</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">Manter tela ligada enquanto o reprodutor estiver expandido</string>\n    <string name=\"crossfade\">Transição suave</string>\n    <string name=\"crossfade_desc\">Transição suave entre as músicas</string>\n    <string name=\"crossfade_duration\">Duração da transição suave</string>\n    <string name=\"crossfade_gapless\">Desativar para álbuns sem intervalos</string>\n    <string name=\"crossfade_gapless_desc\">Não suavizar transição se o álbum for ininterrupto</string>\n    <string name=\"crossfade_beta_title\">Recurso Beta</string>\n    <string name=\"crossfade_beta_message\">Transição suave é uma nova função e podem haver erros. Se você experienciar qualquer problema, por favor reporte-os.\\n\\nEsse recurso desativa a descarga de áudio devido a limitações técnicas.</string>\n    <string name=\"audio_offload_disabled_by_crossfade\">Desabilitado porque a transição suave está ativa</string>\n    <string name=\"error_title\">Erro inesperado</string>\n    <string name=\"error_eq_apply_failed\">Falha ao aplicar o perfil de equalização: %1$s</string>\n    <string name=\"error_playback_failed\">Falha na reprodução</string>\n    <string name=\"not_playing\">Nenhuma música tocando</string>\n    <string name=\"tap_to_play\">Clique para abrir o Metrolist</string>\n    <string name=\"widget_music_player\">Reprodutor de música</string>\n    <string name=\"widget_turntable\">Toca-discos</string>\n    <string name=\"together\">Juntos</string>\n    <string name=\"listen_together\">Ouvir juntos</string>\n    <string name=\"listen_together_server_url\">URL do servidor</string>\n    <string name=\"listen_together_choose_server\">Escolher servidor</string>\n    <string name=\"listen_together_custom_server\">Servidor personalizado</string>\n    <string name=\"listen_together_use_custom_server\">Usar servidor personalizado</string>\n    <string name=\"listen_together_username\">Nome de usuário</string>\n    <string name=\"listen_together_connected\">Conectado</string>\n    <string name=\"listen_together_reconnecting\">Reconectando…</string>\n    <string name=\"listen_together_disconnected\">Desconectado</string>\n    <string name=\"listen_together_connecting\">Conectando…</string>\n    <string name=\"listen_together_error\">Erro de conexão</string>\n    <string name=\"listen_together_create_room\">Criar sala</string>\n    <string name=\"listen_together_create_room_desc\">Crie uma sala e compartilhe o código com amigos</string>\n    <string name=\"listen_together_join_room\">Entrar na sala</string>\n    <string name=\"listen_together_room_code\">Código da Sala</string>\n    <string name=\"listen_together_you_are_host\">Você é o anfitrião</string>\n    <string name=\"listen_together_you_are_guest\">Você é um participante</string>\n    <string name=\"mute\">Mutar</string>\n    <string name=\"unmute\">Desmutar</string>\n    <string name=\"listen_together_join_requests\">Pedidos para entrar</string>\n    <string name=\"listen_together_view_logs\">Ver registros</string>\n    <string name=\"listen_together_logs\">Registros de conexão</string>\n    <string name=\"listen_together_auto_approval_joins\">Auto-aceitar pedidos de entrada</string>\n    <string name=\"enable_high_refresh_rate\">Ativar alta taxa de atualização</string>\n    <string name=\"enable_high_refresh_rate_desc\">Forçar a exibição a rodar na maior taxa de atualização suportada (ex: 120Hz)</string>\n    <string name=\"listening\">Escutando…</string>\n    <string name=\"processing\">Processando…</string>\n    <string name=\"no_match_found\">Nenhuma correspondência encontrada</string>\n    <string name=\"recognition_error\">Erro no reconhecimento</string>\n    <string name=\"try_again\">Tente novamente</string>\n    <string name=\"clear_recognition_history\">Limpar histórico de reconhecimento</string>\n    <string name=\"clear_recognition_history_confirm\">Tem certeza que quer limpar todo o histórico de reconhecimento?</string>\n    <string name=\"delete_from_history\">Apagar do histórico</string>\n    <string name=\"re_listen\">Ouvir novamente</string>\n    <string name=\"play_on_app\">Tocar no Metrolist</string>\n    <string name=\"listen_together_auto_approval_joins_desc\">Aprovar pedidos de entrada automaticamente, ao invés de analisá-los manualmente</string>\n    <string name=\"listen_together_sync_volume\">Sincronizar volume do anfitrião</string>\n    <string name=\"listen_together_sync_volume_desc\">Participantes seguem o nível de volume do anfitrião</string>\n    <string name=\"listen_together_in_top_bar\">Ouvir juntos na barra superior</string>\n    <string name=\"listen_together_in_top_bar_desc\">Mostrar o Ouvir Juntos na barra superior do aplicativo ao invés da barra de navegação</string>\n    <string name=\"listen_together_description\">Ouça a músicas em tempo-real com seus amigos. Crie uma sala para ser o anfitrião ou junte-se a uma sala existente com um código.</string>\n    <string name=\"listen_together_background_disconnect_note\">Nota: Você pode ser desconectado se criar uma sala e trocar para outro aplicativo enquanto nunhuma música estiver tocando.</string>\n    <string name=\"listen_together_not_configured\">Ouvir Juntos não está configurado. Por favor configure o URL do servidor nas configurações → Integrações → Ouvir Juntos.</string>\n    <string name=\"listen_together_room_created\">Sala criada: %s</string>\n    <string name=\"hide_youtube_shorts\">Ocultar vídeos curtos do YouTube</string>\n    <string name=\"listen_together_view_logs_desc\">Depurar conexão e mensagens</string>\n    <string name=\"listen_together_no_logs\">Sem registros ainda</string>\n    <string name=\"listen_together_suggestion_received\">%1$s Solicitado %2$s</string>\n    <string name=\"listen_together_suggestion_sent\">Solicitação enviada ao dono!</string>\n    <string name=\"listen_together_join_request_notification\">%1$s quer se juntar a sala</string>\n    <string name=\"listen_together_notification_channel_name\">Ouvir Juntos</string>\n    <string name=\"listen_together_notification_channel_desc\">Notificações para eventos de Ouvir Juntos</string>\n    <string name=\"listen_together_cannot_edit_username_in_room\">Não é possível editar o nome de usuário enquanto em uma sala</string>\n    <string name=\"waiting_for_approval\">Esperando aprovação do anfitrião</string>\n    <string name=\"invalid_room_code\">Código de sala invalido</string>\n    <string name=\"join_request_denied\">Pedido de entrada recusado</string>\n    <string name=\"join_existing_room\">Entrar em uma sala existente</string>\n    <string name=\"room_code\">Código da sala</string>\n    <string name=\"leave_room\">Sair da sala</string>\n    <string name=\"join_room\">Entrar</string>\n    <string name=\"create_room\">Criar</string>\n    <string name=\"joining_room\">Entrando na sala %s…</string>\n    <string name=\"creating_room\">Criando sala…</string>\n    <string name=\"connect\">Conectar</string>\n    <string name=\"disconnect\">Desconectar</string>\n    <string name=\"create\">Criar</string>\n    <string name=\"join\">Entrar</string>\n    <string name=\"approve\">Aprovar</string>\n    <string name=\"reject\">Rejeitar</string>\n    <string name=\"clear\">Limpar</string>\n    <string name=\"copy\">Copiar</string>\n    <string name=\"copied_to_clipboard\">Copiado para a área de transferência</string>\n    <string name=\"not_set\">Não definido</string>\n    <string name=\"hosting_room\">Hospedar sala</string>\n    <string name=\"in_room\">Na sala</string>\n    <string name=\"pending_requests\">Solicitações pendentes</string>\n    <string name=\"pending_suggestions\">Sugestões pendentes</string>\n    <string name=\"suggest_to_host\">Sugerir ao anfitrião</string>\n    <string name=\"kick_user\">Expulsar</string>\n    <string name=\"host_label\">Anfitrião</string>\n    <string name=\"you_label\">Você</string>\n    <string name=\"connected_users\">Usuários Conectados</string>\n    <string name=\"enter_username\">Digite o nome de usuário</string>\n    <string name=\"enter_room_code\">Digite o código da sala</string>\n    <string name=\"listen_together_settings_desc\">Configure o servidor, o nome de usuário e muito mais</string>\n    <string name=\"error_username_empty\">É necessário nome de usuário.</string>\n    <string name=\"resync\">Ressincronizar</string>\n    <string name=\"copy_code\">Copiar código</string>\n    <string name=\"kick_user_desc\">Remova esta pessoa da sessão</string>\n    <string name=\"permanently_kick_user\">Bloquear permanentemente</string>\n    <string name=\"permanently_kick_user_desc\">Bloqueie os pedidos de entrada dessa pessoa e oculte as sugestões dela</string>\n    <string name=\"listen_together_blocked_users\">Usuários bloqueados</string>\n    <string name=\"listen_together_blocked_users_count\">%d usuário(s) bloqueado(s)</string>\n    <string name=\"listen_together_no_blocked_users\">Nenhum usuário bloqueado</string>\n    <string name=\"unblock\">Desbloquear</string>\n    <string name=\"user_blocked_by_host\">Usuário bloqueado pelo anfitrião</string>\n    <string name=\"ai_lyrics_translation\">Tradução de letras por IA</string>\n    <string name=\"ai_translating_lyrics\">Traduzindo letras de músicas...</string>\n    <string name=\"ai_lyrics_translated\">Letra traduzida</string>\n    <string name=\"ai_provider\">Fornecedor</string>\n    <string name=\"ai_base_url\">URL base</string>\n    <string name=\"transfer_ownership_desc\">Faça dessa pessoa o anfitrião da sala</string>\n    <string name=\"manage_user\">Gerenciar usuário</string>\n    <string name=\"ai_api_key\">Chave de API</string>\n    <string name=\"ai_model\">Modelo</string>\n    <string name=\"ai_translation_mode\">Modo de tradução</string>\n    <string name=\"ai_target_language\">Língua de destino</string>\n    <string name=\"ai_setup_guide\">Credenciais da API</string>\n    <string name=\"ai_translation_literal\">Tradução</string>\n    <string name=\"ai_translation_transcribed\">Transcrição</string>\n    <string name=\"ai_api_key_required\">Chave de API necessária</string>\n    <string name=\"ai_error_api_key_required\">É necessária uma chave de API</string>\n    <string name=\"ai_error_no_lyrics\">Não há letras para traduzir</string>\n    <string name=\"ai_error_lyrics_empty\">A letra está vazia</string>\n    <string name=\"ai_error_language_required\">É necessário o idioma de destino</string>\n    <string name=\"ai_error_unexpected\">Resultado de tradução inesperado</string>\n    <string name=\"ai_error_unknown\">Ocorreu um erro desconhecido</string>\n    <string name=\"ai_error_translation_failed\">A tradução falhou</string>\n    <string name=\"crash_title\">O aplicativo travou</string>\n    <string name=\"crash_description\">Ocorreu um erro inesperado. Por favor, compartilhe o relatório de falha para nos ajudar a corrigir o problema.</string>\n    <string name=\"crash_share_logs\">Compartilhar registros</string>\n    <string name=\"crash_share_title\">Compartilhar relatório de falha</string>\n    <string name=\"crash_report_subject\">Relatório de Acidente da Metrolist</string>\n    <string name=\"crash_close\">Fechar</string>\n    <string name=\"crash_no_log\">Nenhum registro de falha disponível</string>\n    <string name=\"palette_dynamic\">Dinâmico</string>\n    <string name=\"palette_crimson\">Carmesim</string>\n    <string name=\"palette_rose\">Rosa</string>\n    <string name=\"palette_purple\">Roxo</string>\n    <string name=\"palette_deep_purple\">Roxo Profundo</string>\n    <string name=\"palette_indigo\">Índigo</string>\n    <string name=\"palette_blue\">Azul</string>\n    <string name=\"palette_sky_blue\">Azul celeste</string>\n    <string name=\"palette_cyan\">Ciano</string>\n    <string name=\"palette_green\">Verde</string>\n    <string name=\"palette_light_green\">Verde claro</string>\n    <string name=\"palette_lime\">Verde-limão</string>\n    <string name=\"palette_yellow\">Amarelo</string>\n    <string name=\"palette_amber\">Âmbar</string>\n    <string name=\"palette_orange\">Laranja</string>\n    <string name=\"palette_deep_orange\">Laranja Escuro</string>\n    <string name=\"palette_brown\">Marrom</string>\n    <string name=\"palette_grey\">Cinza</string>\n    <string name=\"palette_blue_grey\">Azul acinzentado</string>\n    <string name=\"cd_back\">Voltar</string>\n    <string name=\"cd_pure_black_mode\">Modo Preto Puro</string>\n    <string name=\"cd_light_mode\">Modo claro</string>\n    <string name=\"cd_dark_mode\">Modo escuro</string>\n    <string name=\"cd_system_mode\">Modo de sistema</string>\n    <string name=\"cd_palette_item\">paleta %1$s</string>\n    <string name=\"play_all\">Reproduzir tudo</string>\n    <string name=\"recognize_music\">Reconhecer música</string>\n    <string name=\"tap_to_recognize\">Toque para reconhecer</string>\n    <string name=\"recognition_history\">Histórico de reconhecimento</string>\n    <string name=\"map_csv_columns\">Mapear colunas CSV</string>\n    <string name=\"first_row_is_header\">A primeira linha é o cabeçalho</string>\n    <string name=\"artist_name_column\">Coluna com o nome do artista</string>\n    <string name=\"song_title_column\">Coluna com o título da música</string>\n    <string name=\"youtube_url_column\">Coluna com URL do YouTube (Opcional)</string>\n    <string name=\"continue_action\">Continuar</string>\n    <string name=\"importing_csv\">Importando CSV</string>\n    <string name=\"recently_converted\">Convertido Recentemente</string>\n    <string name=\"column_label\">Col %d</string>\n    <string name=\"transfer_ownership\">Transferir a propriedade</string>\n    <string name=\"palette_teal\">Verde-Azulado</string>\n    <string name=\"prevent_duplicate_tracks_in_queue\">Impedir faixas duplicadas na fila</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">Ao adicionar uma faixa à fila, remova-a da sua posição anterior se já estiver presente</string>\n    <string name=\"ai_translation_literal_desc\">Traduzir significado para o idioma alvo</string>\n    <string name=\"ai_translation_transcribed_desc\">Converter pronúncia para script de destino</string>\n    <string name=\"ai_provider_help\">Obter chaves de API</string>\n    <string name=\"ai_provider_openrouter_help\">Visite https://openrouter.ai para modelos gratuitos e pagos</string>\n    <string name=\"ai_provider_openai_help\">Visite https://plataform.openai.com/api-keys</string>\n    <string name=\"ai_provider_claude_help\">Visite https://console.anthropic.com/settings/keys</string>\n    <string name=\"ai_provider_gemini_help\">Visite https://aistudio.google.com/apikey</string>\n    <string name=\"ai_provider_perplexity_help\">Visite https://perplexity.ai/settings/api</string>\n    <string name=\"ai_provider_xai_help\">Visite https://console.x.ai</string>\n    <string name=\"ai_provider_deepl_help\">Visite https://deepl.com/pro-api para chaves gratuitas e pagas</string>\n    <string name=\"ai_deepl_formality\">Formalidade</string>\n    <string name=\"ai_deepl_formality_default\">Padrão</string>\n    <string name=\"ai_deepl_formality_more\">Mais formal</string>\n    <string name=\"ai_deepl_formality_less\">Menos formal</string>\n    <string name=\"discord_status\">Status</string>\n    <string name=\"discord_status_online\">Online</string>\n    <string name=\"discord_status_idle\">Ocioso</string>\n    <string name=\"discord_status_dnd\">Não perturbar</string>\n    <string name=\"discord_buttons\">Botões</string>\n    <string name=\"discord_button_1\">Botão 1</string>\n    <string name=\"discord_button_2\">Botão 2</string>\n    <string name=\"login_successful\">Login bem sucedido!</string>\n    <string name=\"discord_activity_type\">Tipo de atividade</string>\n    <string name=\"discord_activity_playing\">Jogando</string>\n    <string name=\"discord_activity_listening\">Ouvindo</string>\n    <string name=\"discord_activity_watching\">Assistindo</string>\n    <string name=\"discord_activity_competing\">Competindo</string>\n    <string name=\"discord_button_text_variables\">Variáveis: {song_name}, {artist_name}, {album_name}</string>\n    <string name=\"discord_information_warning\">Esse recurso usa a biblioteca KizzyRPC para se conectar ao Gateway do Discord e definir seu status de Rich Presence. Embora nenhuma suspensão de conta conhecida tenha ocorrido a partir de uso semelhante, este método não é oficialmente suportado pela Discord e pode ser considerado uma violação de Termos de Serviço. Seu token é extraído localmente e nunca enviado para servidores de terceiros. Prossiga a seu próprio critério.</string>\n    <string name=\"discord_rpc_preview\">Visualização de rich presence</string>\n    <string name=\"discord_presence\">Presença</string>\n    <string name=\"discord_connect_description\">Entre com o Discord para compartilhar o que você está ouvindo</string>\n    <string name=\"discord_playing_metrolist\">Jogando Metrolist</string>\n    <string name=\"discord_watching_metrolist\">Assistindo Metrolist</string>\n    <string name=\"discord_competing_metrolist\">Competindo no Metrolist</string>\n    <string name=\"discord_activity_name\">Nome da atividade</string>\n    <string name=\"discord_activity_name_description\">Nome personalizado para a atividade (deixe vazio para padrão)</string>\n    <string name=\"discord_advanced_mode\">Modo avançado</string>\n    <string name=\"discord_advanced_mode_description\">Mostrar opções de personalização adicionais para a Rich Presence</string>\n    <string name=\"player_background_solid\">Sólido</string>\n    <string name=\"resume_on_bluetooth_connect\">Retomar ao conectar com Bluetooth</string>\n    <string name=\"lyrics_romanize_hindi\">Romanizar letras em Hindi</string>\n    <string name=\"lyrics_romanize_punjabi\">Romanizar letras em Punjabi</string>\n    <string name=\"lyrics_romanize_as_main\">Mostrar letras romanizadas como principal</string>\n    <string name=\"enable_lrclib_desc\">Biblioteca de letras sincronizadas feitos pela comunidade</string>\n    <string name=\"enable_kugou_desc\">Pega as letras do KuGou, uma plataforma de música Chinesa popular</string>\n    <string name=\"youtube_music_lyrics_note\">Observação: Letras do YouTube Music serão mostradas automaticamente quando não houver outras fontes disponíveis. As letras do YTM geralmente não são sincronizadas.</string>\n    <string name=\"enable_lyricsplus\">Ativar LyricsPlus</string>\n    <string name=\"enable_lyricsplus_desc\">Letras sincronizadas de diversas fontes</string>\n    <string name=\"lyrics_provider_selection\">Fonte das letras</string>\n    <string name=\"lyrics_provider_selection_desc\">Escolha quais fontes de letras deseja ativar</string>\n    <string name=\"lyrics_provider_priority\">Prioridade de provedor de letras</string>\n    <string name=\"lyrics_provider_priority_desc\">Arraste para reordenar provedores por sua preferência. Mais ao topo &gt; maior prioridade.</string>\n    <string name=\"changelog\">Novidades</string>\n    <string name=\"changelog_empty\">Sem novidades no momento</string>\n    <string name=\"github_releases_url\">https://github.com/MetrolistGroup/Metrolist/releases</string>\n    <string name=\"list_bullet\">•</string>\n    <string name=\"view_on_github\">Ver no GitHub</string>\n    <string name=\"current_version\">Versão atual</string>\n    <string name=\"version_format\">Versão: %s</string>\n    <string name=\"update_settings\">Configurações de atualização</string>\n    <string name=\"check_for_updates_title\">Verificar atualizações</string>\n    <string name=\"checking_for_updates\">Verificando por atualizações…</string>\n    <string name=\"latest_version_format\">Última versão: %s</string>\n    <string name=\"check_for_updates_button\">Verificar atualizações</string>\n    <string name=\"hide_changelog\">Ocultar novidades</string>\n    <string name=\"view_changelog\">Ver novidades</string>\n    <string name=\"failed_to_check_updates\">Erro ao verificar atualizações: %s</string>\n    <string name=\"set_as_default\">Definir como padrão</string>\n    <string name=\"sleep_timer_default_set\">Timer para soneca padrão: %d min</string>\n    <string name=\"display_density\">Densidade de exibição</string>\n    <string name=\"restart\">Reiniciar</string>\n    <string name=\"restart_required\">Reinicialização necessária</string>\n    <string name=\"density_restart_message\">A mudança de densidade de exibição vai ser aplicada após reiniciar o app. Você deseja reiniciar agora?</string>\n    <string name=\"found_in_settings_content\">Encontrado em Configurações &gt; Conteúdo</string>\n    <string name=\"plays\">reproduções</string>\n    <string name=\"error_episode_save\">Falha ao salvar episódio</string>\n    <string name=\"error_episode_remove\">Falha ao remover episódio</string>\n    <string name=\"error_podcast_subscribe\">Falha ao se inscrever no podcast</string>\n    <string name=\"error_podcast_unsubscribe\">Falha ao se desinscrever do podcast</string>\n    <string name=\"listen_together_auto_approval_suggestions\">Aprovar automaticamente sugestões de músicas</string>\n    <string name=\"listen_together_auto_approval_suggestions_desc\">Aprovar e enfileirar sugestões de convidados automaticamente</string>\n    <string name=\"speed_dial\">Acesso rápido</string>\n    <string name=\"pin_to_speed_dial\">Fixar no acesso rápido</string>\n    <string name=\"unpin_from_speed_dial\">Desafixar do acesso rápido</string>\n    <string name=\"randomize_home_order\">Aleatorizar ordem da tela inicial</string>\n    <string name=\"randomize_home_order_desc\">Reordenar seções da tela inicial aleatoriamente com base na relevância</string>\n    <string name=\"daily_discover_sounds_like\">Semelhante a %1$s</string>\n    <string name=\"daily_discover_because_you_listen_to\">Porque você ouve %1$s</string>\n    <string name=\"daily_discover_similar_to\">Parecido com %1$s</string>\n    <string name=\"daily_discover_based_on\">Com base em %1$s</string>\n    <string name=\"daily_discover_for_fans_of\">Para os fãs de %1$s</string>\n    <string name=\"from_the_community\">Da comunidade</string>\n    <string name=\"logout_dialog_title\">Manter dados da biblioteca?</string>\n    <string name=\"logout_dialog_message\">Deseja manter suas playlists e dados da biblioteca? As músicas baixadas serão mantidas de qualquer forma.</string>\n    <string name=\"logout_keep\">Manter</string>\n    <string name=\"logout_clear\">Limpar</string>\n    <string name=\"credits_lead_developer\">Desenvolvedor Líder</string>\n    <string name=\"credits_collaborator\">Colaborador</string>\n    <string name=\"credits_collaborators_section\">Colaboradores</string>\n    <string name=\"credits_license_name\">Licença Pública Geral GNU v3.0</string>\n    <string name=\"credits_license_desc\">Software grátis e de código aberto. Você pode usar, estudar, compartilhar e melhorá-lo.</string>\n    <string name=\"credits_discord\">Servidor do Discord</string>\n    <string name=\"credits_telegram\">Canal do Telegram</string>\n    <string name=\"credits_website\">Site</string>\n    <string name=\"credits_instagram\">Instagram</string>\n    <string name=\"credits_github\">GitHub</string>\n    <string name=\"credits_view_repo\">Ver repositório</string>\n    <string name=\"app_version_info\">%1$s • %2$s</string>\n    <string name=\"like_what_i_do\">Gosta do meu trabalho?</string>\n    <string name=\"buy_mo_a_coffee\">Me compre um café</string>\n    <string name=\"community_and_info\">Comunidade e Informações</string>\n    <string name=\"metrolist\">METROLIST</string>\n    <string name=\"wanna_play_favorite_song\">Quer tocar a música favorita deles?</string>\n    <string name=\"yeah\">Sim</string>\n    <string name=\"stands_with_palestine\">Esse projeto apoia a Palestina 🇵🇸</string>\n    <string name=\"filter_podcasts\">Podcasts</string>\n    <string name=\"view_podcast\">Ver podcast</string>\n    <string name=\"podcast_channels\">Canais de podcast</string>\n    <string name=\"latest_episodes\">Últimos episódios</string>\n    <string name=\"your_shows\">Seus programas</string>\n    <string name=\"new_episodes\">Novos episódios</string>\n    <string name=\"episodes_for_later\">Episódios para depois</string>\n    <string name=\"save_episode_for_later\">Guarde para mais tarde</string>\n    <string name=\"save_episode_for_later_desc\">Adicionar à sua playlist de Episódios para mais tarde</string>\n    <string name=\"remove_episode_from_saved\">Remover dos salvos</string>\n    <string name=\"subscribe_to_podcast\">Salvar podcast na biblioteca</string>\n    <plurals name=\"n_episode\">\n        <item quantity=\"one\">%d episódio</item>\n        <item quantity=\"many\">%d episódios</item>\n        <item quantity=\"other\">%d episódios</item>\n    </plurals>\n    <string name=\"restore_confirm_title\">Restaurar backup?</string>\n    <string name=\"restore_confirm_message\">Isso vai restaurar seus dados do backup.</string>\n    <string name=\"restore_account_warning\">Você vai precisar fazer o login novamente após a restauração. A seguinte conta vai ser desconectada:</string>\n    <string name=\"restore\">Restaurar</string>\n    <string name=\"checking_previous_account\">Verificando se há uma conta anterior…</string>\n    <string name=\"no_account_found\">Nenhuma conta encontrada</string>\n    <string name=\"importing_playlist\">Importando playlist</string>\n    <string name=\"widget_recognizer_name\">Reconhecedor de música</string>\n    <string name=\"widget_recognizer_description\">Identifique músicas tocando ao seu redor diretamente da sua tela inicial</string>\n    <string name=\"widget_recognizer_tap_to_search\">Toque para identificar música</string>\n    <string name=\"widget_recognizer_listening\">Escutando…</string>\n    <string name=\"widget_recognizer_processing\">Identificando…</string>\n    <string name=\"widget_recognizer_no_match\">Nenhuma correspondência encontrada. Tente novamente</string>\n    <string name=\"widget_recognizer_error\">Reconhecimento falhou</string>\n    <string name=\"widget_recognizer_error_generic\">Um erro ocorreu. Por favor, tente novamente</string>\n    <string name=\"widget_recognizer_unknown_song\">Música desconhecida</string>\n    <string name=\"widget_recognizer_unknown_artist\">Artista desconhecido</string>\n    <string name=\"widget_recognizer_mic_desc\">Identificar música</string>\n    <string name=\"widget_recognizer_channel_name\">Reconhecimento de música</string>\n    <string name=\"widget_recognizer_channel_desc\">Mostra uma notificação enquanto identifica uma música do widget</string>\n    <string name=\"widget_recognizer_notification_text\">Gravando áudio para identificar a música…</string>\n    <string name=\"filter_episodes\">Episódios</string>\n    <string name=\"filter_channels\">Canais</string>\n    <string name=\"auto_playlist\">Playlist automática</string>\n    <string name=\"downloaded_episodes\">Episódios baixados</string>\n    <string name=\"no_subscribed_channels\">Sem canais inscritos</string>\n    <string name=\"no_downloaded_episodes\">Sem episódios baixados</string>\n    <plurals name=\"n_channel\">\n        <item quantity=\"one\">%d canal</item>\n        <item quantity=\"many\">%d canais</item>\n        <item quantity=\"other\">%d canais</item>\n    </plurals>\n    <string name=\"enable_automatic_sleeptimer\">Ativar timer para soneca automático</string>\n    <string name=\"sleeptimer_description\">Ativa o timer para soneca automaticamente com o valor padrão em um horário personalizado</string>\n    <string name=\"sleep_timer_repeat_description\">Defi­ne dia e horário personalizados em que o timer para soneca deve ativar automaticamente</string>\n    <string name=\"sleep_timer_repeat\">Repetir</string>\n    <string name=\"sleep_timer_daily\">Diário</string>\n    <string name=\"sleep_timer_weekdays\">Segunda à sexta</string>\n    <string name=\"sleep_timer_weekdays_weekends\">Dias úteis / Finais de semana</string>\n    <string name=\"sleep_timer_weekends\">Finais de semana (Sáb-Dom)</string>\n    <string name=\"sleep_timer_custom\">Personalizado</string>\n    <string name=\"sleep_timer_start_time\">Horário de início</string>\n    <string name=\"sleep_timer_end_time\">Horário de encerramento</string>\n    <string name=\"sleep_timer_monday\">Segunda</string>\n    <string name=\"sleep_timer_tuesday\">Terça</string>\n    <string name=\"sleep_timer_wednesday\">Quarta</string>\n    <string name=\"sleep_timer_thursday\">Quinta</string>\n    <string name=\"sleep_timer_friday\">Sexta</string>\n    <string name=\"sleep_timer_saturday\">Sábado</string>\n    <string name=\"sleep_timer_sunday\">Domingo</string>\n    <string name=\"sleep_timer_stop_after_current_song\">Parar no final da música atual quando o timer acabar</string>\n    <string name=\"sleep_timer_fade_out\">Desvanecer áudio no minuto final</string>\n    <string name=\"view_channel\">Ver canal</string>\n    <string name=\"filter_profiles\">Perfis</string>\n    <string name=\"upload_songs\">Enviar músicas</string>\n    <string name=\"uploading\">Enviando…</string>\n    <string name=\"upload_progress\">%1$d de %2$d</string>\n    <string name=\"upload_complete\">Envio completo</string>\n    <string name=\"upload_failed\">Envio falhou</string>\n    <string name=\"upload_file_too_large\">Arquivo muito grande (máx. 300MB)</string>\n    <string name=\"upload_unsupported_format\">Formato não suportado. Use mp3, m4a, wma, flac, ou ogg</string>\n    <string name=\"delete_uploaded_song\">Apagar música enviada</string>\n    <string name=\"delete_uploaded_song_confirm\">Tem certeza de que deseja apagar essa música enviada? Isto não pode ser desfeito.</string>\n    <string name=\"delete_uploaded_song_success\">Música enviada apagada</string>\n    <string name=\"delete_uploaded_song_failed\">Falha para apagar a música enviada</string>\n    <string name=\"delete_uploaded_songs\">Apagar músicas enviadas</string>\n    <string name=\"delete_uploaded_songs_confirm\">Tem certeza de que deseja apagar %1$d músicas enviadas? Isto não pode ser desfeito.</string>\n    <string name=\"deleted_n_songs\">Apagou %1$d músicas</string>\n    <string name=\"deleting\">Apagando…</string>\n    <string name=\"export_playlist\">Exportar playlist</string>\n    <string name=\"export_as_csv\">Exportar como CSV</string>\n    <string name=\"export_as_m3u\">Exportar como M3U</string>\n    <string name=\"export_success\">Playlist exportada com sucesso</string>\n    <string name=\"export_failed\">Falha em exportar playlist</string>\n    <string name=\"export_option_share\">Compartilhar</string>\n    <string name=\"export_option_save\">Salvar nos Documentos</string>\n    <string name=\"qs_tile_music_recognizer\">Reconhecer música</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-pt-rBR/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Início</string>\n    <string name=\"songs\">Músicas</string>\n    <string name=\"artists\">Artistas</string>\n    <string name=\"albums\">Álbuns</string>\n    <string name=\"playlists\">Listas</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d selecionado</item>\n        <item quantity=\"many\">%d de selecionados</item>\n        <item quantity=\"other\">%d selecionados</item>\n    </plurals>\n    <string name=\"history\">Histórico</string>\n    <string name=\"stats\">Estatísticas</string>\n    <string name=\"mood_and_genres\">Momentos e Gêneros</string>\n    <string name=\"account\">Conta</string>\n    <string name=\"quick_picks\">Escolhas rápidas</string>\n    <string name=\"quick_picks_empty\">Escute mais músicas para gerar suas escolhas rápidas</string>\n    <string name=\"new_release_albums\">Novos álbuns lançados</string>\n    <string name=\"today\">Hoje</string>\n    <string name=\"yesterday\">Ontem</string>\n    <string name=\"this_week\">Essa semana</string>\n    <string name=\"last_week\">Semana passada</string>\n    <string name=\"most_played_songs\">Músicas mais tocadas</string>\n    <string name=\"most_played_artists\">Artistas mais tocados</string>\n    <string name=\"most_played_albums\">Álbuns mais tocados</string>\n    <string name=\"search\">Pesquisar</string>\n    <string name=\"search_yt_music\">Pesquisar no YouTube Music…</string>\n    <string name=\"search_library\">Pesquisar na biblioteca…</string>\n    <string name=\"filter_library\">Biblioteca</string>\n    <string name=\"filter_liked\">Favoritos</string>\n    <string name=\"filter_downloaded\">Baixados</string>\n    <string name=\"filter_all\">Tudo</string>\n    <string name=\"filter_songs\">Músicas</string>\n    <string name=\"filter_videos\">Vídeos</string>\n    <string name=\"filter_albums\">Álbuns</string>\n    <string name=\"filter_artists\">Artistas</string>\n    <string name=\"filter_playlists\">Listas</string>\n    <string name=\"filter_community_playlists\">Listas da comunidade</string>\n    <string name=\"filter_featured_playlists\">Listas em destaque</string>\n    <string name=\"filter_bookmarked\">Salvos</string>\n    <string name=\"no_results_found\">Nenhum resultado encontrado</string>\n    <string name=\"from_your_library\">Da sua biblioteca</string>\n    <string name=\"liked_songs\">Músicas favoritas</string>\n    <string name=\"downloaded_songs\">Músicas baixadas</string>\n    <string name=\"playlist_is_empty\">Esta lista está vazia</string>\n    <string name=\"retry\">Tentar novamente</string>\n    <string name=\"radio\">Rádio</string>\n    <string name=\"shuffle\">Aleatório</string>\n    <string name=\"reset\">Redefinir</string>\n    <string name=\"details\">Detalhes</string>\n    <string name=\"edit\">Editar</string>\n    <string name=\"start_radio\">Iniciar rádio</string>\n    <string name=\"play\">Tocar</string>\n    <string name=\"play_next\">Tocar em seguida</string>\n    <string name=\"add_to_queue\">Adicionar à fila</string>\n    <string name=\"add_to_library\">Adicionar à biblioteca</string>\n    <string name=\"remove_from_library\">Remover da biblioteca</string>\n    <string name=\"action_download\">Baixar</string>\n    <string name=\"downloading\">Baixando</string>\n    <string name=\"remove_download\">Remover dos baixados</string>\n    <string name=\"import_playlist\">Importar lista</string>\n    <string name=\"add_to_playlist\">Adicionar à lista</string>\n    <string name=\"view_artist\">Ver artista</string>\n    <string name=\"view_album\">Ver álbum</string>\n    <string name=\"refetch\">Recarregar</string>\n    <string name=\"share\">Compartilhar</string>\n    <string name=\"delete\">Apagar</string>\n    <string name=\"remove_from_history\">Remover do histórico</string>\n    <string name=\"search_online\">Pesquisar online</string>\n    <string name=\"action_sync\">Sincronizar</string>\n    <string name=\"advanced\">Avançado</string>\n    <string name=\"sort_by_create_date\">Quando adicionado</string>\n    <string name=\"sort_by_name\">Nome</string>\n    <string name=\"sort_by_artist\">Artista</string>\n    <string name=\"sort_by_year\">Ano</string>\n    <string name=\"sort_by_song_count\">Número de músicas</string>\n    <string name=\"sort_by_length\">Duração</string>\n    <string name=\"sort_by_play_time\">Tempo de reprodução</string>\n    <string name=\"sort_by_custom\">Ordem customizada</string>\n    <string name=\"media_id\">ID da mídia</string>\n    <string name=\"mime_type\">Tipo do MIME</string>\n    <string name=\"codecs\">Codificações</string>\n    <string name=\"bitrate\">Taxa de bits</string>\n    <string name=\"sample_rate\">Taxa de amostragem</string>\n    <string name=\"loudness\">Volume</string>\n    <string name=\"volume\">Volume</string>\n    <string name=\"file_size\">Tamanho do arquivo</string>\n    <string name=\"unknown\">Desconhecido</string>\n    <string name=\"copied\">Copiado para a área de transferência</string>\n    <string name=\"edit_lyrics\">Editar letra</string>\n    <string name=\"search_lyrics\">Pesquisar letra</string>\n    <string name=\"edit_song\">Editar música</string>\n    <string name=\"song_title\">Nome da música</string>\n    <string name=\"song_artists\">Artistas da música</string>\n    <string name=\"error_song_title_empty\">O nome da música não pode ser vazio.</string>\n    <string name=\"error_song_artist_empty\">O artista da música não pode ser vazio.</string>\n    <string name=\"save\">Salvar</string>\n    <string name=\"choose_playlist\">Selecione a lista</string>\n    <string name=\"edit_playlist\">Editar lista</string>\n    <string name=\"create_playlist\">Criar lista</string>\n    <string name=\"playlist_name\">Nome da lista</string>\n    <string name=\"error_playlist_name_empty\">O nome da lista não pode ser vazio.</string>\n    <string name=\"edit_artist\">Editar artista</string>\n    <string name=\"artist_name\">Nome do artista</string>\n    <string name=\"error_artist_name_empty\">O nome do artista não pode ser vazio.</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d música</item>\n        <item quantity=\"many\">%d de músicas</item>\n        <item quantity=\"other\">%d músicas</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d artista</item>\n        <item quantity=\"many\">%d de artistas</item>\n        <item quantity=\"other\">%d artistas</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d álbum</item>\n        <item quantity=\"many\">%d de álbuns</item>\n        <item quantity=\"other\">%d álbuns</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d lista</item>\n        <item quantity=\"many\">%d de listas</item>\n        <item quantity=\"other\">%d listas</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d semana</item>\n        <item quantity=\"many\">%d de semanas</item>\n        <item quantity=\"other\">%d semanas</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d mês</item>\n        <item quantity=\"many\">%d de meses</item>\n        <item quantity=\"other\">%d meses</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d ano</item>\n        <item quantity=\"many\">%d de anos</item>\n        <item quantity=\"other\">%d anos</item>\n    </plurals>\n    <string name=\"playlist_imported\">A lista foi importada</string>\n    <string name=\"removed_song_from_playlist\">\\\"%s\\\" foi removido da lista</string>\n    <string name=\"playlist_synced\">A playlist foi sincronizada</string>\n    <string name=\"undo\">Desfazer</string>\n    <string name=\"lyrics_not_found\">A letra não foi encontrada</string>\n    <string name=\"sleep_timer\">Timer para dormir</string>\n    <string name=\"end_of_song\">Fim da música</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">%d minuto</item>\n        <item quantity=\"many\">%d de minutos</item>\n        <item quantity=\"other\">%d minutos</item>\n    </plurals>\n    <string name=\"error_no_stream\">Nenhum canal de reprodução disponível</string>\n    <string name=\"error_no_internet\">Sem conexão à internet</string>\n    <string name=\"error_timeout\">Tempo esgotado</string>\n    <string name=\"error_unknown\">Erro desconhecido</string>\n    <string name=\"action_like\">Favoritar</string>\n    <string name=\"action_remove_like\">Remover dos favoritos</string>\n    <string name=\"action_shuffle_on\">Modo aleatório ativado</string>\n    <string name=\"action_shuffle_off\">Modo aleatório desativado</string>\n    <string name=\"repeat_mode_off\">Modo de repetição desativado</string>\n    <string name=\"repeat_mode_one\">Repetir a música atual</string>\n    <string name=\"repeat_mode_all\">Repetir fila</string>\n    <string name=\"queue_all_songs\">Todas as músicas</string>\n    <string name=\"queue_searched_songs\">Músicas pesquisadas</string>\n    <string name=\"music_player\">Tocador de música</string>\n    <string name=\"settings\">Configurações</string>\n    <string name=\"appearance\">Aparência</string>\n    <string name=\"enable_dynamic_theme\">Ativar tema dinâmico</string>\n    <string name=\"dark_theme\">Tema escuro</string>\n    <string name=\"dark_theme_on\">Ativado</string>\n    <string name=\"dark_theme_off\">Desativado</string>\n    <string name=\"dark_theme_follow_system\">Seguir o sistema</string>\n    <string name=\"pure_black\">Preto profundo</string>\n    <string name=\"default_open_tab\">Aba padrão ao abrir o app</string>\n    <string name=\"customize_navigation_tabs\">Customizar abas de navegação</string>\n    <string name=\"lyrics_text_position\">Posição do texto da letra</string>\n    <string name=\"left\">Esquerda</string>\n    <string name=\"center\">Centro</string>\n    <string name=\"right\">Direita</string>\n    <string name=\"content\">Conteúdo</string>\n    <string name=\"login\">Conectar-se</string>\n    <string name=\"content_language\">Idioma padrão do conteúdo</string>\n    <string name=\"content_country\">País padrão do conteúdo</string>\n    <string name=\"system_default\">Padrão do sistema</string>\n    <string name=\"enable_proxy\">Ativar proxy</string>\n    <string name=\"proxy_type\">Tipo de proxy</string>\n    <string name=\"proxy_url\">URL da proxy</string>\n    <string name=\"restart_to_take_effect\">Reinicie para que tome efeito</string>\n    <string name=\"player_and_audio\">Tocador e áudio</string>\n    <string name=\"audio_quality\">Qualidade do áudio</string>\n    <string name=\"audio_quality_auto\">Automática</string>\n    <string name=\"audio_quality_high\">Alta</string>\n    <string name=\"audio_quality_low\">Baixa</string>\n    <string name=\"persistent_queue\">Fila persistente</string>\n    <string name=\"skip_silence\">Pular silêncio</string>\n    <string name=\"audio_normalization\">Normalização de áudio</string>\n    <string name=\"equalizer\">Equalizador</string>\n    <string name=\"storage\">Armazenamento</string>\n    <string name=\"cache\">Cache</string>\n    <string name=\"image_cache\">Cache de Imagens</string>\n    <string name=\"song_cache\">Cache de Músicas</string>\n    <string name=\"max_cache_size\">Tamanho máximo da cache</string>\n    <string name=\"unlimited\">Ilimitado</string>\n    <string name=\"clear_all_downloads\">Limpar todos os downloads</string>\n    <string name=\"max_image_cache_size\">Tamanho máximo da cache de imagens</string>\n    <string name=\"clear_image_cache\">Limpar a cache de imagens</string>\n    <string name=\"max_song_cache_size\">Tamanho máximo da cache de músicas</string>\n    <string name=\"clear_song_cache\">Limpar a cache de músicas</string>\n    <string name=\"size_used\">%s usados</string>\n    <string name=\"privacy\">Privacidade</string>\n    <string name=\"pause_listen_history\">Pausar o histórico de reprodução</string>\n    <string name=\"clear_listen_history\">Limpar o histórico de reprodução</string>\n    <string name=\"clear_listen_history_confirm\">Você tem certeza que deseja limpar todo o histórico de reprodução?</string>\n    <string name=\"pause_search_history\">Pausar o histórico de pesquisa</string>\n    <string name=\"clear_search_history\">Limpar o histórico de pesquisa</string>\n    <string name=\"clear_search_history_confirm\">Você tem certeza que deseja limpar todo o histórico de pesquisa?</string>\n    <string name=\"enable_kugou\">Ativar o provedor de letras KuGou</string>\n    <string name=\"backup_restore\">Backup e restauração</string>\n    <string name=\"action_backup\">Fazer backup</string>\n    <string name=\"action_restore\">Restaurar</string>\n    <string name=\"imported_playlist\">A lista foi importada</string>\n    <string name=\"backup_create_success\">O backup foi criado com sucesso</string>\n    <string name=\"backup_create_failed\">Não foi possível criar o backup</string>\n    <string name=\"restore_failed\">Falha ao restaurar o backup</string>\n    <string name=\"about\">Sobre</string>\n    <string name=\"app_version\">Versão do app</string>\n    <string name=\"new_version_available\">Há uma nova versão disponível</string>\n    <string name=\"translation_models\">Modelos de Tradução</string>\n    <string name=\"clear_translation_models\">Limpar os modelos de tradução</string>\n    <string name=\"remove_download_playlist_confirm\">Você tem certeza que deseja remover todas as músicas da lista \\\"%s\\\" do armazenamento de Músicas Baixadas?</string>\n    <string name=\"delete_playlist_confirm\">Você tem certeza que deseja apagar a lista \\\"%s\\\"?</string>\n    <string name=\"remove_from_playlist\">Remover da lista</string>\n    <string name=\"skip_duplicates\">Pular duplicados</string>\n    <string name=\"enable_lrclib\">Ativar o provedor de letras LrcLib</string>\n    <string name=\"duplicates\">Duplicados</string>\n    <string name=\"add_anyway\">Adicionar mesmo assim</string>\n    <string name=\"duplicates_description_single\">Esta música já está na sua lista</string>\n    <string name=\"duplicates_description_multiple\">%d músicas já estão na sua lista</string>\n    <string name=\"discord_integration\">Integração com o Discord</string>\n    <string name=\"dismiss\">Ignorar</string>\n    <string name=\"options\">Opções</string>\n    <string name=\"preview\">Prévia</string>\n    <string name=\"login_failed\">Ocorreu algum problema ao conectar-se</string>\n    <string name=\"action_logout\">Desconectar-se</string>\n    <string name=\"enable_discord_rpc\">Ativar o Rich Presence</string>\n    <string name=\"not_logged_in\">Não está conectado à uma conta</string>\n    <string name=\"discord_information\">O Metrolist usa a biblioteca KizzyRPC para definir o estado da sua conta do Discord. Isto envolve o uso da conexão do Discord Gateway, que pode ser considerado uma violação dos termos de serviço do Discord. Porém, não houve nenhum relato de usuários sendo banidos por esta razão. Use por sua própria conta e risco.\\n\\nO Metrolist extrairá somente o seu token, e todo o resto é armazenado localmente.</string>\n    <string name=\"sided\">Ao lado</string>\n    <string name=\"hide_explicit\">Ocultar conteúdo explícito</string>\n    <string name=\"player_text_alignment\">Alinhamento do texto do tocador</string>\n    <string name=\"player_slider_style\">Estilo do controle deslizante do tocador</string>\n    <string name=\"default_\">Padrão</string>\n    <string name=\"squiggly\">Cobrinha</string>\n    <string name=\"your_youtube_playlists\">Suas listas do YouTube</string>\n    <string name=\"action_like_all\">Favoritar tudo</string>\n    <string name=\"action_remove_like_all\">Remover todos os favoritos</string>\n    <string name=\"grid_cell_size\">Tamanho da célula da grade</string>\n    <string name=\"auto_load_more_desc\">Adicionar mais músicas automaticamente quando o fim da fila é atingido, se possível</string>\n    <string name=\"listen_history\">Histórico de reprodução</string>\n    <string name=\"disable_screenshot\">Desativar capturas de tela</string>\n    <string name=\"similar_to\">Parecido com</string>\n    <string name=\"library_song_empty\">Músicas na biblioteca aparecerão aqui</string>\n    <string name=\"library_artist_empty\">Artistas na biblioteca aparecerão aqui</string>\n    <string name=\"library_album_empty\">Álbuns na biblioteca aparecerão aqui</string>\n    <string name=\"other_versions\">Outras versões</string>\n    <string name=\"keep_listening\">Continue ouvindo</string>\n    <string name=\"add_all_to_library\">Adicionar tudo à biblioteca</string>\n    <string name=\"remove_from_queue\">Remover da fila</string>\n    <string name=\"tempo_and_pitch\">Velocidade e Tonalidade</string>\n    <string name=\"player\">Tocador</string>\n    <string name=\"misc\">Outros</string>\n    <string name=\"small\">Pequeno</string>\n    <string name=\"big\">Grande</string>\n    <string name=\"queue\">Fila</string>\n    <string name=\"persistent_queue_desc\">Restaurar sua última fila ao iniciar o app</string>\n    <string name=\"auto_load_more\">Carregar mais músicas automaticamente</string>\n    <string name=\"auto_skip_next_on_error\">Pular automaticamente para a próxima música quando um erro ocorre</string>\n    <string name=\"auto_skip_next_on_error_desc\">Garanta uma experiência de reprodução contínua</string>\n    <string name=\"stop_music_on_task_clear\">Parar música ao remover dos apps recentes</string>\n    <string name=\"search_history\">Histórico de pesquisa</string>\n    <string name=\"disable_screenshot_desc\">Quando esta opção está ativada, as capturas de tela e a visualização do app nos recentes são desativadas.</string>\n    <string name=\"forgotten_favorites\">Favoritos esquecidos</string>\n    <string name=\"library_playlist_empty\">Suas listas aparecerão aqui</string>\n    <string name=\"theme\">Tema</string>\n    <string name=\"remove_all_from_library\">Remover tudo da biblioteca</string>\n    <string name=\"use_login_for_browse\">Usar a conta para a navegação de conteúdo</string>\n    <string name=\"use_login_for_browse_desc\">Isto pode influenciar em que conteúdo você vê, e se você tiver Premium por exemplo, verá álbuns que só estão disponíveis com Premium</string>\n    <string name=\"action_login\">Conectar-se</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-ro/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">Local</string>\n    <string name=\"remote_history\">La distanță</string>\n    <string name=\"back_button_desc\">Înapoi</string>\n    <string name=\"album_cover_desc\">Copertă album</string>\n    <string name=\"top_music_videos\">Top videoclipuri muzicale</string>\n    <string name=\"trending\">Tendințe</string>\n    <string name=\"weeks\">Săptămâni</string>\n    <string name=\"months\">Luni</string>\n    <string name=\"years\">Ani</string>\n    <string name=\"continuous\">Continuu</string>\n    <string name=\"liked\">Apreciate</string>\n    <string name=\"offline\">Descărcate</string>\n    <string name=\"my_top\">Topul meu</string>\n    <string name=\"cached_playlist\">În cache</string>\n    <string name=\"sync_playlist\">Sincronizează playlistul</string>\n    <string name=\"sync_disabled\">Sincronizare dezactivată</string>\n    <string name=\"allows_for_sync_witch_youtube\">Notă: Această setare permite sincronizarea cu YouTube Music. NU poate fi schimbată mai târziu.</string>\n    <string name=\"generating_image\">Se generează imaginea</string>\n    <string name=\"please_wait\">Așteaptă</string>\n    <string name=\"cancel\">Anulează</string>\n    <string name=\"share_lyrics\">Distribuie versurile</string>\n    <string name=\"share_as_text\">Distribuie ca text</string>\n    <string name=\"share_as_image\">Distribuie ca imagine</string>\n    <string name=\"max_selection_limit\">Limita maximă a selecției</string>\n    <string name=\"share_selected\">Distribuie selectatele</string>\n    <string name=\"customize_colors\">Personalizează culorile</string>\n    <string name=\"text_color\">Culoarea textului</string>\n    <string name=\"secondary_text_color\">Culoarea secundară a textului</string>\n    <string name=\"background_color\">Culoarea fundalului</string>\n    <string name=\"remove_from_cache\">Elimină din cache</string>\n    <string name=\"copy_link\">Copiază linkul</string>\n    <string name=\"select\">Selectează tot</string>\n    <string name=\"like_all\">Apreciază toate</string>\n    <string name=\"sort_by_last_updated\">Data actualizării</string>\n    <string name=\"link_copied\">Link copiat în clipboard</string>\n    <string name=\"lyrics\">Versuri</string>\n    <string name=\"already_in_playlist\">Deja în playlist:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d dată</item>\n        <item quantity=\"few\">%d ori</item>\n        <item quantity=\"other\">%d de ori</item>\n    </plurals>\n    <string name=\"similar_content\">Conținut similar</string>\n    <string name=\"player_background_style\">Stilul fundalului playerului</string>\n    <string name=\"follow_theme\">Urmează tema</string>\n    <string name=\"gradient\">Gradient</string>\n    <string name=\"new_player_design\">Design nou al playerului</string>\n    <string name=\"player_background_blur\">Blur</string>\n    <string name=\"player_buttons_style\">Culoarea butoanelor playerului</string>\n    <string name=\"default_style\">Prestabilită</string>\n    <string name=\"enable_swipe_thumbnail\">Activează glisarea pentru schimbarea melodiei</string>\n    <string name=\"swipe_song_to_add\">Glisează melodia la stânga pentru a o adăuga la coadă sau la dreapta pentru a o reda următoarea</string>\n    <string name=\"lyrics_click_change\">Schimbă versurile la apăsare</string>\n    <string name=\"lyrics_auto_scroll\">Derulează automat versurile</string>\n    <string name=\"lyrics_romanize_japanese\">Romanizează versurile în japoneză</string>\n    <string name=\"lyrics_romanize_korean\">Romanizează versurile în coreeană</string>\n    <string name=\"slim\">Subțire</string>\n    <string name=\"slim_navbar\">Bară de navigare compactă</string>\n    <string name=\"auto_playlists\">Playlisturi automate</string>\n    <string name=\"show_liked_playlist\">Arată playlistul \\\"Apreciate\\\"</string>\n    <string name=\"show_downloaded_playlist\">Arată playlistul \\\"Descărcate\\\"</string>\n    <string name=\"show_top_playlist\">Arată playlistul \\\"Top\\\"</string>\n    <string name=\"show_cached_playlist\">Arată playlistul \\\"În cache\\\"</string>\n    <string name=\"advanced_login\">Autentifică-te cu token</string>\n    <string name=\"token_hidden\">Atinge pentru a afișa token-ul</string>\n    <string name=\"token_shown\">Atinge din nou pentru a-l copia sau edita</string>\n    <string name=\"token_adv_login_description\">Aceasta este o metodă AVANSATĂ de autentificare. Ca o metodă alternativă la portalul web, poți să-ți introduci sau să-ți actualizezi token-ul direct aici. De exemplu, acest lucru poate mări viteza de autentificare pe mai multe dispozitive. Reține că orice format nevalid de token pe care aplicația eșuează să-l analizeze nu va fi acceptat</string>\n    <string name=\"yt_sync\">Sincronizează automat cu contul</string>\n    <string name=\"more_content\">Mai mult conținut</string>\n    <string name=\"general\">General</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"set_quick_picks\">Setează alegerile rapide</string>\n    <string name=\"last_song_listened\">Bazate pe ultima melodie ascultată</string>\n    <string name=\"app_language\">Limba aplicației</string>\n    <string name=\"enable_similar_content\">Activează conținut similar</string>\n    <string name=\"similar_content_desc\">Adaugă automat mai multe melodii similare atunci când se ajunge la finalul cozii</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"import_online\">Importă un playlist \\\"m3u\\\"</string>\n    <string name=\"import_csv\">Importă un playlist \\\"csv\\\"</string>\n    <string name=\"playlist_add_local_to_synced_note\">Notă: Adăugarea de melodii locale la playlisturile sincronizate/de la distanță nu este suportată. Orice altă combinație este validă</string>\n    <string name=\"auto_download_on_like\">Descarcă automat la apreciere</string>\n    <string name=\"auto_download_on_like_desc\">Descarcă melodiile automat atunci când le apreciezi</string>\n    <string name=\"swipe_sensitivity\">Sensibilitate glisare mini player</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"clear_song_cache_dialog\">Sigur vrei să ștergi toate melodiile din cache?</string>\n    <string name=\"clear_image_cache_dialog\">Sigur vrei să ștergi toate imaginile din cache?</string>\n    <string name=\"clear_downloads_dialog\">Sigur vrei să ștergi toate descărcările?</string>\n    <string name=\"disable\">Dezactivează</string>\n    <string name=\"not_logged_in_youtube\">Neautentificat la YouTube</string>\n    <string name=\"default_links\">Deschide linkurile suportate</string>\n    <string name=\"open_app_settings_error\">Nu s-au putut deschide setările aplicației</string>\n    <string name=\"release_notes\">Notele lansării</string>\n    <string name=\"top_length\">Lungimea listei Topul meu</string>\n    <string name=\"history_duration\">Durata istoricului</string>\n    <string name=\"information\">Informații</string>\n    <string name=\"description\">Descriere</string>\n    <string name=\"views\">Vizualizări</string>\n    <string name=\"likes\">Aprecieri</string>\n    <string name=\"subscribe\">Abonează-te</string>\n    <string name=\"subscribed\">Abonat(ă)</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">o secundă</item>\n        <item quantity=\"few\">%d secunde</item>\n        <item quantity=\"other\">%d de secunde</item>\n    </plurals>\n    <string name=\"charts\">Clasamente</string>\n    <string name=\"all_time\">Din toate timpurile</string>\n    <string name=\"past_24_hours\">Ultimele 24 de ore</string>\n    <string name=\"past_week\">Ultima săptămână</string>\n    <string name=\"past_month\">Ultima lună</string>\n    <string name=\"past_year\">Ultimul an</string>\n    <string name=\"dislikes\">Aprecieri negative</string>\n    <string name=\"dislike_all\">Apreciază negativ toate</string>\n    <string name=\"default_lib_chips\">Modifică secțiunea prestabilită a bibliotecii</string>\n    <string name=\"new_mini_player_design\">Design nou al mini playerului</string>\n    <string name=\"now_playing\">Acum se redă</string>\n    <string name=\"close\">Închide</string>\n    <string name=\"hide_player_thumbnail\">Ascunde miniatura din player</string>\n    <string name=\"hide_player_thumbnail_desc\">Înlocuiește coperta albumului cu logo-ul aplicației în player</string>\n    <string name=\"seek_forward_dynamic\">+%1$d (de) secunde înainte</string>\n    <string name=\"seek_backward_dynamic\">-%1$d (de) secunde înapoi</string>\n    <string name=\"seek_seconds_addup\">Derulare progresivă</string>\n    <string name=\"seek_seconds_addup_description\">Dacă este activată, adaugă 5 secunde suplimentare în mod incremental la fiecare salt al derulării</string>\n    <string name=\"disable_load_more_when_repeat_all\">Dezactivează \\\"Încarcă mai multe\\\" atunci când se repetă tot</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Nu încărca în mod automat mai multe melodii și conținut similar atunci când modul \\\"Repetă tot\\\" este activat</string>\n    <string name=\"settings_section_ui\">Interfață</string>\n    <string name=\"settings_section_privacy\">Confidențialitate și securitate</string>\n    <string name=\"settings_section_player_content\">Player și conținut</string>\n    <string name=\"settings_section_storage\">Stocare și date</string>\n    <string name=\"settings_section_system\">Sistem și despre</string>\n    <string name=\"starting_radio\">Se pornește radioul</string>\n    <string name=\"config_proxy\">Configurează proxy</string>\n    <string name=\"proxy_username\">Nume de utilizator proxy</string>\n    <string name=\"proxy_password\">Parolă proxy</string>\n    <string name=\"enable_authentication\">Activează autentificarea</string>\n    <string name=\"lyrics_romanize_title\">Romanizare</string>\n    <string name=\"lyrics_romanization\">Romanizarea versurilor</string>\n    <string name=\"lyrics_romanize_russian\">Romanizează versurile în rusă</string>\n    <string name=\"lyrics_romanize_ukrainian\">Romanizează versurile în ucraineană</string>\n    <string name=\"lyrics_romanize_belarusian\">Romanizează versurile în belarusă</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Romanizează versurile în kirghiză</string>\n    <string name=\"lyrics_romanize_serbian\">Romanizează versurile în sârbă</string>\n    <string name=\"lyrics_romanize_bulgarian\">Romanizează versurile în bulgară</string>\n    <string name=\"line_by_line_option_title\">EXPERIMENTAL: Detectează limba linie cu linie</string>\n    <string name=\"line_by_line_option_desc\">Limbile chirilice vor fi detectate linie cu linie în loc de întreaga melodie.</string>\n    <string name=\"line_by_line_dialog_title\">Ești sigur(ă)?</string>\n    <string name=\"line_by_line_dialog_desc\">Aceasta este o funcție experimentală cu posibilitate de succes sau eșec.\\n\\nÎn mod implicit, limba este determinată din întreaga melodie, dar cu această opțiune activată, va fi determinată linie cu linie. Acest lucru va permite ca melodiile multilimbă să funcționeze, DAR limba s-ar putea să nu fie mereu corectă (de exemplu dacă există un vers în ucraineană care nu conține vreo literă specifică limbii ucrainene, ar putea fi romanizat în rusă).\\n\\nDacă nu ai probleme, este recomandat să lași această opțiune dezactivată.</string>\n    <string name=\"romanize_current_track\">Romanizează pista curentă</string>\n    <string name=\"lyrics_romanization_cyrillic\">Chirilic</string>\n    <string name=\"edit_playlist_cover\">Editează coperta playlistului</string>\n    <string name=\"edit_playlist_cover_note\">Notă: Contul tău trebuie să fie asociat cu un număr de telefon verificat pe YouTube Music pentru a putea schimba coperta playlistului.</string>\n    <string name=\"edit_playlist_cover_note_wait\">După selectarea unei imagini, așteaptă un moment pentru ca noua coperta să apară la playlistul tău.</string>\n    <string name=\"choose_from_library\">Alege din bibliotecă</string>\n    <string name=\"remove_custom_image\">Elimină imaginea personalizată</string>\n    <string name=\"audio_offload\">Activează offload</string>\n    <string name=\"audio_offload_description\">Utilizează calea audio offload pentru redarea audio. Dezactivarea acestei setări poate crește utilizarea bateriei, dar poate fi utilă dacă întâmpini probleme cu redarea audio sau cu post procesarea</string>\n    <string name=\"uploaded_playlist\">Încărcate</string>\n    <string name=\"filter_uploaded\">Încărcate</string>\n    <string name=\"show_uploaded_playlist\">Arată playlistul \\\"Încărcate\\\"</string>\n    <string name=\"updater\">Actualizator</string>\n    <string name=\"check_for_updates\">Verifică automat pentru actualizări</string>\n    <string name=\"lyrics_romanize_macedonian\">Romanizează versurile în macedoniană</string>\n    <string name=\"discord_use_details\">Utilizează detalii în loc de stare</string>\n    <string name=\"discord_use_details_description\">Arată titlul melodiei în loc de numele artistului</string>\n    <string name=\"update_notifications\">Activează notificările pentru actualizări</string>\n    <string name=\"update_available_title\">Actualizare disponibilă</string>\n    <string name=\"update_channel_name\">Actualizări ale aplicației</string>\n    <string name=\"update_channel_desc\">Notificări despre versiuni noi</string>\n    <string name=\"integrations\">Integrări</string>\n    <string name=\"username\">Nume de utilizator</string>\n    <string name=\"password\">Parolă</string>\n    <string name=\"lastfm_integration\">Integrare Last.fm</string>\n    <string name=\"lastfm_now_playing\">Trimite Se redă acum</string>\n    <string name=\"enable_scrobbling\">Activează scrobblingul</string>\n    <string name=\"scrobbling_configuration\">Configurare scrobbling</string>\n    <string name=\"scrobble_min_track_duration\">Dă scrobble la melodiile mai lungi de</string>\n    <string name=\"scrobble_delay_percent\">Întârziere pentru scrobble în procente</string>\n    <string name=\"scrobble_delay_minutes\">Întârziere pentru scrobble în minute</string>\n    <string name=\"swipe_song_to_remove\">Glisează melodia pentru a o elimina din playlist</string>\n    <string name=\"last_fm_send_likes\">Trimite aprecierile</string>\n    <string name=\"last_fm_send_likes_description\">Apreciază/dezapreciază melodiile în Last.fm atunci când sunt apreciate/dezapreciate în Metrolist</string>\n    <string name=\"primary_color_style\">Culoare principală</string>\n    <string name=\"tertiary_color_style\">Culoare terțiară</string>\n    <string name=\"auto_scroll\">Resincronizare</string>\n    <string name=\"lyrics_romanize_chinese\">Romanizează versurile în chineză</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"google_cast_description\">Activează difuzarea audio către Chromecast și alt dispozitive compatibile cu Cast</string>\n    <string name=\"logging_in\">Se conectează…</string>\n    <string name=\"hide_video_songs\">Ascunde melodiile video</string>\n    <string name=\"details_desc\">Vezi informațiile melodiei</string>\n    <string name=\"edit_desc\">Modifică titlul sau artistul</string>\n    <string name=\"start_radio_desc\">Creează o stație bazată pe acest element</string>\n    <string name=\"play_next_desc\">Adaugă în partea de sus a cozii</string>\n    <string name=\"add_to_queue_desc\">Adaugă în partea de jos a cozii</string>\n    <string name=\"add_to_library_desc\">Salvează în biblioteca ta</string>\n    <string name=\"download_desc\">Fă disponibil pentru redarea offline</string>\n    <string name=\"add_to_playlist_desc\">Adaugă într-unul dintre playlisturile tale</string>\n    <string name=\"refetch_desc\">Preia cele mai recente metadate de pe YouTube Music</string>\n    <string name=\"share_desc\">Distribuie un link către acest element</string>\n    <string name=\"delete_desc\">Elimină acest element permanent</string>\n    <string name=\"advanced_desc\">Modifică tempo-ul și înălțimea melodiei</string>\n    <string name=\"equalizer_desc\">Ajustează egalizatorul audio</string>\n    <string name=\"enable_dynamic_icon\">Activează pictograma dinamică</string>\n    <string name=\"mini_player\">Mini-player</string>\n    <string name=\"pure_black_mini_player\">Mini-player complet negru</string>\n    <string name=\"cache_size_warning_title\">Stai!</string>\n    <string name=\"cache_size_warning_message\">Ai ales o limită de mărime a cache-ului mai mică decât cea pe care aplicația o folosește în prezent (%1$s). Dacă continui, aplicația ar putea elimina câțiva %2$s stocați pentru a corespunde cu noua limită. Continui oricum?</string>\n    <string name=\"cache_size_warning_confirm\">Continuă</string>\n    <string name=\"download_playlist_desc\">Descarcă toate melodiile pentru redare offline</string>\n    <string name=\"remove_download_playlist_desc\">Elimină toate melodiile descărcate din acest playlist</string>\n    <string name=\"download_in_progress_desc\">Descărcarea este în progres</string>\n    <string name=\"share_playlist_desc\">Distribuie acest playlist cu alții</string>\n    <string name=\"delete_playlist_desc\">Elimină acest playlist permanent</string>\n    <string name=\"sync_playlist_desc\">Sincronizează playlistul cu YouTube Music</string>\n    <string name=\"lyrics_glow_effect\">Activează efectul de strălucire al versurilor</string>\n    <string name=\"lyrics_glow_effect_desc\">Adaugă o animație de strălucire și un efect de bounce la versurile active</string>\n    <string name=\"enable_better_lyrics\">Activează Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">Versuri sincronizate pe silabe pentru orice melodie, pentru karaoke</string>\n    <string name=\"shuffle_playlist_first\">Amestecă playlistul/albumul mai întâi</string>\n    <string name=\"shuffle_playlist_first_desc\">Când amestecarea este pornită, redă toate melodiile din playlistul/albumul original mai întâi, apoi redă conținut similar</string>\n    <string name=\"show_wrapped_card\">Afișează cardul Wrapped</string>\n    <string name=\"lyrics_animation_style\">Stil de animație cuvânt cu cuvânt</string>\n    <string name=\"none\">Fără</string>\n    <string name=\"fade\">Estompare</string>\n    <string name=\"glow\">Strălucire</string>\n    <string name=\"slide\">Glisare</string>\n    <string name=\"karaoke\">Karaoke</string>\n    <string name=\"apple_music_style\">Apple Music</string>\n    <string name=\"lyrics_text_size\">Mărimea textului versurilor</string>\n    <string name=\"lyrics_line_spacing\">Spațierea între versuri</string>\n    <string name=\"album_art_for\">Coperta albumului pentru %s</string>\n    <string name=\"wrapped_total_albums_title\">Ai ascultat</string>\n    <string name=\"wrapped_total_albums_subtitle\">albume unice</string>\n    <string name=\"wrapped_top_album_title\">Albumul tău de top este</string>\n    <string name=\"wrapped_playlist_ready\">Playlistul tău personal este gata</string>\n    <string name=\"wrapped_top_5_albums_title\">Cele 5 albume de top ale tale</string>\n    <string name=\"wrapped_album_listening_time\">Ai ascultat acest album timp de %d (de) minute</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d minute</string>\n    <string name=\"wrapped_no_data\">Fără date</string>\n    <string name=\"wrapped_top_5_artists_title\">Artiștii tăi de top din acest an</string>\n    <string name=\"wrapped_artist_listening_time\">%d (de) minute</string>\n    <string name=\"wrapped_top_5_songs_title\">Melodiile tale de top din acest an</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">Coperta albumului</string>\n    <string name=\"wrapped_top_artist_title\">Artistul tău de top din acest an este</string>\n    <string name=\"wrapped_top_artist_image_content_description\">Imaginea artistului de top</string>\n    <string name=\"wrapped_top_artist_listening_time\">L-ai ascultat/ai ascultat-o timp de %d (de) minute</string>\n    <string name=\"wrapped_top_song_title\">Cea mai redată melodie de-a ta este</string>\n    <string name=\"wrapped_top_song_listening_time\">Ai ascultat-o timp de %d (de) minute</string>\n    <string name=\"wrapped_total_artists_title\">Ai ascultat</string>\n    <string name=\"wrapped_total_artists_subtitle\">artiști unici</string>\n    <string name=\"wrapped_total_songs_title\">Ai ascultat</string>\n    <string name=\"wrapped_total_songs_subtitle\">(de) melodii unice</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_intro_subtitle\">este timpul să vedem ce ai ascultat</string>\n    <string name=\"wrapped_intro_button\">să mergem!</string>\n    <string name=\"wrapped_logo_content_description\">Logo-ul Metrolist</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"wrapped_ready_title\">WRAPPED-UL TĂU ESTE GATA!</string>\n    <string name=\"wrapped_ready_subtitle\">Este timpul să vedem ce ai apreciat în acest an.</string>\n    <string name=\"wrapped_thank_you\">Mulțumim pentru ascultare</string>\n    <string name=\"wrapped_special_thanks\">Mulțumire speciale lui MO Agamy pentru crearea Metrolist</string>\n    <string name=\"wrapped_close\">Închide Wrapped</string>\n    <string name=\"wrapped_playlist_title\">Wrapped-ul tău %s</string>\n    <string name=\"wrapped_create_playlist\">Creează playlist</string>\n    <string name=\"wrapped_playlist_saved\">Playlist salvat</string>\n    <string name=\"casting_to\">Se difuzează către %s</string>\n    <string name=\"progress_percent\">Progres: %s%%</string>\n    <string name=\"listening_to_metrolist\">Ascultă pe Metrolist</string>\n    <string name=\"open\">Deschide</string>\n    <string name=\"failed_to_create_image\">Nu s-a putut crea imaginea: %s</string>\n    <string name=\"copied_title\">Titlul a fost copiat</string>\n    <string name=\"copied_artist\">Artistul a fost copiat</string>\n    <string name=\"error_playing\">Eroare la redare</string>\n    <string name=\"failed_to_parse_proxy\">Nu s-a putut analiza URL-ul proxy-ului.</string>\n    <string name=\"wavy\">Ondulat</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"one\">Un profil</item>\n        <item quantity=\"few\">%d profile</item>\n        <item quantity=\"other\">%d de profile</item>\n    </plurals>\n    <string name=\"equalizer_header\">Egalizator</string>\n    <string name=\"no_profiles\">Niciun profil pentru egalizator</string>\n    <string name=\"import_profile\">Importă profil</string>\n    <string name=\"eq_disabled\">Dezactivat</string>\n    <plurals name=\"band_count\">\n        <item quantity=\"one\">O bandă</item>\n        <item quantity=\"few\">%d benzi</item>\n        <item quantity=\"other\">%d de benzi</item>\n    </plurals>\n    <string name=\"delete_profile_desc\">Șterge profilul</string>\n    <string name=\"delete_profile_confirmation\">Ești sigur(ă) că vrei să ștergi %1$s? Această acțiune nu poate fi anulată.</string>\n    <string name=\"error_file_read\">Nu s-a putut citi fișierul</string>\n    <string name=\"error_file_open\">Nu s-a putut deschide fișierul: %1$s</string>\n    <string name=\"import_error_title\">Eroare la importare</string>\n    <string name=\"pause_music_when_media_is_muted\">Întrerupe muzica atunci volumul media este setat la 0</string>\n    <string name=\"about_artist\">Despre</string>\n    <string name=\"show_more\">Afișează mai mult</string>\n    <string name=\"show_less\">Afișează mai puțin</string>\n    <string name=\"artist_page_settings\">Pagina artistului</string>\n    <string name=\"show_artist_description\">Afișează descrierea artistului</string>\n    <string name=\"show_artist_subscriber_count\">Afișează numărul de abonați</string>\n    <string name=\"show_artist_monthly_listeners\">Afișează numărul de ascultători lunari</string>\n    <string name=\"enable_simpmusic\">Activează SimpMusic Lyrics</string>\n    <string name=\"enable_simpmusic_desc\">Versuri preluate automat de pe Musixmatch și transcrierile YouTube</string>\n    <string name=\"skip_silence_desc\">Avansează rapid prin părțile silențioase ale melodiilor</string>\n    <string name=\"skip_silence_instant\">Omite liniștea instant</string>\n    <string name=\"skip_silence_instant_desc\">Sari direct peste momentele silențioase în loc să mărești viteza de redare</string>\n    <string name=\"remember_shuffle_and_repeat\">Memorează modurile de amestecare și de repetare</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Memorează modurile de amestecare și de repetare atunci când aplicația este repornită</string>\n    <string name=\"lyrics_offset\">Decalaj versuri</string>\n    <string name=\"system_equalizer\">Egalizator de sistem</string>\n    <string name=\"album_art\">Coperta albumului</string>\n    <string name=\"no_song_playing\">Nu se redă nicio melodie</string>\n    <string name=\"tap_to_open\">Atinge pentru a deschide Metrolist</string>\n    <string name=\"previous\">Anterioara</string>\n    <string name=\"play_pause\">Redă/Pauză</string>\n    <string name=\"next\">Următoarea</string>\n    <string name=\"like\">Apreciază</string>\n    <string name=\"widget_description\">Widget pentru playerul muzical cu butoane pentru redare</string>\n    <string name=\"turntable_widget_description\">Widget de muzică circular cu butoane de redare și apreciere</string>\n    <string name=\"persistent_shuffle_title\">Amestecare persistentă</string>\n    <string name=\"persistent_shuffle_desc\">Menține modul de amestecare activat atunci când redai melodii sau playlisturi noi</string>\n    <string name=\"crop_album_art\">Decupează coperta albumului</string>\n    <string name=\"crop_album_art_desc\">Forțează un raport de aspect pătrățos prin decuparea miniaturilor videoclipurilor</string>\n    <string name=\"error_title\">Eroare</string>\n    <string name=\"error_eq_apply_failed\">Nu s-a putut aplica profilul pentru egalizator: %1$s</string>\n    <string name=\"error_playback_failed\">Redarea a eșuat</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">Menține ecranul pornit atunci când playerul este extins</string>\n    <string name=\"listen_together\">Ascultă împreună</string>\n    <string name=\"listen_together_server_url\">URL server</string>\n    <string name=\"listen_together_username\">Nume de utilizator</string>\n    <string name=\"listen_together_connected\">Conectat</string>\n    <string name=\"listen_together_reconnecting\">Se reconectează…</string>\n    <string name=\"listen_together_disconnected\">Neconectat</string>\n    <string name=\"listen_together_connecting\">Se conectează…</string>\n    <string name=\"listen_together_error\">Eroare la conexiune</string>\n    <string name=\"listen_together_create_room\">Creează cameră</string>\n    <string name=\"listen_together_create_room_desc\">Creează o cameră și distribuie codul cu prietenii</string>\n    <string name=\"listen_together_join_room\">Alătură-te camerei</string>\n    <string name=\"listen_together_room_code\">Codul camerei</string>\n    <string name=\"listen_together_you_are_host\">Ești gazda</string>\n    <string name=\"listen_together_you_are_guest\">Ești un invitat</string>\n    <string name=\"listen_together_join_requests\">Cereri de alăturare</string>\n    <string name=\"listen_together_view_logs\">Vezi jurnalele</string>\n    <string name=\"listen_together_view_logs_desc\">Depanare conexiune și mesaje</string>\n    <string name=\"listen_together_logs\">Jurnale de conectare</string>\n    <string name=\"listen_together_no_logs\">Niciun jurnal încă</string>\n    <string name=\"listen_together_description\">Ascultă muzică cu prietenii tăi în timp real. Creează o cameră pentru a fi gazda sau alătură-te unei camere existente cu un cod.</string>\n    <string name=\"listen_together_background_disconnect_note\">Notă: S-ar putea să poți fi deconectat dacă creezi o cameră în timp ce nu se redă muzică și treci la o altă aplicație.</string>\n    <string name=\"listen_together_not_configured\">Funcția Ascultă împreună nu este configurată. Te rugăm să setezi adresa URL a serverului în Setări → Integrări → Ascultă împreună.</string>\n    <string name=\"listen_together_suggestion_received\">%1$s a cerut %2$s</string>\n    <string name=\"listen_together_suggestion_sent\">Sugestie trimisă gazdei!</string>\n    <string name=\"listen_together_join_request_notification\">%1$s vrea să se alăture camerei</string>\n    <string name=\"listen_together_notification_channel_name\">Ascultă împreună</string>\n    <string name=\"listen_together_notification_channel_desc\">Notificări pentru evenimentele funcției Ascultă împreună</string>\n    <string name=\"listen_together_room_created\">Cameră creată: %s</string>\n    <string name=\"listen_together_cannot_edit_username_in_room\">Nu se poate edita numele de utilizator în timp ce ești într-o cameră</string>\n    <string name=\"waiting_for_approval\">Se așteaptă aprobarea de la gazdă</string>\n    <string name=\"invalid_room_code\">Cod de cameră nevalid</string>\n    <string name=\"join_request_denied\">Cerere de alăturare respinsă</string>\n    <string name=\"join_existing_room\">Alătură-te unei camere existente</string>\n    <string name=\"room_code\">Codul camerei</string>\n    <string name=\"leave_room\">Părăsește camera</string>\n    <string name=\"join_room\">Alătură-te</string>\n    <string name=\"create_room\">Creează</string>\n    <string name=\"joining_room\">Se alătură camerei %s…</string>\n    <string name=\"creating_room\">Se creează camera…</string>\n    <string name=\"connect\">Conectează-te</string>\n    <string name=\"disconnect\">Deconectează-te</string>\n    <string name=\"create\">Creează</string>\n    <string name=\"join\">Alătură-te</string>\n    <string name=\"approve\">Aprobă</string>\n    <string name=\"reject\">Respinge</string>\n    <string name=\"clear\">Elimină</string>\n    <string name=\"copy\">Copiază</string>\n    <string name=\"copied_to_clipboard\">Copiat în clipboard</string>\n    <string name=\"not_set\">Nesetat</string>\n    <string name=\"hosting_room\">Găzduiește o cameră</string>\n    <string name=\"in_room\">În cameră</string>\n    <string name=\"pending_requests\">Cereri în așteptare</string>\n    <string name=\"pending_suggestions\">Sugestii în așteptare</string>\n    <string name=\"suggest_to_host\">Sugerează-i gazdei</string>\n    <string name=\"kick_user\">Dă afară</string>\n    <string name=\"host_label\">Gazdă</string>\n    <string name=\"you_label\">Tu</string>\n    <string name=\"connected_users\">Utilizatori conectați</string>\n    <string name=\"enter_username\">Introdu numele de utilizator</string>\n    <string name=\"error_username_empty\">Numele de utilizator este necesar.</string>\n    <string name=\"resync\">Resincronizează</string>\n    <string name=\"crash_title\">Aplicația s-a blocat</string>\n    <string name=\"crash_description\">A apărut o eroare neașteptată. Te rugăm să ne trimiți raportul de eroare pentru a ne ajuta să remediem problema.</string>\n    <string name=\"crash_share_logs\">Partajează jurnalele</string>\n    <string name=\"crash_share_title\">Partajează raportul de eroare</string>\n    <string name=\"crash_report_subject\">Raport de eroare Metrolist</string>\n    <string name=\"crash_close\">Închide</string>\n    <string name=\"crash_no_log\">Niciun jurnal de eroare disponibil</string>\n    <string name=\"palette_dynamic\">Dinamic</string>\n    <string name=\"palette_purple\">Mov</string>\n    <string name=\"palette_deep_purple\">Mov închis</string>\n    <string name=\"palette_indigo\">Indigo</string>\n    <string name=\"palette_blue\">Albastru</string>\n    <string name=\"palette_cyan\">Cian</string>\n    <string name=\"palette_teal\">Turcoaz</string>\n    <string name=\"palette_green\">Verde</string>\n    <string name=\"palette_light_green\">Verde deschis</string>\n    <string name=\"palette_lime\">Lime</string>\n    <string name=\"palette_yellow\">Galben</string>\n    <string name=\"palette_orange\">Portocaliu</string>\n    <string name=\"palette_deep_orange\">Portocaliu închis</string>\n    <string name=\"palette_brown\">Maro</string>\n    <string name=\"palette_grey\">Gri</string>\n    <string name=\"cd_back\">Înapoi</string>\n    <string name=\"not_playing\">Nu se redă nicio melodie</string>\n    <string name=\"tap_to_play\">Atinge pentru a deschide Metrolist</string>\n    <string name=\"widget_music_player\">Player de muzică</string>\n    <string name=\"widget_turntable\">Platan</string>\n    <string name=\"listen_together_choose_server\">Alege serverul</string>\n    <string name=\"listen_together_custom_server\">Server personalizat</string>\n    <string name=\"listen_together_use_custom_server\">Folosește un server personalizat</string>\n    <string name=\"mute\">Mut</string>\n    <string name=\"unmute\">Scoate de pe mut</string>\n    <string name=\"listen_together_auto_approval_joins\">Aprobă automat cererile de alăturare</string>\n    <string name=\"listen_together_auto_approval_joins_desc\">Aprobă în mod automat cererile de alăturare în loc să le evaluezi manual</string>\n    <string name=\"listen_together_sync_volume\">Sincronizează volumul cu gazda</string>\n    <string name=\"listen_together_sync_volume_desc\">Invitații folosesc același nivel al volumului ca și gazda</string>\n    <string name=\"copy_code\">Copiază codul</string>\n    <string name=\"kick_user_desc\">Elimină această persoană din sesiune</string>\n    <string name=\"permanently_kick_user\">Blochează permanent</string>\n    <string name=\"together\">Împreună</string>\n    <string name=\"enter_room_code\">Introdu codul camerei</string>\n    <string name=\"listen_together_settings_desc\">Configurează serverul, numele de utilizator și altele</string>\n    <string name=\"permanently_kick_user_desc\">Blochează cererile de alăturare ale acestei persoane și ascunde-i sugestiile</string>\n    <string name=\"transfer_ownership\">Transferă rolul de proprietar</string>\n    <string name=\"transfer_ownership_desc\">Fă această persoană gazda camerei</string>\n    <string name=\"manage_user\">Gestionează utilizatorul</string>\n    <string name=\"listen_together_blocked_users\">Utilizatori blocați</string>\n    <string name=\"listen_together_blocked_users_count\">%d (de) utilizatori blocați</string>\n    <string name=\"listen_together_no_blocked_users\">Niciun utilizator blocat</string>\n    <string name=\"unblock\">Deblochează</string>\n    <string name=\"user_blocked_by_host\">Utilizator blocat de gazdă</string>\n    <string name=\"ai_lyrics_translation\">Traducerea versurilor cu AI</string>\n    <string name=\"ai_translating_lyrics\">Se traduc versurile...</string>\n    <string name=\"ai_lyrics_translated\">Versuri traduse</string>\n    <string name=\"ai_provider\">Furnizor</string>\n    <string name=\"ai_base_url\">URL de bază</string>\n    <string name=\"ai_api_key\">Cheie API</string>\n    <string name=\"ai_model\">Model</string>\n    <string name=\"ai_translation_mode\">Mod de traducere</string>\n    <string name=\"ai_target_language\">Limba țintă</string>\n    <string name=\"ai_setup_guide\">Acreditări API</string>\n    <string name=\"ai_translation_literal\">Traducere</string>\n    <string name=\"ai_translation_transcribed\">Transcriere</string>\n    <string name=\"ai_api_key_required\">Cheie API necesară</string>\n    <string name=\"ai_error_api_key_required\">Cheia pentru API este necesară</string>\n    <string name=\"ai_error_no_lyrics\">Niciun vers de tradus</string>\n    <string name=\"ai_error_lyrics_empty\">Versurile sunt goale</string>\n    <string name=\"ai_error_language_required\">Limba țintă este necesară</string>\n    <string name=\"ai_error_unexpected\">Rezultat neașteptat al traducerii</string>\n    <string name=\"ai_error_unknown\">A apărut o eroare necunoscută</string>\n    <string name=\"ai_error_translation_failed\">Traducere eșuată</string>\n    <string name=\"palette_rose\">Trandafir</string>\n    <string name=\"palette_sky_blue\">Cer albastru</string>\n    <string name=\"palette_blue_grey\">Albastru-gri</string>\n    <string name=\"cd_pure_black_mode\">Mod negru pur</string>\n    <string name=\"cd_light_mode\">Mod luminos</string>\n    <string name=\"cd_dark_mode\">Mod întunecat</string>\n    <string name=\"cd_system_mode\">Modul systemului</string>\n    <string name=\"cd_palette_item\">Paleta %1$s</string>\n    <string name=\"play_all\">Redă tot</string>\n    <string name=\"palette_crimson\">Stacojiu</string>\n    <string name=\"palette_amber\">Chihlimbar</string>\n    <string name=\"recognize_music\">Recunoaște muzica</string>\n    <string name=\"youtube_url_column\">Coloana cu URL-ul YouTube (opțional)</string>\n    <string name=\"re_listen\">Reascultă</string>\n    <string name=\"clear_recognition_history_confirm\">Ești sigur(ă) că vrei să elimini tot istoricul recunoașterilor?</string>\n    <string name=\"no_match_found\">Nicio potrivire găsită</string>\n    <string name=\"delete_from_history\">Șterge din istoric</string>\n    <string name=\"artist_name_column\">Coloana cu numele artistului</string>\n    <string name=\"processing\">Se procesează…</string>\n    <string name=\"clear_recognition_history\">Elimină istoricul recunoașterilor</string>\n    <string name=\"map_csv_columns\">Mapează coloanele CSV</string>\n    <string name=\"column_label\">Col %d</string>\n    <string name=\"recognition_error\">Eroare la recunoaștere</string>\n    <string name=\"enable_high_refresh_rate_desc\">Forțează afișajul să ruleze la cea mai ridicată rată de reîmprospătare suportată (de ex. 120Hz)</string>\n    <string name=\"first_row_is_header\">Prima linie este antetul</string>\n    <string name=\"try_again\">Încearcă din nou</string>\n    <string name=\"tap_to_recognize\">Atinge pentru a recunoaște</string>\n    <string name=\"recognition_history\">Istoricul recunoașterilor</string>\n    <string name=\"enable_high_refresh_rate\">Activează rata de reîmprospătare ridicată</string>\n    <string name=\"song_title_column\">Coloana cu numele melodiei</string>\n    <string name=\"recently_converted\">Convertite recent</string>\n    <string name=\"importing_csv\">Se importă CSV-ul</string>\n    <string name=\"play_on_app\">Redă pe Metrolist</string>\n    <string name=\"listening\">Se ascultă…</string>\n    <string name=\"continue_action\">Continuă</string>\n    <string name=\"enable\">Activează</string>\n    <string name=\"crossfade_beta_title\">Caracteristică beta</string>\n    <string name=\"crossfade\">Tranziție lină</string>\n    <string name=\"crossfade_desc\">Tranziție lină între melodii</string>\n    <string name=\"crossfade_duration\">Durata tranziției line</string>\n    <string name=\"crossfade_gapless\">Dezactivează pentru albumele fără pauze</string>\n    <string name=\"crossfade_gapless_desc\">Nu aplica efectul de tranziție lină dacă albumul nu are pauze</string>\n    <string name=\"crossfade_beta_message\">Tranziția lină este o caracteristică nouă și este posibil ca aceasta să aibă bug-uri. Dacă întâmpini orice fel de problenă, te rugăm să ne-o raportezi.\\n\\nAceastă funcție dezactivează offload-ul audio datorită unor limitări tehnice.</string>\n    <string name=\"audio_offload_disabled_by_crossfade\">Dezactivat, deoarece funcția Tranziție lină este activată</string>\n    <string name=\"prevent_duplicate_tracks_in_queue\">Previne piesele duplicate în coadă</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">Când adaugi o piesă în coadă, elimin-o din poziția ei anterioară dacă există deja</string>\n    <string name=\"hide_youtube_shorts\">Ascunde YouTube Shorts</string>\n    <string name=\"listen_together_in_top_bar\">Ascultă împreună în bara de sus</string>\n    <string name=\"listen_together_in_top_bar_desc\">Arată funcția Ascultă împreună în bara de sus în loc de în bara de navigare</string>\n    <string name=\"ai_translation_literal_desc\">Tradu sensul în limba țintă</string>\n    <string name=\"ai_translation_transcribed_desc\">Convertește punctuația în scriptul țintă</string>\n    <string name=\"ai_provider_help\">Obține chei API</string>\n    <string name=\"ai_provider_openrouter_help\">Vizitează https://openrouter.ai pentru modele gratuite și plătite</string>\n    <string name=\"ai_provider_openai_help\">Vizitează https://platform.openai.com/api-keys</string>\n    <string name=\"ai_provider_claude_help\">Vizitează https://console.anthropic.com/settings/keys</string>\n    <string name=\"ai_provider_gemini_help\">Vizitează https://aistudio.google.com/apikey</string>\n    <string name=\"ai_provider_perplexity_help\">Vizitează https://perplexity.ai/settings/api</string>\n    <string name=\"ai_provider_xai_help\">Vizitează https://console.x.ai</string>\n    <string name=\"ai_provider_deepl_help\">Vizitează https://deepl.com/pro-api pentru chei gratuite și plătite</string>\n    <string name=\"ai_deepl_formality\">Formalitate</string>\n    <string name=\"ai_deepl_formality_default\">Prestabilită</string>\n    <string name=\"ai_deepl_formality_more\">Mai formal</string>\n    <string name=\"ai_deepl_formality_less\">Mai puțin formal</string>\n    <string name=\"discord_status\">Status</string>\n    <string name=\"discord_status_online\">Online</string>\n    <string name=\"discord_status_idle\">Inactiv</string>\n    <string name=\"discord_status_dnd\">Nu deranja</string>\n    <string name=\"discord_buttons\">Butoane</string>\n    <string name=\"discord_button_1\">Butonul 1</string>\n    <string name=\"discord_button_2\">Butonul 2</string>\n    <string name=\"login_successful\">Autentificare reușită!</string>\n    <string name=\"discord_information_warning\">Această funcție folosește biblioteca KizzyRPC pentru conectarea la gateway-ul Discord pentru a-ți seta statusul Rich Presence. În timp ce nu există suspendări cunoscute ale conturilor întâmpinate prin utilizări similare, această metodă nu este oficial acceptată de către Discord și poate fi considerată o încălcare a Termenilor Serviciului. Tokenul tău este extras local și niciodată trimis către servere terțe. Continui pe propriul risc.</string>\n    <string name=\"discord_activity_type\">Tipul activității</string>\n    <string name=\"discord_activity_playing\">Se joacă</string>\n    <string name=\"discord_activity_listening\">Ascultă</string>\n    <string name=\"discord_activity_watching\">Vizionează</string>\n    <string name=\"discord_activity_competing\">Concurează</string>\n    <string name=\"discord_button_text_variables\">Variabile: {song_name}, {artist_name}, {album_name}</string>\n    <string name=\"discord_rpc_preview\">Previzualizare Rich Presence</string>\n    <string name=\"discord_presence\">Prezență</string>\n    <string name=\"discord_connect_description\">Autentifică-te cu Discord pentru a partaja ce asculți</string>\n    <string name=\"discord_playing_metrolist\">Se joacă Metrolist</string>\n    <string name=\"discord_watching_metrolist\">Vizionează Metrolist</string>\n    <string name=\"discord_competing_metrolist\">Concurează pe Metrolist</string>\n    <string name=\"discord_activity_name\">Numele activității</string>\n    <string name=\"discord_activity_name_description\">Nume personalizat pentru activitate (lasă gol pentru numele prestabilit)</string>\n    <string name=\"discord_advanced_mode\">Mod avansat</string>\n    <string name=\"discord_advanced_mode_description\">Afișează opțiuni adiționale de personalizare pentru Rich Presence</string>\n    <string name=\"resume_on_bluetooth_connect\">Reia redarea la conectarea unui dispozitiv Bluetooth</string>\n    <string name=\"lyrics_romanize_hindi\">Romanizează versurile în hindi</string>\n    <string name=\"lyrics_romanize_punjabi\">Romanizează versurile în punjabi</string>\n    <string name=\"lyrics_romanize_as_main\">Afișează versurile romanizate ca principale</string>\n    <string name=\"player_background_solid\">Solid</string>\n    <string name=\"display_density\">Densitatea afișajului</string>\n    <string name=\"restart\">Repornire</string>\n    <string name=\"restart_required\">Repornire necesară</string>\n    <string name=\"density_restart_message\">Modificarea densității afișajului va avea efect după repornirea aplicației. Dorești să repornești acum?</string>\n    <string name=\"enable_lrclib_desc\">Bază de date pentru versuri sincronizate condusă de comunitate</string>\n    <string name=\"enable_kugou_desc\">Preia versurile de pe KuGou, o platformă populară chinezească de muzică</string>\n    <string name=\"youtube_music_lyrics_note\">NOTĂ: Versurile de pe YouTube Music vor fi afișate automat atunci când alte versuri nu sunt disponibile. Versurile de pe YTM nu sunt de obicei sincronizate.</string>\n    <string name=\"enable_lyricsplus\">Activează LyricsPlus</string>\n    <string name=\"enable_lyricsplus_desc\">Versuri sincronizate din mai multe surse</string>\n    <string name=\"lyrics_provider_selection\">Selecția de furnizori</string>\n    <string name=\"lyrics_provider_selection_desc\">Alege ce furnizori de versuri sunt activați</string>\n    <string name=\"lyrics_provider_priority\">Prioritatea furnizorilor de versuri</string>\n    <string name=\"lyrics_provider_priority_desc\">Trage pentru a reordona furnizorii după preferințe. Cu cât este mai înaltă poziția, cu atât prioritatea este mai înaltă.</string>\n    <string name=\"changelog\">Jurnal de modificări</string>\n    <string name=\"changelog_empty\">Niciun jurnal de modificări disponibil</string>\n    <string name=\"github_releases_url\">https://github.com/MetrolistGroup/Metrolist/releases</string>\n    <string name=\"list_bullet\">•</string>\n    <string name=\"view_on_github\">Vezi pe GitHub</string>\n    <string name=\"current_version\">Versiunea curentă</string>\n    <string name=\"version_format\">Versiune: %s</string>\n    <string name=\"update_settings\">Setări actualizări</string>\n    <string name=\"check_for_updates_title\">Verifică pentru actualizări</string>\n    <string name=\"checking_for_updates\">Se verifică pentru actualizări…</string>\n    <string name=\"latest_version_format\">Cea mai recentă: %s</string>\n    <string name=\"check_for_updates_button\">Verifică pentru actualizări</string>\n    <string name=\"hide_changelog\">Ascunde jurnalul de modificări</string>\n    <string name=\"view_changelog\">Vezi jurnalul modificărilor</string>\n    <string name=\"failed_to_check_updates\">Nu s-a putut verifica pentru actualizări: %s</string>\n    <string name=\"set_as_default\">Setează ca prestabilit</string>\n    <string name=\"sleep_timer_default_set\">Durata prestabilită a temporizatorul pentru somn a fost setată la %d (de) minute</string>\n    <string name=\"plays\">redă</string>\n    <string name=\"error_episode_save\">Nu s-a putut salva episodul</string>\n    <string name=\"error_episode_remove\">Nu s-a putut elimina episodul</string>\n    <string name=\"error_podcast_subscribe\">Nu s-a putut abona la podcast</string>\n    <string name=\"error_podcast_unsubscribe\">Nu s-a putut dezabona de la podcast</string>\n    <string name=\"widget_recognizer_name\">Recunoscător de muzică</string>\n    <string name=\"widget_recognizer_description\">Identifică melodiile care se redau în jurul tău direct de pe ecranul de pornire</string>\n    <string name=\"widget_recognizer_tap_to_search\">Atinge pentru a identifica melodia</string>\n    <string name=\"widget_recognizer_listening\">Se ascultă…</string>\n    <string name=\"widget_recognizer_processing\">Se identifică…</string>\n    <string name=\"widget_recognizer_no_match\">Nicio potrivire găsită. Încearcă din nou</string>\n    <string name=\"widget_recognizer_error\">Recunoaștere eșuată</string>\n    <string name=\"widget_recognizer_error_generic\">A intervenit o eroare. Te rugăm să încerci din nou</string>\n    <string name=\"widget_recognizer_unknown_song\">Melodie necunoscută</string>\n    <string name=\"widget_recognizer_unknown_artist\">Artist necunoscut</string>\n    <string name=\"widget_recognizer_mic_desc\">Identifică melodia</string>\n    <string name=\"widget_recognizer_channel_name\">Recunoaștere muzicală</string>\n    <string name=\"widget_recognizer_channel_desc\">Arată o notificare în timpul identificării unei melodii de pe widget</string>\n    <string name=\"widget_recognizer_notification_text\">Se înregistrează audio pentru identificarea melodiei…</string>\n    <string name=\"listen_together_auto_approval_suggestions\">Aprobă automat sugestiile pentru melodii</string>\n    <string name=\"listen_together_auto_approval_suggestions_desc\">Aprobă și adaugă la coadă în mod automat sugestiile pentru melodii de la invitați</string>\n    <string name=\"importing_playlist\">Importă playlist</string>\n    <string name=\"randomize_home_order\">Ordine aleatorie pe ecranul de pornire</string>\n    <string name=\"randomize_home_order_desc\">Ordonează aleatoriu secțiunile de pe ecranul de pornire cu priorități ponderate</string>\n    <string name=\"daily_discover_because_you_listen_to\">Pentru că asculți %1$s</string>\n    <string name=\"daily_discover_similar_to\">Similar cu %1$s</string>\n    <string name=\"daily_discover_based_on\">Bazate pe %1$s</string>\n    <string name=\"daily_discover_for_fans_of\">Pentru fanii %1$s</string>\n    <string name=\"from_the_community\">De la comunitate</string>\n    <string name=\"logout_dialog_title\">Păstrezi datele din bibliotecă?</string>\n    <string name=\"logout_dialog_message\">Dorești să-ți păstrezi playlisturile și datele din bibliotecă? Melodiile descărcate vor fi păstrate indiferent de situație.</string>\n    <string name=\"logout_keep\">Păstrează</string>\n    <string name=\"logout_clear\">Elimină</string>\n    <string name=\"credits_lead_developer\">Dezvoltator principal</string>\n    <string name=\"credits_collaborator\">Colaborator</string>\n    <string name=\"credits_collaborators_section\">Colaboratori</string>\n    <string name=\"credits_license_name\">Licența Publică Generală GNU v3.0</string>\n    <string name=\"credits_license_desc\">Software liber, cu sursă deschisă. Poți să-l folosești, studiezi, distribui și să-l îmbunătățești.</string>\n    <string name=\"credits_discord\">Server de Discord</string>\n    <string name=\"credits_telegram\">Canal de Telegram</string>\n    <string name=\"credits_website\">Site web</string>\n    <string name=\"credits_instagram\">Instagram</string>\n    <string name=\"credits_github\">GitHub</string>\n    <string name=\"credits_view_repo\">Vezi depozitul</string>\n    <string name=\"app_version_info\">%1$s • %2$s</string>\n    <string name=\"like_what_i_do\">Îți place ceea ce fac?</string>\n    <string name=\"buy_mo_a_coffee\">Cumpără-mi o cafea</string>\n    <string name=\"community_and_info\">Comunitate și informații</string>\n    <string name=\"metrolist\">METROLIST</string>\n    <string name=\"wanna_play_favorite_song\">Vrei să redai melodia lui/ei preferată?</string>\n    <string name=\"yeah\">Da</string>\n    <string name=\"stands_with_palestine\">Acest proiect ține cu Palestina 🇵🇸</string>\n    <string name=\"filter_podcasts\">Podcasturi</string>\n    <string name=\"view_podcast\">Vezi podcastul</string>\n    <string name=\"podcast_channels\">Canale de podcasturi</string>\n    <string name=\"latest_episodes\">Cele mai recente episoade</string>\n    <string name=\"your_shows\">Emisiunile tale</string>\n    <string name=\"new_episodes\">Episoade noi</string>\n    <string name=\"episodes_for_later\">Episoade pentru mai târziu</string>\n    <string name=\"save_episode_for_later\">Salvează pentru mai târziu</string>\n    <string name=\"save_episode_for_later_desc\">Adaugă la playlistul Episoade pentru mai târziu</string>\n    <string name=\"remove_episode_from_saved\">Eliminată din salvate</string>\n    <string name=\"subscribe_to_podcast\">Salvează podcastul în bibliotecă</string>\n    <plurals name=\"n_episode\">\n        <item quantity=\"one\">Un episod</item>\n        <item quantity=\"few\">%d episoade</item>\n        <item quantity=\"other\">%d de episoade</item>\n    </plurals>\n    <string name=\"filter_episodes\">Episoade</string>\n    <string name=\"filter_channels\">Canale</string>\n    <string name=\"auto_playlist\">Playlist automat</string>\n    <string name=\"downloaded_episodes\">Episoade descărcate</string>\n    <string name=\"no_subscribed_channels\">Nu te-ai abonat la niciun canal</string>\n    <string name=\"no_downloaded_episodes\">Niciun episod descărcat</string>\n    <plurals name=\"n_channel\">\n        <item quantity=\"one\">Un canal</item>\n        <item quantity=\"few\">%d canale</item>\n        <item quantity=\"other\">%d de canale</item>\n    </plurals>\n    <string name=\"restore_confirm_title\">Restaurezi backupul?</string>\n    <string name=\"restore_confirm_message\">Acest proces îți va restaura datele aplicației din backup.</string>\n    <string name=\"restore_account_warning\">Trebuie să te autentifici din nou după restaurare. Următorul cont va fi deconectat:</string>\n    <string name=\"restore\">Restaurează</string>\n    <string name=\"checking_previous_account\">Se verifică pentru conturi anterioare…</string>\n    <string name=\"no_account_found\">Niciun cont găsit</string>\n    <string name=\"found_in_settings_content\">Poate fi găsită în Setări &gt; Conținut</string>\n    <string name=\"speed_dial\">Acces rapid</string>\n    <string name=\"pin_to_speed_dial\">Fixează la Acces rapid</string>\n    <string name=\"unpin_from_speed_dial\">Anulează fixarea la Acces rapid</string>\n    <string name=\"daily_discover_sounds_like\">Sună ca %1$s</string>\n    <string name=\"enable_automatic_sleeptimer\">Activează temporizatorul de somn automat</string>\n    <string name=\"sleeptimer_description\">Activează temporizatorul de somn în mod automat cu valoarea prestabilită setată la o perioadă de timp personalizată</string>\n    <string name=\"sleep_timer_repeat_description\">Setează o zi și o oră personalizată la care temporizatorul de somn ar trebui să se activeze automat</string>\n    <string name=\"sleep_timer_repeat\">Repetă</string>\n    <string name=\"sleep_timer_daily\">Zilnic</string>\n    <string name=\"sleep_timer_weekdays\">De luni până vineri</string>\n    <string name=\"sleep_timer_weekdays_weekends\">Zile lucrătoare / Weekenduri</string>\n    <string name=\"sleep_timer_weekends\">Weekenduri (sâmbătă-duminică)</string>\n    <string name=\"sleep_timer_custom\">Personalizat</string>\n    <string name=\"sleep_timer_start_time\">Ora de început</string>\n    <string name=\"sleep_timer_end_time\">Ora de final</string>\n    <string name=\"sleep_timer_monday\">Luni</string>\n    <string name=\"sleep_timer_tuesday\">Marți</string>\n    <string name=\"sleep_timer_wednesday\">Miercuri</string>\n    <string name=\"sleep_timer_thursday\">Joi</string>\n    <string name=\"sleep_timer_friday\">Vineri</string>\n    <string name=\"sleep_timer_saturday\">Sâmbătă</string>\n    <string name=\"sleep_timer_sunday\">Duminică</string>\n    <string name=\"sleep_timer_stop_after_current_song\">Stop la sfârșitul melodiei curente atunci când temporizatorul se oprește</string>\n    <string name=\"sleep_timer_fade_out\">Estompare la minutul final</string>\n    <string name=\"view_channel\">Vezi canalul</string>\n    <string name=\"filter_profiles\">Profiluri</string>\n    <string name=\"upload_songs\">Încarcă melodii</string>\n    <string name=\"uploading\">Se încarcă…</string>\n    <string name=\"upload_progress\">%1$d din %2$d</string>\n    <string name=\"upload_complete\">Încărcare finalizată</string>\n    <string name=\"upload_failed\">Încărcare eșuată</string>\n    <string name=\"upload_file_too_large\">Fișier prea mare (maxim 300 MB)</string>\n    <string name=\"upload_unsupported_format\">Format neacceptat. Folosește mp3, m4a, wma, flac sau ogg</string>\n    <string name=\"delete_uploaded_song\">Șterge melodia încărcată</string>\n    <string name=\"delete_uploaded_song_confirm\">Ești sigur(ă) că vrei să ștergi această melodie încărcată? Această acțiune nu poate fi anulată.</string>\n    <string name=\"delete_uploaded_song_success\">Melodia încărcată a fost ștearsă</string>\n    <string name=\"delete_uploaded_song_failed\">Nu s-a putut șterge melodia încărcată</string>\n    <string name=\"delete_uploaded_songs\">Șterge melodiile încărcate</string>\n    <string name=\"delete_uploaded_songs_confirm\">Ești sigur(ă) că vrei să ștergi %1$d (de) melodii încărcate? Această acțiune nu poate fi anulată.</string>\n    <string name=\"deleted_n_songs\">S-au șters %1$d (de) melodii</string>\n    <string name=\"deleting\">Șe șterge…</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-ro/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Acasă</string>\n    <string name=\"songs\">Melodii</string>\n    <string name=\"artists\">Artiști</string>\n    <string name=\"albums\">Albume</string>\n    <string name=\"playlists\">Playlisturi</string>\n    <string name=\"history\">Istoric</string>\n    <string name=\"stats\">Statistici</string>\n    <string name=\"mood_and_genres\">Stare de spirit și genuri</string>\n    <string name=\"account\">Cont</string>\n    <string name=\"quick_picks\">Alegeri rapide</string>\n    <string name=\"quick_picks_empty\">Ascultă melodii pentru a-ți genera alegerile rapide</string>\n    <string name=\"forgotten_favorites\">Preferințe uitate</string>\n    <string name=\"keep_listening\">Continuă să asculți</string>\n    <string name=\"your_youtube_playlists\">Playlisturile tale de pe YouTube</string>\n    <string name=\"similar_to\">Similar cu</string>\n    <string name=\"new_release_albums\">Albume noi</string>\n    <string name=\"today\">Astăzi</string>\n    <string name=\"yesterday\">Ieri</string>\n    <string name=\"this_week\">Săptămâna aceasta</string>\n    <string name=\"last_week\">Săptămâna trecută</string>\n    <string name=\"most_played_songs\">Cele mai redate melodii</string>\n    <string name=\"most_played_artists\">Cei mai redați artiști</string>\n    <string name=\"most_played_albums\">Cele mai redate albume</string>\n    <string name=\"search\">Caută</string>\n    <string name=\"search_yt_music\">Caută pe YouTube Music…</string>\n    <string name=\"search_library\">Caută în bibliotecă…</string>\n    <string name=\"filter_library\">Bibliotecă</string>\n    <string name=\"filter_liked\">Apreciate</string>\n    <string name=\"filter_downloaded\">Descărcate</string>\n    <string name=\"filter_all\">Toate</string>\n    <string name=\"filter_songs\">Melodii</string>\n    <string name=\"filter_videos\">Videoclipuri</string>\n    <string name=\"filter_albums\">Albume</string>\n    <string name=\"filter_artists\">Artiști</string>\n    <string name=\"filter_playlists\">Playlisturi</string>\n    <string name=\"filter_community_playlists\">Playlisturi comunitare</string>\n    <string name=\"filter_featured_playlists\">Playlisturi promovate</string>\n    <string name=\"filter_bookmarked\">Marcate</string>\n    <string name=\"no_results_found\">Niciun rezultat găsit</string>\n    <string name=\"library_song_empty\">Melodiile din bibliotecă vor apărea aici</string>\n    <string name=\"library_artist_empty\">Artiștii din bibliotecă vor apărea aici</string>\n    <string name=\"library_album_empty\">Albumele din bibliotecă vor apărea aici</string>\n    <string name=\"library_playlist_empty\">Playlisturile tale vor apărea aici</string>\n    <string name=\"from_your_library\">Din biblioteca ta</string>\n    <string name=\"other_versions\">Alte versiuni</string>\n    <string name=\"liked_songs\">Melodii apreciate</string>\n    <string name=\"downloaded_songs\">Melodii descărcate</string>\n    <string name=\"playlist_is_empty\">Playlistul este gol</string>\n    <string name=\"delete_playlist_confirm\">Sigur vrei să ștergi playlistul \\\"%s\\\"?</string>\n    <string name=\"retry\">Reîncearcă</string>\n    <string name=\"radio\">Radio</string>\n    <string name=\"shuffle\">Amestecă</string>\n    <string name=\"reset\">Resetează</string>\n    <string name=\"details\">Detalii</string>\n    <string name=\"edit\">Editează</string>\n    <string name=\"start_radio\">Pornește radioul</string>\n    <string name=\"play\">Redă</string>\n    <string name=\"play_next\">Redă următoarea</string>\n    <string name=\"add_to_queue\">Adaugă la coadă</string>\n    <string name=\"add_to_library\">Adaugă în bibliotecă</string>\n    <string name=\"add_all_to_library\">Adaugă toate în bibliotecă</string>\n    <string name=\"remove_from_library\">Elimină din bibliotecă</string>\n    <string name=\"remove_all_from_library\">Elimină toate din bibliotecă</string>\n    <string name=\"action_download\">Descarcă</string>\n    <string name=\"downloading\">Se descarcă</string>\n    <string name=\"remove_download\">Elimină descărcarea</string>\n    <string name=\"import_playlist\">Importă playlistul</string>\n    <string name=\"add_to_playlist\">Adaugă în playlist</string>\n    <string name=\"view_artist\">Vezi artistul</string>\n    <string name=\"view_album\">Vezi albumul</string>\n    <string name=\"share\">Distribuie</string>\n    <string name=\"delete\">Șterge</string>\n    <string name=\"remove_from_history\">Elimină din istoric</string>\n    <string name=\"remove_from_playlist\">Elimină din playlist</string>\n    <string name=\"remove_from_queue\">Elimină din coadă</string>\n    <string name=\"search_online\">Caută online</string>\n    <string name=\"action_sync\">Sincronizează</string>\n    <string name=\"advanced\">Avansat</string>\n    <string name=\"tempo_and_pitch\">Tempo și tonalitate</string>\n    <string name=\"sort_by_create_date\">Data adăugării</string>\n    <string name=\"sort_by_name\">Nume</string>\n    <string name=\"sort_by_artist\">Artist</string>\n    <string name=\"sort_by_year\">An</string>\n    <string name=\"sort_by_song_count\">Numărul melodiilor</string>\n    <string name=\"sort_by_length\">Lungime</string>\n    <string name=\"sort_by_play_time\">Timpul redării</string>\n    <string name=\"sort_by_custom\">Ordine personalizată</string>\n    <string name=\"media_id\">ID media</string>\n    <string name=\"mime_type\">MIME type</string>\n    <string name=\"codecs\">Codecuri</string>\n    <string name=\"bitrate\">Rată de biți</string>\n    <string name=\"sample_rate\">Rată de eșantionare</string>\n    <string name=\"loudness\">Intensitate sonoră</string>\n    <string name=\"volume\">Volum</string>\n    <string name=\"file_size\">Dimensiune fișier</string>\n    <string name=\"unknown\">Necunoscut</string>\n    <string name=\"copied\">Copiat în clipboard</string>\n    <string name=\"edit_lyrics\">Editează versurile</string>\n    <string name=\"search_lyrics\">Caută versuri</string>\n    <string name=\"edit_song\">Editează melodia</string>\n    <string name=\"song_title\">Titlul melodiei</string>\n    <string name=\"song_artists\">Artiștii melodiei</string>\n    <string name=\"error_song_title_empty\">Numele melodiei nu poate fi gol.</string>\n    <string name=\"error_song_artist_empty\">Artistul melodiei nu poate fi gol.</string>\n    <string name=\"save\">Salvează</string>\n    <string name=\"choose_playlist\">Alege playlistul</string>\n    <string name=\"edit_playlist\">Editează playlistul</string>\n    <string name=\"create_playlist\">Creează playlist</string>\n    <string name=\"playlist_name\">Numele playlistului</string>\n    <string name=\"error_playlist_name_empty\">Numele playlistului nu poate fi gol.</string>\n    <string name=\"edit_artist\">Editează artistul</string>\n    <string name=\"artist_name\">Numele artistului</string>\n    <string name=\"error_artist_name_empty\">Numele artistului nu poate fi gol.</string>\n    <string name=\"duplicates\">Duplicate</string>\n    <string name=\"skip_duplicates\">Omite duplicatele</string>\n    <string name=\"add_anyway\">Adaugă oricum</string>\n    <string name=\"duplicates_description_single\">Melodia este deja în playlistul tău</string>\n    <string name=\"duplicates_description_multiple\">%d (de) melodii sunt deja în playlistul tău</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d melodie</item>\n        <item quantity=\"few\">%d melodii</item>\n        <item quantity=\"other\">%d de melodii</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d artist</item>\n        <item quantity=\"few\">%d artiști</item>\n        <item quantity=\"other\">%d de artiști</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d album</item>\n        <item quantity=\"few\">%d albume</item>\n        <item quantity=\"other\">%d de albume</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d playlist</item>\n        <item quantity=\"few\">%d playlisturi</item>\n        <item quantity=\"other\">%d de playlisturi</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d săptămână</item>\n        <item quantity=\"few\">%d săptămâni</item>\n        <item quantity=\"other\">%d de săptămâni</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d lună</item>\n        <item quantity=\"few\">%d luni</item>\n        <item quantity=\"other\">%d de luni</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d an</item>\n        <item quantity=\"few\">%d ani</item>\n        <item quantity=\"other\">%d de ani</item>\n    </plurals>\n    <string name=\"playlist_imported\">Playlist importat</string>\n    <string name=\"removed_song_from_playlist\">S-a eliminat \\\"%s\\\" din playlist</string>\n    <string name=\"playlist_synced\">Playlist sincronizat</string>\n    <string name=\"undo\">Anulează</string>\n    <string name=\"lyrics_not_found\">Nu s-au găsit versuri</string>\n    <string name=\"sleep_timer\">Temporizator de somn</string>\n    <string name=\"end_of_song\">Sfârșitul melodiei</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">Un minut</item>\n        <item quantity=\"few\">%d minute</item>\n        <item quantity=\"other\">%d de minute</item>\n    </plurals>\n    <string name=\"error_no_stream\">Niciun stream disponibil</string>\n    <string name=\"error_no_internet\">Fără conexiune la rețea</string>\n    <string name=\"error_timeout\">Timeout</string>\n    <string name=\"error_unknown\">Eroare necunoscută</string>\n    <string name=\"action_like\">Apreciază</string>\n    <string name=\"action_like_all\">Apreciază tot</string>\n    <string name=\"action_remove_like\">Elimină aprecierea</string>\n    <string name=\"action_remove_like_all\">Elimină toate aprecierile</string>\n    <string name=\"action_shuffle_on\">Amestecare pornită</string>\n    <string name=\"action_shuffle_off\">Amestecare oprită</string>\n    <string name=\"repeat_mode_off\">Mod repetare oprit</string>\n    <string name=\"repeat_mode_one\">Repetă melodia curentă</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d selectat</item>\n        <item quantity=\"few\">%d selectate</item>\n        <item quantity=\"other\">%d selectate</item>\n    </plurals>\n    <string name=\"repeat_mode_all\">Repetă coada</string>\n    <string name=\"queue_all_songs\">Toate melodiile</string>\n    <string name=\"queue_searched_songs\">Melodii căutate</string>\n    <string name=\"music_player\">Player de muzică</string>\n    <string name=\"settings\">Setări</string>\n    <string name=\"appearance\">Aspect</string>\n    <string name=\"theme\">Temă</string>\n    <string name=\"enable_dynamic_theme\">Activează tema dinamică</string>\n    <string name=\"dark_theme\">Temă întunecată</string>\n    <string name=\"dark_theme_on\">Pornită</string>\n    <string name=\"dark_theme_off\">Oprită</string>\n    <string name=\"dark_theme_follow_system\">Urmează sistemul</string>\n    <string name=\"pure_black\">Negru pur</string>\n    <string name=\"customize_navigation_tabs\">Personalizează filele de navigare</string>\n    <string name=\"player\">Player</string>\n    <string name=\"player_text_alignment\">Alinierea textului din player</string>\n    <string name=\"lyrics_text_position\">Poziția textului versurilor</string>\n    <string name=\"left\">Stânga</string>\n    <string name=\"center\">Centru</string>\n    <string name=\"right\">Dreapta</string>\n    <string name=\"player_slider_style\">Stilul slider-ului player-ului</string>\n    <string name=\"default_\">Implicit</string>\n    <string name=\"misc\">Diverse</string>\n    <string name=\"default_open_tab\">Fila prestabilită deschisă</string>\n    <string name=\"grid_cell_size\">Mărimea celulelor grilei</string>\n    <string name=\"small\">Mică</string>\n    <string name=\"big\">Mare</string>\n    <string name=\"content\">Conținut</string>\n    <string name=\"action_logout\">Deconectează-te</string>\n    <string name=\"action_login\">Autentifică-te</string>\n    <string name=\"login\">Autentificare</string>\n    <string name=\"not_logged_in\">Nu ești autentificat</string>\n    <string name=\"login_failed\">Autentificare eșuată</string>\n    <string name=\"content_language\">Limba prestabilită a conținutului</string>\n    <string name=\"content_country\">Țara prestabilită a conținutului</string>\n    <string name=\"system_default\">Prestabilită de sistem</string>\n    <string name=\"enable_proxy\">Activează proxy</string>\n    <string name=\"proxy_type\">Tip proxy</string>\n    <string name=\"proxy_url\">URL proxy</string>\n    <string name=\"restart_to_take_effect\">Repornește pentru a lua efect</string>\n    <string name=\"player_and_audio\">Player și audio</string>\n    <string name=\"audio_quality\">Calitate audio</string>\n    <string name=\"audio_quality_auto\">Automată</string>\n    <string name=\"audio_quality_high\">Înaltă</string>\n    <string name=\"audio_quality_low\">Scăzută</string>\n    <string name=\"queue\">Coadă</string>\n    <string name=\"persistent_queue\">Coadă persistentă</string>\n    <string name=\"persistent_queue_desc\">Restaurează ultima ta coadă atunci când aplicația pornește</string>\n    <string name=\"auto_load_more\">Încarcă automat mai multe melodii</string>\n    <string name=\"auto_load_more_desc\">Adaugă în mod automat mai multe melodii când ajungi la finalul cozii, dacă este posibil</string>\n    <string name=\"skip_silence\">Omite liniștea</string>\n    <string name=\"audio_normalization\">Normalizare audio</string>\n    <string name=\"auto_skip_next_on_error\">Sari automat la următoarea melodie atunci când apare o eroare</string>\n    <string name=\"auto_skip_next_on_error_desc\">Asigură-ți experiența continuă de redare</string>\n    <string name=\"stop_music_on_task_clear\">Oprește muzica la terminarea sarcinii</string>\n    <string name=\"equalizer\">Egalizator</string>\n    <string name=\"storage\">Stocare</string>\n    <string name=\"cache\">Cache</string>\n    <string name=\"image_cache\">Cache imagini</string>\n    <string name=\"song_cache\">Cache melodii</string>\n    <string name=\"max_cache_size\">Dimensiunea maximă a cache-ului</string>\n    <string name=\"unlimited\">Nelimitată</string>\n    <string name=\"clear_all_downloads\">Elimină toate descărcările</string>\n    <string name=\"max_image_cache_size\">Dimensiunea maximă a cache-ului imaginilor</string>\n    <string name=\"clear_image_cache\">Șterge cache-ul imaginilor</string>\n    <string name=\"max_song_cache_size\">Dimensiunea maximă pentru cache-ul melodiilor</string>\n    <string name=\"clear_song_cache\">Șterge cache-ul melodiilor</string>\n    <string name=\"size_used\">%s utilizat</string>\n    <string name=\"privacy\">Confidențialitate</string>\n    <string name=\"listen_history\">Istoric ascultări</string>\n    <string name=\"pause_listen_history\">Pune pe pauză istoricul ascultărilor</string>\n    <string name=\"clear_listen_history\">Șterge istoricul ascultărilor</string>\n    <string name=\"clear_listen_history_confirm\">Sigur vrei să ștergi tot istoricul ascultărilor?</string>\n    <string name=\"search_history\">Istoric căutări</string>\n    <string name=\"pause_search_history\">Pune pe pauză istoricul căutărilor</string>\n    <string name=\"clear_search_history\">Șterge istoricul căutărilor</string>\n    <string name=\"clear_search_history_confirm\">Sigur vrei să ștergi tot istoricul căutărilor?</string>\n    <string name=\"use_login_for_browse\">Folosește autentificarea pentru a răsfoi conținut</string>\n    <string name=\"use_login_for_browse_desc\">Această setare poate influența ce conținut vezi, de exemplu poate arăta albume doar pentru utilizatorii Premium dacă ești autentificat cu un cont Premium</string>\n    <string name=\"disable_screenshot\">Dezactivează capturile de ecran</string>\n    <string name=\"disable_screenshot_desc\">Când această opțiune este pornită, capturile de ecran sunt dezactivate și aplicația nu poate fi văzută în Recente.</string>\n    <string name=\"enable_lrclib\">Activează furnizorul de versuri LrcLib</string>\n    <string name=\"enable_kugou\">Activează furnizorul de versuri KuGou</string>\n    <string name=\"hide_explicit\">Ascunde conținutul explicit</string>\n    <string name=\"backup_restore\">Backup și restaurare</string>\n    <string name=\"action_backup\">Fă backup</string>\n    <string name=\"action_restore\">Restaurează</string>\n    <string name=\"imported_playlist\">Playlist importat</string>\n    <string name=\"backup_create_success\">Backup creat cu succes</string>\n    <string name=\"backup_create_failed\">Nu s-a putut creea backup-ul</string>\n    <string name=\"restore_failed\">Nu s-a putut restaura backup-ul</string>\n    <string name=\"discord_integration\">Integrare cu Discord</string>\n    <string name=\"discord_information\">Metrolist folosește biblioteca KizzyRPC pentru a-ți seta statusul contului tău de Discord. Acest lucru presupune folosirea conexiunii Discord Gateway, ceea ce ar putea fi considerată o încălcare a Termenilor serviciului Discord. Însă, nu există cazuri cunoscute de conturi de utilizator care au fost suspendate din acest motiv. Folosește pe propriul tău risc.\\n\\nMetrolist îți va extrage doar token-ul. Orice altceva este stocat local.</string>\n    <string name=\"dismiss\">Închide</string>\n    <string name=\"options\">Opțiuni</string>\n    <string name=\"preview\">Previzualizează</string>\n    <string name=\"enable_discord_rpc\">Activează Rich Presence</string>\n    <string name=\"about\">Despre</string>\n    <string name=\"app_version\">Versiunea aplicației</string>\n    <string name=\"new_version_available\">Versiune nouă disponibilă</string>\n    <string name=\"translation_models\">Modele de traducere</string>\n    <string name=\"clear_translation_models\">Elimină modelele de traducere</string>\n    <string name=\"squiggly\">Ondulat</string>\n    <string name=\"refetch\">Reîncarcă</string>\n    <string name=\"sided\">Într-o parte</string>\n    <string name=\"remove_download_playlist_confirm\">Sigur vrei să ștergi toate cele %s (de) melodii din playlist din Melodii descărcate?</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-ru/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"back_button_desc\">Назад</string>\n    <string name=\"please_wait\">Пожалуйста, подождите</string>\n    <string name=\"remove_from_cache\">Удалить из кэша</string>\n    <string name=\"select\">Выбрать всё</string>\n    <string name=\"link_copied\">Ссылка скопирована в буфер обмена</string>\n    <string name=\"lyrics\">Текст</string>\n    <string name=\"already_in_playlist\">Уже в плейлисте:</string>\n    <string name=\"similar_content\">Похожий контент</string>\n    <string name=\"player_background_style\">Стиль фона плеера</string>\n    <string name=\"follow_theme\">Следовать теме</string>\n    <string name=\"gradient\">Градиент</string>\n    <string name=\"player_buttons_style\">Цветовой стиль кнопок плеера</string>\n    <string name=\"swipe_song_to_add\">Свайп песни влево - добавить её в очередь, свайп песни вправо - воспроизвести её следующей</string>\n    <string name=\"slim\">Тонкий</string>\n    <string name=\"token_hidden\">Нажмите, чтобы показать токен</string>\n    <string name=\"token_shown\">Нажмите еще раз, чтобы скопировать или изменить</string>\n    <string name=\"proxy\">Прокси</string>\n    <string name=\"last_song_listened\">Основано на последнем прослушанном треке</string>\n    <string name=\"app_language\">Язык приложения</string>\n    <string name=\"enable_similar_content\">Включить похожий контент</string>\n    <string name=\"similar_content_desc\">Автоматически добавлять больше похожих треков при достижении конца очереди</string>\n    <string name=\"views\">Просмотры</string>\n    <string name=\"enable_swipe_thumbnail\">Включить переключение треков свайпом</string>\n    <string name=\"copy_link\">Скопировать ссылку</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">%d секунда</item>\n        <item quantity=\"few\">%d секунды</item>\n        <item quantity=\"many\">%d секунд</item>\n        <item quantity=\"other\">%d секунд</item>\n    </plurals>\n    <string name=\"allows_for_sync_witch_youtube\">Примечание: Это позволит синхронизировать плейлист с YouTube Music. Этот параметр НЕЛЬЗЯ изменить позже.</string>\n    <string name=\"player_background_blur\">Размытие</string>\n    <string name=\"slim_navbar\">Тонкая панель навигации</string>\n    <string name=\"dislikes\">Дизлайки</string>\n    <string name=\"token_adv_login_description\">Это ПРОДВИНУТЫЙ способ входа. Вместо использования веб-портала вы можете ввести или обновить токен для входа здесь. Это может быть полезно для быстрого входа на нескольких устройствах. Учтите, что приложение не примет токены с неверным форматом</string>\n    <string name=\"likes\">Лайки</string>\n    <string name=\"charts\">Чарты</string>\n    <string name=\"album_cover_desc\">Обложка альбома</string>\n    <string name=\"top_music_videos\">Лучшие музыкальные клипы</string>\n    <string name=\"weeks\">Недели</string>\n    <string name=\"months\">Месяцы</string>\n    <string name=\"years\">Годы</string>\n    <string name=\"liked\">Понравившиеся</string>\n    <string name=\"offline\">Скачанные</string>\n    <string name=\"my_top\">Мой топ</string>\n    <string name=\"cached_playlist\">Кэшированные</string>\n    <string name=\"sync_playlist\">Синхронизировать плейлист</string>\n    <string name=\"sync_disabled\">Синхронизация выключена</string>\n    <string name=\"cancel\">Отмена</string>\n    <string name=\"share_lyrics\">Поделиться текстом</string>\n    <string name=\"trending\">Тренды</string>\n    <string name=\"generating_image\">Генерация изображения</string>\n    <string name=\"local_history\">Локальная</string>\n    <string name=\"remote_history\">Удалённая</string>\n    <string name=\"text_color\">Цвет текста</string>\n    <string name=\"background_color\">Цвет фона</string>\n    <string name=\"continuous\">Непрерывно</string>\n    <string name=\"share_as_text\">Поделиться как текст</string>\n    <string name=\"share_as_image\">Поделиться как изображение</string>\n    <string name=\"max_selection_limit\">Максимальный лимит выделения</string>\n    <string name=\"share_selected\">Поделиться выделенным</string>\n    <string name=\"customize_colors\">Настроить цвета</string>\n    <string name=\"secondary_text_color\">Цвет вторичного текста</string>\n    <string name=\"like_all\">Нравится всё</string>\n    <string name=\"dislike_all\">Не нравится всё</string>\n    <string name=\"sort_by_last_updated\">Дата обновления</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d раз</item>\n        <item quantity=\"few\">%d раза</item>\n        <item quantity=\"many\">%d раз</item>\n        <item quantity=\"other\">%d раз</item>\n    </plurals>\n    <string name=\"new_player_design\">Новый дизайн плеера</string>\n    <string name=\"default_style\">По умолчанию</string>\n    <string name=\"lyrics_click_change\">Изменение текста песни по клику</string>\n    <string name=\"lyrics_auto_scroll\">Автопрокрутка текстов песен</string>\n    <string name=\"lyrics_romanize_japanese\">Романизировать тексты на японском</string>\n    <string name=\"lyrics_romanize_korean\">Романизировать тексты на корейском</string>\n    <string name=\"auto_playlists\">Автоплейлисты</string>\n    <string name=\"show_liked_playlist\">Показать плейлист \\\"Понравившиеся\\\"</string>\n    <string name=\"show_downloaded_playlist\">Показать плейлист \\\"Скачанные\\\"</string>\n    <string name=\"show_top_playlist\">Показать плейлист \\\"Топ\\\"</string>\n    <string name=\"show_cached_playlist\">Показать плейлист \\\"Кэшированные\\\"</string>\n    <string name=\"advanced_login\">Вход в систему с помощью токена</string>\n    <string name=\"yt_sync\">Автосинхронизация с аккаунтом</string>\n    <string name=\"more_content\">Доп. контент</string>\n    <string name=\"general\">Общие</string>\n    <string name=\"default_lib_chips\">Изменение чипа библиотеки (по умолчанию)</string>\n    <string name=\"set_quick_picks\">Установить быстрый выбор</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"import_online\">Импортировать плейлисты \\\"m3u\\\"</string>\n    <string name=\"import_csv\">Импортировать плейлисты csv</string>\n    <string name=\"playlist_add_local_to_synced_note\">Примечание: добавление локальных композиций в синхронизированные/удаленные плейлисты не поддерживается. Любая другая комбинация допустима</string>\n    <string name=\"auto_download_on_like\">Автоскачивание по понравившимся</string>\n    <string name=\"auto_download_on_like_desc\">Автоматически скачивать треки, отмечаемые как понравившиеся</string>\n    <string name=\"clear_song_cache_dialog\">Вы уверены, что хотите очистить все закэшированные треки?</string>\n    <string name=\"clear_downloads_dialog\">Вы уверены, что хотите очистить все скачанные?</string>\n    <string name=\"not_logged_in_youtube\">Вы не вошли в YouTube</string>\n    <string name=\"default_links\">Открыть поддерживаемые ссылки</string>\n    <string name=\"open_app_settings_error\">Невозможно открыть настройки приложения</string>\n    <string name=\"release_notes\">Примечания к выпуску</string>\n    <string name=\"all_time\">Всё время</string>\n    <string name=\"past_24_hours\">Последние 24 часа</string>\n    <string name=\"past_week\">Прошедшую неделю</string>\n    <string name=\"past_month\">Последний месяц</string>\n    <string name=\"past_year\">Прошлый год</string>\n    <string name=\"top_length\">Длина моего топ-листа</string>\n    <string name=\"history_duration\">Длительность истории</string>\n    <string name=\"information\">Информация</string>\n    <string name=\"description\">Описание</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"disable\">Отключить</string>\n    <string name=\"subscribe\">Подписаться</string>\n    <string name=\"subscribed\">Вы подписаны</string>\n    <string name=\"clear_image_cache_dialog\">Вы уверены, что хотите очистить все закэшированные изображения?</string>\n    <string name=\"swipe_sensitivity\">Чувствительность свайпа мини-плеера</string>\n    <string name=\"new_mini_player_design\">Новый дизайн мини‑плеера</string>\n    <string name=\"now_playing\">Сейчас играет</string>\n    <string name=\"close\">Закрыть</string>\n    <string name=\"hide_player_thumbnail\">Скрыть миниатюру проигрывателя</string>\n    <string name=\"hide_player_thumbnail_desc\">Заменить в проигрывателе обложку альбома на значок приложения</string>\n    <string name=\"seek_forward_dynamic\">+%1$d секунд вперёд</string>\n    <string name=\"seek_backward_dynamic\">-%1$d секунд назад</string>\n    <string name=\"seek_seconds_addup\">Накопительная перемотка</string>\n    <string name=\"seek_seconds_addup_description\">Если включено, при каждом пропуске перемотки добавляется ещё 5 секунд</string>\n    <string name=\"disable_load_more_when_repeat_all\">Отключить автозагрузку при повторе всех треков</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Отключить автоматическую подгрузку песен и похожего контента при включённом режиме повтора всех треков</string>\n    <string name=\"settings_section_ui\">Интерфейс</string>\n    <string name=\"settings_section_privacy\">Конфиденциальность и безопасность</string>\n    <string name=\"settings_section_player_content\">Плеер и контент</string>\n    <string name=\"settings_section_storage\">Хранилище и данные</string>\n    <string name=\"settings_section_system\">Система и сведения</string>\n    <string name=\"starting_radio\">Запуск радио</string>\n    <string name=\"config_proxy\">Настроить прокси</string>\n    <string name=\"proxy_username\">Имя пользователя прокси</string>\n    <string name=\"proxy_password\">Пароль прокси</string>\n    <string name=\"enable_authentication\">Включить аутентификацию</string>\n    <string name=\"lyrics_romanization_cyrillic\">Кириллица</string>\n    <string name=\"lyrics_romanize_title\">Романизация</string>\n    <string name=\"lyrics_romanization\">Романизация текста</string>\n    <string name=\"lyrics_romanize_russian\">Романизировать тексты на русском</string>\n    <string name=\"lyrics_romanize_ukrainian\">Романизировать тексты на украинском</string>\n    <string name=\"lyrics_romanize_belarusian\">Романизировать тексты на белорусском</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Романизировать тексты на киргизском</string>\n    <string name=\"lyrics_romanize_serbian\">Романизировать тексты на сербском</string>\n    <string name=\"lyrics_romanize_bulgarian\">Романизировать тексты на болгарском</string>\n    <string name=\"line_by_line_option_title\">ЭКСПЕРИМЕНТАЛЬНО: Определять язык построчно</string>\n    <string name=\"line_by_line_option_desc\">Текст на кириллице будет определяться построчно, а не для всей песни.</string>\n    <string name=\"line_by_line_dialog_title\">Вы уверены?</string>\n    <string name=\"line_by_line_dialog_desc\">Это экспериментальная функция, которая может работать не всегда.\\n\\nПо умолчанию язык определяется для всей песни, но с этой опцией он будет определяться построчно. Это позволит работать с многоязычными песнями, НО язык не всегда может быть правильным (например, если есть украинская строка, которая не содержит украинских букв, она может быть романизирована как русская).\\n\\nЕсли у вас нет проблем, рекомендуется оставить эту опцию выключенной.</string>\n    <string name=\"romanize_current_track\">Романизировать текущий трек</string>\n    <string name=\"edit_playlist_cover\">Редактировать обложку плейлиста</string>\n    <string name=\"edit_playlist_cover_note\">Примечание: чтобы изменить обложку плейлиста, ваш аккаунт должен быть привязан к номеру телефона и подтверждён в YouTube Music.</string>\n    <string name=\"edit_playlist_cover_note_wait\">После выбора изображения подождите немного, пока новая обложка появится в вашем плейлисте.</string>\n    <string name=\"remove_custom_image\">Удалить собственное изображение</string>\n    <string name=\"choose_from_library\">Выбрать из библиотеки</string>\n    <string name=\"audio_offload\">Включить аппаратное ускорение аудио</string>\n    <string name=\"audio_offload_description\">Использовать аппаратный путь воспроизведения аудио. Отключение этой функции может увеличить расход энергии, но может быть полезным, если у вас возникают проблемы с воспроизведением или постобработкой звука</string>\n    <string name=\"uploaded_playlist\">Загруженные</string>\n    <string name=\"filter_uploaded\">Загруженные</string>\n    <string name=\"show_uploaded_playlist\">Показать плейлист \\\"Загруженные\\\"</string>\n    <string name=\"lyrics_romanize_macedonian\">Романизировать тексты на македонском</string>\n    <string name=\"updater\">Обновления</string>\n    <string name=\"check_for_updates\">Автоматически проверять наличие обновлений</string>\n    <string name=\"update_notifications\">Включить уведомления об обновлениях</string>\n    <string name=\"update_available_title\">Доступно обновление</string>\n    <string name=\"update_channel_name\">Обновления приложения</string>\n    <string name=\"update_channel_desc\">Уведомления о новых версиях</string>\n    <string name=\"discord_use_details\">Использовать подробности вместо статуса</string>\n    <string name=\"discord_use_details_description\">Показывать в заголовке название трека вместо исполнителя</string>\n    <string name=\"integrations\">Интеграции</string>\n    <string name=\"username\">Имя пользователя</string>\n    <string name=\"password\">Пароль</string>\n    <string name=\"lastfm_integration\">Интеграция с Last.fm</string>\n    <string name=\"enable_scrobbling\">Включить скробблинг</string>\n    <string name=\"lastfm_now_playing\">Отправлять данные о текущем треке</string>\n    <string name=\"scrobbling_configuration\">Настройки скробблинга</string>\n    <string name=\"scrobble_min_track_duration\">Скробблить треки длиннее</string>\n    <string name=\"scrobble_delay_percent\">Задержка скробблинга (в процентах)</string>\n    <string name=\"scrobble_delay_minutes\">Задержка скробблинга (в минутах)</string>\n    <string name=\"swipe_song_to_remove\">Свайп по песне, чтобы удалить её из плейлиста</string>\n    <string name=\"last_fm_send_likes\">Отправлять лайки/дизлайки</string>\n    <string name=\"last_fm_send_likes_description\">Добавлять/убирать отметку «любимая» на Last.fm, когда ставится/снимается отметка «Нравится» в Metrolist</string>\n    <string name=\"lyrics_romanize_chinese\">Романизировать тексты на китайском</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"google_cast_description\">Включить трансляцию звука на Chromecast и другие устройства с поддержкой Cast</string>\n    <string name=\"hide_video_songs\">Скрыть видео</string>\n    <string name=\"primary_color_style\">Основной цвет</string>\n    <string name=\"auto_scroll\">Синхронизировать</string>\n    <string name=\"details_desc\">Просмотреть информацию о треке</string>\n    <string name=\"edit_desc\">Изменить название или исполнителя</string>\n    <string name=\"start_radio_desc\">Создать радиостанцию на основе этого элемента</string>\n    <string name=\"play_next_desc\">Добавить в начало очереди</string>\n    <string name=\"add_to_queue_desc\">Добавить в конец очереди</string>\n    <string name=\"add_to_library_desc\">Сохранить в вашу библиотеку</string>\n    <string name=\"download_desc\">Сделать доступным офлайн</string>\n    <string name=\"add_to_playlist_desc\">Добавить в один из ваших плейлистов</string>\n    <string name=\"refetch_desc\">Обновить метаданные из YouTube Music</string>\n    <string name=\"share_desc\">Поделиться ссылкой на этот элемент</string>\n    <string name=\"delete_desc\">Безвозвратно удалить этот элемент</string>\n    <string name=\"advanced_desc\">Изменить темп и высоту тона трека</string>\n    <string name=\"equalizer_desc\">Настроить эквалайзер</string>\n    <string name=\"enable_dynamic_icon\">Включить динамический значок</string>\n    <string name=\"mini_player\">Мини-плеер</string>\n    <string name=\"pure_black_mini_player\">Чисто чёрный мини-плеер</string>\n    <string name=\"cache_size_warning_title\">Внимание!</string>\n    <string name=\"cache_size_warning_message\">Вы выбрали лимит кэша меньше того, который приложение использует сейчас (%1$s). Если вы продолжите, часть данных %2$s может быть удалена, чтобы уложиться в новый лимит. Всё равно продолжить?</string>\n    <string name=\"cache_size_warning_confirm\">Продолжить</string>\n    <string name=\"tertiary_color_style\">Третичный цвет</string>\n    <string name=\"logging_in\">Выполняется вход…</string>\n    <string name=\"download_playlist_desc\">Скачать все треки для офлайн воспроизведения</string>\n    <string name=\"remove_download_playlist_desc\">Удалить все скачанные треки из этого плейлиста</string>\n    <string name=\"download_in_progress_desc\">Идёт скачивание</string>\n    <string name=\"share_playlist_desc\">Поделиться этим плейлистом с другими</string>\n    <string name=\"delete_playlist_desc\">Удалить этот плейлист навсегда</string>\n    <string name=\"sync_playlist_desc\">Синхронизировать плейлист с YouTube Music</string>\n    <string name=\"enable_better_lyrics\">Включить Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">Синхронизация текста по слогам для любой песни, для караоке</string>\n    <string name=\"lyrics_animation_style\">Построчный стиль анимации</string>\n    <string name=\"none\">Нет</string>\n    <string name=\"fade\">Затухание</string>\n    <string name=\"glow\">Свечение</string>\n    <string name=\"slide\">Скольжение</string>\n    <string name=\"karaoke\">Караоке</string>\n    <string name=\"apple_music_style\">Apple Music</string>\n    <string name=\"lyrics_text_size\">Размер текста песни</string>\n    <string name=\"lyrics_line_spacing\">Межстрочный интервал текста</string>\n    <string name=\"shuffle_playlist_first\">Перемешивать плейлист/альбом вначале</string>\n    <string name=\"shuffle_playlist_first_desc\">Во время перемешивания сначала проигрывать все треки из исходного плейлиста/альбома, а затем похожий контент</string>\n    <string name=\"lyrics_glow_effect\">Включить эффект свечения текста</string>\n    <string name=\"lyrics_glow_effect_desc\">Добавить анимацию свечения и эффект подпрыгивания для активных строк текста</string>\n    <string name=\"show_wrapped_card\">Показать карточку Wrapped</string>\n    <string name=\"album_art_for\">Обложка альбома для %s</string>\n    <string name=\"wrapped_total_albums_title\">Вы прослушали</string>\n    <string name=\"wrapped_total_albums_subtitle\">уникальных альбомов</string>\n    <string name=\"wrapped_top_album_title\">Ваш самый прослушиваемый альбом</string>\n    <string name=\"wrapped_playlist_ready\">Ваш персональный плейлист готов</string>\n    <string name=\"wrapped_top_5_albums_title\">Ваши топ-5 альбомов</string>\n    <string name=\"wrapped_album_listening_time\">Вы слушали этот альбом %d минут</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d минут</string>\n    <string name=\"wrapped_no_data\">Нет данных</string>\n    <string name=\"wrapped_top_5_artists_title\">Ваши топ-исполнители года</string>\n    <string name=\"wrapped_artist_listening_time\">%d минут</string>\n    <string name=\"wrapped_top_5_songs_title\">Ваши топ-треки года</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">Обложка альбома</string>\n    <string name=\"wrapped_top_artist_title\">Ваш главный исполнитель года</string>\n    <string name=\"wrapped_top_artist_image_content_description\">Изображение главного исполнителя</string>\n    <string name=\"wrapped_top_artist_listening_time\">Вы слушали их %d минут</string>\n    <string name=\"wrapped_top_song_title\">Ваш самый прослушиваемый трек</string>\n    <string name=\"wrapped_top_song_listening_time\">Вы слушали %d минут</string>\n    <string name=\"wrapped_total_artists_title\">Вы слушали</string>\n    <string name=\"wrapped_total_artists_subtitle\">уникальных исполнителей</string>\n    <string name=\"wrapped_total_songs_title\">Вы слушали</string>\n    <string name=\"wrapped_total_songs_subtitle\">уникальных треков</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_intro_subtitle\">пришло время узнать, что вы слушали</string>\n    <string name=\"wrapped_intro_button\">поехали!</string>\n    <string name=\"wrapped_logo_content_description\">Логотип Metrolist</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"wrapped_ready_title\">ВАШ WRAPPED ГОТОВ!</string>\n    <string name=\"wrapped_ready_subtitle\">Время узнать, что вам понравилось в этом году.</string>\n    <string name=\"wrapped_thank_you\">Спасибо за прослушивание</string>\n    <string name=\"wrapped_special_thanks\">Особая благодарность MO Agamy за создание Metrolist</string>\n    <string name=\"wrapped_close\">Закрыть Wrapped</string>\n    <string name=\"wrapped_playlist_title\">Ваш Wrapped %s</string>\n    <string name=\"wrapped_create_playlist\">Создать плейлист</string>\n    <string name=\"wrapped_playlist_saved\">Плейлист сохранён</string>\n    <string name=\"casting_to\">Трансляция на %s</string>\n    <string name=\"progress_percent\">Прогресс %s%%</string>\n    <string name=\"listening_to_metrolist\">Вы слушаете Metrolist</string>\n    <string name=\"open\">Открыть</string>\n    <string name=\"failed_to_create_image\">Не удалось создать изображение: %s</string>\n    <string name=\"copied_title\">Название скопировано</string>\n    <string name=\"copied_artist\">Исполнитель скопирован</string>\n    <string name=\"error_playing\">Ошибка воспроизведения</string>\n    <string name=\"failed_to_parse_proxy\">Не удалось разобрать url-адрес прокси.</string>\n    <string name=\"wavy\">Волнистый</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"one\">%d профиль</item>\n        <item quantity=\"few\">%d профиля</item>\n        <item quantity=\"many\">%d профилей</item>\n        <item quantity=\"other\">%d профилей</item>\n    </plurals>\n    <string name=\"equalizer_header\">Эквалайзер</string>\n    <string name=\"no_profiles\">Нет профилей эквалайзера</string>\n    <string name=\"import_profile\">Импортировать профиль</string>\n    <string name=\"eq_disabled\">Отключено</string>\n    <plurals name=\"band_count\">\n        <item quantity=\"one\">%d полоса</item>\n        <item quantity=\"few\">%d полосы</item>\n        <item quantity=\"many\">%d полос</item>\n        <item quantity=\"other\">%d полос</item>\n    </plurals>\n    <string name=\"delete_profile_desc\">Удалить профиль</string>\n    <string name=\"delete_profile_confirmation\">Вы уверены, что хотите удалить «%1$s»? Это действие нельзя отменить.</string>\n    <string name=\"error_file_read\">Не удалось прочитать файл</string>\n    <string name=\"error_file_open\">Не удалось открыть файл: %1$s</string>\n    <string name=\"import_error_title\">Ошибка импорта</string>\n    <string name=\"pause_music_when_media_is_muted\">Останавливать воспроизведение при отключении звука</string>\n    <string name=\"enable_simpmusic\">Включить SimpMusic Lyrics</string>\n    <string name=\"enable_simpmusic_desc\">Автоматически загружаемые тексты из Musixmatch и субтитров YouTube</string>\n    <string name=\"no_song_playing\">Сейчас ничего не играет</string>\n    <string name=\"album_art\">Обложка альбома</string>\n    <string name=\"tap_to_open\">Нажмите, чтобы открыть Metrolist</string>\n    <string name=\"previous\">Предыдущий</string>\n    <string name=\"play_pause\">Играть/Пауза</string>\n    <string name=\"next\">Следующий</string>\n    <string name=\"like\">Нравится</string>\n    <string name=\"widget_description\">Виджет музыкального плеера с элементами управления воспроизведением</string>\n    <string name=\"turntable_widget_description\">Круглый музыкальный виджет с кнопками воспроизведения и лайка</string>\n    <string name=\"system_equalizer\">Системный эквалайзер</string>\n    <string name=\"remember_shuffle_and_repeat\">Запоминать перемешивание и повтор</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Запоминать режим перемешивания и повтора при перезапуске приложения</string>\n    <string name=\"lyrics_offset\">Смещение текста песни</string>\n    <string name=\"skip_silence_desc\">Ускорять воспроизведение тихих участков треков</string>\n    <string name=\"skip_silence_instant\">Мгновенно пропускать тишину</string>\n    <string name=\"skip_silence_instant_desc\">Пропускать тихие фрагменты сразу вместо их ускорения</string>\n    <string name=\"about_artist\">О исполнителе</string>\n    <string name=\"show_more\">Показать больше</string>\n    <string name=\"show_less\">Показать меньше</string>\n    <string name=\"artist_page_settings\">Страница исполнителя</string>\n    <string name=\"show_artist_description\">Показывать описание исполнителя</string>\n    <string name=\"show_artist_subscriber_count\">Показывать количество подписчиков</string>\n    <string name=\"show_artist_monthly_listeners\">Показывать количество слушателей в месяц</string>\n    <string name=\"persistent_shuffle_title\">Постоянное перемешивание</string>\n    <string name=\"persistent_shuffle_desc\">Сохранять режим перемешивания при воспроизведении новых треков или плейлистов</string>\n    <string name=\"error_playback_failed\">Не удалось воспроизвести</string>\n    <string name=\"error_title\">Ошибка</string>\n    <string name=\"error_eq_apply_failed\">Не удалось применить профиль эквалайзера: %1$s</string>\n    <string name=\"crop_album_art\">Обрезать обложку альбома</string>\n    <string name=\"crop_album_art_desc\">Принудительно использовать квадратное соотношение сторон, обрезая миниатюры видео</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">Не выключать экран при развернутом плеере</string>\n    <string name=\"listen_together\">Совместное прослушивание</string>\n    <string name=\"listen_together_server_url\">Адрес сервера</string>\n    <string name=\"listen_together_username\">Имя пользователя</string>\n    <string name=\"listen_together_connected\">Подключено</string>\n    <string name=\"listen_together_reconnecting\">Переподключение…</string>\n    <string name=\"listen_together_disconnected\">Отключено</string>\n    <string name=\"listen_together_connecting\">Подключение…</string>\n    <string name=\"listen_together_error\">Ошибка подключения</string>\n    <string name=\"listen_together_create_room\">Создать комнату</string>\n    <string name=\"listen_together_create_room_desc\">Создайте комнату и поделитесь кодом с друзьями</string>\n    <string name=\"listen_together_join_room\">Присоединиться к комнате</string>\n    <string name=\"listen_together_room_code\">Код комнаты</string>\n    <string name=\"listen_together_you_are_host\">Вы — хост</string>\n    <string name=\"listen_together_you_are_guest\">Вы — гость</string>\n    <string name=\"listen_together_join_requests\">Запросы на вход</string>\n    <string name=\"listen_together_view_logs\">Посмотреть логи</string>\n    <string name=\"listen_together_view_logs_desc\">Отладка соединения и сообщений</string>\n    <string name=\"listen_together_logs\">Логи подключения</string>\n    <string name=\"listen_together_no_logs\">Логов пока нет</string>\n    <string name=\"listen_together_description\">Слушайте музыку с друзьями в реальном времени. Создайте комнату или присоединитесь к существующей по коду.</string>\n    <string name=\"listen_together_background_disconnect_note\">Примечание: возможно отключение, если вы создадите комнату без воспроизведения музыки и затем переключитесь в другое приложение.</string>\n    <string name=\"listen_together_not_configured\">Совместное прослушивание не настроено. Укажите адрес сервера в разделе «Настройки → Интеграции → Совместное прослушивание».</string>\n    <string name=\"listen_together_suggestion_received\">%1$s предложил(а): %2$s</string>\n    <string name=\"listen_together_suggestion_sent\">Предложение отправлено хосту!</string>\n    <string name=\"listen_together_join_request_notification\">%1$s хочет присоединиться к комнате</string>\n    <string name=\"listen_together_notification_channel_name\">Совместное прослушивание</string>\n    <string name=\"listen_together_notification_channel_desc\">Уведомления о событиях совместного прослушивания</string>\n    <string name=\"listen_together_room_created\">Комната создана: %s</string>\n    <string name=\"listen_together_cannot_edit_username_in_room\">Нельзя изменить имя пользователя, находясь в комнате</string>\n    <string name=\"waiting_for_approval\">Ожидание одобрения от хоста</string>\n    <string name=\"invalid_room_code\">Неверный код комнаты</string>\n    <string name=\"join_request_denied\">Запрос на вход отклонён</string>\n    <string name=\"join_existing_room\">Присоединиться к существующей комнате</string>\n    <string name=\"room_code\">Код комнаты</string>\n    <string name=\"leave_room\">Покинуть комнату</string>\n    <string name=\"join_room\">Войти</string>\n    <string name=\"create_room\">Создать</string>\n    <string name=\"joining_room\">Подключение к комнате %s…</string>\n    <string name=\"creating_room\">Создание комнаты…</string>\n    <string name=\"connect\">Подключиться</string>\n    <string name=\"disconnect\">Отключиться</string>\n    <string name=\"create\">Создать</string>\n    <string name=\"join\">Войти</string>\n    <string name=\"approve\">Одобрить</string>\n    <string name=\"reject\">Отклонить</string>\n    <string name=\"clear\">Очистить</string>\n    <string name=\"copy\">Копировать</string>\n    <string name=\"copied_to_clipboard\">Скопировано в буфер обмена</string>\n    <string name=\"not_set\">Не указано</string>\n    <string name=\"hosting_room\">Хостинг комнаты</string>\n    <string name=\"in_room\">В комнате</string>\n    <string name=\"pending_requests\">Ожидающие запросы</string>\n    <string name=\"pending_suggestions\">Ожидающие предложения</string>\n    <string name=\"suggest_to_host\">Предложить хосту</string>\n    <string name=\"kick_user\">Выгнать</string>\n    <string name=\"host_label\">Хост</string>\n    <string name=\"you_label\">Вы</string>\n    <string name=\"connected_users\">Подключённые пользователи</string>\n    <string name=\"enter_username\">Введите имя пользователя</string>\n    <string name=\"error_username_empty\">Имя пользователя обязательно.</string>\n    <string name=\"resync\">Синхронизировать заново</string>\n    <string name=\"mute\">Отключить звук</string>\n    <string name=\"unmute\">Включить звук</string>\n    <string name=\"crash_title\">Сбой приложения</string>\n    <string name=\"crash_description\">Произошла непредвиденная ошибка. Пожалуйста, отправьте отчет о сбое, чтобы помочь нам устранить проблему.</string>\n    <string name=\"crash_share_logs\">Поделиться логами</string>\n    <string name=\"crash_share_title\">Поделиться отчетом о сбое</string>\n    <string name=\"crash_report_subject\">Отчет о сбое Metrolist</string>\n    <string name=\"crash_close\">Закрыть</string>\n    <string name=\"crash_no_log\">Лог сбоя недоступен</string>\n    <string name=\"palette_dynamic\">Динамическая</string>\n    <string name=\"palette_crimson\">Багровый</string>\n    <string name=\"palette_rose\">Розовый</string>\n    <string name=\"palette_purple\">Фиолетовый</string>\n    <string name=\"palette_deep_purple\">Тёмно-фиолетовый</string>\n    <string name=\"palette_indigo\">Индиго</string>\n    <string name=\"palette_blue\">Синий</string>\n    <string name=\"palette_sky_blue\">Небесно-синий</string>\n    <string name=\"palette_cyan\">Циановый</string>\n    <string name=\"palette_teal\">Бирюзовый</string>\n    <string name=\"palette_green\">Зелёный</string>\n    <string name=\"palette_light_green\">Светло-зелёный</string>\n    <string name=\"palette_lime\">Лаймовый</string>\n    <string name=\"palette_yellow\">Жёлтый</string>\n    <string name=\"palette_amber\">Янтарный</string>\n    <string name=\"palette_orange\">Оранжевый</string>\n    <string name=\"palette_deep_orange\">Тёмно-оранжевый</string>\n    <string name=\"palette_brown\">Коричневый</string>\n    <string name=\"palette_grey\">Серый</string>\n    <string name=\"palette_blue_grey\">Сине-серый</string>\n    <string name=\"cd_back\">Назад</string>\n    <string name=\"cd_pure_black_mode\">Режим чистого чёрного</string>\n    <string name=\"cd_light_mode\">Светлый режим</string>\n    <string name=\"cd_dark_mode\">Тёмный режим</string>\n    <string name=\"cd_system_mode\">Системный режим</string>\n    <string name=\"cd_palette_item\">Палитра «%1$s»</string>\n    <string name=\"listen_together_choose_server\">Выбрать сервер</string>\n    <string name=\"listen_together_custom_server\">Пользовательский сервер</string>\n    <string name=\"listen_together_use_custom_server\">Использовать пользовательский сервер</string>\n    <string name=\"listen_together_auto_approval_joins\">Автоодобрение запросов на вход</string>\n    <string name=\"listen_together_auto_approval_joins_desc\">Запросы на вход будут одобряться автоматически, без ручной проверки</string>\n    <string name=\"listen_together_sync_volume\">Синхронизация громкости хоста</string>\n    <string name=\"listen_together_sync_volume_desc\">У гостей будет та же громкость, что и у хоста</string>\n    <string name=\"copy_code\">Скопировать код</string>\n    <string name=\"kick_user_desc\">Удалить этого пользователя из сессии</string>\n    <string name=\"permanently_kick_user\">Заблокировать навсегда</string>\n    <string name=\"permanently_kick_user_desc\">Заблокировать запросы этого пользователя и скрыть его предложения</string>\n    <string name=\"transfer_ownership\">Передать владение</string>\n    <string name=\"transfer_ownership_desc\">Сделать этого пользователя хостом комнаты</string>\n    <string name=\"manage_user\">Управление пользователем</string>\n    <string name=\"listen_together_blocked_users\">Заблокированные пользователи</string>\n    <string name=\"listen_together_blocked_users_count\">заблокировано пользователей: %d</string>\n    <string name=\"listen_together_no_blocked_users\">Нет заблокированных пользователей</string>\n    <string name=\"unblock\">Разблокировать</string>\n    <string name=\"user_blocked_by_host\">Пользователь заблокирован хостом</string>\n    <string name=\"not_playing\">Ничего не играет</string>\n    <string name=\"tap_to_play\">Нажмите, чтобы открыть Metrolist</string>\n    <string name=\"widget_music_player\">Музыкальный плеер</string>\n    <string name=\"widget_turntable\">Виниловый проигрыватель</string>\n    <string name=\"together\">Вместе</string>\n    <string name=\"enter_room_code\">Введите код комнаты</string>\n    <string name=\"listen_together_settings_desc\">Настройка сервера, имени пользователя и прочего</string>\n    <string name=\"ai_lyrics_translation\">ИИ-перевод текста</string>\n    <string name=\"ai_translating_lyrics\">Перевод текста...</string>\n    <string name=\"ai_lyrics_translated\">Текст переведён</string>\n    <string name=\"ai_provider\">Провайдер</string>\n    <string name=\"ai_base_url\">Базовый URL</string>\n    <string name=\"ai_api_key\">API-ключ</string>\n    <string name=\"ai_model\">Модель</string>\n    <string name=\"ai_translation_mode\">Режим перевода</string>\n    <string name=\"ai_target_language\">Язык перевода</string>\n    <string name=\"ai_setup_guide\">Учетные данные API</string>\n    <string name=\"ai_translation_literal\">Перевод</string>\n    <string name=\"ai_translation_transcribed\">Транскрипция</string>\n    <string name=\"ai_api_key_required\">Требуется API-ключ</string>\n    <string name=\"ai_error_api_key_required\">Необходим API-ключ</string>\n    <string name=\"ai_error_no_lyrics\">Нет текста для перевода</string>\n    <string name=\"ai_error_lyrics_empty\">Текст песни пуст</string>\n    <string name=\"ai_error_language_required\">Не выбран язык перевода</string>\n    <string name=\"ai_error_unexpected\">Неожиданный результат перевода</string>\n    <string name=\"ai_error_unknown\">Произошла неизвестная ошибка</string>\n    <string name=\"ai_error_translation_failed\">Ошибка перевода</string>\n    <string name=\"play_all\">Играть все</string>\n    <string name=\"recognize_music\">Распознать музыку</string>\n    <string name=\"youtube_url_column\">Столбец ссылки на YouTube (необязательно)</string>\n    <string name=\"re_listen\">Прослушать снова</string>\n    <string name=\"clear_recognition_history_confirm\">Вы уверены, что хотите очистить всю историю распознавания?</string>\n    <string name=\"no_match_found\">Совпадений не найдено</string>\n    <string name=\"delete_from_history\">Удалить из истории</string>\n    <string name=\"artist_name_column\">Столбец имени исполнителя</string>\n    <string name=\"processing\">Обработка…</string>\n    <string name=\"clear_recognition_history\">Очистить историю распознавания</string>\n    <string name=\"map_csv_columns\">Сопоставление столбцов CSV</string>\n    <string name=\"column_label\">Столбец %d</string>\n    <string name=\"recognition_error\">Ошибка распознавания</string>\n    <string name=\"enable_high_refresh_rate_desc\">Принудительно использовать максимальную поддерживаемую частоту обновления экрана (например, 120 Гц)</string>\n    <string name=\"first_row_is_header\">Первая строка — заголовок</string>\n    <string name=\"try_again\">Попробовать снова</string>\n    <string name=\"tap_to_recognize\">Нажмите, чтобы распознать</string>\n    <string name=\"recognition_history\">История распознавания</string>\n    <string name=\"enable_high_refresh_rate\">Включить высокую частоту обновления</string>\n    <string name=\"song_title_column\">Столбец названия песни</string>\n    <string name=\"recently_converted\">Недавно конвертированные</string>\n    <string name=\"importing_csv\">Импорт CSV</string>\n    <string name=\"play_on_app\">Играть в Metrolist</string>\n    <string name=\"listening\">Слушаем…</string>\n    <string name=\"continue_action\">Продолжить</string>\n    <string name=\"enable\">Включить</string>\n    <string name=\"crossfade\">Кроссфейд</string>\n    <string name=\"crossfade_desc\">Плавный переход между треками</string>\n    <string name=\"crossfade_duration\">Длительность кроссфейда</string>\n    <string name=\"crossfade_gapless\">Отключать для альбомов без пауз</string>\n    <string name=\"crossfade_gapless_desc\">Не применять кроссфейд, если альбом воспроизводится без пауз</string>\n    <string name=\"crossfade_beta_title\">Бета-функция</string>\n    <string name=\"crossfade_beta_message\">Кроссфейд - новая функция, которая может работать нестабильно. Пожалуйста, сообщите нам, если столкнётесь с ошибками.\\n\\nЭта функция отключает аппаратное ускорение аудио из-за технических ограничений.</string>\n    <string name=\"audio_offload_disabled_by_crossfade\">Отключено, так как включён кроссфейд</string>\n    <string name=\"hide_youtube_shorts\">Скрыть YouTube Shorts</string>\n    <string name=\"listen_together_in_top_bar\">Совместное прослушивание в верхней панели</string>\n    <string name=\"listen_together_in_top_bar_desc\">Показывать «Совместное прослушивание» на верхней панели вместо панели навигации</string>\n    <string name=\"discord_advanced_mode_description\">Показать дополнительные параметры настройки Rich Presence</string>\n    <string name=\"discord_activity_playing\">Играет</string>\n    <string name=\"discord_activity_listening\">Слушает</string>\n    <string name=\"discord_activity_watching\">Смотрит</string>\n    <string name=\"discord_activity_competing\">Соревнуется</string>\n    <string name=\"discord_button_text_variables\">Переменные: {song_name}, {artist_name}, {album_name}</string>\n    <string name=\"discord_rpc_preview\">Предпросмотр Rich Presence</string>\n    <string name=\"discord_presence\">Присутствие</string>\n    <string name=\"discord_connect_description\">Войдите через Discord, чтобы поделиться тем, что вы слушаете</string>\n    <string name=\"discord_playing_metrolist\">Играет в Metrolist</string>\n    <string name=\"prevent_duplicate_tracks_in_queue\">Исключать дубликаты в очереди</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">При добавлении трека в очередь удалять его из предыдущей позиции, если он уже присутствует</string>\n    <string name=\"ai_translation_literal_desc\">Перевести смысл на язык перевода</string>\n    <string name=\"ai_translation_transcribed_desc\">Преобразовать произношение в письменность языка перевода</string>\n    <string name=\"ai_provider_help\">Получить API-ключи</string>\n    <string name=\"ai_provider_openrouter_help\">Перейдите на https://openrouter.ai для получения бесплатных и платных моделей</string>\n    <string name=\"ai_provider_openai_help\">Перейдите на https://platform.openai.com/api-keys</string>\n    <string name=\"ai_provider_claude_help\">Перейдите на https://console.anthropic.com/settings/keys</string>\n    <string name=\"ai_provider_gemini_help\">Перейдите на https://aistudio.google.com/apikey</string>\n    <string name=\"ai_provider_perplexity_help\">Перейдите на https://perplexity.ai/settings/api</string>\n    <string name=\"ai_provider_xai_help\">Перейдите на https://console.x.ai</string>\n    <string name=\"ai_provider_deepl_help\">Перейдите на https://deepl.com/pro-api для получения бесплатных и платных ключей</string>\n    <string name=\"ai_deepl_formality\">Степень формальности</string>\n    <string name=\"ai_deepl_formality_default\">По умолчанию</string>\n    <string name=\"ai_deepl_formality_more\">Более формально</string>\n    <string name=\"ai_deepl_formality_less\">Менее формально</string>\n    <string name=\"discord_status\">Статус</string>\n    <string name=\"discord_status_online\">В сети</string>\n    <string name=\"discord_status_idle\">Неактивен</string>\n    <string name=\"discord_status_dnd\">Не беспокоить</string>\n    <string name=\"discord_buttons\">Кнопки</string>\n    <string name=\"discord_button_1\">Кнопка 1</string>\n    <string name=\"discord_button_2\">Кнопка 2</string>\n    <string name=\"login_successful\">Вход выполнен успешно!</string>\n    <string name=\"discord_information_warning\">Эта функция использует библиотеку KizzyRPC для подключения к шлюзу Discord и установки статуса Rich Presence. Хотя случаев блокировки аккаунтов при подобном использовании не зафиксировано, данный метод официально не поддерживается Discord и может считаться нарушением Условий использования. Ваш токен извлекается локально и никогда не отправляется на сторонние серверы. Используйте на свой страх и риск.</string>\n    <string name=\"discord_activity_type\">Тип активности</string>\n    <string name=\"discord_watching_metrolist\">Смотрит Metrolist</string>\n    <string name=\"discord_competing_metrolist\">Соревнуется в Metrolist</string>\n    <string name=\"discord_activity_name\">Название активности</string>\n    <string name=\"discord_activity_name_description\">Пользовательское название активности (оставьте пустым для значения по умолчанию)</string>\n    <string name=\"discord_advanced_mode\">Расширенный режим</string>\n    <string name=\"player_background_solid\">Сплошной</string>\n    <string name=\"resume_on_bluetooth_connect\">Возобновлять при подключении Bluetooth</string>\n    <string name=\"lyrics_romanize_hindi\">Романизировать тексты на хинди</string>\n    <string name=\"lyrics_romanize_punjabi\">Романизировать тексты на панджаби</string>\n    <string name=\"lyrics_romanize_as_main\">Показывать романизированные тексты как основные</string>\n    <string name=\"display_density\">Плотность интерфейса</string>\n    <string name=\"restart\">Перезапустить</string>\n    <string name=\"restart_required\">Требуется перезапуск</string>\n    <string name=\"density_restart_message\">Изменения плотности интерфейса вступят в силу после перезапуска приложения. Перезапустить сейчас?</string>\n    <string name=\"enable_lrclib_desc\">База синхронизированных текстов песен, поддерживаемая сообществом</string>\n    <string name=\"enable_kugou_desc\">Берет тексты песен из KuGou, популярной китайской музыкальной платформы</string>\n    <string name=\"youtube_music_lyrics_note\">ПРИМЕЧАНИЕ: Тексты из YouTube Music будут показаны автоматически, если другие недоступны. Обычно тексты из YTM не синхронизированы.</string>\n    <string name=\"enable_lyricsplus\">Включить LyricsPlus</string>\n    <string name=\"enable_lyricsplus_desc\">Синхронизированные тексты из нескольких источников</string>\n    <string name=\"lyrics_provider_selection\">Выбор поставщиков</string>\n    <string name=\"lyrics_provider_selection_desc\">Выберите, какие поставщики текстов будут включены</string>\n    <string name=\"lyrics_provider_priority\">Приоритет поставщиков текстов</string>\n    <string name=\"lyrics_provider_priority_desc\">Перетащите, чтобы изменить порядок поставщиков. Чем выше позиция -&gt; тем выше приоритет.</string>\n    <string name=\"changelog\">Список изменений</string>\n    <string name=\"changelog_empty\">Список изменений недоступен</string>\n    <string name=\"github_releases_url\">https://github.com/MetrolistGroup/Metrolist/releases</string>\n    <string name=\"list_bullet\">•</string>\n    <string name=\"view_on_github\">Открыть на GitHub</string>\n    <string name=\"current_version\">Текущая версия</string>\n    <string name=\"version_format\">Версия: %s</string>\n    <string name=\"update_settings\">Настройки обновления</string>\n    <string name=\"check_for_updates_title\">Проверка обновлений</string>\n    <string name=\"checking_for_updates\">Проверка обновлений…</string>\n    <string name=\"latest_version_format\">Последняя: %s</string>\n    <string name=\"check_for_updates_button\">Проверить обновления</string>\n    <string name=\"hide_changelog\">Скрыть список изменений</string>\n    <string name=\"view_changelog\">Показать список изменений</string>\n    <string name=\"failed_to_check_updates\">Не удалось проверить обновления: %s</string>\n    <string name=\"set_as_default\">Установить по умолчанию</string>\n    <string name=\"sleep_timer_default_set\">Таймер сна по умолчанию установлен на %d мин</string>\n    <string name=\"found_in_settings_content\">Находится в Настройки &gt; Контент</string>\n    <string name=\"plays\">прослушиваний</string>\n    <string name=\"error_episode_save\">Не удалось сохранить выпуск</string>\n    <string name=\"error_episode_remove\">Не удалось удалить выпуск</string>\n    <string name=\"error_podcast_subscribe\">Не удалось подписаться на подкаст</string>\n    <string name=\"error_podcast_unsubscribe\">Не удалось отписаться от подкаста</string>\n    <string name=\"listen_together_auto_approval_suggestions\">Автоодобрение предложений песен</string>\n    <string name=\"listen_together_auto_approval_suggestions_desc\">Предложения песен от гостей будут одобряться автоматически и добавляться в очередь</string>\n    <string name=\"speed_dial\">Быстрый набор</string>\n    <string name=\"pin_to_speed_dial\">Закрепить в Быстром наборе</string>\n    <string name=\"unpin_from_speed_dial\">Открепить от Быстрого набора</string>\n    <string name=\"randomize_home_order\">Случайный порядок на главном экране</string>\n    <string name=\"randomize_home_order_desc\">Случайно менять порядок разделов на главном экране с учётом приоритетов</string>\n    <string name=\"daily_discover_sounds_like\">Звучит как %1$s</string>\n    <string name=\"daily_discover_because_you_listen_to\">Потому что вы слушаете %1$s</string>\n    <string name=\"daily_discover_similar_to\">Похоже на %1$s</string>\n    <string name=\"daily_discover_based_on\">На основе %1$s</string>\n    <string name=\"daily_discover_for_fans_of\">Для фанатов %1$s</string>\n    <string name=\"from_the_community\">От сообщества</string>\n    <string name=\"logout_dialog_title\">Сохранить данные библиотеки?</string>\n    <string name=\"logout_dialog_message\">Хотите ли вы сохранить свои плейлисты и данные библиотеки? Скачанные треки будут сохранены в любом случае.</string>\n    <string name=\"logout_keep\">Сохранить</string>\n    <string name=\"logout_clear\">Очистить</string>\n    <string name=\"credits_lead_developer\">Ведущий разработчик</string>\n    <string name=\"credits_collaborator\">Соавтор</string>\n    <string name=\"credits_collaborators_section\">Соавторы</string>\n    <string name=\"credits_license_name\">GNU General Public License v3.0</string>\n    <string name=\"credits_license_desc\">Свободное программное обеспечение с открытым исходным кодом. Вы можете использовать, изучать, распространять и улучшать его.</string>\n    <string name=\"credits_discord\">Сервер Discord</string>\n    <string name=\"credits_telegram\">Telegram-канал</string>\n    <string name=\"credits_website\">Веб-сайт</string>\n    <string name=\"credits_instagram\">Instagram</string>\n    <string name=\"credits_github\">GitHub</string>\n    <string name=\"credits_view_repo\">Открыть репозиторий</string>\n    <string name=\"app_version_info\">%1$s • %2$s</string>\n    <string name=\"like_what_i_do\">Нравится то, что я делаю?</string>\n    <string name=\"buy_mo_a_coffee\">Угостить меня кофе</string>\n    <string name=\"community_and_info\">Сообщество и информация</string>\n    <string name=\"metrolist\">METROLIST</string>\n    <string name=\"wanna_play_favorite_song\">Хотите включить их любимую песню?</string>\n    <string name=\"yeah\">Да</string>\n    <string name=\"stands_with_palestine\">Этот проект поддерживает Палестину 🇵🇸</string>\n    <string name=\"filter_podcasts\">Подкасты</string>\n    <string name=\"view_podcast\">Перейти к подкасту</string>\n    <string name=\"podcast_channels\">Каналы подкастов</string>\n    <string name=\"latest_episodes\">Последние выпуски</string>\n    <string name=\"your_shows\">Ваши шоу</string>\n    <string name=\"new_episodes\">Новые выпуски</string>\n    <string name=\"episodes_for_later\">Сохраненные выпуски</string>\n    <string name=\"save_episode_for_later\">Сохранить на потом</string>\n    <string name=\"save_episode_for_later_desc\">Добавить в плейлист «Сохраненные выпуски»</string>\n    <string name=\"remove_episode_from_saved\">Удалить из сохранённых</string>\n    <string name=\"subscribe_to_podcast\">Сохранить подкаст в библиотеку</string>\n    <plurals name=\"n_episode\">\n        <item quantity=\"one\">%d выпуск</item>\n        <item quantity=\"few\">%d выпуска</item>\n        <item quantity=\"many\">%d выпусков</item>\n        <item quantity=\"other\">%d выпусков</item>\n    </plurals>\n    <string name=\"restore_confirm_title\">Восстановить из резервной копии?</string>\n    <string name=\"restore_confirm_message\">Данные приложения будут восстановлены из резервной копии.</string>\n    <string name=\"restore_account_warning\">Вам потребуется снова войти в аккаунт после восстановления. Будет выполнен выход из следующего аккаунта:</string>\n    <string name=\"restore\">Восстановить</string>\n    <string name=\"checking_previous_account\">Проверка предыдущего аккаунта…</string>\n    <string name=\"no_account_found\">Аккаунт не найден</string>\n    <string name=\"importing_playlist\">Импорт плейлиста</string>\n    <string name=\"widget_recognizer_name\">Распознавание музыки</string>\n    <string name=\"widget_recognizer_description\">Узнавайте, какая музыка играет рядом, прямо с главного экрана</string>\n    <string name=\"widget_recognizer_tap_to_search\">Нажмите для распознавания трека</string>\n    <string name=\"widget_recognizer_listening\">Слушаем…</string>\n    <string name=\"widget_recognizer_processing\">Определяем…</string>\n    <string name=\"widget_recognizer_no_match\">Совпадений не найдено. Попробуйте снова</string>\n    <string name=\"widget_recognizer_error\">Не удалось распознать</string>\n    <string name=\"widget_recognizer_error_generic\">Произошла ошибка. Пожалуйста, попробуйте снова</string>\n    <string name=\"widget_recognizer_unknown_song\">Неизвестный трек</string>\n    <string name=\"widget_recognizer_unknown_artist\">Неизвестный исполнитель</string>\n    <string name=\"widget_recognizer_mic_desc\">Определить трек</string>\n    <string name=\"widget_recognizer_channel_name\">Распознавание музыки</string>\n    <string name=\"widget_recognizer_channel_desc\">Показывает уведомление при распознавании трека через виджет</string>\n    <string name=\"widget_recognizer_notification_text\">Запись звука для распознавания трека…</string>\n    <string name=\"filter_episodes\">Выпуски</string>\n    <string name=\"filter_channels\">Каналы</string>\n    <string name=\"auto_playlist\">Автоплейлист</string>\n    <string name=\"downloaded_episodes\">Скачанные выпуски</string>\n    <string name=\"no_subscribed_channels\">Нет подписанных каналов</string>\n    <string name=\"no_downloaded_episodes\">Нет скачанных выпусков</string>\n    <plurals name=\"n_channel\">\n        <item quantity=\"one\">%d канал</item>\n        <item quantity=\"few\">%d канала</item>\n        <item quantity=\"many\">%d каналов</item>\n        <item quantity=\"other\">%d каналов</item>\n    </plurals>\n    <string name=\"view_channel\">Перейти к каналу</string>\n    <string name=\"filter_profiles\">Профили</string>\n    <string name=\"enable_automatic_sleeptimer\">Включить автоматический таймер сна</string>\n    <string name=\"sleeptimer_description\">Автоматически включает таймер сна со значением по умолчанию в заданное время</string>\n    <string name=\"sleep_timer_repeat_description\">Установите день и время, когда таймер сна будет включаться автоматически</string>\n    <string name=\"sleep_timer_repeat\">Повтор</string>\n    <string name=\"sleep_timer_daily\">Ежедневно</string>\n    <string name=\"sleep_timer_weekdays\">По будням</string>\n    <string name=\"sleep_timer_weekdays_weekends\">Будни / Выходные</string>\n    <string name=\"sleep_timer_weekends\">Выходные (Сб–Вс)</string>\n    <string name=\"sleep_timer_custom\">Выборочно</string>\n    <string name=\"sleep_timer_start_time\">Время начала</string>\n    <string name=\"sleep_timer_end_time\">Время окончания</string>\n    <string name=\"sleep_timer_monday\">Понедельник</string>\n    <string name=\"sleep_timer_tuesday\">Вторник</string>\n    <string name=\"sleep_timer_wednesday\">Среда</string>\n    <string name=\"sleep_timer_thursday\">Четверг</string>\n    <string name=\"sleep_timer_friday\">Пятница</string>\n    <string name=\"sleep_timer_saturday\">Суббота</string>\n    <string name=\"sleep_timer_sunday\">Воскресенье</string>\n    <string name=\"sleep_timer_stop_after_current_song\">Остановить по окончании текущего трека</string>\n    <string name=\"sleep_timer_fade_out\">Плавное затухание в последнюю минуту</string>\n    <string name=\"upload_songs\">Загрузить треки</string>\n    <string name=\"uploading\">Загрузка…</string>\n    <string name=\"upload_progress\">%1$d из %2$d</string>\n    <string name=\"upload_complete\">Загрузка завершена</string>\n    <string name=\"upload_failed\">Ошибка загрузки</string>\n    <string name=\"upload_file_too_large\">Файл слишком велик (макс. 300 МБ)</string>\n    <string name=\"upload_unsupported_format\">Неподдерживаемый формат. Используйте mp3, m4a, wma, flac или ogg</string>\n    <string name=\"delete_uploaded_song\">Удалить загруженный трек</string>\n    <string name=\"delete_uploaded_song_confirm\">Вы уверены, что хотите удалить этот загруженный трек? Это действие нельзя отменить.</string>\n    <string name=\"delete_uploaded_song_success\">Загруженный трек удален</string>\n    <string name=\"delete_uploaded_song_failed\">Не удалось удалить загруженный трек</string>\n    <string name=\"delete_uploaded_songs\">Удалить загруженные треки</string>\n    <string name=\"delete_uploaded_songs_confirm\">Вы уверены, что хотите удалить загруженные треки (%1$d)? Это действие нельзя отменить.</string>\n    <string name=\"deleted_n_songs\">Удалено треков: %1$d</string>\n    <string name=\"deleting\">Удаление…</string>\n    <string name=\"export_playlist\">Экспортировать плейлист</string>\n    <string name=\"export_as_csv\">Экспорт в CSV</string>\n    <string name=\"export_as_m3u\">Экспорт в M3U</string>\n    <string name=\"export_success\">Плейлист успешно экспортирован</string>\n    <string name=\"export_failed\">Не удалось экспортировать плейлист</string>\n    <string name=\"export_option_share\">Поделиться</string>\n    <string name=\"export_option_save\">Сохранить в «Документы»</string>\n    <string name=\"qs_tile_music_recognizer\">Распознать музыку</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-ru/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <string name=\"home\">Главная</string>\n    <string name=\"songs\">Композиции</string>\n    <string name=\"artists\">Исполнители</string>\n    <string name=\"albums\">Альбомы</string>\n    <string name=\"playlists\">Плейлисты</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d выбран</item>\n        <item quantity=\"few\">%d выбрано</item>\n        <item quantity=\"many\">%d выбрано</item>\n        <item quantity=\"other\">%d выбрано</item>\n    </plurals>\n    <string name=\"history\">История</string>\n    <string name=\"stats\">Статистика</string>\n    <string name=\"mood_and_genres\">Настроение и жанры</string>\n    <string name=\"account\">Аккаунт</string>\n    <string name=\"quick_picks\">Быстрый выбор</string>\n    <string name=\"quick_picks_empty\">Послушайте несколько песен, чтобы создать ваш быстрый выбор</string>\n    <string name=\"forgotten_favorites\">Забытые избранные</string>\n    <string name=\"keep_listening\">Продолжайте слушать</string>\n    <string name=\"your_youtube_playlists\">Ваши плейлисты YouTube</string>\n    <string name=\"similar_to\">Похожие на</string>\n    <string name=\"new_release_albums\">Новые релизы альбомов</string>\n    <string name=\"today\">Сегодня</string>\n    <string name=\"yesterday\">Вчера</string>\n    <string name=\"this_week\">На этой неделе</string>\n    <string name=\"last_week\">На прошлой неделе</string>\n    <string name=\"most_played_songs\">Самые прослушиваемые песни</string>\n    <string name=\"most_played_artists\">Самые прослушиваемые исполнители</string>\n    <string name=\"most_played_albums\">Самые прослушиваемые альбомы</string>\n    <string name=\"search\">Поиск</string>\n    <string name=\"search_yt_music\">Поиск в YouTube Music…</string>\n    <string name=\"search_library\">Поиск в библиотеке…</string>\n    <string name=\"filter_library\">Библиотека</string>\n    <string name=\"filter_liked\">Понравившиеся</string>\n    <string name=\"filter_downloaded\">Загруженные</string>\n    <string name=\"filter_all\">Все</string>\n    <string name=\"filter_songs\">Композиции</string>\n    <string name=\"filter_videos\">Видео</string>\n    <string name=\"filter_albums\">Альбомы</string>\n    <string name=\"filter_artists\">Исполнители</string>\n    <string name=\"filter_playlists\">Плейлисты</string>\n    <string name=\"filter_community_playlists\">Плейлисты сообщества</string>\n    <string name=\"filter_featured_playlists\">Избранные плейлисты</string>\n    <string name=\"filter_bookmarked\">Добавлено в закладки</string>\n    <string name=\"no_results_found\">Результаты не найдены</string>\n    <string name=\"library_song_empty\">Здесь будут отображаться песни из вашей библиотеки</string>\n    <string name=\"library_artist_empty\">Здесь будут отображаться исполнители из вашей библиотеки</string>\n    <string name=\"library_album_empty\">Здесь будут отображаться альбомы из вашей библиотеки</string>\n    <string name=\"library_playlist_empty\">Здесь будут отображаться ваши плейлисты</string>\n    <string name=\"from_your_library\">Из вашей библиотеки</string>\n    <string name=\"other_versions\">Другие версии</string>\n    <string name=\"liked_songs\">Любимые треки</string>\n    <string name=\"downloaded_songs\">Загруженная музыка</string>\n    <string name=\"playlist_is_empty\">Плейлист пуст</string>\n    <string name=\"remove_download_playlist_confirm\">Вы уверены, что хотите удалить все песни из плейлиста «%s» из хранилища загруженной музыки?</string>\n    <string name=\"delete_playlist_confirm\">Вы уверены, что хотите удалить плейлист «%s»?</string>\n    <string name=\"retry\">Повторить</string>\n    <string name=\"radio\">Радио</string>\n    <string name=\"shuffle\">Перемешать</string>\n    <string name=\"reset\">Сбросить</string>\n    <string name=\"details\">Подробнее</string>\n    <string name=\"edit\">Редактировать</string>\n    <string name=\"start_radio\">Запустить радио</string>\n    <string name=\"play\">Играть</string>\n    <string name=\"play_next\">Играть следующим</string>\n    <string name=\"add_to_queue\">Добавить в очередь</string>\n    <string name=\"add_to_library\">Добавить в библиотеку</string>\n    <string name=\"add_all_to_library\">Добавить все в библиотеку</string>\n    <string name=\"remove_from_library\">Удалить из библиотеки</string>\n    <string name=\"remove_all_from_library\">Удалить все из библиотеки</string>\n    <string name=\"action_download\">Загрузить</string>\n    <string name=\"downloading\">Загрузка</string>\n    <string name=\"remove_download\">Удалить из загруженных</string>\n    <string name=\"import_playlist\">Импортировать плейлист</string>\n    <string name=\"add_to_playlist\">Добавить в плейлист</string>\n    <string name=\"view_artist\">Перейти к исполнителю</string>\n    <string name=\"view_album\">Перейти к альбому</string>\n    <string name=\"refetch\">Обновить</string>\n    <string name=\"share\">Поделиться</string>\n    <string name=\"delete\">Удалить</string>\n    <string name=\"remove_from_history\">Удалить из истории</string>\n    <string name=\"remove_from_playlist\">Удалить из плейлиста</string>\n    <string name=\"remove_from_queue\">Удалить из очереди</string>\n    <string name=\"search_online\">Поиск в Интернете</string>\n    <string name=\"action_sync\">Синхронизировать</string>\n    <string name=\"advanced\">Контроль аудио</string>\n    <string name=\"tempo_and_pitch\">Темп и высота тона</string>\n    <string name=\"sort_by_create_date\">Недавно добавленные</string>\n    <string name=\"sort_by_name\">Название</string>\n    <string name=\"sort_by_artist\">Исполнитель</string>\n    <string name=\"sort_by_year\">Год</string>\n    <string name=\"sort_by_song_count\">Количество треков</string>\n    <string name=\"sort_by_length\">Длительность</string>\n    <string name=\"sort_by_play_time\">Время воспроизведения</string>\n    <string name=\"sort_by_custom\">Польз. порядок</string>\n    <string name=\"media_id\">Идентификатор медиа</string>\n    <string name=\"mime_type\">Тип MIME</string>\n    <string name=\"codecs\">Кодеки</string>\n    <string name=\"bitrate\">Битрейт</string>\n    <string name=\"sample_rate\">Частота дискретизации</string>\n    <string name=\"loudness\">Громкость</string>\n    <string name=\"volume\">Уровень громкости</string>\n    <string name=\"file_size\">Размер файла</string>\n    <string name=\"unknown\">Неизвестно</string>\n    <string name=\"copied\">Скопировано в буфер обмена</string>\n    <string name=\"edit_lyrics\">Редактировать текст песни</string>\n    <string name=\"search_lyrics\">Поиск текста песни</string>\n    <string name=\"edit_song\">Редактировать композицию</string>\n    <string name=\"song_title\">Название композиции</string>\n    <string name=\"song_artists\">Исполнители композиции</string>\n    <string name=\"error_song_title_empty\">Укажите название композиции.</string>\n    <string name=\"error_song_artist_empty\">Укажите исполнителей композиции.</string>\n    <string name=\"save\">Сохранить</string>\n    <string name=\"choose_playlist\">Выбрать плейлист</string>\n    <string name=\"edit_playlist\">Редактировать плейлист</string>\n    <string name=\"create_playlist\">Создать плейлист</string>\n    <string name=\"playlist_name\">Название плейлиста</string>\n    <string name=\"error_playlist_name_empty\">Укажите название плейлиста.</string>\n    <string name=\"edit_artist\">Редактировать исполнителя</string>\n    <string name=\"artist_name\">Имя исполнителя</string>\n    <string name=\"error_artist_name_empty\">Укажите имя исполнителя.</string>\n    <string name=\"duplicates\">Дубликаты</string>\n    <string name=\"skip_duplicates\">Пропустить дубликаты</string>\n    <string name=\"add_anyway\">Добавить в любом случае</string>\n    <string name=\"duplicates_description_single\">Эта песня уже в вашем плейлисте</string>\n    <string name=\"duplicates_description_multiple\">%d песен уже в вашем плейлисте</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d композиция</item>\n        <item quantity=\"few\">%d композиции</item>\n        <item quantity=\"many\">%d композиций</item>\n        <item quantity=\"other\">%d композиций</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d исполнитель</item>\n        <item quantity=\"few\">%d исполнителя</item>\n        <item quantity=\"many\">%d исполнителей</item>\n        <item quantity=\"other\">%d исполнителей</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d альбом</item>\n        <item quantity=\"few\">%d альбома</item>\n        <item quantity=\"many\">%d альбомов</item>\n        <item quantity=\"other\">%d альбомов</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d плейлист</item>\n        <item quantity=\"few\">%d плейлиста</item>\n        <item quantity=\"many\">%d плейлистов</item>\n        <item quantity=\"other\">%d плейлистов</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d неделя</item>\n        <item quantity=\"few\">%d недели</item>\n        <item quantity=\"many\">%d недель</item>\n        <item quantity=\"other\">%d недель</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d месяц</item>\n        <item quantity=\"few\">%d месяца</item>\n        <item quantity=\"many\">%d месяцев</item>\n        <item quantity=\"other\">%d месяцев</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d год</item>\n        <item quantity=\"few\">%d года</item>\n        <item quantity=\"many\">%d лет</item>\n        <item quantity=\"other\">%d лет</item>\n    </plurals>\n    <string name=\"playlist_imported\">Плейлист импортирован</string>\n    <string name=\"removed_song_from_playlist\">«%s» удалена из плейлиста</string>\n    <string name=\"playlist_synced\">Плейлист синхронизирован</string>\n    <string name=\"undo\">Отменить</string>\n    <string name=\"lyrics_not_found\">Текст песни не найден</string>\n    <string name=\"sleep_timer\">Таймер сна</string>\n    <string name=\"end_of_song\">Конец песни</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">%d минута</item>\n        <item quantity=\"few\">%d минуты</item>\n        <item quantity=\"many\">%d минут</item>\n        <item quantity=\"other\">%d минут</item>\n    </plurals>\n    <string name=\"error_no_stream\">Нет доступных потоков</string>\n    <string name=\"error_no_internet\">Нет подключения к сети</string>\n    <string name=\"error_timeout\">Тайм-аут</string>\n    <string name=\"error_unknown\">Неизвестная ошибка</string>\n    <string name=\"action_like\">Поставить «Нравится»</string>\n    <string name=\"action_like_all\">Отметить все как «Нравится»</string>\n    <string name=\"action_remove_like\">Убрать «Нравится»</string>\n    <string name=\"action_remove_like_all\">Убрать все отметки «Нравится»</string>\n    <string name=\"action_shuffle_on\">Включить перемешивание</string>\n    <string name=\"action_shuffle_off\">Выключить перемешивание</string>\n    <string name=\"repeat_mode_off\">Выключить повторение</string>\n    <string name=\"repeat_mode_one\">Повторить текущую песню</string>\n    <string name=\"repeat_mode_all\">Повторить очередь</string>\n    <string name=\"queue_all_songs\">Все композиции</string>\n    <string name=\"queue_searched_songs\">Искомые композиции</string>\n    <string name=\"music_player\">Музыкальный плеер</string>\n    <string name=\"settings\">Настройки</string>\n    <string name=\"appearance\">Внешний вид</string>\n    <string name=\"theme\">Тема</string>\n    <string name=\"enable_dynamic_theme\">Включить динамическую тему</string>\n    <string name=\"dark_theme\">Темная тема</string>\n    <string name=\"dark_theme_on\">Вкл.</string>\n    <string name=\"dark_theme_off\">Выкл.</string>\n    <string name=\"dark_theme_follow_system\">Использовать настройки системы</string>\n    <string name=\"pure_black\">Режим чистого черного цвета</string>\n    <string name=\"customize_navigation_tabs\">Настройка вкладок навигации</string>\n    <string name=\"player\">Плеер</string>\n    <string name=\"player_text_alignment\">Выравнивание текста плеера</string>\n    <string name=\"lyrics_text_position\">Расположение текста песни</string>\n    <string name=\"sided\">Сбоку</string>\n    <string name=\"left\">Слева</string>\n    <string name=\"center\">По центру</string>\n    <string name=\"right\">Справа</string>\n    <string name=\"player_slider_style\">Стиль ползунка плеера</string>\n    <string name=\"default_\">По умолчанию</string>\n    <string name=\"squiggly\">Волнистый</string>\n    <string name=\"misc\">Разное</string>\n    <string name=\"default_open_tab\">Вкладка навигации по умолчанию</string>\n    <string name=\"grid_cell_size\">Размер ячейки сетки</string>\n    <string name=\"small\">Маленький</string>\n    <string name=\"big\">Большой</string>\n    <string name=\"content\">Контент</string>\n    <string name=\"login\">Логин</string>\n    <string name=\"not_logged_in\">Не авторизовано</string>\n    <string name=\"content_language\">Язык контента</string>\n    <string name=\"content_country\">Страна контента</string>\n    <string name=\"system_default\">По умолчанию системы</string>\n    <string name=\"enable_proxy\">Включить прокси</string>\n    <string name=\"proxy_type\">Тип прокси</string>\n    <string name=\"proxy_url\">URL прокси</string>\n    <string name=\"restart_to_take_effect\">Требуется перезапуск</string>\n    <string name=\"player_and_audio\">Плеер и аудио</string>\n    <string name=\"audio_quality\">Качество аудио</string>\n    <string name=\"audio_quality_auto\">Авто</string>\n    <string name=\"audio_quality_high\">Высокое</string>\n    <string name=\"audio_quality_low\">Низкое</string>\n    <string name=\"queue\">Очередь</string>\n    <string name=\"persistent_queue\">Постоянная очередь</string>\n    <string name=\"persistent_queue_desc\">Восстанавливать последнюю очередь при запуске приложения</string>\n    <string name=\"auto_load_more\">Автозагрузка большего количества песен</string>\n    <string name=\"auto_load_more_desc\">Автоматически добавлять больше песен при достижении конца очереди, если это возможно</string>\n    <string name=\"skip_silence\">Пропускать тишину в композициях</string>\n    <string name=\"audio_normalization\">Нормализация аудио</string>\n    <string name=\"auto_skip_next_on_error\">Автопереход к следующей композиции при ошибке</string>\n    <string name=\"auto_skip_next_on_error_desc\">Обеспечить непрерывное воспроизведение</string>\n    <string name=\"stop_music_on_task_clear\">Останавливать воспроизведение при очистке задач</string>\n    <string name=\"equalizer\">Эквалайзер</string>\n    <string name=\"storage\">Хранилище</string>\n    <string name=\"cache\">Кэш</string>\n    <string name=\"image_cache\">Кэш изображений</string>\n    <string name=\"song_cache\">Кэш аудио</string>\n    <string name=\"max_cache_size\">Максимальный размер кэша</string>\n    <string name=\"unlimited\">Неограниченно</string>\n    <string name=\"clear_all_downloads\">Очистить все загрузки</string>\n    <string name=\"max_image_cache_size\">Макс. размер кэша изображений</string>\n    <string name=\"clear_image_cache\">Очистить кэш изображений</string>\n    <string name=\"max_song_cache_size\">Макс. размер кэша аудио</string>\n    <string name=\"clear_song_cache\">Очистить кэш аудио</string>\n    <string name=\"size_used\">%s использовано</string>\n    <string name=\"privacy\">Конфиденциальность</string>\n    <string name=\"listen_history\">История прослушивания</string>\n    <string name=\"pause_listen_history\">Приостановить историю прослушивания</string>\n    <string name=\"clear_listen_history\">Очистить историю прослушивания</string>\n    <string name=\"clear_listen_history_confirm\">Вы уверены, что хотите очистить всю историю прослушивания?</string>\n    <string name=\"search_history\">История поиска</string>\n    <string name=\"pause_search_history\">Приостановить историю поиска</string>\n    <string name=\"clear_search_history\">Очистить историю поиска</string>\n    <string name=\"clear_search_history_confirm\">Вы уверены, что хотите очистить всю историю поиска?</string>\n    <string name=\"disable_screenshot\">Отключить снимок экрана</string>\n    <string name=\"disable_screenshot_desc\">При включении этой опции скриншоты и отображение приложения в списке последних отключаются.</string>\n    <string name=\"enable_lrclib\">Включить провайдера текстов LrcLib</string>\n    <string name=\"enable_kugou\">Включить провайдера текстов KuGou</string>\n    <string name=\"hide_explicit\">Скрывать контент с нецензурной лексикой</string>\n    <string name=\"backup_restore\">Резервное копирование</string>\n    <string name=\"action_backup\">Создать резервную копию</string>\n    <string name=\"action_restore\">Восстановить из резервной копии</string>\n    <string name=\"imported_playlist\">Импортированный плейлист</string>\n    <string name=\"backup_create_success\">Резервная копия создана успешно</string>\n    <string name=\"backup_create_failed\">Не удалось создать резервную копию</string>\n    <string name=\"restore_failed\">Не удалось восстановить резервную копию</string>\n    <string name=\"discord_integration\">Интеграция с Discord</string>\n    <string name=\"discord_information\">Metrolist использует библиотеку KizzyRPC для установки статуса вашего аккаунта Discord. Это включает использование соединения через Discord Gateway, что может считаться нарушением условий использования Discord. Однако, на данный момент нет известных случаев блокировки учетных записей пользователей по этой причине. Используйте на свой страх и риск.\\n\\nMetrolist будет извлекать только ваш токен, а все остальное хранится локально.</string>\n    <string name=\"dismiss\">Закрыть</string>\n    <string name=\"options\">Настройки</string>\n    <string name=\"preview\">Предпросмотр</string>\n    <string name=\"login_failed\">Ошибка входа</string>\n    <string name=\"action_logout\">Выйти</string>\n    <string name=\"enable_discord_rpc\">Включить Rich Presence</string>\n    <string name=\"about\">О приложении</string>\n    <string name=\"app_version\">Версия приложения</string>\n    <string name=\"new_version_available\">Доступна новая версия</string>\n    <string name=\"translation_models\">Модели перевода</string>\n    <string name=\"clear_translation_models\">Очистить модели перевода</string>\n    <string name=\"use_login_for_browse\">Авторизуйтесь для просмотра контента</string>\n    <string name=\"use_login_for_browse_desc\">Это влияет на отображение контента и, в частности, открывает доступ к премиум-альбомам при входе с премиум-учётной записью</string>\n    <string name=\"action_login\">Войти</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-sk/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">Miestny</string>\n    <string name=\"remote_history\">Vzdialené</string>\n    <string name=\"charts\">Grafy</string>\n    <string name=\"back_button_desc\">Späť</string>\n    <string name=\"album_cover_desc\">Obal albumu</string>\n    <string name=\"top_music_videos\">Najlepšie videá s hudbou</string>\n    <string name=\"trending\">Trendy</string>\n    <string name=\"weeks\">Týždne</string>\n    <string name=\"months\">Mesiace</string>\n    <string name=\"years\">Roky</string>\n    <string name=\"continuous\">Súvislé</string>\n    <string name=\"liked\">Páči sa mi</string>\n    <string name=\"offline\">Stiahnuté</string>\n    <string name=\"my_top\">Moje najlepšie</string>\n    <string name=\"cached_playlist\">Vo vyrovnávacej pamäti</string>\n    <string name=\"sync_playlist\">Synchronizácia playlistu</string>\n    <string name=\"sync_disabled\">Synchronizácia zakázaná</string>\n    <string name=\"allows_for_sync_witch_youtube\">Poznámka: Toto umožňuje synchronizáciu s YouTube Music. Toto sa neskôr NEDÁ zmeniť.</string>\n    <string name=\"generating_image\">Generovanie obrázka</string>\n    <string name=\"please_wait\">Prosím, počkajte</string>\n    <string name=\"cancel\">Zrušiť</string>\n    <string name=\"share_lyrics\">Zdieľať texty piesní</string>\n    <string name=\"share_as_text\">Zdieľať ako text</string>\n    <string name=\"share_as_image\">Zdieľať ako obrázok</string>\n    <string name=\"max_selection_limit\">Maximálny limit výberu</string>\n    <string name=\"share_selected\">Zdieľať vybrané</string>\n    <string name=\"customize_colors\">Prispôsobiť farby</string>\n    <string name=\"text_color\">Farba textu</string>\n    <string name=\"secondary_text_color\">Farba sekundárneho textu</string>\n    <string name=\"background_color\">Farba pozadia</string>\n    <string name=\"remove_from_cache\">Odstrániť z vyrovnávacej pamäte</string>\n    <string name=\"copy_link\">Kopírovať odkaz</string>\n    <string name=\"select\">Vybrať všetko</string>\n    <string name=\"like_all\">Všetko sa mi páči</string>\n    <string name=\"dislike_all\">Nepáči sa mi všetko</string>\n    <string name=\"sort_by_last_updated\">Dátum aktualizácie</string>\n    <string name=\"link_copied\">Odkaz skopírovaný do schránky</string>\n    <string name=\"starting_radio\">Spustenie rádia</string>\n    <string name=\"now_playing\">Práve sa prehráva</string>\n    <string name=\"lyrics\">Text piesne</string>\n    <string name=\"close\">Zatvoriť</string>\n    <string name=\"hide_player_thumbnail\">Skryť miniatúru prehrávača</string>\n    <string name=\"hide_player_thumbnail_desc\">Nahradiť obal albumu logom aplikácie v prehrávači</string>\n    <string name=\"already_in_playlist\">Už v playliste:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d raz</item>\n        <item quantity=\"few\">%d krát</item>\n        <item quantity=\"many\">%d krát</item>\n        <item quantity=\"other\">%d krát</item>\n    </plurals>\n    <string name=\"seek_forward_dynamic\">+%1$d sekundy dopredu</string>\n    <string name=\"seek_backward_dynamic\">%1$d sekúnd dozadu</string>\n    <string name=\"seek_seconds_addup\">Progresívne hľadanie</string>\n    <string name=\"seek_seconds_addup_description\">Ak je povolené, pri každom preskočení vyhľadávania sa pripočíta 5 sekúnd navyše</string>\n    <string name=\"similar_content\">Podobný obsah</string>\n    <string name=\"player_background_style\">Štýl pozadia prehrávača</string>\n    <string name=\"follow_theme\">Podľa témy</string>\n    <string name=\"gradient\">Prechod</string>\n    <string name=\"new_player_design\">Nový dizajn prehrávača</string>\n    <string name=\"new_mini_player_design\">Nový dizajn mini prehrávača</string>\n    <string name=\"player_background_blur\">Rozostrenie</string>\n    <string name=\"player_buttons_style\">Farby tlačidiel prehrávača</string>\n    <string name=\"default_style\">Predvolené</string>\n    <string name=\"enable_swipe_thumbnail\">Povoliť zmenu skladby potiahnutím prstom</string>\n    <string name=\"swipe_song_to_add\">Potiahnutím skladby doľava ju pridáte do frontu alebo doprava ju prehráte ako ďalšiu</string>\n    <string name=\"lyrics_click_change\">Zmena textu po kliknutí</string>\n    <string name=\"lyrics_auto_scroll\">Automatické posúvanie textu piesní</string>\n    <string name=\"lyrics_romanize_japanese\">Romanizácia japonských textov</string>\n    <string name=\"lyrics_romanize_korean\">Romanizácia kórejských textov</string>\n    <string name=\"slim\">Tenký</string>\n    <string name=\"slim_navbar\">Tenký spodný navigačný panel</string>\n    <string name=\"auto_playlists\">Automatické zoznamy skladieb</string>\n    <string name=\"show_liked_playlist\">Zobraziť zoznam skladieb s „Páči sa mi to“</string>\n    <string name=\"show_downloaded_playlist\">Zobraziť zoznam skladieb „Stiahnuté“</string>\n    <string name=\"show_top_playlist\">Zobraziť playlist „Najlepší“</string>\n    <string name=\"show_cached_playlist\">Zobraziť playlist „Uložený vo vyrovnávacej pamäti“</string>\n    <string name=\"advanced_login\">Prihlásenie pomocou tokenu</string>\n    <string name=\"token_hidden\">Klepnutím zobrazíte token</string>\n    <string name=\"token_shown\">Klepnite znova pre kopírovanie alebo úpravu</string>\n    <string name=\"token_adv_login_description\">Toto je POKROČILÁ metóda prihlásenia. Ako alternatívu k webovému portálu môžete priamo zadať alebo aktualizovať svoj prihlasovací token tu. Môže to napríklad zrýchliť prihlásenie na viacerých zariadeniach. Prosím, nepoužívajte</string>\n    <string name=\"yt_sync\">Automatická synchronizácia s účtom</string>\n    <string name=\"more_content\">Viac obsahu</string>\n    <string name=\"general\">Všeobecné</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"default_lib_chips\">Zmeniť predvolený čip knižnice</string>\n    <string name=\"set_quick_picks\">Nastaviť rýchle výbery</string>\n    <string name=\"last_song_listened\">Na základe poslednej počúvanej skladby</string>\n    <string name=\"app_language\">Jazyk aplikácie</string>\n    <string name=\"enable_similar_content\">Povoliť podobný obsah</string>\n    <string name=\"similar_content_desc\">Automaticky pridávať ďalšie podobné skladby po dosiahnutí konca frontu</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"import_online\">Importovať zoznamy skladieb vo formáte „m3u“</string>\n    <string name=\"import_csv\">Importovať zoznamy skladieb vo formáte „csv“</string>\n    <string name=\"playlist_add_local_to_synced_note\">Poznámka: Pridávanie lokálnych skladieb do synchronizovaných/vzdialených zoznamov skladieb nie je podporované. Akákoľvek iná kombinácia je platná</string>\n    <string name=\"auto_download_on_like\">Automatické sťahovanie pri lajkovaní</string>\n    <string name=\"auto_download_on_like_desc\">Automaticky sťahujte skladby, keď sa vám páčia</string>\n    <string name=\"swipe_sensitivity\">Citlivosť potiahnutia prstom v mini prehrávači</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"clear_song_cache_dialog\">Naozaj chcete vymazať všetky skladby uložené vo vyrovnávacej pamäti?</string>\n    <string name=\"clear_image_cache_dialog\">Naozaj chcete vymazať všetky obrázky z vyrovnávacej pamäte?</string>\n    <string name=\"clear_downloads_dialog\">Naozaj chcete vymazať všetky stiahnuté súbory?</string>\n    <string name=\"disable\">Zakázať</string>\n    <string name=\"not_logged_in_youtube\">Nie ste prihlásený/á na YouTube</string>\n    <string name=\"default_links\">Otvoriť podporované odkazy</string>\n    <string name=\"open_app_settings_error\">Nastavenia aplikácie sa nepodarilo otvoriť</string>\n    <string name=\"release_notes\">Poznámky k vydaniu</string>\n    <string name=\"all_time\">Vždy</string>\n    <string name=\"past_24_hours\">Posledných 24 hodín</string>\n    <string name=\"past_week\">Minulý týždeň</string>\n    <string name=\"past_month\">Minulý mesiac</string>\n    <string name=\"past_year\">Minulý rok</string>\n    <string name=\"top_length\">Dĺžka môjho zoznamu najlepších</string>\n    <string name=\"history_duration\">Trvanie histórie</string>\n    <string name=\"information\">Informácie</string>\n    <string name=\"description\">Popis</string>\n    <string name=\"views\">Zhliadnuťia</string>\n    <string name=\"likes\">Lajky</string>\n    <string name=\"dislikes\">Dislajky</string>\n    <string name=\"subscribe\">Odobrať</string>\n    <string name=\"subscribed\">Odoberané</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">1 sekunda</item>\n        <item quantity=\"few\">%d sekundy</item>\n        <item quantity=\"many\">%d sekúnd</item>\n        <item quantity=\"other\">%d sekúnd</item>\n    </plurals>\n    <string name=\"disable_load_more_when_repeat_all\">Zakázať načítanie ďalších položiek pri opakovaní všetkých položiek</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Nenačítavať automaticky ďalšie skladby a podobný obsah, keď je zapnutý režim opakovania všetkých skladieb</string>\n    <string name=\"settings_section_ui\">Rozhranie</string>\n    <string name=\"settings_section_privacy\">Súkromie a bezpečnosť</string>\n    <string name=\"settings_section_player_content\">Prehrávač a obsah</string>\n    <string name=\"settings_section_storage\">Úložisko a dáta</string>\n    <string name=\"settings_section_system\">Systém a informácie</string>\n    <string name=\"config_proxy\">Konfigurácia proxy servera</string>\n    <string name=\"proxy_username\">Používateľské meno proxy servera</string>\n    <string name=\"proxy_password\">Heslo proxy servera</string>\n    <string name=\"enable_authentication\">Povoliť overenie</string>\n    <string name=\"lyrics_romanization_cyrillic\">Cyrilika</string>\n    <string name=\"lyrics_romanize_title\">romanizácia</string>\n    <string name=\"lyrics_romanization\">Romanizácia textov</string>\n    <string name=\"lyrics_romanize_russian\">Romanizovať ruské texty</string>\n    <string name=\"lyrics_romanize_ukrainian\">Romanizovať ukrajinské texty</string>\n    <string name=\"lyrics_romanize_belarusian\">Romanizácia bieloruských textov</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Romanizovať kirgizské texty</string>\n    <string name=\"lyrics_romanize_serbian\">Romanizovať srbské texty</string>\n    <string name=\"lyrics_romanize_bulgarian\">Romanizovať bulharské texty</string>\n    <string name=\"line_by_line_option_title\">EXPERIMENTÁLNE: Zistenie jazyka riadok po riadku</string>\n    <string name=\"line_by_line_option_desc\">Cyrilika bude detekovaná riadok po riadku, nie v celej skladbe.</string>\n    <string name=\"line_by_line_dialog_title\">Si si istý/á?</string>\n    <string name=\"line_by_line_dialog_desc\">Toto je experimentálna funkcia, ktorá sa buď posúva dopredu, alebo dozadu.\\n\\nŠtandardne sa jazyk určuje z celej skladby, ale ak je táto možnosť zapnutá, bude sa určovať riadok po riadku. To umožní fungovanie viacjazyčných skladieb, ALE jazyk nemusí byť vždy správny (napríklad, ak existuje ukrajinský text, ktorý neobsahuje žiadne písmená špecifické pre ukrajinčinu, môže byť namiesto toho romanizovaný ako ruský).\\n\\nAk nemáte problémy, odporúča sa túto možnosť ponechať vypnutú.</string>\n    <string name=\"romanize_current_track\">Romanizovať aktuálnu skladbu</string>\n    <string name=\"show_uploaded_playlist\">Zobraziť zoznam skladieb \\\"Nahrané\\\"</string>\n    <string name=\"uploaded_playlist\">Nahrané</string>\n    <string name=\"filter_uploaded\">Nahrané</string>\n    <string name=\"edit_playlist_cover\">Upraviť obal playlistu</string>\n    <string name=\"edit_playlist_cover_note\">Poznámka: Na zmenu obalu playlistu musí byť váš účet prepojený s telefónnym číslom a overený v službe YouTube Music.</string>\n    <string name=\"edit_playlist_cover_note_wait\">Po výbere obrázka chvíľu počkajte, kým sa nový obal zobrazí vo vašom zozname skladieb.</string>\n    <string name=\"choose_from_library\">Vyberte si z knižnice</string>\n    <string name=\"remove_custom_image\">Odstrániť vlastný obrázok</string>\n    <string name=\"updater\">Aktualizátor</string>\n    <string name=\"check_for_updates\">Automaticky kontrolovať aktualizácie</string>\n    <string name=\"update_notifications\">Povoliť upozornenia na aktualizácie</string>\n    <string name=\"update_available_title\">Aktualizácia je k dispozícii</string>\n    <string name=\"update_channel_name\">Aktualizácie aplikácií</string>\n    <string name=\"update_channel_desc\">Upozornenia na nové verzie</string>\n    <string name=\"audio_offload\">Povoliť odľahčenie</string>\n    <string name=\"audio_offload_description\">Na prehrávanie zvuku použite cestu pre odľahčenie zvuku. Zakázanie tejto funkcie môže zvýšiť spotrebu energie, ale môže byť užitočné, ak máte problémy s prehrávaním zvuku alebo následným spracovaním</string>\n    <string name=\"lyrics_romanize_macedonian\">Romanizovať macedónske texty</string>\n    <string name=\"swipe_song_to_remove\">Prejdením prstom odstráňte skladbu zo zoznamu skladieb</string>\n    <string name=\"discord_use_details\">Použite podrobnosti namiesto štátu</string>\n    <string name=\"discord_use_details_description\">Zobrazovať názov skladby výrazne namiesto mien interpretov</string>\n    <string name=\"integrations\">Integrácie</string>\n    <string name=\"username\">Používateľské meno</string>\n    <string name=\"password\">Heslo</string>\n    <string name=\"lastfm_integration\">Integrácia s Last.fm</string>\n    <string name=\"enable_scrobbling\">Povoliť scrobbling</string>\n    <string name=\"lastfm_now_playing\">Odoslať Práve sa prehráva</string>\n    <string name=\"scrobbling_configuration\">Konfigurácia scrobblingu</string>\n    <string name=\"scrobble_min_track_duration\">Scrobble piesne dlhšie ako</string>\n    <string name=\"scrobble_delay_percent\">Percentuálne oneskorenie Scrobble</string>\n    <string name=\"scrobble_delay_minutes\">Minúty oneskorenia Scrobble</string>\n    <string name=\"last_fm_send_likes\">Odoslať označenia Páči sa mi to/Nepáči sa mi to</string>\n    <string name=\"last_fm_send_likes_description\">Piesne, ktoré sa mi páčia/nepáčia, sa mi páčia na Last.fm, keď sa mi páčia/nepáčia na Metroliste</string>\n    <string name=\"download_playlist_desc\">Stiahnuť všetky skladby na počúvanie offline</string>\n    <string name=\"remove_download_playlist_desc\">Odstrániť všetky stiahnuté skladby z tohto playlistu</string>\n    <string name=\"download_in_progress_desc\">Prebieha sťahovanie</string>\n    <string name=\"share_playlist_desc\">Zdieľ\\'ať tento playlist s ostatnými</string>\n    <string name=\"delete_playlist_desc\">Natrvalo odstrániť tento playlist</string>\n    <string name=\"sync_playlist_desc\">Synchronizácia playlistu s YouTube Music</string>\n    <string name=\"primary_color_style\">Primárna farba</string>\n    <string name=\"tertiary_color_style\">Terciárna farba</string>\n    <string name=\"lyrics_glow_effect\">Povoliť efekt žiariaceho textu lyriky</string>\n    <string name=\"lyrics_glow_effect_desc\">Pridajte k aktívnej lyriky žiarivú animáciu a efekt odrážania</string>\n    <string name=\"enable_better_lyrics\">Povoliť Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">Použiť poskytovateľa Better Lyrics pre texty synchronizované slovíčko po slovíčku</string>\n    <string name=\"auto_scroll\">Znovu synchronizovať</string>\n    <string name=\"shuffle_playlist_first\">Najprv prehrávať playlist/album náhodne</string>\n    <string name=\"shuffle_playlist_first_desc\">Pri náhodnom prehrávaní najprv prehrávaj všetky skladby z pôvodného playlistu/alba, potom podobný obsah</string>\n    <string name=\"show_wrapped_card\">Zobraziť Wrapped kartu</string>\n    <string name=\"lyrics_romanize_chinese\">Prepis čínskych textov do latinky</string>\n    <string name=\"google_cast_description\">Povoliť prehrávanie zvuku na Chromecast a d\\'alšie zariadenia s podporou Cast</string>\n    <string name=\"logging_in\">Prihlasovanie…</string>\n    <string name=\"hide_video_songs\">Skryť skladby z videa</string>\n    <string name=\"details_desc\">Zobraziť informácie o skladbe</string>\n    <string name=\"edit_desc\">Zmeniť názov alebo interpreta</string>\n    <string name=\"start_radio_desc\">Vytvorte stanicu na základe tejto položky</string>\n    <string name=\"play_next_desc\">Pridať na začiatok frontu</string>\n    <string name=\"add_to_queue_desc\">Pridať na koniec frontu</string>\n    <string name=\"add_to_library_desc\">Uložiť do knižnice</string>\n    <string name=\"download_desc\">Sprístupniť na prehrávanie offline</string>\n    <string name=\"add_to_playlist_desc\">Pridať do jedného zo svojich zoznamov skladieb</string>\n    <string name=\"refetch_desc\">Načítajte najnovšie metadáta z YouTube Music</string>\n    <string name=\"share_desc\">Zdieľať odkaz na túto položku</string>\n    <string name=\"delete_desc\">Natrvalo odstrániť túto položku</string>\n    <string name=\"advanced_desc\">Zmeňte tempo a výšku tónu skladby</string>\n    <string name=\"equalizer_desc\">Úprava zvukového ekvalizéra</string>\n    <string name=\"enable_dynamic_icon\">Povoliť dynamickú ikonu</string>\n    <string name=\"mini_player\">Mini-prehrávač</string>\n    <string name=\"pure_black_mini_player\">Čisto čierny mini prehrávač</string>\n    <string name=\"cache_size_warning_title\">Počkajte!</string>\n    <string name=\"cache_size_warning_message\">Zvolili ste limit veľ\\'kosti vyrovnávacej pamäte, ktorý je menší než ten ktorý aplikácia momentálne používa (%1$s). Ak budete pokračovať, aplikácia môže odstrániť niektoré uložené %2$s, aby sa prispôsobila novému limitu. Chcete napriek tomu pokračovať?</string>\n    <string name=\"cache_size_warning_confirm\">Pokračovať</string>\n    <string name=\"lyrics_animation_style\">Štýl animácie slovo po slove</string>\n    <string name=\"none\">Žiadne</string>\n    <string name=\"fade\">Vyblednutie</string>\n    <string name=\"glow\">Žiara</string>\n    <string name=\"slide\">Posunúť</string>\n    <string name=\"karaoke\">Karaoke</string>\n    <string name=\"apple_music_style\">Apple Music</string>\n    <string name=\"lyrics_text_size\">Veľkosť textu lyriky</string>\n    <string name=\"lyrics_line_spacing\">Rozostupy lyriky</string>\n    <string name=\"album_art_for\">Obal albumu pre %s</string>\n    <string name=\"wrapped_total_albums_title\">Vypočuli ste si</string>\n    <string name=\"wrapped_total_albums_subtitle\">jedinečné albumy</string>\n    <string name=\"wrapped_top_album_title\">Váš najlepší album je</string>\n    <string name=\"wrapped_playlist_ready\">Váš osobný playlist je pripravený</string>\n    <string name=\"wrapped_top_5_albums_title\">Vašich 5 top albumov</string>\n    <string name=\"wrapped_album_listening_time\">Tento album ste počúvali %d minút</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d minút</string>\n    <string name=\"wrapped_no_data\">Žiadne údaje</string>\n    <string name=\"wrapped_top_5_artists_title\">Vaši najobľúbenejší interpreti roka</string>\n    <string name=\"wrapped_artist_listening_time\">%d minút</string>\n    <string name=\"wrapped_top_5_songs_title\">Vaše najobľ\\'úbenejšie skladby roka</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">Obal albumu</string>\n    <string name=\"wrapped_top_artist_title\">Váš top interpret roka je</string>\n    <string name=\"wrapped_top_artist_image_content_description\">Obrázok najobľúbenejšieho interpreta</string>\n    <string name=\"wrapped_top_artist_listening_time\">Počúvali ste ich %d minút</string>\n    <string name=\"wrapped_top_song_title\">Tvoja najhranejšia skladba je</string>\n    <string name=\"wrapped_top_song_listening_time\">Počúvali ste %d minút</string>\n    <string name=\"wrapped_total_artists_title\">Vypočuli ste si</string>\n    <string name=\"wrapped_total_artists_subtitle\">Jedineční interpreti</string>\n    <string name=\"wrapped_total_songs_title\">Vypočuli ste si</string>\n    <string name=\"wrapped_total_songs_subtitle\">Jedinečné skladby</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_intro_subtitle\">Je čas zistiť, čo ste počúvali</string>\n    <string name=\"wrapped_intro_button\">poďme!</string>\n    <string name=\"wrapped_logo_content_description\">Metrolist Logo</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"wrapped_ready_title\">VAŠE WRAPPED JE HOTOVÉ!</string>\n    <string name=\"wrapped_ready_subtitle\">Je čas pozrieť sa, čo sa vám tento rok páčilo.</string>\n    <string name=\"wavy\">Vlnitý</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"enable_simpmusic\">Povoliť texty piesní SimpMusic</string>\n    <string name=\"artist_page_settings\">Stránka interpreta</string>\n    <string name=\"show_artist_description\">Zobraziť popis interpreta</string>\n    <string name=\"crop_album_art\">Orezať obal albumu</string>\n    <string name=\"show_artist_monthly_listeners\">Zobraziť mesačných poslucháčov</string>\n    <string name=\"show_more\">Ukázať viac</string>\n    <string name=\"crop_album_art_desc\">Vynútenie štvorcového pomeru strán orezaním miniatúr videa</string>\n    <string name=\"enable_simpmusic_desc\">Pre synchronizované texty piesní použite poskytovateľa textov SimpMusic</string>\n    <string name=\"show_artist_subscriber_count\">Zobraziť počet odberateľov</string>\n    <string name=\"show_less\">Ukázať menej</string>\n    <string name=\"about_artist\">O umelcovi</string>\n    <string name=\"enable\">Povoliť</string>\n    <string name=\"player_background_solid\">Solídny</string>\n    <string name=\"prevent_duplicate_tracks_in_queue\">Zabráňte duplicitným skladbám vo fronte</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">Pri pridávaní skladby do frontu ju odstrániť z jej predchádzajúcej pozície, ak už existuje</string>\n    <string name=\"skip_silence_desc\">Rýchly posun vpred cez tiché časti skladieb</string>\n    <string name=\"skip_silence_instant\">Okamžite preskočte ticho</string>\n    <string name=\"skip_silence_instant_desc\">Preskočte vpred počas tichých chvíľ namiesto zrýchlenia prehrávania</string>\n    <string name=\"persistent_shuffle_title\">Trvalé náhodné prehrávanie</string>\n    <string name=\"persistent_shuffle_desc\">Pri spustení nových skladieb alebo zoznamov skladieb nechajte zapnuté náhodné prehrávanie</string>\n    <string name=\"remember_shuffle_and_repeat\">Zapamätajte si náhodné prehrávanie a opakovanie</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Zapamätajte si režim náhodného prehrávania a opakovania pri reštartovaní aplikácie</string>\n    <string name=\"pause_music_when_media_is_muted\">Pozastaviť hudbu, keď sú médiá stlmené</string>\n    <string name=\"resume_on_bluetooth_connect\">Pokračovať po pripojení Bluetooth</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">Ponechať obrazovku zapnutú, keď je prehrávač rozbalený</string>\n    <string name=\"crossfade\">Prelínanie</string>\n    <string name=\"crossfade_desc\">Prelínanie medzi skladbami</string>\n    <string name=\"crossfade_duration\">Trvanie prelínania</string>\n    <string name=\"crossfade_gapless\">Zakázať pre albumy bez medzier</string>\n    <string name=\"crossfade_gapless_desc\">Nepoužívajte prelínanie, ak je album bez medzier</string>\n    <string name=\"crossfade_beta_title\">Funkcia beta</string>\n    <string name=\"crossfade_beta_message\">Prelínanie je nová funkcia a môže obsahovať chyby. Ak narazíte na nejaké problémy, nahláste ich.\\n\\nTáto funkcia z dôvodu technických obmedzení zakazuje prenos zvuku.</string>\n    <string name=\"lyrics_romanize_hindi\">Romanizovať hindské texty</string>\n    <string name=\"lyrics_romanize_punjabi\">Romanizovať pandžábske texty</string>\n    <string name=\"lyrics_offset\">Text skladby Offset</string>\n    <string name=\"audio_offload_disabled_by_crossfade\">Zakázané, pretože je aktívne prelínanie</string>\n    <string name=\"lyrics_romanize_as_main\">Zobraziť romanizované lyriky ako hlavné</string>\n    <string name=\"hide_youtube_shorts\">Skryť YouTube Shorts</string>\n    <string name=\"wrapped_thank_you\">Ďakujeme za vypočutie</string>\n    <string name=\"wrapped_special_thanks\">Špeciálne poďakovanie MO Agamy za vytvorenie Metrolistu</string>\n    <string name=\"wrapped_close\">Zatvoriť wrapped</string>\n    <string name=\"wrapped_playlist_title\">Vaše %s Wrapped</string>\n    <string name=\"wrapped_create_playlist\">Vytvoriť playlist</string>\n    <string name=\"wrapped_playlist_saved\">Playlist uložený</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"one\">profil</item>\n        <item quantity=\"few\">profily</item>\n        <item quantity=\"many\">profilov</item>\n        <item quantity=\"other\">profilov</item>\n    </plurals>\n    <string name=\"equalizer_header\">Ekvalizér</string>\n    <string name=\"no_profiles\">Žiadne profily ekvalizéra</string>\n    <string name=\"import_profile\">Importovať profil</string>\n    <string name=\"system_equalizer\">Systémový ekvalizér</string>\n    <string name=\"eq_disabled\">Zakázané</string>\n    <plurals name=\"band_count\">\n        <item quantity=\"one\">%d pásmo</item>\n        <item quantity=\"few\">%d pásma</item>\n        <item quantity=\"many\">%d pásiem</item>\n        <item quantity=\"other\">%d pásiem</item>\n    </plurals>\n    <string name=\"delete_profile_desc\">Odstrániť profil</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-sk/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Domov</string>\n    <string name=\"songs\">Piesne</string>\n    <string name=\"artists\">Umelci</string>\n    <string name=\"albums\">Albumy</string>\n    <string name=\"playlists\">Playlisty</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d vybrané</item>\n        <item quantity=\"few\">%d vybrané</item>\n        <item quantity=\"many\">%d vybrané</item>\n        <item quantity=\"other\">%d vybrané</item>\n    </plurals>\n    <string name=\"history\">História</string>\n    <string name=\"stats\">Štatistiky</string>\n    <string name=\"mood_and_genres\">Nálada a žánre</string>\n    <string name=\"account\">Účet</string>\n    <string name=\"quick_picks\">Rýchle výbery</string>\n    <string name=\"quick_picks_empty\">Počúvajte skladby a vygenerujte si rýchle tipy</string>\n    <string name=\"forgotten_favorites\">Zabudnuté obľúbené</string>\n    <string name=\"keep_listening\">Počúvať ďalej</string>\n    <string name=\"your_youtube_playlists\">Vaše playlisty na YouTube</string>\n    <string name=\"similar_to\">Podobné ako</string>\n    <string name=\"new_release_albums\">Nové vydania albumov</string>\n    <string name=\"today\">Dnes</string>\n    <string name=\"yesterday\">Včera</string>\n    <string name=\"this_week\">Tento týždeň</string>\n    <string name=\"last_week\">Minulý týždeň</string>\n    <string name=\"most_played_songs\">Najhranejšie skladby</string>\n    <string name=\"most_played_artists\">Najhranejší interpreti</string>\n    <string name=\"most_played_albums\">Najprehrávanejšie albumy</string>\n    <string name=\"search\">Hľadať</string>\n    <string name=\"search_yt_music\">Vyhľadávať YouTube Music…</string>\n    <string name=\"search_library\">Hľadať v knižnici…</string>\n    <string name=\"filter_library\">Knižnica</string>\n    <string name=\"filter_liked\">Obľúbené</string>\n    <string name=\"filter_downloaded\">Stiahnuté</string>\n    <string name=\"filter_all\">Všetko</string>\n    <string name=\"filter_songs\">Skladby</string>\n    <string name=\"filter_videos\">Videá</string>\n    <string name=\"filter_albums\">Albumy</string>\n    <string name=\"filter_artists\">Interpreti</string>\n    <string name=\"filter_playlists\">Playlisty</string>\n    <string name=\"filter_community_playlists\">Zdieľané playlisty</string>\n    <string name=\"filter_featured_playlists\">Odporúčané playlisty</string>\n    <string name=\"filter_bookmarked\">Označené</string>\n    <string name=\"no_results_found\">Nenašli sa žiadne výsledky</string>\n    <string name=\"library_song_empty\">Knižnica</string>\n    <string name=\"library_artist_empty\">Tu uvidíš umelcov zo svojej knižnice</string>\n    <string name=\"library_album_empty\">Tu uvidíš albumy zo svojej knižnice</string>\n    <string name=\"library_playlist_empty\">Tu uvidíš svoje playlisty</string>\n    <string name=\"from_your_library\">Z tvojej zbierky</string>\n    <string name=\"other_versions\">Ďalšie verzie</string>\n    <string name=\"liked_songs\">Obľúbené skladby</string>\n    <string name=\"downloaded_songs\">Stiahnuté skladby</string>\n    <string name=\"playlist_is_empty\">Playlist je prázdny</string>\n    <string name=\"remove_download_playlist_confirm\">Naozaj chcete odstrániť všetky skladby z playlistu \\\"%s\\\" zo stiahnutých skladieb?</string>\n    <string name=\"delete_playlist_confirm\">Naozaj chcete odstrániť playlist \\\"%s\\\"?</string>\n    <string name=\"retry\">Skúsiť znova</string>\n    <string name=\"radio\">Rádio</string>\n    <string name=\"shuffle\">Prehrávať náhodne</string>\n    <string name=\"reset\">Resetovať</string>\n    <string name=\"details\">Podrobnosti</string>\n    <string name=\"edit\">Upraviť</string>\n    <string name=\"start_radio\">Spustiť rádio</string>\n    <string name=\"play\">Prehrať</string>\n    <string name=\"play_next\">Prehrať ďalšie</string>\n    <string name=\"add_to_queue\">Pridať do fronty</string>\n    <string name=\"add_to_library\">Pridať do knižnice</string>\n    <string name=\"add_all_to_library\">Pridať všetko do knižnice</string>\n    <string name=\"remove_from_library\">Odstrániť z knižnice</string>\n    <string name=\"remove_all_from_library\">Odstrániť všetko z knižnice</string>\n    <string name=\"action_download\">Stiahnuť</string>\n    <string name=\"downloading\">Sťahuje sa</string>\n    <string name=\"remove_download\">Odstrániť stiahnuté</string>\n    <string name=\"import_playlist\">Importovať playlist</string>\n    <string name=\"add_to_playlist\">Pridať do playlistu</string>\n    <string name=\"view_artist\">Zobraziť umelca</string>\n    <string name=\"view_album\">Zobraziť album</string>\n    <string name=\"refetch\">Obnoviť</string>\n    <string name=\"share\">Zdieľať</string>\n    <string name=\"delete\">Vymazať</string>\n    <string name=\"remove_from_history\">Odstrániť z histórie</string>\n    <string name=\"remove_from_playlist\">Odstrániť z playlistu</string>\n    <string name=\"remove_from_queue\">Odstrániť z fronty</string>\n    <string name=\"search_online\">Vyhľadať online</string>\n    <string name=\"action_sync\">Synchronizovať</string>\n    <string name=\"advanced\">Pokročilé</string>\n    <string name=\"tempo_and_pitch\">Tempo a výška tónu</string>\n    <string name=\"sort_by_create_date\">Dátum pridania</string>\n    <string name=\"sort_by_name\">Názov</string>\n    <string name=\"sort_by_artist\">Interpret</string>\n    <string name=\"sort_by_year\">Rok</string>\n    <string name=\"sort_by_song_count\">Počet skladieb</string>\n    <string name=\"sort_by_length\">Dĺžka</string>\n    <string name=\"sort_by_play_time\">Čas prehrávania</string>\n    <string name=\"song_title\">Názov skladby</string>\n    <string name=\"sort_by_custom\">Vlastné poradie</string>\n    <string name=\"media_id\">ID médiá</string>\n    <string name=\"mime_type\">Typ MIME</string>\n    <string name=\"codecs\">Kodeky</string>\n    <string name=\"bitrate\">Rýchlosť prenosu</string>\n    <string name=\"sample_rate\">Vzorkovacia frekvencia</string>\n    <string name=\"loudness\">Hlasitosť</string>\n    <string name=\"volume\">Úroveň hlasitosti</string>\n    <string name=\"file_size\">Veľkosť súboru</string>\n    <string name=\"unknown\">Neznámy</string>\n    <string name=\"copied\">Kopírované do schránky</string>\n    <string name=\"edit_lyrics\">Upraviť text piesne</string>\n    <string name=\"search_lyrics\">Hľadať text piesne</string>\n    <string name=\"edit_song\">Upraviť skladbu</string>\n    <string name=\"song_artists\">Interpreti skladby</string>\n    <string name=\"error_song_title_empty\">Názov skladby nemôže byť prázdny.</string>\n    <string name=\"error_song_artist_empty\">Interpret skladby nesmie byť prázdny.</string>\n    <string name=\"save\">Uložiť</string>\n    <string name=\"choose_playlist\">Vybrať playlist</string>\n    <string name=\"edit_playlist\">Upraviť playlist</string>\n    <string name=\"create_playlist\">Vytvoriť playlist</string>\n    <string name=\"playlist_name\">Názov playlistu</string>\n    <string name=\"error_playlist_name_empty\">Názov playlistu nemôže byť prázdny.</string>\n    <string name=\"edit_artist\">Upraviť interpreta</string>\n    <string name=\"artist_name\">Meno interpreta</string>\n    <string name=\"error_artist_name_empty\">Názov interpreta nesmie byť prázdny.</string>\n    <string name=\"duplicates\">Duplikáty</string>\n    <string name=\"skip_duplicates\">Preskočiť duplikáty</string>\n    <string name=\"add_anyway\">Pridať aj tak</string>\n    <string name=\"duplicates_description_single\">Skladba už je vo vašom playliste</string>\n    <string name=\"duplicates_description_multiple\">%d skladieb je už vo vašom playliste</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d skladba</item>\n        <item quantity=\"few\">%d skladby</item>\n        <item quantity=\"many\">%d skladieb</item>\n        <item quantity=\"other\">%d skladieb</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d interpret</item>\n        <item quantity=\"few\">%d interpreti</item>\n        <item quantity=\"many\">%d interpretov</item>\n        <item quantity=\"other\">%d interpretov</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d album</item>\n        <item quantity=\"few\">%d albumy</item>\n        <item quantity=\"many\">%d albumov</item>\n        <item quantity=\"other\">%d albumov</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d playlist</item>\n        <item quantity=\"few\">%d playlisty</item>\n        <item quantity=\"many\">%d playlistov</item>\n        <item quantity=\"other\">%d playlistov</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d týždeň</item>\n        <item quantity=\"few\">%d týždne</item>\n        <item quantity=\"many\">%d týždňov</item>\n        <item quantity=\"other\">%d týždňov</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d mesiac</item>\n        <item quantity=\"few\">%d mesiace</item>\n        <item quantity=\"many\">%d mesiacov</item>\n        <item quantity=\"other\">%d mesiacov</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d rok</item>\n        <item quantity=\"few\">%d roky</item>\n        <item quantity=\"many\">%d rokov</item>\n        <item quantity=\"other\">%d rokov</item>\n    </plurals>\n    <string name=\"playlist_imported\">Playlist bol importovaný</string>\n    <string name=\"removed_song_from_playlist\">„%s“ bol odstránený z playlistu</string>\n    <string name=\"playlist_synced\">Playlist bol synchronizovaný</string>\n    <string name=\"undo\">Vrátiť späť</string>\n    <string name=\"lyrics_not_found\">Text piesne sa nenašiel</string>\n    <string name=\"sleep_timer\">Časovač vypnutia</string>\n    <string name=\"end_of_song\">Koniec skladby</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">%d minúta</item>\n        <item quantity=\"few\">%d minúty</item>\n        <item quantity=\"many\">%d minút</item>\n        <item quantity=\"other\">%d minút</item>\n    </plurals>\n    <string name=\"error_no_stream\">Žiadny stream nie je k dispozícii</string>\n    <string name=\"error_no_internet\">Bez pripojenia na internet</string>\n    <string name=\"error_timeout\">Vypršal čas</string>\n    <string name=\"error_unknown\">Neznáma chyba</string>\n    <string name=\"action_like\">Páči sa mi</string>\n    <string name=\"action_like_all\">Páči sa mi všetko</string>\n    <string name=\"action_remove_like\">Zrušiť „Páči sa mi“</string>\n    <string name=\"action_remove_like_all\">Zrušiť všetky „Páči sa mi“</string>\n    <string name=\"action_shuffle_on\">Náhodné prehrávanie zapnuté</string>\n    <string name=\"action_shuffle_off\">Náhodné prehrávanie vypnuté</string>\n    <string name=\"repeat_mode_off\">Režim opakovania vypnutý</string>\n    <string name=\"repeat_mode_one\">Opakovať aktuálnu skladbu</string>\n    <string name=\"repeat_mode_all\">Opakovať frontu</string>\n    <string name=\"queue_all_songs\">Všetky skladby</string>\n    <string name=\"queue_searched_songs\">Vyhľadané skladby</string>\n    <string name=\"music_player\">Prehrávač hudby</string>\n    <string name=\"settings\">Nastavenia</string>\n    <string name=\"appearance\">Vzhľad</string>\n    <string name=\"theme\">Téma</string>\n    <string name=\"enable_dynamic_theme\">Povoliť dynamickú tému</string>\n    <string name=\"dark_theme\">Tmavý režim</string>\n    <string name=\"dark_theme_on\">Zapnuté</string>\n    <string name=\"dark_theme_off\">Vypnuté</string>\n    <string name=\"dark_theme_follow_system\">Podľa systému</string>\n    <string name=\"pure_black\">Úplná čierna</string>\n    <string name=\"customize_navigation_tabs\">Prispôsobiť navigačné karty</string>\n    <string name=\"player\">Prehrávač</string>\n    <string name=\"player_text_alignment\">Zarovnanie textu prehrávača</string>\n    <string name=\"lyrics_text_position\">Pozícia textu piesne</string>\n    <string name=\"sided\">Bočné</string>\n    <string name=\"left\">Vľavo</string>\n    <string name=\"center\">Na stred</string>\n    <string name=\"right\">Vpravo</string>\n    <string name=\"player_slider_style\">Štýl posuvníka prehrávača</string>\n    <string name=\"default_\">Predvolené</string>\n    <string name=\"squiggly\">Vlnovkové</string>\n    <string name=\"misc\">Rôzne</string>\n    <string name=\"default_open_tab\">Predvolená otvorená karta</string>\n    <string name=\"grid_cell_size\">Veľkosť bunky mriežky</string>\n    <string name=\"small\">Malé</string>\n    <string name=\"big\">Veľké</string>\n    <string name=\"content\">Kontent</string>\n    <string name=\"action_logout\">Odhlásiť sa</string>\n    <string name=\"action_login\">Prihlásiť sa</string>\n    <string name=\"login\">Prihlásenie</string>\n    <string name=\"not_logged_in\">Nie ste prihlásený</string>\n    <string name=\"login_failed\">Prihlásenie zlyhalo</string>\n    <string name=\"content_language\">Predvolený jazyk obsahu</string>\n    <string name=\"content_country\">Predvolená krajina obsahu</string>\n    <string name=\"system_default\">Predvolené systémom</string>\n    <string name=\"enable_proxy\">Povoliť proxy</string>\n    <string name=\"proxy_type\">Typ proxy</string>\n    <string name=\"proxy_url\">URL proxy</string>\n    <string name=\"restart_to_take_effect\">Pre prejavenie zmien reštartujte</string>\n    <string name=\"player_and_audio\">Prehrávač a zvuk</string>\n    <string name=\"audio_quality\">Kvalita zvuku</string>\n    <string name=\"audio_quality_auto\">Automaticky</string>\n    <string name=\"audio_quality_high\">Vysoká</string>\n    <string name=\"audio_quality_low\">Nízka</string>\n    <string name=\"queue\">Fronta</string>\n    <string name=\"persistent_queue\">Trvalá fronta</string>\n    <string name=\"persistent_queue_desc\">Obnoviť poslednú frontu pri spustení aplikácie</string>\n    <string name=\"auto_load_more\">Automatické načítanie ďalších skladieb</string>\n    <string name=\"auto_load_more_desc\">Automaticky pridávať ďalšie skladby, keď sa dosiahne koniec fronty, ak je to možné</string>\n    <string name=\"skip_silence\">Preskočiť ticho</string>\n    <string name=\"audio_normalization\">Normalizácia zvuku</string>\n    <string name=\"auto_skip_next_on_error\">Automaticky preskočiť na ďalšiu skladbu, keď nastane chyba</string>\n    <string name=\"auto_skip_next_on_error_desc\">Zabezpečte plynulé prehrávanie</string>\n    <string name=\"stop_music_on_task_clear\">Zastaviť hudbu pri vymazaní úlohy</string>\n    <string name=\"equalizer\">Ekvalizér</string>\n    <string name=\"storage\">Úložisko</string>\n    <string name=\"cache\">Medzipamäť</string>\n    <string name=\"image_cache\">Medzipamäť obrázkov</string>\n    <string name=\"song_cache\">Medzipamäť skladieb</string>\n    <string name=\"max_cache_size\">Maximálna veľkosť medzipamäte</string>\n    <string name=\"unlimited\">Neobmedzené</string>\n    <string name=\"clear_all_downloads\">Vymazať všetky stiahnuté súbory</string>\n    <string name=\"max_image_cache_size\">Maximálna veľkosť medzipamäte obrázkov</string>\n    <string name=\"clear_image_cache\">Vymazať medzipamäť obrázkov</string>\n    <string name=\"max_song_cache_size\">Maximálna veľkosť medzipamäte skladieb</string>\n    <string name=\"clear_song_cache\">Vymazať medzipamäť skladieb</string>\n    <string name=\"size_used\">Použité: %s</string>\n    <string name=\"privacy\">Súkromie</string>\n    <string name=\"listen_history\">História počúvania</string>\n    <string name=\"pause_listen_history\">Pozastaviť históriu počúvania</string>\n    <string name=\"clear_listen_history\">Vymazať históriu počúvania</string>\n    <string name=\"clear_listen_history_confirm\">Naozaj chcete vymazať celú históriu počúvania?</string>\n    <string name=\"search_history\">História vyhľadávania</string>\n    <string name=\"pause_search_history\">Pozastaviť históriu vyhľadávania</string>\n    <string name=\"clear_search_history\">Vymazať históriu vyhľadávania</string>\n    <string name=\"clear_search_history_confirm\">Naozaj chcete vymazať celú históriu vyhľadávania?</string>\n    <string name=\"use_login_for_browse\">Použiť prihlásenie na prehliadanie obsahu</string>\n    <string name=\"use_login_for_browse_desc\">Toto môže ovplyvniť, aký obsah uvidíte, napríklad zobrazí len prémiové albumy, ak ste prihlásený pomocou účtu</string>\n    <string name=\"disable_screenshot\">Zakázať snímky obrazovky</string>\n    <string name=\"disable_screenshot_desc\">Keď je táto možnosť zapnutá, snímky obrazovky a zobrazenie aplikácie v nedávnych položkách sú zakázané.</string>\n    <string name=\"enable_lrclib\">Povoliť poskytovateľa textov LrcLib</string>\n    <string name=\"enable_kugou\">Povoliť poskytovateľa textov KuGou</string>\n    <string name=\"hide_explicit\">Skryť nevhodný obsah</string>\n    <string name=\"backup_restore\">Záloha a obnova</string>\n    <string name=\"action_backup\">Zálohovať</string>\n    <string name=\"action_restore\">Obnoviť</string>\n    <string name=\"imported_playlist\">Importovaný zoznam skladieb</string>\n    <string name=\"backup_create_success\">Záloha bola úspešne vytvorená</string>\n    <string name=\"backup_create_failed\">Nepodarilo sa vytvoriť zálohu</string>\n    <string name=\"restore_failed\">Nepodarilo sa obnoviť zálohu</string>\n    <string name=\"discord_integration\">Integrácia Discordu</string>\n    <string name=\"discord_information\">Metrolist používa knižnicu KizzyRPC na nastavenie stavu vášho účtu Discord. To zahŕňa použitie pripojenia Discord Gateway, čo môže byť považované za porušenie TOS Discordu. Zatiaľ však nie sú známe prípady pozastavenia účtov používateľov z tohto dôvodu. Používanie na vlastné riziko.\\n\\nMetrolist bude extrahovať iba váš token, všetko ostatné sa ukladá lokálne.</string>\n    <string name=\"dismiss\">Zatvoriť</string>\n    <string name=\"options\">Možnosťi</string>\n    <string name=\"preview\">Náhľad</string>\n    <string name=\"enable_discord_rpc\">Zapnúť Rich Presence</string>\n    <string name=\"about\">Informácie</string>\n    <string name=\"app_version\">Verzia aplikácie</string>\n    <string name=\"new_version_available\">Nová verzia je dostupná</string>\n    <string name=\"translation_models\">Prekladové modely</string>\n    <string name=\"clear_translation_models\">Jasné modely prekladu</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-sl/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">Lokalni</string>\n    <string name=\"remote_history\">Oddaljeno</string>\n    <string name=\"charts\">Grafikoni</string>\n    <string name=\"back_button_desc\">Nazaj</string>\n    <string name=\"album_cover_desc\">Naslovnica albuma</string>\n    <string name=\"top_music_videos\">Najboljši glasbeni videoposnetki</string>\n    <string name=\"trending\">Trendi</string>\n    <string name=\"weeks\">Tedni</string>\n    <string name=\"months\">Meseci</string>\n    <string name=\"years\">Leta</string>\n    <string name=\"continuous\">Neprekinjeno</string>\n    <string name=\"liked\">Priljubljeni</string>\n    <string name=\"offline\">Preneseno</string>\n    <string name=\"my_top\">Moj top</string>\n    <string name=\"cached_playlist\">Predpomnjeno</string>\n    <string name=\"sync_playlist\">Sinhronizacija seznama predvajanja</string>\n    <string name=\"sync_disabled\">Sinhronizacija onemogočena</string>\n    <string name=\"allows_for_sync_witch_youtube\">Opomba: To omogoča sinhronizacijo z aplikacijo YouTube Music. Tega kasneje NI mogoče spremeniti.</string>\n    <string name=\"generating_image\">Generiranje slike</string>\n    <string name=\"please_wait\">Prosim, počakajte</string>\n    <string name=\"cancel\">Prekliči</string>\n    <string name=\"share_lyrics\">Deli besedila pesmi</string>\n    <string name=\"share_as_text\">Deli kot besedilo</string>\n    <string name=\"share_as_image\">Deli kot sliko</string>\n    <string name=\"max_selection_limit\">Maksimalna meja izbire</string>\n    <string name=\"share_selected\">Deli izbrano</string>\n    <string name=\"customize_colors\">Prilagajanje barv</string>\n    <string name=\"text_color\">Barva besedila</string>\n    <string name=\"secondary_text_color\">Barva sekundarnega besedila</string>\n    <string name=\"background_color\">Barva ozadja</string>\n    <string name=\"remove_from_cache\">Odstrani iz predpomnilnika</string>\n    <string name=\"copy_link\">Kopiraj povezavo</string>\n    <string name=\"select\">Izberi vse</string>\n    <string name=\"like_all\">Všečkaj vse</string>\n    <string name=\"dislike_all\">Označiti vse kot nepriljubljeno</string>\n    <string name=\"sort_by_last_updated\">Datum posodobitve</string>\n    <string name=\"link_copied\">Povezava kopirana v odložišče</string>\n    <string name=\"lyrics\">Besedila</string>\n    <string name=\"already_in_playlist\">Že v seznamu predvajanja:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%dkrat</item>\n        <item quantity=\"two\">%dkrat</item>\n        <item quantity=\"few\">%dkrat</item>\n        <item quantity=\"other\">%dkrat</item>\n    </plurals>\n    <string name=\"similar_content\">Podobna vsebina</string>\n    <string name=\"player_background_style\">Slog ozadja predvajalnika</string>\n    <string name=\"follow_theme\">Sledi temi</string>\n    <string name=\"gradient\">Gradient</string>\n    <string name=\"new_player_design\">Nova oblika predvajalnika</string>\n    <string name=\"player_background_blur\">Zameglitev</string>\n    <string name=\"player_buttons_style\">Barve gumbov predvajalnika</string>\n    <string name=\"default_style\">Privzeto</string>\n    <string name=\"enable_swipe_thumbnail\">Omogoči poteg za menjavo skladbe</string>\n    <string name=\"swipe_song_to_add\">Če želite pesem dodati v čakalno vrsto, jo podrsnite v levo, če jo želite predvajati naslednjo, pa v desno</string>\n    <string name=\"lyrics_click_change\">Spremeni besedila ob kliku</string>\n    <string name=\"lyrics_auto_scroll\">Samodejno pomikanje besedil</string>\n    <string name=\"lyrics_romanize_japanese\">Romaniziraj japonska besedila</string>\n    <string name=\"lyrics_romanize_korean\">Romaniziraj korejska besedila</string>\n    <string name=\"slim\">Tanek</string>\n    <string name=\"slim_navbar\">Tanka spodnja navigacijska vrstica</string>\n    <string name=\"auto_playlists\">Avtomatski seznami predvajanja</string>\n    <string name=\"show_liked_playlist\">Prikaži seznam predvajanja \\\"Všeč mi je\\\"</string>\n    <string name=\"show_downloaded_playlist\">Prikaži seznam predvajanja \\\"Preneseno\\\"</string>\n    <string name=\"show_top_playlist\">Prikaži seznam predvajanja \\\"Top\\\"</string>\n    <string name=\"show_cached_playlist\">Prikaži seznam predvajanja \\\"Predpomnjeno\\\"</string>\n    <string name=\"advanced_login\">Prijava s tokenom</string>\n    <string name=\"token_hidden\">Tapnite za prikaz tokena</string>\n    <string name=\"token_shown\">Ponovno tapnite za kopiranje ali urejanje</string>\n    <string name=\"token_adv_login_description\">To je NAPREDEN način prijave. Namesto spletnega portala lahko tukaj neposredno vnesete ali posodobite svoj prijavni token. S tem lahko na primer pospešite prijavo v več napravah. Upoštevajte, da vsi neveljavni formati tokenov, ki jih aplikacija ne uspe razčleniti, ne bodo sprejeti</string>\n    <string name=\"yt_sync\">Samodejna sinhronizacija z računom</string>\n    <string name=\"more_content\">Več vsebine</string>\n    <string name=\"general\">Splošno</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"default_lib_chips\">Sprememba privzetega knjižničnega čipa</string>\n    <string name=\"set_quick_picks\">Prilagajanje hitrih izbir</string>\n    <string name=\"last_song_listened\">Na podlagi zadnje poslušane pesmi</string>\n    <string name=\"app_language\">Jezik aplikacije</string>\n    <string name=\"enable_similar_content\">Omogoči podobno vsebino</string>\n    <string name=\"similar_content_desc\">Samodejno dodajanje podobnih skladb, ko je dosežen konec čakalne vrste</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"import_online\">Uvoz seznamov predvajanja \\\"m3u\\\"</string>\n    <string name=\"import_csv\">Uvoz seznamov predvajanja \\\"csv\\\"</string>\n    <string name=\"playlist_add_local_to_synced_note\">Opomba: Dodajanje lokalnih skladb na sinhronizirane/oddaljene sezname predvajanja ni podprto. Vsaka druga kombinacija je veljavna</string>\n    <string name=\"auto_download_on_like\">Samodejno prenašanje ob všečkanju</string>\n    <string name=\"auto_download_on_like_desc\">Samodejno prenašanje pesmi ob všečkanju</string>\n    <string name=\"swipe_sensitivity\">Občutljivost premikanja mini predvajalnika</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"clear_song_cache_dialog\">Ali ste prepričani, da želite počistiti vse pesmi v predpomnilniku?</string>\n    <string name=\"clear_image_cache_dialog\">Ali ste prepričani, da želite počistiti vse slike v predpomnilniku?</string>\n    <string name=\"clear_downloads_dialog\">Ali ste prepričani, da želite počistiti vse prenose?</string>\n    <string name=\"disable\">Onemogoči</string>\n    <string name=\"not_logged_in_youtube\">Niste prijavljeni na YouTube</string>\n    <string name=\"default_links\">Odpri podprte povezave</string>\n    <string name=\"open_app_settings_error\">Ni bilo mogoče odpreti nastavitev aplikacije</string>\n    <string name=\"release_notes\">Opombe ob izdaji</string>\n    <string name=\"all_time\">Ves čas</string>\n    <string name=\"past_24_hours\">Zadnjih 24 ur</string>\n    <string name=\"past_week\">Pretekli teden</string>\n    <string name=\"past_month\">Pretekli mesec</string>\n    <string name=\"past_year\">Preteklo leto</string>\n    <string name=\"top_length\">Dolžina mojega seznama Top</string>\n    <string name=\"history_duration\">Trajanje zgodovine</string>\n    <string name=\"information\">Informacija</string>\n    <string name=\"description\">Opis</string>\n    <string name=\"views\">Ogledi</string>\n    <string name=\"likes\">Všečki</string>\n    <string name=\"dislikes\">Ne Všeč</string>\n    <string name=\"subscribe\">Naroči se</string>\n    <string name=\"subscribed\">Naročeni ste</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">%d sekunda</item>\n        <item quantity=\"two\">%d sekundi</item>\n        <item quantity=\"few\">%d sekunde</item>\n        <item quantity=\"other\">%d sekund</item>\n    </plurals>\n    <string name=\"new_mini_player_design\">Nova oblika mini predvajalnika</string>\n    <string name=\"now_playing\">Igra zdaj</string>\n    <string name=\"close\">Zapri</string>\n    <string name=\"hide_player_thumbnail\">Skrij predogled predvajalnika</string>\n    <string name=\"hide_player_thumbnail_desc\">Zamenjaj podobo albuma z logotipom aplikacije v predvajalniku</string>\n    <string name=\"seek_forward_dynamic\">+%1$d sekund naprej</string>\n    <string name=\"seek_backward_dynamic\">-%1$d sekund nazaj</string>\n    <string name=\"seek_seconds_addup\">Napredno iskanje</string>\n    <string name=\"seek_seconds_addup_description\">Če je omogočeno, se ob vsakem preskoku časa pri iskanju postopoma doda dodatnih 5 sekund</string>\n    <string name=\"disable_load_more_when_repeat_all\">Onemogoči samodejno nalaganje, ko je vklopljeno ponavljanje vseh pesmi</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Ne nalagaj samodejno več pesmi in podobne vsebine, ko je vklopljen način ponavljanja vseh</string>\n    <string name=\"uploaded_playlist\">Naloženo</string>\n    <string name=\"filter_uploaded\">Naloženo</string>\n    <string name=\"about_artist\">O izvajalcu</string>\n    <string name=\"show_more\">Prikaži več</string>\n    <string name=\"show_less\">Prikaži manj</string>\n    <string name=\"artist_page_settings\">Stran izvajalca</string>\n    <string name=\"show_artist_description\">Prikaži opis izvajalca</string>\n    <string name=\"show_artist_subscriber_count\">Prikaži število naročnikov</string>\n    <string name=\"show_artist_monthly_listeners\">Prikaži število mesečnih poslušalcev</string>\n    <string name=\"download_playlist_desc\">Prenesi vse pesmi za predvajanje brez povezave</string>\n    <string name=\"remove_download_playlist_desc\">Odstrani vse prenesene pesmi s tega seznama predvajanja</string>\n    <string name=\"download_in_progress_desc\">Prenos je v teku</string>\n    <string name=\"share_playlist_desc\">Deli ta seznam predvajanja z drugimi</string>\n    <string name=\"delete_playlist_desc\">Trajno odstrani ta seznam predvajanja</string>\n    <string name=\"sync_playlist_desc\">Sinhroniziraj seznam predvajanja z YouTube Music</string>\n    <string name=\"starting_radio\">Zaganjanje radia</string>\n    <string name=\"crop_album_art\">Obreži sliko albuma</string>\n    <string name=\"crop_album_art_desc\">Vsili kvadratno razmerje stranic z obrezovanjem sličic videoposnetkov</string>\n    <string name=\"primary_color_style\">Primarna barva</string>\n    <string name=\"tertiary_color_style\">Terciarna barva</string>\n    <string name=\"wavy\">Valovit</string>\n    <string name=\"swipe_song_to_remove\">Podrsaj pesem, da jo odstraniš s seznama predvajanja</string>\n    <string name=\"lyrics_glow_effect\">Omogoči svetleč učinek besedila</string>\n    <string name=\"lyrics_glow_effect_desc\">Dodaj svetlečo animacijo in učinek poskakovanja aktivnemu besedilu</string>\n    <string name=\"enable_better_lyrics\">Omogoči Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">Uporabi ponudnika Better Lyrics za besedila, sinhronizirana po besedah</string>\n    <string name=\"enable_simpmusic\">Omogoči SimpMusic Lyrics</string>\n    <string name=\"enable_simpmusic_desc\">Uporabi ponudnika SimpMusic Lyrics za sinhronizirana besedila</string>\n    <string name=\"auto_scroll\">Ponovno sinhroniziraj</string>\n    <string name=\"show_uploaded_playlist\">Prikaži seznam predvajanja \\\"Naloženo\\\"</string>\n    <string name=\"shuffle_playlist_first\">Najprej premešaj seznam predvajanja/album</string>\n    <string name=\"shuffle_playlist_first_desc\">Pri mešanju najprej predvajaj vse pesmi z izvirnega seznama predvajanja/albuma, nato pa podobno vsebino</string>\n    <string name=\"show_wrapped_card\">Prikaži kartico Wrapped</string>\n    <string name=\"skip_silence_desc\">Hitro previjaj skozi tihe dele pesmi</string>\n    <string name=\"skip_silence_instant\">Takoj preskoči tišino</string>\n    <string name=\"skip_silence_instant_desc\">Preskoči naprej med tihimi trenutki namesto pospeševanja predvajanja</string>\n    <string name=\"edit_playlist_cover\">Uredi naslovnico seznama predvajanja</string>\n    <string name=\"settings_section_ui\">Vmesnik</string>\n    <string name=\"settings_section_privacy\">Zasebnost in varnost</string>\n    <string name=\"update_channel_desc\">Obvestila o novih različicah</string>\n    <string name=\"discord_use_details\">Uporabi podrobnosti namesto statusa</string>\n    <string name=\"discord_use_details_description\">Izpostavi naslov pesmi namesto izvajalca</string>\n    <string name=\"integrations\">Integracije</string>\n    <string name=\"username\">Uporabniško ime</string>\n    <string name=\"password\">Geslo</string>\n    <string name=\"lastfm_integration\">Integracija z Last.fm</string>\n    <string name=\"enable_scrobbling\">Omogoči beleženje</string>\n    <string name=\"lastfm_now_playing\">Pošlji status trenutnega predvajanja</string>\n    <string name=\"scrobbling_configuration\">Nastavitve beleženja</string>\n    <string name=\"scrobble_min_track_duration\">Beleži pesmi, daljše od</string>\n    <string name=\"scrobble_delay_percent\">Odstotek zakasnitve beleženja</string>\n    <string name=\"scrobble_delay_minutes\">Zakasnitev beleženja v minutah</string>\n    <string name=\"last_fm_send_likes\">Pošlji všečke/nevšečke</string>\n    <string name=\"last_fm_send_likes_description\">Dodaj/odstrani pesmi med priljubljenimi na Last.fm, ko jih všečkaš/odvšečkaš v Metrolistu</string>\n    <string name=\"lyrics_romanize_chinese\">Romaniziraj kitajska besedila</string>\n    <string name=\"edit_playlist_cover_note\">Opomba: Vaš račun mora biti povezan s telefonsko številko in preverjen na YouTube Music, da lahko spremenite naslovnico seznama predvajanja.</string>\n    <string name=\"settings_section_player_content\">Predvajalnik in vsebina</string>\n    <string name=\"settings_section_storage\">Shramba in podatki</string>\n    <string name=\"settings_section_system\">Sistem in o programu</string>\n    <string name=\"config_proxy\">Konfiguriraj posredniški strežnik (proxy)</string>\n    <string name=\"proxy_username\">Uporabniško ime za posredniški strežnik (proxy)</string>\n    <string name=\"proxy_password\">Geslo posredniškega strežnika (proxy)</string>\n    <string name=\"enable_authentication\">Omogoči preverjanje pristnosti</string>\n    <string name=\"lyrics_romanization_cyrillic\">Cirilica</string>\n    <string name=\"lyrics_romanize_title\">Romanizacija</string>\n    <string name=\"lyrics_romanization\">Romanizacija besedil</string>\n    <string name=\"lyrics_romanize_russian\">Romaniziraj ruska besedila</string>\n    <string name=\"lyrics_romanize_ukrainian\">Romaniziraj ukrajinska besedila</string>\n    <string name=\"lyrics_romanize_belarusian\">Romaniziraj beloruska besedila</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Romaniziraj kirgiška besedila</string>\n    <string name=\"lyrics_romanize_serbian\">Romaniziraj srbska besedila</string>\n    <string name=\"lyrics_romanize_bulgarian\">Romaniziraj bolgarska besedila</string>\n    <string name=\"line_by_line_option_title\">EKSPERIMENTALNO: Zaznaj jezik po vrsticah</string>\n    <string name=\"line_by_line_option_desc\">Jezik v cirilici bo zaznan po vrsticah namesto na podlagi celotne pesmi.</string>\n    <string name=\"line_by_line_dialog_title\">Ali ste prepričani?</string>\n    <string name=\"line_by_line_dialog_desc\">To je eksperimentalna funkcija.\\n\\nPrivzeto se jezik določi na podlagi celotne pesmi, z vklopom te možnosti pa se bo določil po vrsticah. To bo omogočilo delovanje večjezičnih pesmi, VENDAR jezik morda ne bo vedno pravilen (na primer: če ukrajinsko besedilo ne vsebuje črk, značilnih za ukrajinščino, se lahko romanizira kot ruščina).\\n\\nČe nimate težav, priporočamo, da to možnost pustite izklopljeno.</string>\n    <string name=\"romanize_current_track\">Romaniziraj trenutno skladbo</string>\n    <string name=\"edit_playlist_cover_note_wait\">Po izbiri slike počakajte trenutek, da se na seznamu predvajanja prikaže nova naslovnica.</string>\n    <string name=\"audio_offload\">Omogoči razbremenitev</string>\n    <string name=\"audio_offload_description\">Za predvajanje zvoka uporabi razbremenjeno zvočno pot. Izklop te možnosti lahko poveča porabo energije, vendar je lahko koristen, če imate težave s predvajanjem zvoka ali z naknadno obdelavo</string>\n    <string name=\"choose_from_library\">Izberi iz knjižnice</string>\n    <string name=\"remove_custom_image\">Odstrani naslovnico po meri</string>\n    <string name=\"lyrics_romanize_macedonian\">Romaniziraj makedonska besedila</string>\n    <string name=\"updater\">Posodobitve</string>\n    <string name=\"check_for_updates\">Samodejno preverjanje posodobitev</string>\n    <string name=\"update_notifications\">Omogoči obvestila o posodobitvah</string>\n    <string name=\"update_available_title\">Na voljo je posodobitev</string>\n    <string name=\"update_channel_name\">Posodobitve aplikacije</string>\n    <string name=\"hide_video_songs\">Skrij videospote</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"google_cast_description\">Omogoči predvajanje zvoka na Chromecast in druge naprave s funkcijo Cast</string>\n    <string name=\"details_desc\">Prikaži podatke o skladbi</string>\n    <string name=\"edit_desc\">Spremeni naslov ali izvajalca</string>\n    <string name=\"start_radio_desc\">Ustvari radijsko postajo na podlagi tega elementa</string>\n    <string name=\"play_next_desc\">Dodaj na vrh čakalne vrste</string>\n    <string name=\"add_to_queue_desc\">Dodaj na konec čakalne vrste</string>\n    <string name=\"add_to_library_desc\">Shrani v knjižnico</string>\n    <string name=\"download_desc\">Omogoči predvajanje brez povezave</string>\n    <string name=\"add_to_playlist_desc\">Dodaj na enega od seznamov predvajanja</string>\n    <string name=\"refetch_desc\">Pridobi najnovejše metapodatke iz YouTube Music</string>\n    <string name=\"share_desc\">Deli povezavo do tega elementa</string>\n    <string name=\"delete_desc\">Trajno odstrani ta element</string>\n    <string name=\"advanced_desc\">Spremeni tempo in višino tona skladbe</string>\n    <string name=\"equalizer_desc\">Prilagodi izenačevalnik zvoka</string>\n    <string name=\"enable_dynamic_icon\">Omogoči dinamično ikono</string>\n    <string name=\"mini_player\">Mini predvajalnik</string>\n    <string name=\"pure_black_mini_player\">Popolnoma črn mini predvajalnik</string>\n    <string name=\"cache_size_warning_title\">Pozor!</string>\n    <string name=\"cache_size_warning_message\">Izbrali ste omejitev velikosti predpomnilnika, ki je manjša od tega, kar aplikacija trenutno uporablja (%1$s). Če nadaljujete, lahko aplikacija odstrani nekaj predpomnjenih %2$s, da bo ustrezala novi omejitvi. Želite vseeno nadaljevati?</string>\n    <string name=\"cache_size_warning_confirm\">Nadaljuj</string>\n    <string name=\"logging_in\">Prijavljanje…</string>\n    <string name=\"lyrics_animation_style\">Slog animacije besedo za besedo</string>\n    <string name=\"none\">Brez</string>\n    <string name=\"fade\">Bledenje</string>\n    <string name=\"glow\">Sij</string>\n    <string name=\"slide\">Drsenje</string>\n    <string name=\"karaoke\">Karaoke</string>\n    <string name=\"apple_music_style\">Apple Music</string>\n    <string name=\"lyrics_text_size\">Velikost besedila</string>\n    <string name=\"lyrics_line_spacing\">Razmik med vrsticami besedila</string>\n    <string name=\"album_art_for\">Naslovnica albuma za %s</string>\n    <string name=\"wrapped_total_albums_title\">Poslušali ste</string>\n    <string name=\"wrapped_total_albums_subtitle\">edinstvenih albumov</string>\n    <string name=\"wrapped_top_album_title\">Vaš najboljši album je</string>\n    <string name=\"wrapped_playlist_ready\">Vaš osebni seznam predvajanja je pripravljen</string>\n    <string name=\"wrapped_top_5_albums_title\">Vaših najboljših 5 albumov</string>\n    <string name=\"wrapped_album_listening_time\">Ta album ste poslušali %d minut</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d minut</string>\n    <string name=\"wrapped_no_data\">Ni podatkov</string>\n    <string name=\"wrapped_top_5_artists_title\">Vaši najboljši izvajalci leta</string>\n    <string name=\"wrapped_artist_listening_time\">%d minut</string>\n    <string name=\"wrapped_top_5_songs_title\">Tvoje najboljše skladbe leta</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">Naslovnica albuma</string>\n    <string name=\"wrapped_top_artist_title\">Tvoj najboljši izvajalec leta je</string>\n    <string name=\"wrapped_top_artist_image_content_description\">Slika najboljšega izvajalca</string>\n    <string name=\"wrapped_top_artist_listening_time\">Poslušal si ga %d minut</string>\n    <string name=\"wrapped_top_song_title\">Tvoja največkrat predvajana skladba je</string>\n    <string name=\"wrapped_top_song_listening_time\">Poslušal si jo %d minut</string>\n    <string name=\"wrapped_total_artists_title\">Poslušal si</string>\n    <string name=\"wrapped_total_artists_subtitle\">različnih izvajalcev</string>\n    <string name=\"casting_to\">Predvajanje na %s</string>\n    <string name=\"progress_percent\">Napredek: %s%%</string>\n    <string name=\"listening_to_metrolist\">Poslušaš Metrolist</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-sl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Dom</string>\n    <string name=\"songs\">Skladbe</string>\n    <string name=\"artists\">Izvajalci</string>\n    <string name=\"albums\">Albumi</string>\n    <string name=\"playlists\">Seznami predavanja</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d izbran</item>\n        <item quantity=\"two\">%d izbrana</item>\n        <item quantity=\"few\">%d izbrani</item>\n        <item quantity=\"other\">%d izbranih</item>\n    </plurals>\n    <string name=\"history\">Zgodovina</string>\n    <string name=\"stats\">Statistika</string>\n    <string name=\"mood_and_genres\">Razpoloženje in žanri</string>\n    <string name=\"account\">Račun</string>\n    <string name=\"quick_picks\">Hitri izbori</string>\n    <string name=\"quick_picks_empty\">Poslušajte skladbe in ustvarite hitre izbire</string>\n    <string name=\"forgotten_favorites\">Pozabljene priljubljene</string>\n    <string name=\"your_youtube_playlists\">Vaši seznami predvajanja YouTube</string>\n    <string name=\"similar_to\">Podobno kot</string>\n    <string name=\"new_release_albums\">Novi izdani albumi</string>\n    <string name=\"today\">Danes</string>\n    <string name=\"yesterday\">Včeraj</string>\n    <string name=\"this_week\">Ta teden</string>\n    <string name=\"last_week\">Prejšnji teden</string>\n    <string name=\"most_played_songs\">Najbolj predvajane skladbe</string>\n    <string name=\"most_played_artists\">Najbolj predvajani izvajalci</string>\n    <string name=\"most_played_albums\">Najbolj predvajani albumi</string>\n    <string name=\"search\">Išči</string>\n    <string name=\"search_yt_music\">Iskanje v YouTube Music…</string>\n    <string name=\"search_library\">Iskanje v knjižnici…</string>\n    <string name=\"filter_library\">Knjižnica</string>\n    <string name=\"filter_liked\">Všečkano</string>\n    <string name=\"filter_downloaded\">Preneseno</string>\n    <string name=\"filter_all\">Vse</string>\n    <string name=\"keep_listening\">Nadaljuj z poslušanjem</string>\n    <string name=\"filter_songs\">Glasbe</string>\n    <string name=\"filter_videos\">Video</string>\n    <string name=\"filter_albums\">Albumi</string>\n    <string name=\"filter_artists\">Izvajalci</string>\n    <string name=\"filter_playlists\">Seznami predvajanja</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-sv/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"lyrics_auto_scroll\">Skrolla låttexter automatiskt</string>\n    <string name=\"token_shown\">Tryck igen för att kopiera eller redigera</string>\n    <string name=\"default_lib_chips\">Byt förvalt bibliotekschip</string>\n    <string name=\"general\">Allmänt</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"set_quick_picks\">Ställ in snabbval</string>\n    <string name=\"last_song_listened\">Baserat på den senast lyssnade låten</string>\n    <string name=\"app_language\">Appspråk</string>\n    <string name=\"similar_content_desc\">Lägg automatiskt till liknande låtar när kön tar slut</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"auto_download_on_like\">Ladda ner automatiskt vid gilla</string>\n    <string name=\"history_duration\">Historikens längd</string>\n    <string name=\"enable_similar_content\">Aktivera liknande innehåll</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">1 sekund</item>\n        <item quantity=\"other\">%d sekunder</item>\n    </plurals>\n    <string name=\"import_online\">Importera M3U-spellista</string>\n    <string name=\"auto_download_on_like_desc\">Ladda automatiskt ner låtar när du gillar dem</string>\n    <string name=\"open_app_settings_error\">Kunde inte öppna appinställningar</string>\n    <string name=\"past_24_hours\">Senaste 24 timmarna</string>\n    <string name=\"all_time\">All tid</string>\n    <string name=\"information\">Information</string>\n    <string name=\"description\">Beskrivning</string>\n    <string name=\"views\">Visningar</string>\n    <string name=\"likes\">Gillningar</string>\n    <string name=\"dislikes\">Ogillningar</string>\n    <string name=\"background_color\">Bakgrundsfärg</string>\n    <string name=\"share_as_text\">Dela som text</string>\n    <string name=\"show_liked_playlist\">Visa spellistan \\\"Gillade\\\"</string>\n    <string name=\"share_as_image\">Dela som bild</string>\n    <string name=\"top_length\">Längd på min topplista</string>\n    <string name=\"show_downloaded_playlist\">Visa spellistan \\\"Nedladdade\\\"</string>\n    <string name=\"past_week\">Senaste veckan</string>\n    <string name=\"import_csv\">Importera \\\"csv\\\" spellista</string>\n    <string name=\"clear_song_cache_dialog\">Är du säker på att du vill rensa alla cachade låtar?</string>\n    <string name=\"token_adv_login_description\">Detta är en AVANCERAD inloggningsmetod. Som ett alternativ till webbportalen kan du ange eller uppdatera din inloggningstoken här. Detta kan till exempel göra det snabbare att logga in på flera enheter. Observera att ogiltiga token-format som appen inte kan tolka kommer att nekas</string>\n    <string name=\"playlist_add_local_to_synced_note\">Obs! Att lägga till lokala låtar i synkade/fjärrspellistor stöds inte. Alla andra kombinationer fungerar</string>\n    <string name=\"past_month\">Senaste månaden</string>\n    <string name=\"past_year\">Senaste året</string>\n    <string name=\"back_button_desc\">Tillbaka</string>\n    <string name=\"local_history\">Lokal</string>\n    <string name=\"remote_history\">Fjärrhistorik</string>\n    <string name=\"charts\">Listor</string>\n    <string name=\"album_cover_desc\">Skivomslag</string>\n    <string name=\"trending\">Populärt just nu</string>\n    <string name=\"weeks\">Veckor</string>\n    <string name=\"months\">Månader</string>\n    <string name=\"years\">År</string>\n    <string name=\"continuous\">Oavbruten</string>\n    <string name=\"offline\">Nedladdade</string>\n    <string name=\"liked\">Gillade</string>\n    <string name=\"my_top\">Mina topp</string>\n    <string name=\"cached_playlist\">Cachade</string>\n    <string name=\"sync_playlist\">Synka spellista</string>\n    <string name=\"sync_disabled\">Synkning inaktiverad</string>\n    <string name=\"allows_for_sync_witch_youtube\">Obs! Det här möjliggör synkronisering med YouTube Music. Det går INTE att ångra i efterhand.</string>\n    <string name=\"generating_image\">Skapar bild</string>\n    <string name=\"please_wait\">Vänligen vänta</string>\n    <string name=\"cancel\">Avbryt</string>\n    <string name=\"share_lyrics\">Dela låttexter</string>\n    <string name=\"max_selection_limit\">Maximalt antal val</string>\n    <string name=\"share_selected\">Dela valda</string>\n    <string name=\"customize_colors\">Anpassa färger</string>\n    <string name=\"text_color\">Textfärg</string>\n    <string name=\"secondary_text_color\">Sekundär textfärg</string>\n    <string name=\"top_music_videos\">Populära musikvideor</string>\n    <string name=\"slim_navbar\">Dölj texter i nedersta menyn</string>\n    <string name=\"auto_playlists\">Automatiska spellistor</string>\n    <string name=\"remove_from_cache\">Ta bort från cache</string>\n    <string name=\"copy_link\">Kopiera länk</string>\n    <string name=\"select\">Välj alla</string>\n    <string name=\"like_all\">Gilla alla</string>\n    <string name=\"dislike_all\">Ogilla alla</string>\n    <string name=\"sort_by_last_updated\">Uppdateringsdatum</string>\n    <string name=\"link_copied\">Länk kopierad</string>\n    <string name=\"lyrics\">Låttext</string>\n    <string name=\"already_in_playlist\">Redan i spellistan:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d gång</item>\n        <item quantity=\"other\">%d gånger</item>\n    </plurals>\n    <string name=\"similar_content\">Liknande innehåll</string>\n    <string name=\"player_background_style\">Utseende på spelarens bakgrund</string>\n    <string name=\"follow_theme\">Följ tema</string>\n    <string name=\"gradient\">Färgövergång</string>\n    <string name=\"player_background_blur\">Suddighet</string>\n    <string name=\"player_buttons_style\">Färger på spelarens knappar</string>\n    <string name=\"default_style\">Standard</string>\n    <string name=\"enable_swipe_thumbnail\">Aktivera svepning för att byta låt</string>\n    <string name=\"swipe_song_to_add\">Svep låten till vänster för att lägga den i kön eller till höger för att spela den härnäst</string>\n    <string name=\"lyrics_click_change\">Ändra låttext genom att klicka</string>\n    <string name=\"slim\">Smal</string>\n    <string name=\"show_top_playlist\">Visa spellistan \\\"Topp\\\"</string>\n    <string name=\"show_cached_playlist\">Visa spellista \\\"Cachade\\\"</string>\n    <string name=\"advanced_login\">Avancerad inloggning (token)</string>\n    <string name=\"token_hidden\">Tryck för att visa token</string>\n    <string name=\"default_links\">Öppna supporterade länkar</string>\n    <string name=\"clear_downloads_dialog\">Är du säker på att du vill rensa alla nedladdningar?</string>\n    <string name=\"not_logged_in_youtube\">Inte inloggad på YouTube</string>\n    <string name=\"release_notes\">Versionsinfo</string>\n    <string name=\"new_player_design\">Ny spelar design</string>\n    <string name=\"new_mini_player_design\">Ny mini spelar design</string>\n    <string name=\"uploaded_playlist\">Upladdad</string>\n    <string name=\"filter_uploaded\">Upladdad</string>\n    <string name=\"about_artist\">Om artisten</string>\n    <string name=\"show_more\">Visa mer</string>\n    <string name=\"show_less\">Visa mindre</string>\n    <string name=\"artist_page_settings\">Artistens sida</string>\n    <string name=\"show_artist_description\">Visa information om artisten</string>\n    <string name=\"show_artist_subscriber_count\">Visa följare</string>\n    <string name=\"show_artist_monthly_listeners\">Visa månatliga lysnare</string>\n    <string name=\"download_playlist_desc\">Ladda ner alla låtar för att lyssna offline</string>\n    <string name=\"remove_download_playlist_desc\">Ta bort alla laddade låtar fråm den här spellista</string>\n    <string name=\"download_in_progress_desc\">Hämtningen pågår</string>\n    <string name=\"share_playlist_desc\">Dela den här spellista med andra</string>\n    <string name=\"delete_playlist_desc\">Radera den här spellista för alltid</string>\n    <string name=\"sync_playlist_desc\">Synka spellistan med YouTube Music</string>\n    <string name=\"starting_radio\">Börjar spela radio</string>\n    <string name=\"now_playing\">Spelar nu</string>\n    <string name=\"close\">Stäng</string>\n    <string name=\"hide_player_thumbnail\">Dölj spelarens miniatyrbild</string>\n    <string name=\"hide_player_thumbnail_desc\">Ersätta album bilder med appens logo på spelaren</string>\n    <string name=\"crop_album_art\">Skära album bild</string>\n    <string name=\"seek_forward_dynamic\">+%1$d skunder framåt</string>\n    <string name=\"seek_backward_dynamic\">-%1$d sekunder bakåt</string>\n    <string name=\"seek_seconds_addup\">Progresivt seek</string>\n    <string name=\"seek_seconds_addup_description\">När på, tilläger 5 sekunder gradvis efter varje seek skip</string>\n    <string name=\"primary_color_style\">Primär färg</string>\n    <string name=\"tertiary_color_style\">Tertiär färg</string>\n    <string name=\"wavy\">Viftande</string>\n    <string name=\"swipe_song_to_remove\">Swipe låten för att ta den bort från spellistan</string>\n    <string name=\"lyrics_glow_effect\">Slå på glödande effekt på låttexter</string>\n    <string name=\"lyrics_glow_effect_desc\">Tilläg glödande animation och stunsade effekt till akriva låttexter</string>\n    <string name=\"enable_better_lyrics\">Slå på Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">Använd Better Lyrics provinatör för att ha ord för ord synkade låttexter</string>\n    <string name=\"enable_simpmusic\">Slå på SimpMusic Lyrics</string>\n    <string name=\"enable_simpmusic_desc\">Använd SimpMusic Lyrics provintör för att ha synkat låttexter</string>\n    <string name=\"auto_scroll\">Synka en gång till</string>\n    <string name=\"show_uploaded_playlist\">Visa spellista \\\"Uppladdade\\\"</string>\n    <string name=\"shuffle_playlist_first\">Blanda spellistan/albumet först</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-ta/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"charts\">பட்டியல்கள்</string>\n    <string name=\"back_button_desc\">பின்செல்</string>\n    <string name=\"album_cover_desc\">ஆல்பம் கவர்</string>\n    <string name=\"top_music_videos\">சிறந்த இசை வீடியோக்கள்</string>\n    <string name=\"trending\">டிரெண்டிங்</string>\n    <string name=\"offline\">பதிவிறக்கம் செய்யப்பட்டது</string>\n    <string name=\"months\">மாதங்கள்</string>\n    <string name=\"weeks\">வாரங்கள்</string>\n    <string name=\"years\">ஆண்டுகள்</string>\n    <string name=\"continuous\">தொடர்ச்சி</string>\n    <string name=\"liked\">பிடித்திருந்தது</string>\n    <string name=\"my_top\">என் சிறந்த</string>\n    <string name=\"cached_playlist\">பதுக்கம் செய்யப்பட்ட</string>\n    <string name=\"local_history\">அகம்</string>\n    <string name=\"remote_history\">புறம்</string>\n    <string name=\"sync_playlist\">ஒத்திசைவு பாடல்கள்</string>\n    <string name=\"sync_disabled\">ஒத்திசைவு முடக்கப்பட்டுள்ளது</string>\n    <string name=\"uploaded_playlist\">பதிவேற்றப்பட்டது</string>\n    <string name=\"filter_uploaded\">பதிவேற்றப்பட்டது</string>\n    <string name=\"allows_for_sync_witch_youtube\">குறிப்பு: இது YouTube Music உடன் ஒத்திசைக்க அனுமதிக்கிறது. இதை பின்னர் மாற்ற முடியாது.</string>\n    <string name=\"generating_image\">படத்தை உருவாக்குகிறது</string>\n    <string name=\"please_wait\">தயவுசெய்து காத்திருக்கவும்</string>\n    <string name=\"cancel\">ரத்துசெய்</string>\n    <string name=\"enable\">இயக்கு</string>\n    <string name=\"share_lyrics\">பாடல் வரிகளைப் பகிரவும்</string>\n    <string name=\"share_as_text\">உரையாகப் பகிரவும்</string>\n    <string name=\"share_as_image\">படமாகப் பகிரவும்</string>\n    <string name=\"max_selection_limit\">அதிகபட்ச தேர்வு வரம்பு</string>\n    <string name=\"share_selected\">தேர்ந்தெடுத்தவற்றைப் பகிர்</string>\n    <string name=\"customize_colors\">வண்ணங்களைத் தனிப்பயனாக்குங்கள்</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-ta/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">வீடு</string>\n    <string name=\"songs\">பாடல்கள்</string>\n    <string name=\"artists\">கலைஞர்கள்</string>\n    <string name=\"quick_picks\">விரைவான தேர்வுகள்</string>\n    <string name=\"forgotten_favorites\">மறந்துபோன பிடித்தவை</string>\n    <string name=\"most_played_artists\">அதிகம் விளையாடிய கலைஞர்கள்</string>\n    <string name=\"most_played_albums\">அதிகம் விளையாடிய ஆல்பங்கள்</string>\n    <string name=\"search\">தேடல்</string>\n    <string name=\"filter_downloaded\">பதிவிறக்கம்</string>\n    <string name=\"filter_all\">அனைத்தும்</string>\n    <string name=\"filter_songs\">பாடல்கள்</string>\n    <string name=\"filter_videos\">வீடியோக்கள்</string>\n    <string name=\"filter_albums\">ஆல்பம்</string>\n    <string name=\"filter_artists\">கலைஞர்கள்</string>\n    <string name=\"filter_playlists\">பிளேலிச்ட்கள்</string>\n    <string name=\"filter_community_playlists\">சமூக பிளேலிச்ட்கள்</string>\n    <string name=\"import_playlist\">பிளேலிச்ட்டை இறக்குமதி செய்யுங்கள்</string>\n    <string name=\"add_to_playlist\">பிளேலிச்ட்டில் சேர்க்கவும்</string>\n    <string name=\"view_artist\">கலைஞரைக் காண்க</string>\n    <string name=\"view_album\">ஆல்பத்தைக் காண்க</string>\n    <string name=\"refetch\">ரீஃபெட்ச்</string>\n    <string name=\"share\">பங்கு</string>\n    <string name=\"delete\">நீக்கு</string>\n    <string name=\"search_online\">ஆன்லைனில் தேடுங்கள்</string>\n    <string name=\"action_sync\">ஒத்திசை</string>\n    <string name=\"sort_by_artist\">கலைஞர்</string>\n    <string name=\"sort_by_year\">ஆண்டு</string>\n    <string name=\"sort_by_song_count\">பாடல் எண்ணிக்கை</string>\n    <string name=\"sort_by_length\">நீளம்</string>\n    <string name=\"sort_by_play_time\">விளையாடும் நேரம்</string>\n    <string name=\"sample_rate\">மாதிரி வீதம்</string>\n    <string name=\"loudness\">உரித்தல்</string>\n    <string name=\"volume\">தொகுதி</string>\n    <string name=\"song_title\">பாடல் தலைப்பு</string>\n    <string name=\"song_artists\">பாடல் கலைஞர்கள்</string>\n    <string name=\"error_song_title_empty\">பாடல் தலைப்பு காலியாக இருக்க முடியாது.</string>\n    <string name=\"error_song_artist_empty\">பாடல் கலைஞர் காலியாக இருக்க முடியாது.</string>\n    <string name=\"save\">சேமி</string>\n    <string name=\"duplicates_description_multiple\">%d பாடல்கள் ஏற்கனவே உங்கள் பிளேலிச்ட்டில் உள்ளன</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d பாடல்</item>\n        <item quantity=\"other\">%d பாடல்கள்</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d கலைஞர்</item>\n        <item quantity=\"other\">%d கலைஞர்கள்</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d ஆல்பம்</item>\n        <item quantity=\"other\">%d ஆல்பங்கள்</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d பிளேலிச்ட்</item>\n        <item quantity=\"other\">%d பிளேலிச்ட்கள்</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d வாரம்</item>\n        <item quantity=\"other\">%d வாரங்கள்</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d மாதம்</item>\n        <item quantity=\"other\">%d மாதங்கள்</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d ஆண்டு</item>\n        <item quantity=\"other\">%d ஆண்டுகள்</item>\n    </plurals>\n    <string name=\"playlist_imported\">பிளேலிச்ட் இறக்குமதி செய்யப்பட்டது</string>\n    <string name=\"playlist_synced\">பிளேலிச்ட் ஒத்திசைக்கப்பட்டது</string>\n    <string name=\"error_timeout\">நேரம் முடிந்தது</string>\n    <string name=\"error_unknown\">தெரியாத பிழை</string>\n    <string name=\"action_like\">போன்ற</string>\n    <string name=\"action_like_all\">எல்லாவற்றையும் போல</string>\n    <string name=\"action_remove_like\">அகற்றவும்</string>\n    <string name=\"action_remove_like_all\">எல்லா விருப்பங்களையும் அகற்று</string>\n    <string name=\"action_shuffle_on\">கலக்கு</string>\n    <string name=\"action_shuffle_off\">கலக்கவும்</string>\n    <string name=\"queue_all_songs\">அனைத்து பாடல்களும்</string>\n    <string name=\"queue_searched_songs\">தேடியது பாடல்கள்</string>\n    <string name=\"dark_theme\">இருண்ட கருப்பொருள்</string>\n    <string name=\"dark_theme_on\">ஆன்</string>\n    <string name=\"dark_theme_off\">அணை</string>\n    <string name=\"dark_theme_follow_system\">அமைப்பைப் பின்தொடரவும்</string>\n    <string name=\"customize_navigation_tabs\">வழிசெலுத்தல் தாவல்களைத் தனிப்பயனாக்குங்கள்</string>\n    <string name=\"player\">வீரர்</string>\n    <string name=\"misc\">இதர</string>\n    <string name=\"default_open_tab\">இயல்புநிலை திறந்த தாவல்</string>\n    <string name=\"content_language\">இயல்புநிலை உள்ளடக்க மொழி</string>\n    <string name=\"content_country\">இயல்புநிலை உள்ளடக்க நாடு</string>\n    <string name=\"system_default\">கணினி இயல்புநிலை</string>\n    <string name=\"enable_proxy\">ப்ராக்சியை இயக்கவும்</string>\n    <string name=\"proxy_type\">பதிலாள் வகை</string>\n    <string name=\"queue\">வரிசை</string>\n    <string name=\"persistent_queue\">தொடர்ச்சியான வரிசை</string>\n    <string name=\"auto_load_more\">ஆட்டோ ஏற்றும் கூடுதல் பாடல்கள்</string>\n    <string name=\"song_cache\">பாடல் கேச்</string>\n    <string name=\"max_cache_size\">அதிகபட்ச கேச் அளவு</string>\n    <string name=\"disable_screenshot\">ச்கிரீன்சாட்டை முடக்கு</string>\n    <string name=\"action_restore\">மீட்டெடு</string>\n    <string name=\"clear_translation_models\">மொழிபெயர்ப்பு மாதிரிகளை அழிக்கவும்</string>\n    <string name=\"albums\">ஆல்பம்</string>\n    <string name=\"playlists\">பிளேலிச்ட்கள்</string>\n    <string name=\"history\">வரலாறு</string>\n    <string name=\"stats\">புள்ளிவிவரங்கள்</string>\n    <string name=\"mood_and_genres\">மனநிலை மற்றும் வகைகள்</string>\n    <string name=\"account\">கணக்கு</string>\n    <string name=\"quick_picks_empty\">உங்கள் விரைவான தேர்வுகளை உருவாக்க பாடல்களைக் கேளுங்கள்</string>\n    <string name=\"keep_listening\">கேட்டுக்கொண்டே இருங்கள்</string>\n    <string name=\"your_youtube_playlists\">உங்கள் யூடியூப் பிளேலிச்ட்கள்</string>\n    <string name=\"similar_to\">ஒத்த</string>\n    <string name=\"new_release_albums\">புதிய வெளியீட்டு ஆல்பங்கள்</string>\n    <string name=\"today\">இன்று</string>\n    <string name=\"yesterday\">நேற்று</string>\n    <string name=\"this_week\">இந்த வாரம்</string>\n    <string name=\"last_week\">கடந்த வாரம்</string>\n    <string name=\"most_played_songs\">பெரும்பாலான பாடல்கள்</string>\n    <string name=\"search_yt_music\">யூடியூப் இசையைத் தேடுங்கள்…</string>\n    <string name=\"search_library\">தேடல் நூலகம்…</string>\n    <string name=\"filter_library\">நூலகம்</string>\n    <string name=\"filter_liked\">பிடித்திருந்தது</string>\n    <string name=\"filter_featured_playlists\">சிறப்பு பிளேலிச்ட்கள்</string>\n    <string name=\"filter_bookmarked\">புக்மார்க்கு செய்யப்பட்டது</string>\n    <string name=\"no_results_found\">முடிவுகள் எதுவும் கிடைக்கவில்லை</string>\n    <string name=\"library_song_empty\">நூலக பாடல்கள் இங்கே காண்பிக்கப்படும்</string>\n    <string name=\"library_artist_empty\">நூலக கலைஞர்கள் இங்கே காண்பிக்கப்படுவார்கள்</string>\n    <string name=\"library_album_empty\">நூலக ஆல்பங்கள் இங்கே காண்பிக்கப்படும்</string>\n    <string name=\"library_playlist_empty\">உங்கள் பிளேலிச்ட்கள் இங்கே காண்பிக்கப்படும்</string>\n    <string name=\"from_your_library\">உங்கள் நூலகத்திலிருந்து</string>\n    <string name=\"other_versions\">பிற பதிப்புகள்</string>\n    <string name=\"liked_songs\">பாடல்கள் விரும்பின</string>\n    <string name=\"downloaded_songs\">பதிவிறக்கம் செய்யப்பட்ட பாடல்கள்</string>\n    <string name=\"playlist_is_empty\">பிளேலிச்ட் காலியாக உள்ளது</string>\n    <string name=\"remove_download_playlist_confirm\">பதிவிறக்கம் செய்யப்பட்ட பாடல்கள் சேமிப்பகத்திலிருந்து அனைத்து \\\"%s\\\" பிளேலிச்ட் பாடல்களையும் அகற்ற விரும்புகிறீர்களா?</string>\n    <string name=\"delete_playlist_confirm\">பிளேலிச்ட்டை \\\"%s\\\" நீக்க விரும்புகிறீர்களா?</string>\n    <string name=\"retry\">மீண்டும் முயற்சிக்கவும்</string>\n    <string name=\"radio\">வானொலி</string>\n    <string name=\"shuffle\">கலக்கு</string>\n    <string name=\"reset\">மீட்டமை</string>\n    <string name=\"details\">விவரங்கள்</string>\n    <string name=\"edit\">தொகு</string>\n    <string name=\"start_radio\">வானொலியைத் தொடங்கவும்</string>\n    <string name=\"play\">விளையாடுங்கள்</string>\n    <string name=\"play_next\">அடுத்து விளையாடுங்கள்</string>\n    <string name=\"add_to_queue\">வரிசையில் சேர்க்கவும்</string>\n    <string name=\"add_to_library\">நூலகத்தில் சேர்க்கவும்</string>\n    <string name=\"add_all_to_library\">அனைத்தையும் நூலகத்தில் சேர்க்கவும்</string>\n    <string name=\"remove_from_library\">நூலகத்திலிருந்து அகற்று</string>\n    <string name=\"remove_all_from_library\">அனைத்தையும் நூலகத்திலிருந்து அகற்றவும்</string>\n    <string name=\"action_download\">பதிவிறக்கம்</string>\n    <string name=\"downloading\">பதிவிறக்குகிறது</string>\n    <string name=\"remove_download\">பதிவிறக்கத்தை அகற்று</string>\n    <string name=\"remove_from_history\">வரலாற்றிலிருந்து அகற்று</string>\n    <string name=\"remove_from_playlist\">பிளேலிச்ட்டிலிருந்து அகற்று</string>\n    <string name=\"remove_from_queue\">வரிசையிலிருந்து அகற்று</string>\n    <string name=\"advanced\">மேம்பட்ட</string>\n    <string name=\"tempo_and_pitch\">டெம்போ மற்றும் சுருதி</string>\n    <string name=\"sort_by_create_date\">தேதி சேர்க்கப்பட்டது</string>\n    <string name=\"sort_by_name\">பெயர்</string>\n    <string name=\"sort_by_custom\">தனிப்பயன் வரிசை</string>\n    <string name=\"media_id\">மீடியா ஐடி</string>\n    <string name=\"mime_type\">மைம் வகை</string>\n    <string name=\"codecs\">கோடெக்குகள்</string>\n    <string name=\"bitrate\">பிட்ரேட்</string>\n    <string name=\"file_size\">கோப்பு அளவு</string>\n    <string name=\"unknown\">தெரியவில்லை</string>\n    <string name=\"copied\">இடைநிலைப்பலகைக்கு நகலெடுக்கப்பட்டது</string>\n    <string name=\"edit_lyrics\">பாடல் திருத்து</string>\n    <string name=\"search_lyrics\">பாடல் தேடல்</string>\n    <string name=\"edit_song\">பாடலைத் திருத்து</string>\n    <string name=\"choose_playlist\">பிளேலிச்ட்டைத் தேர்வுசெய்க</string>\n    <string name=\"edit_playlist\">பிளேலிச்ட்டைத் திருத்து</string>\n    <string name=\"create_playlist\">பிளேலிச்ட்டை உருவாக்கவும்</string>\n    <string name=\"playlist_name\">பிளேலிச்ட் பெயர்</string>\n    <string name=\"error_playlist_name_empty\">பிளேலிச்ட் பெயர் காலியாக இருக்க முடியாது.</string>\n    <string name=\"edit_artist\">கலைஞரைத் திருத்து</string>\n    <string name=\"artist_name\">கலைஞரின் பெயர்</string>\n    <string name=\"error_artist_name_empty\">கலைஞரின் பெயர் காலியாக இருக்க முடியாது.</string>\n    <string name=\"duplicates\">நகல்கள்</string>\n    <string name=\"skip_duplicates\">நகல்களைத் தவிர்க்கவும்</string>\n    <string name=\"add_anyway\">எப்படியும் சேர்க்கவும்</string>\n    <string name=\"duplicates_description_single\">பாடல் ஏற்கனவே உங்கள் பிளேலிச்ட்டில் உள்ளது</string>\n    <string name=\"removed_song_from_playlist\">பிளேலிச்ட்டில் இருந்து \\\"%s\\\" அகற்றப்பட்டது</string>\n    <string name=\"undo\">செயல்தவிர்</string>\n    <string name=\"lyrics_not_found\">வரிகள் காணப்படவில்லை</string>\n    <string name=\"sleep_timer\">தூக்க நேரங்குறிகருவி</string>\n    <string name=\"end_of_song\">பாடலின் முடிவு</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">1 மணித்துளி</item>\n        <item quantity=\"other\">%d நிமிடங்கள்</item>\n    </plurals>\n    <string name=\"error_no_stream\">ச்ட்ரீம் எதுவும் கிடைக்கவில்லை</string>\n    <string name=\"error_no_internet\">பிணைய இணைப்பு இல்லை</string>\n    <string name=\"repeat_mode_off\">பயன்முறையை மீண்டும் செய்யவும்</string>\n    <string name=\"repeat_mode_one\">தற்போதைய பாடலை மீண்டும் செய்யவும்</string>\n    <string name=\"repeat_mode_all\">வரிசையை மீண்டும் செய்யவும்</string>\n    <string name=\"music_player\">மியூசிக் பிளேயர்</string>\n    <string name=\"settings\">அமைப்புகள்</string>\n    <string name=\"appearance\">தோற்றம்</string>\n    <string name=\"theme\">கருப்பொருள்</string>\n    <string name=\"enable_dynamic_theme\">மாறும் கருப்பொருள் இயக்கவும்</string>\n    <string name=\"pure_black\">தூய கருப்பு</string>\n    <string name=\"player_text_alignment\">பிளேயர் உரை சீரமைப்பு</string>\n    <string name=\"lyrics_text_position\">பாடல் உரை நிலை</string>\n    <string name=\"sided\">பக்க</string>\n    <string name=\"left\">இடது</string>\n    <string name=\"center\">நடுவண்</string>\n    <string name=\"right\">வலது</string>\n    <string name=\"player_slider_style\">பிளேயர் ச்லைடர் பாணி</string>\n    <string name=\"default_\">இயல்புநிலை</string>\n    <string name=\"squiggly\">மோசமான</string>\n    <string name=\"grid_cell_size\">கட்டம் செல் அளவு</string>\n    <string name=\"small\">சிறிய</string>\n    <string name=\"big\">பெரியது</string>\n    <string name=\"content\">உள்ளடக்கம்</string>\n    <string name=\"login\">புகுபதிவு</string>\n    <string name=\"not_logged_in\">உள்நுழையவில்லை</string>\n    <string name=\"proxy_url\">பதிலாள் முகவரி</string>\n    <string name=\"restart_to_take_effect\">நடைமுறைக்கு வர மறுதொடக்கம்</string>\n    <string name=\"player_and_audio\">பிளேயர் மற்றும் ஆடியோ</string>\n    <string name=\"audio_quality\">ஆடியோ தகுதி</string>\n    <string name=\"audio_quality_auto\">தானி</string>\n    <string name=\"audio_quality_high\">உயர்ந்த</string>\n    <string name=\"audio_quality_low\">குறைந்த</string>\n    <string name=\"persistent_queue_desc\">பயன்பாடு தொடங்கும் போது உங்கள் கடைசி வரிசையை மீட்டெடுக்கவும்</string>\n    <string name=\"auto_load_more_desc\">முடிந்தால், வரிசையின் முடிவை எட்டும்போது தானாகவே அதிகமான பாடல்களைச் சேர்க்கவும்</string>\n    <string name=\"skip_silence\">ம .னத்தைத் தவிர்க்கவும்</string>\n    <string name=\"audio_normalization\">ஆடியோ இயல்பாக்கம்</string>\n    <string name=\"auto_skip_next_on_error\">பிழை ஏற்படும்போது அடுத்த பாடலுக்கு தானாகத் தவிர்க்கவும்</string>\n    <string name=\"auto_skip_next_on_error_desc\">உங்கள் தொடர்ச்சியான பின்னணி அனுபவத்தை உறுதிப்படுத்தவும்</string>\n    <string name=\"stop_music_on_task_clear\">பணியில் இசையை நிறுத்துங்கள்</string>\n    <string name=\"equalizer\">சமநிலைப்படுத்தி</string>\n    <string name=\"storage\">சேமிப்பு</string>\n    <string name=\"cache\">கேச்</string>\n    <string name=\"image_cache\">பட தற்காலிக சேமிப்பு</string>\n    <string name=\"unlimited\">வரம்பற்றது</string>\n    <string name=\"clear_all_downloads\">எல்லா பதிவிறக்கங்களையும் அழிக்கவும்</string>\n    <string name=\"max_image_cache_size\">அதிகபட்ச பட கேச் அளவு</string>\n    <string name=\"clear_image_cache\">தெளிவான பட தற்காலிக சேமிப்பு</string>\n    <string name=\"max_song_cache_size\">அதிகபட்ச பாடல் கேச் அளவு</string>\n    <string name=\"clear_song_cache\">தெளிவான பாடல் தற்காலிக சேமிப்பு</string>\n    <string name=\"size_used\">%s பயன்படுத்தப்படுகின்றன</string>\n    <string name=\"privacy\">தனியுரிமை</string>\n    <string name=\"listen_history\">வரலாற்றைக் கேளுங்கள்</string>\n    <string name=\"pause_listen_history\">இடைநிறுத்தப்பட்ட வரலாற்றைக் கேளுங்கள்</string>\n    <string name=\"clear_listen_history\">கேளுங்கள் வரலாற்றைக் கேளுங்கள்</string>\n    <string name=\"clear_listen_history_confirm\">எல்லா கேட்கும் வரலாற்றையும் அழிக்க விரும்புகிறீர்களா?</string>\n    <string name=\"search_history\">தேடல் வரலாறு</string>\n    <string name=\"pause_search_history\">தேடல் வரலாறு இடைநிறுத்தவும்</string>\n    <string name=\"clear_search_history\">தேடல் வரலாற்றை அழிக்கவும்</string>\n    <string name=\"clear_search_history_confirm\">எல்லா தேடல் வரலாற்றையும் அழிக்க விரும்புகிறீர்களா?</string>\n    <string name=\"use_login_for_browse\">உள்ளடக்கத்தை உலாவுவதற்கு உள்நுழைவைப் பயன்படுத்தவும்</string>\n    <string name=\"use_login_for_browse_desc\">நீங்கள் பார்க்கும் உள்ளடக்கத்தை இது பாதிக்கும், எடுத்துக்காட்டாக, நீங்கள் காப்பீடு கணக்கில் உள்நுழைந்துள்ளால் காப்பீடு மட்டும் ஆல்பங்களைக் காட்டுகிறது</string>\n    <string name=\"disable_screenshot_desc\">இந்த விருப்பம் இயக்கத்தில் இருக்கும்போது, ச்கிரீன் சாட்கள் மற்றும் ரெசென்ட்களில் பயன்பாட்டின் பார்வை முடக்கப்பட்டுள்ளன.</string>\n    <string name=\"enable_lrclib\">LRCLIB பாடல் வழங்குநரை இயக்கு</string>\n    <string name=\"enable_kugou\">குகோ பாடல் வழங்குநரை இயக்கு</string>\n    <string name=\"hide_explicit\">வெளிப்படையான உள்ளடக்கத்தை மறைக்கவும்</string>\n    <string name=\"backup_restore\">காப்புப்பிரதி மற்றும் மீட்டமை</string>\n    <string name=\"action_backup\">காப்புப்பிரதி</string>\n    <string name=\"imported_playlist\">இறக்குமதி செய்யப்பட்ட பிளேலிச்ட்</string>\n    <string name=\"backup_create_success\">காப்புப்பிரதி வெற்றிகரமாக உருவாக்கப்பட்டது</string>\n    <string name=\"backup_create_failed\">காப்புப்பிரதியை உருவாக்க முடியவில்லை</string>\n    <string name=\"restore_failed\">காப்புப்பிரதியை மீட்டெடுப்பதில் தோல்வி</string>\n    <string name=\"discord_integration\">முரண்பாடு ஒருங்கிணைப்பு</string>\n    <string name=\"discord_information\">உங்கள் முரண்பாடு கணக்கின் நிலையை அமைக்க இன்டெர்டூன் கிச்சார்பிசி நூலகத்தைப் பயன்படுத்துகிறது. இது முரண்பாடான நுழைவாயில் இணைப்பைப் பயன்படுத்துவதை உள்ளடக்குகிறது, இது டிச்கார்டின் TOS இன் மீறலாகக் கருதப்படலாம். இருப்பினும், இந்த காரணத்திற்காக பயனர் கணக்குகள் இடைநிறுத்தப்பட்டதாக அறியப்படாத வழக்குகள் எதுவும் இல்லை. உங்கள் சொந்த ஆபத்தில் பயன்படுத்தவும்.\\n\\n இன்னெர்டூன் உங்கள் கிள்ளாக்கை மட்டுமே பிரித்தெடுக்கும், மற்ற அனைத்தும் உள்நாட்டில் சேமிக்கப்படும்.</string>\n    <string name=\"dismiss\">தள்ளுபடி</string>\n    <string name=\"options\">விருப்பங்கள்</string>\n    <string name=\"preview\">முன்னோட்டம்</string>\n    <string name=\"login_failed\">உள்நுழைவு தோல்வியடைந்தது</string>\n    <string name=\"action_logout\">வெளியேறு</string>\n    <string name=\"enable_discord_rpc\">பணக்கார இருப்பை இயக்கவும்</string>\n    <string name=\"about\">பற்றி</string>\n    <string name=\"app_version\">பயன்பாட்டு பதிப்பு</string>\n    <string name=\"new_version_available\">புதிய பதிப்பு கிடைக்கிறது</string>\n    <string name=\"translation_models\">மொழிபெயர்ப்பு மாதிரிகள்</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d தேர்ந்தெடுக்கப்பட்டது</item>\n        <item quantity=\"other\">%d தேர்ந்தெடுக்கப்பட்டன</item>\n    </plurals>\n    <string name=\"action_login\">புகுபதிகை</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-te/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">స్థానిక</string>\n    <string name=\"remote_history\">దూరస్థ</string>\n    <string name=\"charts\">పట్టికలు</string>\n    <string name=\"back_button_desc\">వెనుకకు</string>\n    <string name=\"album_cover_desc\">ఆల్బమ్ ముఖచిత్రం</string>\n    <string name=\"top_music_videos\">అగ్ర సంగీత చిత్రాలు</string>\n    <string name=\"trending\">ప్రాచుర్యంలో ఉన్నవి</string>\n    <string name=\"weeks\">వారాలు</string>\n    <string name=\"months\">నెలలు</string>\n    <string name=\"years\">సంవత్సరాలు</string>\n    <string name=\"continuous\">నిరంతరం</string>\n    <string name=\"liked\">నచ్చినవి</string>\n    <string name=\"offline\">దింపబడినవి</string>\n    <string name=\"my_top\">నా అగ్రస్థానంలో ఉన్నవి</string>\n    <string name=\"cached_playlist\">తాత్కాలిక నిల్వ</string>\n    <string name=\"sync_playlist\">జాబితాను సమకాలీకరించు</string>\n    <string name=\"sync_disabled\">సమకాలీకరణ నిలిపివేయబడింది</string>\n    <string name=\"allows_for_sync_witch_youtube\">గమనిక: ఇది యూట్యూబ్ మ్యూజిక్‌తో సమకాలీకరించడానికి అనుమతిస్తుంది. దీనిని తర్వాత మార్చడం సాధ్యం కాదు.</string>\n    <string name=\"generating_image\">చిత్రాన్ని సృష్టిస్తోంది</string>\n    <string name=\"please_wait\">దయచేసి వేచి ఉండండి</string>\n    <string name=\"cancel\">రద్దు చేయి</string>\n    <string name=\"share_lyrics\">పద్యాలను పంచుకోండి</string>\n    <string name=\"share_as_text\">పాఠ్యంలా పంచుకోండి</string>\n    <string name=\"share_as_image\">చిత్రంలా పంచుకోండి</string>\n    <string name=\"max_selection_limit\">గరిష్ట ఎంపిక పరిమితి</string>\n    <string name=\"share_selected\">ఎంచుకున్నవి పంచుకోండి</string>\n    <string name=\"customize_colors\">రంగులను అనుకూలించండి</string>\n    <string name=\"text_color\">వచన రంగు</string>\n    <string name=\"secondary_text_color\">ద్వితీయ వచన రంగు</string>\n    <string name=\"background_color\">నేపథ్య రంగు</string>\n    <string name=\"remove_from_cache\">తాత్కాలిక నిల్వ నుండి తొలగించు</string>\n    <string name=\"copy_link\">లింక్‌ను కాపీ చేయి</string>\n    <string name=\"select\">అన్నింటిని ఎంచుకోండి</string>\n    <string name=\"like_all\">అన్నింటినీ నచ్చినవిగా గుర్తించు</string>\n    <string name=\"dislike_all\">అన్నింటినీ నచ్చనివిగా గుర్తించు</string>\n    <string name=\"sort_by_last_updated\">నవీకరించబడిన తేదీ</string>\n    <string name=\"link_copied\">లింక్ క్లిప్‌బోర్డుకు కాపీ చేయబడింది</string>\n    <string name=\"starting_radio\">సంగీత కేంద్రం ప్రారంభమవుతోంది</string>\n    <string name=\"now_playing\">ఇప్పుడు వింటున్నది</string>\n    <string name=\"close\">మూసివేయి</string>\n    <string name=\"hide_player_thumbnail\">చిత్రపటాన్ని చిన్నచిత్రాన్ని దాచు</string>\n    <string name=\"hide_player_thumbnail_desc\">ఆల్బమ్ చిత్రాన్ని యాప్ లోగోతో భర్తీ చేయి</string>\n    <string name=\"already_in_playlist\">ఇప్పటికే జాబితాలో ఉంది:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d సారి</item>\n        <item quantity=\"other\">%d సార్లు</item>\n    </plurals>\n    <string name=\"seek_forward_dynamic\">+%1$d సెకన్లు ముందుకు</string>\n    <string name=\"seek_backward_dynamic\">-%1$d సెకన్లు వెనుకకు</string>\n    <string name=\"seek_seconds_addup\">క్రమంగా ముందుకు/వెనక్కి జరగడం</string>\n    <string name=\"lyrics\">గీతం</string>\n    <string name=\"similar_content\">ఇదే విధమైన అంశాలు</string>\n    <string name=\"player_background_style\">వాయించే తెర నేపథ్య శైలి</string>\n    <string name=\"follow_theme\">రూపకల్పనను అనుసరించు</string>\n    <string name=\"gradient\">రంగుల సమ్మేళనం</string>\n    <string name=\"new_player_design\">కొత్త వాయించే తెర రూపకల్పన</string>\n    <string name=\"new_mini_player_design\">కొత్త చిన్న వాయించే తెర రూపకల్పన</string>\n    <string name=\"player_background_blur\">మసకబార్చు</string>\n    <string name=\"player_buttons_style\">వాయించే తెర మీటల రంగులు</string>\n    <string name=\"default_style\">డిఫాల్ట్</string>\n    <string name=\"seek_seconds_addup_description\">సక్రియమైనట్లయితే, ప్రతి సీక్ స్కిప్‌కి 5 అదనపు సెకండ్లను క్రమంగా జోడిస్తుంది</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-te/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"reset\">తిరిగి మొదటికి</string>\n    <string name=\"artists\">కళాకారులు</string>\n    <string name=\"edit\">సవరించు</string>\n    <string name=\"most_played_songs\">ఎక్కువగా వినిన పాటలు</string>\n    <string name=\"filter_all\">అన్ని</string>\n    <string name=\"history\">చరిత్ర</string>\n    <string name=\"stats\">గణాంకాలు</string>\n    <string name=\"account\">ఖాతా</string>\n    <string name=\"forgotten_favorites\">మరచిపోయిన మదురాలు</string>\n    <string name=\"new_release_albums\">కొత్త చిత్రాలు</string>\n    <string name=\"today\">ఈరోజు</string>\n    <string name=\"yesterday\">నిన్న</string>\n    <string name=\"this_week\">ఈ వారం</string>\n    <string name=\"last_week\">ముందు వారం</string>\n    <string name=\"most_played_artists\">ఎక్కువగా వినిన కళాకారులు</string>\n    <string name=\"most_played_albums\">ఎక్కువగా వినిన చిత్రాలు</string>\n    <string name=\"search\">వెతుకు</string>\n    <string name=\"filter_liked\">నచ్చినవి</string>\n    <string name=\"filter_songs\">పాటలు</string>\n    <string name=\"filter_videos\">వీడియో</string>\n    <string name=\"filter_albums\">చిత్రాలు</string>\n    <string name=\"filter_artists\">కళాకారులు</string>\n    <string name=\"liked_songs\">నచ్చిన పాటలు</string>\n    <string name=\"retry\">మళ్ళీ ప్రయత్నించండి</string>\n    <string name=\"home\">ఇల్లు</string>\n    <string name=\"details\">వివరాలు</string>\n    <string name=\"add_to_queue\">వరుసలోకి జోడించు</string>\n    <string name=\"view_artist\">కళాకారుల వివరాలు చూడు</string>\n    <string name=\"view_album\">చిత్రాల వివరాలు చూడు</string>\n    <string name=\"share\">షేర్</string>\n    <string name=\"delete\">తోలగించు</string>\n    <string name=\"remove_from_history\">చరిత్ర నుంచి తొలగించు</string>\n    <string name=\"sort_by_name\">పేరు</string>\n    <string name=\"sort_by_artist\">కళాకారుడు</string>\n    <string name=\"sort_by_year\">సంవత్సరం</string>\n    <string name=\"sort_by_play_time\">పాట నిడివి</string>\n    <string name=\"volume\">శబ్దం</string>\n    <string name=\"unknown\">తెలియనిది</string>\n    <string name=\"edit_song\">పాటని సవరించండి</string>\n    <string name=\"song_artists\">పాట పాడినవారు</string>\n    <string name=\"error_song_artist_empty\">పాట గాయకులు కాళీ గా ఉండకూడదు.</string>\n    <string name=\"edit_artist\">గాయకులని మార్చు</string>\n    <string name=\"artist_name\">గాయకుల పేర్లు</string>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d సంవత్సరం</item>\n        <item quantity=\"other\">%d ఏళ్లు</item>\n    </plurals>\n    <string name=\"end_of_song\">పాట ముగిసింది</string>\n    <string name=\"error_timeout\">సమయం ముగిసింది</string>\n    <string name=\"error_unknown\">తేలియని లోపం</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d పాట</item>\n        <item quantity=\"other\">%dపాటలు</item>\n    </plurals>\n    <string name=\"action_like\">ఇష్టం</string>\n    <string name=\"action_like_all\">అన్ని ఇష్టం</string>\n    <string name=\"action_remove_like\">ఇష్ట పడిన వాటిని తోలగించు</string>\n    <string name=\"action_remove_like_all\">ఇష్ట పడినవి అన్ని తోలగించు</string>\n    <string name=\"queue_all_songs\">అన్ని పాటలు</string>\n    <string name=\"queue_searched_songs\">వెతికిన పాటలు</string>\n    <string name=\"left\">ఎడమ</string>\n    <string name=\"center\">మద్యలో</string>\n    <string name=\"right\">కుడి వైపు</string>\n    <string name=\"misc\">మిగిలినవి</string>\n    <string name=\"small\">చిన్నది</string>\n    <string name=\"big\">పెద్దది</string>\n    <string name=\"audio_quality_low\">తక్కువ</string>\n    <string name=\"skip_silence\">నిశ్శబ్దాన్ని దాటవేయి</string>\n    <string name=\"storage\">నిల్వ</string>\n    <string name=\"unlimited\">అపరిమితం</string>\n    <string name=\"options\">ఎంపికలు</string>\n    <string name=\"about\">గురించి</string>\n    <string name=\"songs\">పాటలు</string>\n    <string name=\"keep_listening\">వింటూ ఉండండి</string>\n    <string name=\"play_next\">తరువాతి పాట</string>\n    <string name=\"refetch\">మరలా సోదించు</string>\n    <string name=\"sort_by_length\">పొడవు</string>\n    <string name=\"song_title\">పాట పేరు</string>\n    <string name=\"no_results_found\">ఏమి దొరకలేదు</string>\n    <string name=\"play\">మొదలు పెట్టు</string>\n    <string name=\"sort_by_create_date\">జోడించిన తేదీ</string>\n    <string name=\"sort_by_song_count\">పాటల లెక్క</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">ఒక నిమిషం</item>\n        <item quantity=\"other\">%d నిమిషాలు</item>\n    </plurals>\n    <string name=\"error_song_title_empty\">పాట పేరు కాళీ గా ఉండకూడదు.</string>\n    <string name=\"error_artist_name_empty\">గాయకుల పేర్లు కాళీగా ఉంచకూడదు.</string>\n    <string name=\"repeat_mode_one\">ఈ పాటని మళ్ళీ వినిపించు</string>\n    <string name=\"queue\">వరుస</string>\n    <string name=\"audio_quality_high\">ఎక్కువ</string>\n    <string name=\"listen_history\">పాటలు వినిన చరిత్ర</string>\n    <string name=\"albums\">ఆల్బమ్‌లు</string>\n    <string name=\"playlists\">ప్లేలిస్ట్‌లు</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d ఎంచుకోబడింది</item>\n        <item quantity=\"other\">%d ఎంచుకోబడినవి</item>\n    </plurals>\n    <string name=\"mood_and_genres\">మూడ్ మరియు కళా ప్రక్రియలు</string>\n    <string name=\"quick_picks\">త్వరిత ఎంపికలు</string>\n    <string name=\"quick_picks_empty\">మీ త్వరిత ఎంపికలను రూపొందించడానికి పాటలను వినండి</string>\n    <string name=\"your_youtube_playlists\">మీ యూట్యూబ్ప్లేజాబితాలు</string>\n    <string name=\"similar_to\">దీనికి సమానమైనది</string>\n    <string name=\"search_yt_music\">యూట్యూబ్ మ్యూజిక్‌లో వెతకండి…</string>\n    <string name=\"search_library\">లైబ్రరీలో వెతకండి…</string>\n    <string name=\"filter_library\">లైబ్రరీ</string>\n    <string name=\"filter_downloaded\">డౌన్‌లోడ్ చేయబడింది</string>\n    <string name=\"filter_playlists\">ప్లేలిస్ట్‌లు</string>\n    <string name=\"filter_community_playlists\">కమ్యూనిటీ ప్లేలిస్ట్‌లు</string>\n    <string name=\"filter_featured_playlists\">ఫీచర్ చేసిన ప్లేలిస్ట్‌లు</string>\n    <string name=\"action_download\">డౌన్లోడ్</string>\n    <string name=\"downloading\">డౌన్‌లోడ్ అవుతోంది</string>\n    <string name=\"remove_download\">డౌన్‌లోడ్ ని తొలగించు</string>\n    <string name=\"import_playlist\">ప్లేజాబితా దిగుమతి చేయండి</string>\n    <string name=\"add_to_playlist\">ప్లేలిస్ట్ లో వేసుకోండి</string>\n    <string name=\"remove_from_playlist\">ప్లేలిస్ట్ నుండి తీసివేయండి</string>\n    <string name=\"remove_all_from_library\">లైబ్రరీ నుండి అన్నీ తీసివేయి</string>\n    <string name=\"search_online\">ఆన్‌లైన్‌లో శోధించండి</string>\n    <string name=\"action_sync\">సమకాలీకరించు</string>\n    <string name=\"advanced\">ముందుకు</string>\n    <string name=\"tempo_and_pitch\">టెంపో మరియు పిచ్</string>\n    <string name=\"sort_by_custom\">అనుకూల క్రమం</string>\n    <string name=\"media_id\">మీడియా ఐడి</string>\n    <string name=\"mime_type\">మైమ్ రకం</string>\n    <string name=\"codecs\">కోడెక్‌లు</string>\n    <string name=\"sample_rate\">మాదిరి రేటు</string>\n    <string name=\"loudness\">శబ్ద తీవ్రత</string>\n    <string name=\"copied\">క్లిప్బోర్డ్కు కాపీ చేయబడింది</string>\n    <string name=\"edit_lyrics\">సాహిత్యాన్ని సవరించండి</string>\n    <string name=\"search_lyrics\">సాహిత్యాన్ని వెతకండి</string>\n    <string name=\"save\">భద్రపరుచుకోండి</string>\n    <string name=\"choose_playlist\">ప్లేలిస్ట్‌ని ఎంచుకోండి</string>\n    <string name=\"edit_playlist\">ప్లేలిస్ట్ను సవరించండి</string>\n    <string name=\"create_playlist\">ప్లేలిస్ట్ను సృష్టించుకోండి</string>\n    <string name=\"playlist_name\">ప్లేలిస్ట్ను పేరు</string>\n    <string name=\"error_playlist_name_empty\">ప్లేలిస్ట్ను పేరు ఖాళీగా ఉండకూడదు.</string>\n    <string name=\"duplicates\">నకిలీలు</string>\n    <string name=\"skip_duplicates\">నకిలీలను డేటివేయాండి</string>\n    <string name=\"add_anyway\">ఏదేమైనా జోడించు</string>\n    <string name=\"duplicates_description_single\">మీ ప్లేలిస్ట్‌లో పాట ఇప్పటికే ఉంది</string>\n    <string name=\"duplicates_description_multiple\">%d పాటలు ఇప్పటికే మీ ప్లేజాబితాలో ఉన్నాయి</string>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d కళాకారుడు</item>\n        <item quantity=\"other\">%d కళాకారులు</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d సంకలనం</item>\n        <item quantity=\"other\">%d సంకలనాలు</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d ప్లేలిస్ట్</item>\n        <item quantity=\"other\">%d ప్లేలిస్ట్‌లు</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d వారం</item>\n        <item quantity=\"other\">%d వారాలు</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d నెల</item>\n        <item quantity=\"other\">%d నెలలు</item>\n    </plurals>\n    <string name=\"playlist_imported\">ప్లేలిస్ట్ దిగుమతి చేయబడింది</string>\n    <string name=\"removed_song_from_playlist\">ప్లేలిస్ట్‌ నుండి \\\"%s\\\" తీసివేయబడింది</string>\n    <string name=\"playlist_synced\">ప్లేలిస్ట్‌లు సమకాలీకరించబడింది</string>\n    <string name=\"undo\">అన్డు</string>\n    <string name=\"lyrics_not_found\">సాహిత్యం కనుగొనబడలేదు</string>\n    <string name=\"sleep_timer\">నిద్ర టైమర్</string>\n    <string name=\"error_no_internet\">నెట్వర్క్ కనెక్షన్ లేదు</string>\n    <string name=\"action_shuffle_on\">కలాపటం ఆన్ చేయబడిండి</string>\n    <string name=\"action_shuffle_off\">కలాపటం ఆఫ్ చేయబడిండి</string>\n    <string name=\"repeat_mode_off\">పునరావృత విధానం ఆఫ్ చేయబడిండి</string>\n    <string name=\"repeat_mode_all\">పునరావృత వరుస</string>\n    <string name=\"music_player\">సంగీత ప్లేయర్</string>\n    <string name=\"settings\">సెట్టింగ్‌లు</string>\n    <string name=\"appearance\">ప్రదర్శన</string>\n    <string name=\"theme\">థీమ్</string>\n    <string name=\"filter_bookmarked\">బుక్‌మార్క్ చేయబడింది</string>\n    <string name=\"library_song_empty\">లైబ్రరీ పాటలు ఇక్కడ కనిపిస్తాయి</string>\n    <string name=\"library_artist_empty\">లైబ్రరీ లో కళాకారులు ఇక్కడ కనిపిస్తారు</string>\n    <string name=\"library_album_empty\">లైబ్రరీ ఆల్బమ్‌లు ఇక్కడ కనిపిస్తాయి</string>\n    <string name=\"library_playlist_empty\">మీ ప్లేలిస్ట్‌లు ఇక్కడ కనిపిస్తాయి</string>\n    <string name=\"from_your_library\">మీ లైబ్రరీ నుండి</string>\n    <string name=\"other_versions\">ఇతర వెర్షన్లు</string>\n    <string name=\"downloaded_songs\">డౌన్‌లోడ్ చేసిన పాటలు</string>\n    <string name=\"playlist_is_empty\">ప్లేలిస్ట్ ఖాళీగా ఉంది</string>\n    <string name=\"remove_download_playlist_confirm\">డౌన్‌లోడ్ చేసిన పాటల నిల్వ నుండి అన్ని \\\"%s\\\" ప్లేలిస్ట్ పాటలను మీరు నిజంగా తీసివేయాలనుకుంటున్నారా?</string>\n    <string name=\"delete_playlist_confirm\">మీరు నిజంగా \\\"%s\\\" ప్లేలిస్ట్ తొలగించాలనుకుంటున్నారా?</string>\n    <string name=\"radio\">ఆకాశవాణి</string>\n    <string name=\"shuffle\">కలాపటం</string>\n    <string name=\"start_radio\">ఆకాశవాణి నీ మొదళ్ళు పెట్టండి</string>\n    <string name=\"add_to_library\">లైబ్రరీకి జోడించు</string>\n    <string name=\"add_all_to_library\">లైబ్రరీకి అన్నీ జోడించండి</string>\n    <string name=\"remove_from_library\">లైబ్రరీకి అన్నీ జోడించండి</string>\n    <string name=\"remove_from_queue\">క్యూ నుండి తొలగించండి</string>\n    <string name=\"bitrate\">బిట్రేట్</string>\n    <string name=\"file_size\">ఫైల్ పరిమాణం</string>\n    <string name=\"error_no_stream\">సంగీతం దొరకలేదు</string>\n    <string name=\"enable_dynamic_theme\">డైనమిక్ థీమ్‌ని ఎనేబుల్ చేయండి</string>\n    <string name=\"dark_theme\">డార్క్ థీమ్</string>\n    <string name=\"dark_theme_on\">ఆన్</string>\n    <string name=\"dark_theme_off\">ఆఫ్</string>\n    <string name=\"dark_theme_follow_system\">ఫాలో సిస్టమ్</string>\n    <string name=\"pure_black\">స్వచ్ఛమైన నలుపు</string>\n    <string name=\"customize_navigation_tabs\">నావిగేషన్ ట్యాబ్‌లను అనుకూలపరచండి</string>\n    <string name=\"player\">ప్లేయర్</string>\n    <string name=\"player_text_alignment\">ప్లేయర్ వచన సమలేఖనం</string>\n    <string name=\"lyrics_text_position\">సాహిత్యం వచన స్థానం</string>\n    <string name=\"sided\">పక్కాకి</string>\n    <string name=\"player_slider_style\">ప్లేయర్ స్లయిడర్ శైలి</string>\n    <string name=\"default_\">మముల్గా</string>\n    <string name=\"squiggly\">స్క్విగ్లీ</string>\n    <string name=\"default_open_tab\">డిఫాల్ట్ తెరిచిన ట్యాబ్</string>\n    <string name=\"grid_cell_size\">గ్రిడ్ సెల్ సైజు</string>\n    <string name=\"content\">విషయం</string>\n    <string name=\"action_logout\">లాగ్ అవుట్</string>\n    <string name=\"action_login\">లాగిన్</string>\n    <string name=\"login\">లాగిన్</string>\n    <string name=\"not_logged_in\">లాగిన్ కాలేదు</string>\n    <string name=\"login_failed\">లాగిన్ విఫలమైంది</string>\n    <string name=\"content_language\">డిఫాల్ట్ కంటెంట్ భాష</string>\n    <string name=\"content_country\">డిఫాల్ట్ కంటెంట్ దేశం</string>\n    <string name=\"system_default\">సిస్టమ్ డిఫాల్ట్</string>\n    <string name=\"enable_proxy\">ప్రాక్సీ ప్రారంభించు</string>\n    <string name=\"proxy_type\">ప్రాక్సీ రకం</string>\n    <string name=\"proxy_url\">ప్రాక్సీ URL</string>\n    <string name=\"restart_to_take_effect\">ప్రభావం చూపేందుకు పునఃప్రారంభించండి</string>\n    <string name=\"player_and_audio\">ప్లేయర్ మరియు ఆడియో</string>\n    <string name=\"audio_quality\">ఆడియో నాణ్యత</string>\n    <string name=\"audio_quality_auto\">స్వీయ</string>\n    <string name=\"persistent_queue\">నిరంతర క్యూ</string>\n    <string name=\"persistent_queue_desc\">యాప్ ప్రారంభమైనప్పుడు మీ చివరి క్యూను పునరుద్ధరించండి</string>\n    <string name=\"auto_load_more\">మరిన్ని పాటలను ఆటో లోడ్ చేయి</string>\n    <string name=\"auto_load_more_desc\">వీలైతే, క్యూ ముగింపుకు చేరుకున్నప్పుడు మరిన్ని పాటలను స్వయంచాలకంగా జోడించండి</string>\n    <string name=\"audio_normalization\">ఆడియో సాధారణీకరణ</string>\n    <string name=\"auto_skip_next_on_error\">లోపం సంభవించినప్పుడు తదుపరి పాటకు స్వయంచాలకంగా దాటవేయి</string>\n    <string name=\"auto_skip_next_on_error_desc\">మీ నిరంతర ప్లేబ్యాక్ అనుభవాన్ని నిర్ధారించుకోండి</string>\n    <string name=\"stop_music_on_task_clear\">టాస్క్ క్లియర్‌లో మ్యూజిక్ ఆపండి</string>\n    <string name=\"equalizer\">సమానంగా</string>\n    <string name=\"cache\">కాష్</string>\n    <string name=\"image_cache\">చిత్రం కాష్</string>\n    <string name=\"song_cache\">పాట కాష్</string>\n    <string name=\"max_cache_size\">గరిష్ట కాష్ పరిమాణం</string>\n    <string name=\"clear_all_downloads\">అన్ని డౌన్‌లోడ్‌లను క్లియర్ చేయి</string>\n    <string name=\"max_image_cache_size\">గరిష్ట చిత్రం కాష్ పరిమాణం</string>\n    <string name=\"clear_image_cache\">చిత్రం కాష్‌ను క్లియర్ చేయండి</string>\n    <string name=\"max_song_cache_size\">గరిష్ట పాట కాష్ పరిమాణం</string>\n    <string name=\"clear_song_cache\">పాట కాష్‌ను క్లియర్ చేయండి</string>\n    <string name=\"size_used\">%s ఉపయోగించబడింది</string>\n    <string name=\"privacy\">గోప్యత</string>\n    <string name=\"pause_listen_history\">విన్న చరిత్రను పాజ్ చేయండి</string>\n    <string name=\"clear_listen_history\">వినే చరిత్రను క్లియర్ చేయండి</string>\n    <string name=\"clear_listen_history_confirm\">మీరు ఖచ్చితంగా మొత్తం వినే చరిత్రను క్లియర్ చేయాలనుకుంటున్నారా?</string>\n    <string name=\"search_history\">శోధన చరిత్ర</string>\n    <string name=\"pause_search_history\">శోధన చరిత్రను పాజ్ చేయండి</string>\n    <string name=\"clear_search_history\">శోధన చరిత్రను క్లియర్ చేయండి</string>\n    <string name=\"clear_search_history_confirm\">మీరు ఖచ్చితంగా మొత్తం శోధన చరిత్రను క్లియర్ చేయాలనుకుంటున్నారా?</string>\n    <string name=\"use_login_for_browse\">కంటెంట్ బ్రౌజింగ్ కోసం లాగిన్ ఉపయోగించండి</string>\n    <string name=\"use_login_for_browse_desc\">ఇది మీరు చూసే కంటెంట్‌ను ప్రభావితం చేస్తుంది మరియు ఉదాహరణకు మీరు ప్రీమియం ఖాతాతో లాగిన్ అయి ఉంటే ప్రీమియం-మాత్రమే ఆల్బమ్‌లను చూపుతుంది</string>\n    <string name=\"disable_screenshot\">స్క్రీన్‌షాట్‌ను నిలిపివేయండి</string>\n    <string name=\"disable_screenshot_desc\">ఈ ఎంపిక ఆన్‌లో ఉన్నప్పుడు, స్క్రీన్‌షాట్‌లు మరియు ఇటీవలి వాటిలో యాప్‌ల వీక్షణ నిలిపివేయబడతాయి.</string>\n    <string name=\"enable_lrclib\">LrcLib లిరిక్స్ ప్రొవైడర్‌ను ప్రారంభించండి</string>\n    <string name=\"enable_kugou\">KuGou లిరిక్స్ ప్రొవైడర్‌ను ప్రారంభించండి</string>\n    <string name=\"hide_explicit\">అభ్యంతరకరమైన కంటెంట్‌ను దాచు</string>\n    <string name=\"backup_restore\">బ్యాకప్ మరియు పునరుద్ధరణ</string>\n    <string name=\"action_backup\">బ్యాకప్</string>\n    <string name=\"action_restore\">పునరుద్ధరణ</string>\n    <string name=\"imported_playlist\">ప్లేజాబితా దిగుమతి చేయబడింది</string>\n    <string name=\"backup_create_success\">బ్యాకప్ విజయవంతంగా సృష్టించబడింది</string>\n    <string name=\"backup_create_failed\">బ్యాకప్‌ను సృష్టించలేకపోయింది</string>\n    <string name=\"restore_failed\">బ్యాకప్‌ను పునరుద్ధరించడంలో విఫలమైంది</string>\n    <string name=\"discord_integration\">డిస్కార్డ్ ఇంటిగ్రేషన్</string>\n    <string name=\"discord_information\">ఇన్నర్‌ట్యూన్ మీ డిస్కార్డ్ తా స్థితిని సెట్ చేసేందుకు కిజ్జీఆర్‌పిసి లైబ్రరీని ఉపయోగిస్తుంది. ఇది డిస్కార్డ్ Gateway కనెక్షన్ ఉపయోగించడం ద్వారా జరుగుతుంది, ఇది Discord యొక్క TOSను ఉల్లంఘించడం గా పరిగణించబడవచ్చు. అయితే, ఈ కారణం వల్ల వినియోగదారుల ఖాతాలు సస్పెండ్ అయిన సందర్భాలు ఎవ్వా తెలిసినవి కాదు. మీ ధైర్యంతో ఉపయోగించండి.\\n\\nఇన్నర్‌ట్యూన్ మీ టోకెన్ ను మాత్రమే తీసుకుందని, మిగతా అన్ని విషయాలు స్థానికంగా నిల్వ చేయబడ్డాయి.</string>\n    <string name=\"dismiss\">తొలగించు</string>\n    <string name=\"preview\">పరిదృశ్యం</string>\n    <string name=\"enable_discord_rpc\">రిచ్ ప్రెజెన్స్‌ను ఎనేబుల్ చేయండి</string>\n    <string name=\"app_version\">యాప్ వెర్షన్</string>\n    <string name=\"new_version_available\">కొత్త వెర్షన్ అందుబాటులో ఉంది</string>\n    <string name=\"translation_models\">అనువాద మోడల్స్</string>\n    <string name=\"clear_translation_models\">అనువాద నమూనాలను క్లియర్ చెయ్యండి</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-th/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">ในเครื่อง</string>\n    <string name=\"remote_history\">จากบัญชี</string>\n    <string name=\"charts\">ชาร์ตเพลง</string>\n    <string name=\"back_button_desc\">ย้อนกลับ</string>\n    <string name=\"album_cover_desc\">ปกอัลบั้ม</string>\n    <string name=\"top_music_videos\">มิวสิกวิดีโอยอดนิยม</string>\n    <string name=\"trending\">กำลังมาแรง</string>\n    <string name=\"weeks\">สัปดาห์</string>\n    <string name=\"months\">เดือน</string>\n    <string name=\"years\">ปี</string>\n    <string name=\"continuous\">ต่อเนื่อง</string>\n    <string name=\"liked\">ถูกใจ</string>\n    <string name=\"offline\">ดาวน์โหลดแล้ว</string>\n    <string name=\"my_top\">เพลงที่คุณฟังบ่อยที่สุด</string>\n    <string name=\"cached_playlist\">แคชแล้ว</string>\n    <string name=\"uploaded_playlist\">อัปโหลดแล้ว</string>\n    <string name=\"filter_uploaded\">อัปโหลดแล้ว</string>\n    <string name=\"sync_playlist\">ซิงค์เพลย์ลิสต์</string>\n    <string name=\"sync_disabled\">ปิดการซิงค์แล้ว</string>\n    <string name=\"allows_for_sync_witch_youtube\">หมายเหตุ: การเปิดใช้งานนี้จะทำให้สามารถซิงค์กับ YouTube Music ได้ และไม่สามารถเปลี่ยนแปลงภายหลังได้</string>\n    <string name=\"generating_image\">กำลังสร้างรูปภาพ…</string>\n    <string name=\"please_wait\">โปรดรอสักครู่</string>\n    <string name=\"cancel\">ยกเลิก</string>\n    <string name=\"enable\">เปิดใช้งาน</string>\n    <string name=\"share_lyrics\">แชร์เนื้อเพลง</string>\n    <string name=\"share_as_text\">แชร์เป็นข้อความ</string>\n    <string name=\"share_as_image\">แชร์เป็นรูปภาพ</string>\n    <string name=\"share_selected\">แชร์ที่เลือก</string>\n    <string name=\"customize_colors\">ปรับแต่งสี</string>\n    <string name=\"text_color\">สีข้อความ</string>\n    <string name=\"secondary_text_color\">สีข้อความรอง</string>\n    <string name=\"background_color\">สีพื้นหลัง</string>\n    <string name=\"remove_from_cache\">ลบออกจากแคช</string>\n    <string name=\"about_artist\">เกี่ยวกับศิลปิน</string>\n    <string name=\"show_more\">แสดงเพิ่มเติม</string>\n    <string name=\"show_less\">แสดงน้อยลง</string>\n    <string name=\"show_artist_description\">แสดงคำอธิบายศิลปิน</string>\n    <string name=\"show_artist_subscriber_count\">แสดงจำนวนผู้ติดตาม</string>\n    <string name=\"max_selection_limit\">จำนวนที่เลือกได้สูงสุด</string>\n    <string name=\"artist_page_settings\">หน้าศิลปิน</string>\n    <string name=\"show_artist_monthly_listeners\">แสดงจำนวนผู้ฟังรายเดือน</string>\n    <string name=\"download_playlist_desc\">ดาวน์โหลดเพลงทั้งหมดไว้ฟังแบบออฟไลน์</string>\n    <string name=\"remove_download_playlist_desc\">ลบเพลงที่ดาวน์โหลดทั้งหมดออกจากเพลย์ลิสต์นี้</string>\n    <string name=\"download_in_progress_desc\">กำลังดาวน์โหลด</string>\n    <string name=\"share_playlist_desc\">แชร์เพลย์ลิสต์นี้ให้ผู้อื่น</string>\n    <string name=\"delete_playlist_desc\">ลบเพลย์ลิสต์นี้อย่างถาวร</string>\n    <string name=\"sync_playlist_desc\">ซิงค์เพลย์ลิสต์กับ YouTube Music</string>\n    <string name=\"copy_link\">คัดลอกลิงก์</string>\n    <string name=\"select\">เลือกทั้งหมด</string>\n    <string name=\"like_all\">ถูกใจทั้งหมด</string>\n    <string name=\"dislike_all\">ไม่ถูกใจทั้งหมด</string>\n    <string name=\"link_copied\">คัดลอกลิงก์แล้ว</string>\n    <string name=\"sort_by_last_updated\">วันที่อัปเดตล่าสุด</string>\n    <string name=\"starting_radio\">กำลังเริ่มวิทยุ</string>\n    <string name=\"now_playing\">กำลังเล่น</string>\n    <string name=\"lyrics\">เนื้อเพลง</string>\n    <string name=\"close\">ปิด</string>\n    <string name=\"hide_player_thumbnail\">ซ่อนภาพตัวอย่างในเครื่องเล่น</string>\n    <string name=\"hide_player_thumbnail_desc\">แทนที่ภาพปกอัลบั้มด้วยโลโก้แอปในเครื่องเล่น</string>\n    <string name=\"crop_album_art\">ครอบตัดภาพปกอัลบั้ม</string>\n    <string name=\"crop_album_art_desc\">บังคับให้เป็นอัตราส่วนสี่เหลี่ยมจัตุรัสโดยครอบตัดภาพตัวอย่างวิดีโอ</string>\n    <string name=\"already_in_playlist\">มีอยู่ในเพลย์ลิสต์แล้ว:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"other\">%d ครั้ง</item>\n    </plurals>\n    <string name=\"seek_forward_dynamic\">+%1$d วินาที</string>\n    <string name=\"seek_backward_dynamic\">-%1$d วินาที</string>\n    <string name=\"seek_seconds_addup_description\">หากเปิดใช้งาน จะเพิ่มเวลาอีก 5 วินาทีแบบสะสมทุกครั้งที่กดข้าม</string>\n    <string name=\"seek_seconds_addup\">เพิ่มเวลาข้ามแบบต่อเนื่อง</string>\n    <string name=\"similar_content\">เนื้อหาที่คล้ายกัน</string>\n    <string name=\"player_background_style\">รูปแบบพื้นหลังเครื่องเล่น</string>\n    <string name=\"follow_theme\">ตามธีมระบบ</string>\n    <string name=\"gradient\">ไล่ระดับสี</string>\n    <string name=\"new_player_design\">ดีไซน์เครื่องเล่นใหม่</string>\n    <string name=\"new_mini_player_design\">ดีไซน์มินิเพลเยอร์ใหม่</string>\n    <string name=\"player_background_blur\">เบลอพื้นหลัง</string>\n    <string name=\"player_buttons_style\">สีปุ่มเครื่องเล่น</string>\n    <string name=\"default_style\">ค่าเริ่มต้น</string>\n    <string name=\"primary_color_style\">สีหลัก</string>\n    <string name=\"tertiary_color_style\">สีระดับที่สาม</string>\n    <string name=\"wavy\">คลื่น</string>\n    <string name=\"enable_swipe_thumbnail\">เปิดใช้งานการปัดเพื่อเปลี่ยนเพลง</string>\n    <string name=\"swipe_song_to_add\">ปัดเพลงไปทางซ้ายเพื่อเพิ่มในคิว หรือปัดไปทางขวาเพื่อเล่นเป็นลำดับถัดไป</string>\n    <string name=\"swipe_song_to_remove\">ปัดเพลงเพื่อเอาออกจากเพลย์ลิสต์</string>\n    <string name=\"lyrics_click_change\">เปลี่ยนเนื้อเพลงเมื่อแตะ</string>\n    <string name=\"lyrics_auto_scroll\">เลื่อนเนื้อเพลงอัตโนมัติ</string>\n    <string name=\"lyrics_glow_effect\">เปิดเอฟเฟกต์เรืองแสงของเนื้อเพลง</string>\n    <string name=\"lyrics_glow_effect_desc\">เพิ่มแอนิเมชันเรืองแสงและเอฟเฟกต์เด้งให้กับเนื้อเพลงที่กำลังแสดง</string>\n    <string name=\"enable_better_lyrics\">เปิดใช้งาน Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">เนื้อเพลงแบบซิงค์ระดับพยางค์สำหรับทุกเพลง เหมาะสำหรับร้องคาราโอเกะ</string>\n    <string name=\"enable_simpmusic\">เปิดใช้งาน SimpMusic Lyrics</string>\n    <string name=\"enable_simpmusic_desc\">ดึงเนื้อเพลงอัตโนมัติจาก Musixmatch และ YouTube Transcript</string>\n    <string name=\"auto_scroll\">ซิงค์ใหม่</string>\n    <string name=\"slim\">บาง</string>\n    <string name=\"slim_navbar\">แถบนำทางด้านล่างแบบบาง</string>\n    <string name=\"auto_playlists\">เพลย์ลิสต์อัตโนมัติ</string>\n    <string name=\"show_liked_playlist\">แสดงเพลย์ลิสต์ \\\"ถูกใจ\\\"</string>\n    <string name=\"show_downloaded_playlist\">แสดงเพลย์ลิสต์ \\\"ดาวน์โหลดแล้ว\\\"</string>\n    <string name=\"show_cached_playlist\">แสดงเพลย์ลิสต์ \\\"แคช\\\"</string>\n    <string name=\"show_uploaded_playlist\">แสดงเพลย์ลิสต์ \\\"อัปโหลดแล้ว\\\"</string>\n    <string name=\"shuffle_playlist_first\">เล่นแบบสุ่มจากเพลย์ลิสต์/อัลบั้มก่อน</string>\n    <string name=\"shuffle_playlist_first_desc\">เมื่อเปิดโหมดสุ่ม จะเล่นเพลงทั้งหมดจากเพลย์ลิสต์/อัลบั้มเดิมก่อน แล้วจึงเล่นเนื้อหาที่คล้ายกัน</string>\n    <string name=\"show_wrapped_card\">แสดงการ์ด Wrapped</string>\n    <string name=\"skip_silence_desc\">ข้ามช่วงที่เงียบของเพลงอย่างรวดเร็ว</string>\n    <string name=\"skip_silence_instant\">ข้ามช่วงเงียบทันที</string>\n    <string name=\"skip_silence_instant_desc\">กระโดดข้ามช่วงเงียบแทนการเร่งความเร็วการเล่น</string>\n    <string name=\"show_top_playlist\">แสดงเพลย์ลิสต์ \\\"เพลงที่คุณฟังบ่อยที่สุด\\\"</string>\n    <string name=\"advanced_login\">เข้าสู่ระบบด้วยโทเค็น</string>\n    <string name=\"token_hidden\">แตะเพื่อแสดงโทเค็น</string>\n    <string name=\"token_shown\">แตะอีกครั้งเพื่อคัดลอกหรือแก้ไข</string>\n    <string name=\"token_adv_login_description\">นี่เป็นวิธีเข้าสู่ระบบขั้นสูง คุณสามารถกรอกหรืออัปเดตโทเค็นเข้าสู่ระบบได้โดยตรงแทนการใช้เว็บพอร์ทัล วิธีนี้ช่วยให้เข้าสู่ระบบบนหลายอุปกรณ์ได้รวดเร็วขึ้น โปรดทราบว่าโทเค็นที่มีรูปแบบไม่ถูกต้องจะไม่สามารถใช้งานได้</string>\n    <string name=\"yt_sync\">ซิงค์กับบัญชีโดยอัตโนมัติ</string>\n    <string name=\"more_content\">เนื้อหาเพิ่มเติม</string>\n    <string name=\"edit_playlist_cover\">แก้ไขปกเพลย์ลิสต์</string>\n    <string name=\"edit_playlist_cover_note\">หมายเหตุ: บัญชีของคุณต้องเชื่อมโยงกับหมายเลขโทรศัพท์และยืนยันตัวตนบน YouTube Music ก่อนจึงจะสามารถเปลี่ยนปกเพลย์ลิสต์ได้</string>\n    <string name=\"edit_playlist_cover_note_wait\">หลังจากเลือกภาพแล้ว โปรดรอสักครู่เพื่อให้ปกใหม่แสดงในเพลย์ลิสต์ของคุณ</string>\n    <string name=\"choose_from_library\">เลือกจากคลัง</string>\n    <string name=\"remove_custom_image\">ลบภาพที่กำหนดเอง</string>\n    <string name=\"general\">ทั่วไป</string>\n    <string name=\"proxy\">พร็อกซี</string>\n    <string name=\"default_lib_chips\">เปลี่ยนแท็บเริ่มต้นในคลัง</string>\n    <string name=\"last_song_listened\">อิงจากเพลงล่าสุดที่ฟัง</string>\n    <string name=\"app_language\">ภาษาของแอป</string>\n    <string name=\"set_quick_picks\">ตั้งค่าการเลือกด่วน</string>\n    <string name=\"config_proxy\">ตั้งค่าพร็อกซี</string>\n    <string name=\"proxy_username\">ชื่อผู้ใช้พร็อกซี</string>\n    <string name=\"proxy_password\">รหัสผ่านพร็อกซี</string>\n    <string name=\"enable_authentication\">เปิดใช้งานการยืนยันตัวตน</string>\n    <string name=\"discord_use_details\">ใช้รายละเอียดแทนสถานะ</string>\n    <string name=\"discord_use_details_description\">แสดงชื่อเพลงอย่างเด่นชัดแทนชื่อศิลปิน</string>\n    <string name=\"enable_similar_content\">เปิดใช้งานเนื้อหาที่คล้ายกัน</string>\n    <string name=\"similar_content_desc\">เพิ่มเพลงที่คล้ายกันโดยอัตโนมัติเมื่อถึงท้ายคิว</string>\n    <string name=\"persistent_shuffle_title\">โหมดสุ่มคงที่</string>\n    <string name=\"persistent_shuffle_desc\">คงสถานะโหมดสุ่มไว้เมื่อเริ่มเพลงหรือเพลย์ลิสต์ใหม่</string>\n    <string name=\"remember_shuffle_and_repeat\">จดจำการตั้งค่าโหมดสุ่มและเล่นซ้ำ</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">จดจำโหมดสุ่มและเล่นซ้ำเมื่อเปิดแอปใหม่</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"import_online\">นำเข้าเพลย์ลิสต์ \\\"m3u\\\"</string>\n    <string name=\"import_csv\">นำเข้าเพลย์ลิสต์ \\\"csv\\\"</string>\n    <string name=\"playlist_add_local_to_synced_note\">หมายเหตุ: ไม่รองรับการเพิ่มเพลงในเครื่องลงในเพลย์ลิสต์ที่ซิงค์/ระยะไกล การเพิ่มในรูปแบบอื่นสามารถใช้งานได้ตามปกติ</string>\n    <string name=\"auto_download_on_like\">ดาวน์โหลดอัตโนมัติเมื่อกดถูกใจ</string>\n    <string name=\"auto_download_on_like_desc\">ดาวน์โหลดเพลงโดยอัตโนมัติเมื่อคุณกดถูกใจ</string>\n    <string name=\"swipe_sensitivity\">ความไวในการปัดของมินิเพลเยอร์</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"clear_song_cache_dialog\">คุณแน่ใจหรือไม่ว่าต้องการลบแคชเพลงทั้งหมด?</string>\n    <string name=\"clear_image_cache_dialog\">คุณแน่ใจหรือไม่ว่าต้องการลบแคชรูปภาพทั้งหมด?</string>\n    <string name=\"clear_downloads_dialog\">คุณแน่ใจหรือไม่ว่าต้องการลบไฟล์ที่ดาวน์โหลดทั้งหมด?</string>\n    <string name=\"disable\">ปิดใช้งาน</string>\n    <string name=\"not_logged_in_youtube\">ยังไม่ได้เข้าสู่ระบบ YouTube</string>\n    <string name=\"default_links\">เปิดลิงก์ที่รองรับ</string>\n    <string name=\"open_app_settings_error\">ไม่สามารถเปิดการตั้งค่าแอปได้</string>\n    <string name=\"release_notes\">บันทึกการอัปเดต</string>\n    <string name=\"all_time\">ตลอดเวลา</string>\n    <string name=\"past_24_hours\">24 ชั่วโมงที่ผ่านมา</string>\n    <string name=\"past_week\">สัปดาห์ที่ผ่านมา</string>\n    <string name=\"past_month\">เดือนที่ผ่านมา</string>\n    <string name=\"past_year\">ปีที่ผ่านมา</string>\n    <string name=\"top_length\">จำนวนรายการใน \\\"เพลงที่คุณฟังบ่อยที่สุด\\\"</string>\n    <string name=\"history_duration\">ระยะเวลาการเก็บประวัติ</string>\n    <string name=\"information\">ข้อมูล</string>\n    <string name=\"description\">คำอธิบาย</string>\n    <string name=\"views\">ยอดเข้าชม</string>\n    <string name=\"likes\">จำนวนถูกใจ</string>\n    <string name=\"dislikes\">จำนวนไม่ถูกใจ</string>\n    <string name=\"subscribe\">ติดตาม</string>\n    <string name=\"subscribed\">ติดตามแล้ว</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"other\">%d วินาที</item>\n    </plurals>\n    <string name=\"disable_load_more_when_repeat_all\">ปิดการโหลดเพิ่มเมื่อเปิดโหมดเล่นซ้ำทั้งหมด</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">ไม่โหลดเพลงและเนื้อหาที่คล้ายกันเพิ่มโดยอัตโนมัติเมื่อเปิดโหมดเล่นซ้ำทั้งหมด</string>\n    <string name=\"pause_music_when_media_is_muted\">หยุดเพลงเมื่อปิดเสียงสื่อ</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">เปิดหน้าจอไว้เมื่อขยายเครื่องเล่น</string>\n    <string name=\"crossfade\">ครอสเฟด</string>\n    <string name=\"crossfade_desc\">ครอสเฟดระหว่างเพลง</string>\n    <string name=\"crossfade_duration\">ระยะเวลาครอสเฟด</string>\n    <string name=\"crossfade_gapless\">ปิดสำหรับอัลบั้มแบบไม่มีช่วงว่าง</string>\n    <string name=\"crossfade_gapless_desc\">ไม่ใช้ครอสเฟดหากอัลบั้มไม่มีช่วงว่างระหว่างเพลง</string>\n    <string name=\"crossfade_beta_title\">ฟีเจอร์เบต้า</string>\n    <string name=\"crossfade_beta_message\">ครอสเฟดเป็นฟีเจอร์ใหม่และอาจมีข้อผิดพลาด หากพบปัญหา โปรดรายงานให้เราทราบ\\n\\nฟีเจอร์นี้จะปิดการใช้งาน audio offload เนื่องจากข้อจำกัดทางเทคนิค</string>\n    <string name=\"lyrics_romanization_cyrillic\">ซีริลลิก</string>\n    <string name=\"lyrics_romanize_title\">การถอดเสียง</string>\n    <string name=\"lyrics_romanization\">การถอดเสียงเนื้อเพลง</string>\n    <string name=\"lyrics_romanize_japanese\">ถอดเสียงเนื้อเพลงภาษาญี่ปุ่น</string>\n    <string name=\"lyrics_romanize_korean\">ถอดเสียงเนื้อเพลงภาษาเกาหลี</string>\n    <string name=\"lyrics_romanize_chinese\">ถอดเสียงเนื้อเพลงภาษาจีน</string>\n    <string name=\"lyrics_romanize_russian\">ถอดเสียงเนื้อเพลงภาษารัสเซีย</string>\n    <string name=\"lyrics_romanize_ukrainian\">ถอดเสียงเนื้อเพลงภาษายูเครน</string>\n    <string name=\"lyrics_romanize_belarusian\">ถอดเสียงเนื้อเพลงภาษาเบลารุส</string>\n    <string name=\"lyrics_romanize_kyrgyz\">ถอดเสียงเนื้อเพลงภาษาคีร์กีซ</string>\n    <string name=\"lyrics_romanize_serbian\">ถอดเสียงเนื้อเพลงภาษาเซอร์เบีย</string>\n    <string name=\"lyrics_romanize_bulgarian\">ถอดเสียงเนื้อเพลงภาษาบัลแกเรีย</string>\n    <string name=\"line_by_line_option_title\">ทดลองใช้: ตรวจจับภาษาทีละบรรทัด</string>\n    <string name=\"line_by_line_option_desc\">ตรวจจับภาษากลุ่มซีริลลิกแบบทีละบรรทัดแทนการตรวจทั้งเพลง</string>\n    <string name=\"line_by_line_dialog_title\">คุณแน่ใจหรือไม่?</string>\n    <string name=\"line_by_line_dialog_desc\">ฟีเจอร์นี้ยังอยู่ในขั้นทดลองและอาจทำงานได้ไม่สม่ำเสมอ\\n\\nโดยปกติ ระบบจะตรวจจับภาษาจากทั้งเพลง แต่เมื่อเปิดตัวเลือกนี้ จะตรวจจับทีละบรรทัดแทน ซึ่งช่วยให้เพลงหลายภาษาใช้งานได้ดีขึ้น อย่างไรก็ตาม ภาษาอาจตรวจจับไม่ถูกต้องเสมอไป (เช่น หากมีเนื้อเพลงภาษายูเครนที่ไม่มีตัวอักษรเฉพาะ ระบบอาจถอดเสียงเป็นภาษารัสเซียแทน)\\n\\nหากคุณไม่พบปัญหา แนะนำให้ปิดตัวเลือกนี้ไว้</string>\n    <string name=\"romanize_current_track\">ถอดเสียงเพลงที่กำลังเล่น</string>\n    <string name=\"lyrics_offset\">การหน่วงเวลาเนื้อเพลง</string>\n    <string name=\"settings_section_ui\">อินเทอร์เฟซ</string>\n    <string name=\"settings_section_privacy\">ความเป็นส่วนตัวและความปลอดภัย</string>\n    <string name=\"settings_section_player_content\">เครื่องเล่นและเนื้อหา</string>\n    <string name=\"settings_section_storage\">พื้นที่จัดเก็บและข้อมูล</string>\n    <string name=\"settings_section_system\">ระบบและเกี่ยวกับแอป</string>\n    <string name=\"updater\">ตัวอัปเดต</string>\n    <string name=\"check_for_updates\">ตรวจสอบการอัปเดตอัตโนมัติ</string>\n    <string name=\"update_notifications\">เปิดการแจ้งเตือนการอัปเดต</string>\n    <string name=\"update_available_title\">มีการอัปเดตใหม่</string>\n    <string name=\"update_channel_name\">การอัปเดตแอป</string>\n    <string name=\"update_channel_desc\">การแจ้งเตือนเกี่ยวกับเวอร์ชันใหม่</string>\n    <string name=\"audio_offload\">เปิดใช้งาน Audio Offload</string>\n    <string name=\"audio_offload_description\">ใช้เส้นทางเสียงแบบ offload สำหรับการเล่นเสียง การปิดใช้งานอาจทำให้ใช้พลังงานเพิ่มขึ้น แต่สามารถช่วยแก้ปัญหาการเล่นเสียงหรือการประมวลผลเสียงเพิ่มเติมได้</string>\n    <string name=\"audio_offload_disabled_by_crossfade\">ปิดใช้งานเนื่องจากเปิดครอสเฟดอยู่</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"google_cast_description\">เปิดใช้งานการส่งเสียงไปยัง Chromecast และอุปกรณ์ที่รองรับ Cast อื่น ๆ</string>\n    <string name=\"lyrics_romanize_macedonian\">ถอดเสียงเนื้อเพลงภาษามาซิโดเนีย</string>\n    <string name=\"integrations\">การเชื่อมต่อบริการ</string>\n    <string name=\"username\">ชื่อผู้ใช้</string>\n    <string name=\"password\">รหัสผ่าน</string>\n    <string name=\"lastfm_integration\">การเชื่อมต่อ Last.fm</string>\n    <string name=\"enable_scrobbling\">เปิดใช้งาน Scrobbling</string>\n    <string name=\"lastfm_now_playing\">ส่งสถานะ “กำลังเล่น”</string>\n    <string name=\"last_fm_send_likes\">ส่งสถานะถูกใจ/ยกเลิกถูกใจ</string>\n    <string name=\"last_fm_send_likes_description\">กด Love/Unlove เพลงใน Last.fm เมื่อมีการกดถูกใจ/ยกเลิกถูกใจใน Metrolist</string>\n    <string name=\"logging_in\">กำลังเข้าสู่ระบบ…</string>\n    <string name=\"scrobbling_configuration\">การตั้งค่า Scrobbling</string>\n    <string name=\"scrobble_min_track_duration\">Scrobble เพลงที่ยาวกว่า</string>\n    <string name=\"scrobble_delay_percent\">เปอร์เซ็นต์หน่วงเวลาก่อน Scrobble</string>\n    <string name=\"scrobble_delay_minutes\">หน่วงเวลาก่อน Scrobble (นาที)</string>\n    <string name=\"hide_video_songs\">ซ่อนเพลงที่เป็นวิดีโอ</string>\n    <string name=\"hide_youtube_shorts\">ซ่อน YouTube Shorts</string>\n    <string name=\"details_desc\">ดูข้อมูลเพลง</string>\n    <string name=\"edit_desc\">เปลี่ยนชื่อเพลงหรือศิลปิน</string>\n    <string name=\"start_radio_desc\">สร้างสถานีจากรายการนี้</string>\n    <string name=\"play_next_desc\">เพิ่มไว้ลำดับถัดไป</string>\n    <string name=\"add_to_queue_desc\">เพิ่มไว้ท้ายคิว</string>\n    <string name=\"add_to_library_desc\">บันทึกไปยังคลังของคุณ</string>\n    <string name=\"download_desc\">ดาวน์โหลดเพื่อฟังแบบออฟไลน์</string>\n    <string name=\"add_to_playlist_desc\">เพิ่มไปยังเพลย์ลิสต์ของคุณ</string>\n    <string name=\"refetch_desc\">ดึงข้อมูลล่าสุดจาก YouTube Music</string>\n    <string name=\"share_desc\">แชร์ลิงก์ของรายการนี้</string>\n    <string name=\"delete_desc\">ลบรายการนี้อย่างถาวร</string>\n    <string name=\"advanced_desc\">ปรับความเร็วและคีย์เสียงของเพลง</string>\n    <string name=\"equalizer_desc\">ปรับอีควอไลเซอร์เสียง</string>\n    <string name=\"enable_dynamic_icon\">เปิดใช้งานไอคอนไดนามิก</string>\n    <string name=\"mini_player\">มินิเพลเยอร์</string>\n    <string name=\"pure_black_mini_player\">มินิเพลเยอร์สีดำสนิท</string>\n    <string name=\"cache_size_warning_title\">เดี๋ยวก่อน!</string>\n    <string name=\"cache_size_warning_message\">คุณได้เลือกขีดจำกัดแคชที่น้อยกว่าที่แอปกำลังใช้อยู่ในขณะนี้ (%1$s) หากดำเนินการต่อ แอปอาจลบ %2$s บางส่วนที่ถูกแคชไว้เพื่อให้ตรงกับขีดจำกัดใหม่ ต้องการดำเนินการต่อหรือไม่?</string>\n    <string name=\"cache_size_warning_confirm\">ดำเนินการต่อ</string>\n    <string name=\"lyrics_animation_style\">รูปแบบแอนิเมชันแบบคำต่อคำ</string>\n    <string name=\"none\">ไม่มี</string>\n    <string name=\"fade\">จางเข้า</string>\n    <string name=\"glow\">เรืองแสง</string>\n    <string name=\"slide\">เลื่อนเข้า</string>\n    <string name=\"karaoke\">คาราโอเกะ</string>\n    <string name=\"apple_music_style\">สไตล์ Apple Music</string>\n    <string name=\"lyrics_text_size\">ขนาดตัวอักษรเนื้อเพลง</string>\n    <string name=\"lyrics_line_spacing\">ระยะห่างบรรทัดเนื้อเพลง</string>\n    <string name=\"album_art_for\">ปกอัลบั้มของ %s</string>\n    <string name=\"wrapped_total_albums_title\">คุณฟังไปทั้งหมด</string>\n    <string name=\"wrapped_total_albums_subtitle\">อัลบั้มที่ไม่ซ้ำกัน</string>\n    <string name=\"wrapped_top_album_title\">อัลบั้มที่คุณฟังมากที่สุดคือ</string>\n    <string name=\"wrapped_playlist_ready\">เพลย์ลิสต์ส่วนตัวของคุณพร้อมแล้ว</string>\n    <string name=\"wrapped_top_5_albums_title\">5 อัลบั้มที่คุณฟังมากที่สุด</string>\n    <string name=\"wrapped_album_listening_time\">คุณฟังอัลบั้มนี้ไปแล้ว %d นาที</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d นาที</string>\n    <string name=\"wrapped_no_data\">ไม่มีข้อมูล</string>\n    <string name=\"wrapped_top_5_artists_title\">ศิลปินที่คุณฟังมากที่สุดในปีนี้</string>\n    <string name=\"wrapped_artist_listening_time\">%d นาที</string>\n    <string name=\"wrapped_top_5_songs_title\">เพลงที่คุณฟังมากที่สุดในปีนี้</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">ปกอัลบั้ม</string>\n    <string name=\"wrapped_top_artist_title\">ศิลปินที่คุณฟังมากที่สุดในปีนี้คือ</string>\n    <string name=\"wrapped_top_artist_image_content_description\">รูปภาพศิลปินอันดับหนึ่ง</string>\n    <string name=\"wrapped_top_artist_listening_time\">คุณฟังศิลปินคนนี้ไปแล้ว %d นาที</string>\n    <string name=\"wrapped_top_song_title\">เพลงที่คุณเปิดฟังมากที่สุดคือ</string>\n    <string name=\"wrapped_top_song_listening_time\">คุณฟังเพลงนี้ไปแล้ว %d นาที</string>\n    <string name=\"wrapped_total_artists_title\">คุณฟังไปทั้งหมด</string>\n    <string name=\"wrapped_total_artists_subtitle\">ศิลปินที่ไม่ซ้ำกัน</string>\n    <string name=\"wrapped_total_songs_title\">คุณฟังไปทั้งหมด</string>\n    <string name=\"wrapped_total_songs_subtitle\">เพลงที่ไม่ซ้ำกัน</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_intro_subtitle\">ได้เวลามาดูสิ่งที่คุณฟังไปตลอดปี</string>\n    <string name=\"wrapped_intro_button\">ไปดูกันเลย!</string>\n    <string name=\"wrapped_logo_content_description\">โลโก้ Metrolist</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"wrapped_ready_title\">WRAPPED ของคุณพร้อมแล้ว!</string>\n    <string name=\"wrapped_ready_subtitle\">มาดูสิ่งที่คุณชื่นชอบในปีนี้กัน</string>\n    <string name=\"wrapped_thank_you\">ขอบคุณที่รับฟัง</string>\n    <string name=\"wrapped_special_thanks\">ขอขอบคุณเป็นพิเศษแก่ MO Agamy ผู้สร้าง Metrolist</string>\n    <string name=\"wrapped_close\">ปิด Wrapped</string>\n    <string name=\"wrapped_playlist_title\">Wrapped %s ของคุณ</string>\n    <string name=\"wrapped_create_playlist\">สร้างเพลย์ลิสต์</string>\n    <string name=\"wrapped_playlist_saved\">บันทึกเพลย์ลิสต์แล้ว</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"other\">%d โปรไฟล์</item>\n    </plurals>\n    <string name=\"equalizer_header\">อีควอไลเซอร์</string>\n    <string name=\"no_profiles\">ไม่มีโปรไฟล์อีควอไลเซอร์</string>\n    <string name=\"import_profile\">นำเข้าโปรไฟล์</string>\n    <string name=\"system_equalizer\">อีควอไลเซอร์ของระบบ</string>\n    <string name=\"eq_disabled\">ปิดใช้งาน</string>\n    <plurals name=\"band_count\">\n        <item quantity=\"other\">%d แบนด์</item>\n    </plurals>\n    <string name=\"delete_profile_desc\">ลบโปรไฟล์</string>\n    <string name=\"delete_profile_confirmation\">คุณแน่ใจหรือไม่ว่าต้องการลบ %1$s? การดำเนินการนี้ไม่สามารถย้อนกลับได้</string>\n    <string name=\"error_file_read\">ไม่สามารถอ่านไฟล์ได้</string>\n    <string name=\"error_file_open\">ไม่สามารถเปิดไฟล์: %1$s</string>\n    <string name=\"import_error_title\">ข้อผิดพลาดในการนำเข้า</string>\n    <string name=\"error_title\">ข้อผิดพลาด</string>\n    <string name=\"error_eq_apply_failed\">ไม่สามารถใช้โปรไฟล์ EQ : %1$s</string>\n    <string name=\"casting_to\">กำลังแคสต์ไปยัง %s</string>\n    <string name=\"progress_percent\">ความคืบหน้า %s%%</string>\n    <string name=\"listening_to_metrolist\">กำลังฟังผ่าน Metrolist</string>\n    <string name=\"open\">เปิด</string>\n    <string name=\"failed_to_create_image\">ไม่สามารถสร้างรูปภาพ: %s</string>\n    <string name=\"copied_title\">คัดลอกชื่อเพลงแล้ว</string>\n    <string name=\"copied_artist\">คัดลอกชื่อศิลปินแล้ว</string>\n    <string name=\"error_playing\">เกิดข้อผิดพลาดในการเล่น</string>\n    <string name=\"failed_to_parse_proxy\">ไม่สามารถอ่านค่า Proxy URL ได้</string>\n    <string name=\"error_playback_failed\">การเล่นล้มเหลว</string>\n    <string name=\"album_art\">ปกอัลบั้ม</string>\n    <string name=\"no_song_playing\">ไม่มีเพลงกำลังเล่น</string>\n    <string name=\"tap_to_open\">แตะเพื่อเปิด Metrolist</string>\n    <string name=\"previous\">ก่อนหน้า</string>\n    <string name=\"play_pause\">เล่น/หยุด</string>\n    <string name=\"next\">ถัดไป</string>\n    <string name=\"like\">ถูกใจ</string>\n    <string name=\"not_playing\">ไม่มีเพลงกำลังเล่น</string>\n    <string name=\"tap_to_play\">แตะเพื่อเปิด Metrolist</string>\n    <string name=\"widget_description\">วิดเจ็ตเครื่องเล่นเพลงพร้อมปุ่มควบคุมการเล่น</string>\n    <string name=\"turntable_widget_description\">วิดเจ็ตเพลงแบบวงกลม พร้อมปุ่มเล่นและถูกใจ</string>\n    <string name=\"widget_music_player\">เครื่องเล่นเพลง</string>\n    <string name=\"widget_turntable\">เทิร์นเทเบิล</string>\n    <string name=\"together\">ร่วมกัน</string>\n    <string name=\"listen_together\">ฟังเพลงร่วมกัน</string>\n    <string name=\"listen_together_server_url\">URL เซิร์ฟเวอร์</string>\n    <string name=\"listen_together_choose_server\">เลือกเซิร์ฟเวอร์</string>\n    <string name=\"listen_together_custom_server\">เซิร์ฟเวอร์กำหนดเอง</string>\n    <string name=\"listen_together_use_custom_server\">ใช้เซิร์ฟเวอร์กำหนดเอง</string>\n    <string name=\"listen_together_username\">ชื่อผู้ใช้</string>\n    <string name=\"listen_together_connected\">เชื่อมต่อแล้ว</string>\n    <string name=\"listen_together_reconnecting\">กำลังเชื่อมต่อใหม่…</string>\n    <string name=\"listen_together_disconnected\">ตัดการเชื่อมต่อแล้ว</string>\n    <string name=\"listen_together_connecting\">กำลังเชื่อมต่อ…</string>\n    <string name=\"listen_together_error\">เกิดข้อผิดพลาดในการเชื่อมต่อ</string>\n    <string name=\"listen_together_create_room\">สร้างห้อง</string>\n    <string name=\"listen_together_create_room_desc\">สร้างห้องและแชร์รหัสให้เพื่อน</string>\n    <string name=\"listen_together_join_room\">เข้าร่วมห้อง</string>\n    <string name=\"listen_together_room_code\">รหัสห้อง</string>\n    <string name=\"listen_together_you_are_host\">คุณคือโฮสต์</string>\n    <string name=\"listen_together_you_are_guest\">คุณคือผู้เข้าร่วม</string>\n    <string name=\"mute\">ปิดเสียง</string>\n    <string name=\"unmute\">เปิดเสียง</string>\n    <string name=\"listen_together_join_requests\">คำขอเข้าร่วม</string>\n    <string name=\"listen_together_view_logs\">ดูบันทึกการทำงาน</string>\n    <string name=\"listen_together_view_logs_desc\">ตรวจสอบการเชื่อมต่อและข้อความเพื่อแก้ไขปัญหา</string>\n    <string name=\"listen_together_logs\">บันทึกการเชื่อมต่อ</string>\n    <string name=\"listen_together_no_logs\">ยังไม่มีบันทึกการทำงาน</string>\n    <string name=\"listen_together_auto_approval_joins\">อนุมัติคำขอเข้าร่วมอัตโนมัติ</string>\n    <string name=\"listen_together_auto_approval_joins_desc\">อนุมัติคำขอเข้าร่วมโดยอัตโนมัติ แทนการตรวจสอบด้วยตนเอง</string>\n    <string name=\"listen_together_sync_volume\">ซิงก์ระดับเสียงกับโฮสต์</string>\n    <string name=\"listen_together_sync_volume_desc\">ผู้เข้าร่วมจะใช้ระดับเสียงเดียวกับโฮสต์</string>\n    <string name=\"listen_together_in_top_bar\">แสดง Listen Together บนแถบด้านบน</string>\n    <string name=\"listen_together_in_top_bar_desc\">แสดง Listen Together บนแถบด้านบนของแอป แทนแถบนำทางด้านล่าง</string>\n    <string name=\"listen_together_description\">ฟังเพลงกับเพื่อนแบบเรียลไทม์ สร้างห้องเพื่อเป็นโฮสต์ หรือเข้าร่วมห้องที่มีอยู่แล้วด้วยรหัส</string>\n    <string name=\"listen_together_background_disconnect_note\">หมายเหตุ: คุณอาจถูกตัดการเชื่อมต่อ หากสร้างห้องในขณะที่ไม่มีเพลงเล่นอยู่ แล้วสลับไปใช้แอปอื่น</string>\n    <string name=\"listen_together_not_configured\">ยังไม่ได้ตั้งค่า Listen Together โปรดกำหนด URL เซิร์ฟเวอร์ใน การตั้งค่า → การเชื่อมต่อบริการ → Listen Together</string>\n    <string name=\"listen_together_suggestion_received\">%1$s ขอให้เล่น %2$s</string>\n    <string name=\"listen_together_suggestion_sent\">ส่งคำแนะนำไปยังโฮสต์แล้ว!</string>\n    <string name=\"listen_together_join_request_notification\">%1$s ต้องการเข้าร่วมห้อง</string>\n    <string name=\"listen_together_notification_channel_name\">Listen Together</string>\n    <string name=\"listen_together_notification_channel_desc\">การแจ้งเตือนเกี่ยวกับกิจกรรมของ Listen Together</string>\n    <string name=\"listen_together_room_created\">สร้างห้อง %s แล้ว</string>\n    <string name=\"listen_together_cannot_edit_username_in_room\">ไม่สามารถแก้ไขชื่อผู้ใช้ขณะอยู่ในห้องได้</string>\n    <string name=\"waiting_for_approval\">กำลังรอการอนุมัติจากโฮสต์</string>\n    <string name=\"invalid_room_code\">รหัสห้องไม่ถูกต้อง</string>\n    <string name=\"join_request_denied\">คำขอเข้าร่วมถูกปฏิเสธ</string>\n    <string name=\"join_existing_room\">เข้าร่วมห้องที่มีอยู่</string>\n    <string name=\"room_code\">รหัสห้อง</string>\n    <string name=\"leave_room\">ออกจากห้อง</string>\n    <string name=\"join_room\">เข้าร่วม</string>\n    <string name=\"create_room\">สร้าง</string>\n    <string name=\"joining_room\">กำลังเข้าร่วมห้อง %s…</string>\n    <string name=\"creating_room\">กำลังสร้างห้อง…</string>\n    <string name=\"connect\">เชื่อมต่อ</string>\n    <string name=\"disconnect\">ตัดการเชื่อมต่อ</string>\n    <string name=\"create\">สร้าง</string>\n    <string name=\"join\">เข้าร่วม</string>\n    <string name=\"approve\">อนุมัติ</string>\n    <string name=\"reject\">ปฏิเสธ</string>\n    <string name=\"clear\">ล้าง</string>\n    <string name=\"copy\">คัดลอก</string>\n    <string name=\"copied_to_clipboard\">คัดลอกไปยังคลิปบอร์ดแล้ว</string>\n    <string name=\"not_set\">ยังไม่ได้ตั้งค่า</string>\n    <string name=\"hosting_room\">กำลังเป็นโฮสต์</string>\n    <string name=\"in_room\">อยู่ในห้อง</string>\n    <string name=\"pending_requests\">คำขอที่รอดำเนินการ</string>\n    <string name=\"pending_suggestions\">คำแนะนำที่รอดำเนินการ</string>\n    <string name=\"suggest_to_host\">แนะนำให้โฮสต์</string>\n    <string name=\"kick_user\">นำออก</string>\n    <string name=\"host_label\">โฮสต์</string>\n    <string name=\"you_label\">คุณ</string>\n    <string name=\"connected_users\">ผู้ใช้ที่เชื่อมต่ออยู่</string>\n    <string name=\"enter_username\">กรอกชื่อผู้ใช้</string>\n    <string name=\"enter_room_code\">กรอกรหัสห้อง</string>\n    <string name=\"listen_together_settings_desc\">ตั้งค่าเซิร์ฟเวอร์ ชื่อผู้ใช้ และอื่น ๆ</string>\n    <string name=\"error_username_empty\">จำเป็นต้องระบุชื่อผู้ใช้</string>\n    <string name=\"resync\">ซิงก์ใหม่</string>\n    <string name=\"copy_code\">คัดลอกรหัส</string>\n    <string name=\"kick_user_desc\">นำบุคคลนี้ออกจากเซสชัน</string>\n    <string name=\"permanently_kick_user\">บล็อกถาวร</string>\n    <string name=\"permanently_kick_user_desc\">บล็อกคำขอเข้าร่วมของบุคคลนี้ และซ่อนคำแนะนำของเขา</string>\n    <string name=\"transfer_ownership\">โอนสิทธิ์ความเป็นเจ้าของ</string>\n    <string name=\"transfer_ownership_desc\">ให้บุคคลนี้เป็นโฮสต์ของห้อง</string>\n    <string name=\"manage_user\">จัดการผู้ใช้</string>\n    <string name=\"listen_together_blocked_users\">ผู้ใช้ที่ถูกบล็อก</string>\n    <string name=\"listen_together_blocked_users_count\">บล็อกผู้ใช้แล้ว %d คน</string>\n    <string name=\"listen_together_no_blocked_users\">ไม่มีผู้ใช้ที่ถูกบล็อก</string>\n    <string name=\"unblock\">เลิกบล็อก</string>\n    <string name=\"user_blocked_by_host\">ผู้ใช้ถูกบล็อกโดยโฮสต์</string>\n    <string name=\"ai_lyrics_translation\">แปลเนื้อเพลงด้วย AI</string>\n    <string name=\"ai_translating_lyrics\">กำลังแปลเนื้อเพลง...</string>\n    <string name=\"ai_lyrics_translated\">แปลเนื้อเพลงแล้ว</string>\n    <string name=\"ai_provider\">ผู้ให้บริการ</string>\n    <string name=\"ai_base_url\">Base URL</string>\n    <string name=\"ai_api_key\">API Key</string>\n    <string name=\"ai_model\">Model</string>\n    <string name=\"ai_translation_mode\">โหมดการแปล</string>\n    <string name=\"ai_target_language\">ภาษาปลายทาง</string>\n    <string name=\"ai_setup_guide\">API Credentials</string>\n    <string name=\"ai_translation_literal\">การแปลภาษา</string>\n    <string name=\"ai_translation_transcribed\">การถอดเสียง</string>\n    <string name=\"ai_api_key_required\">จำเป็นต้องใช้ API Key</string>\n    <string name=\"ai_error_api_key_required\">จำเป็นต้องระบุ API Key</string>\n    <string name=\"ai_error_no_lyrics\">ไม่มีเนื้อเพลงให้แปล</string>\n    <string name=\"ai_error_lyrics_empty\">เนื้อเพลงว่างเปล่า</string>\n    <string name=\"ai_error_language_required\">จำเป็นต้องระบุภาษาปลายทาง</string>\n    <string name=\"ai_error_unexpected\">ผลลัพธ์การแปลไม่ถูกต้องตามที่คาดไว้</string>\n    <string name=\"ai_error_unknown\">เกิดข้อผิดพลาดที่ไม่ทราบสาเหตุ</string>\n    <string name=\"ai_error_translation_failed\">การแปลล้มเหลว</string>\n    <string name=\"crash_title\">แอปหยุดทำงาน</string>\n    <string name=\"crash_description\">เกิดข้อผิดพลาดที่ไม่คาดคิด โปรดแชร์รายงานข้อขัดข้องเพื่อช่วยให้เราแก้ไขปัญหา</string>\n    <string name=\"crash_share_logs\">แชร์บันทึกข้อขัดข้อง</string>\n    <string name=\"crash_share_title\">แชร์รายงานข้อขัดข้อง</string>\n    <string name=\"crash_report_subject\">รายงานข้อขัดข้องของ Metrolist</string>\n    <string name=\"crash_close\">ปิด</string>\n    <string name=\"crash_no_log\">ไม่มีบันทึกข้อขัดข้อง</string>\n    <string name=\"palette_dynamic\">ไดนามิก</string>\n    <string name=\"palette_crimson\">คริมสัน</string>\n    <string name=\"palette_rose\">โรส</string>\n    <string name=\"palette_purple\">ม่วง</string>\n    <string name=\"palette_deep_purple\">ม่วงเข้ม</string>\n    <string name=\"palette_indigo\">อินดิโก</string>\n    <string name=\"palette_blue\">น้ำเงิน</string>\n    <string name=\"palette_sky_blue\">ฟ้าท้องฟ้า</string>\n    <string name=\"palette_cyan\">ไซแอน</string>\n    <string name=\"palette_teal\">ทีล</string>\n    <string name=\"palette_green\">เขียว</string>\n    <string name=\"palette_light_green\">เขียวอ่อน</string>\n    <string name=\"palette_lime\">ไลม์</string>\n    <string name=\"palette_yellow\">เหลือง</string>\n    <string name=\"palette_amber\">แอมเบอร์</string>\n    <string name=\"palette_orange\">ส้ม</string>\n    <string name=\"palette_deep_orange\">ส้มเข้ม</string>\n    <string name=\"palette_brown\">น้ำตาล</string>\n    <string name=\"palette_grey\">เทา</string>\n    <string name=\"palette_blue_grey\">เทาน้ำเงิน</string>\n    <string name=\"cd_back\">ย้อนกลับ</string>\n    <string name=\"cd_pure_black_mode\">โหมดดำสนิท</string>\n    <string name=\"cd_light_mode\">โหมดสว่าง</string>\n    <string name=\"cd_dark_mode\">โหมดมืด</string>\n    <string name=\"cd_system_mode\">ตามค่าระบบ</string>\n    <string name=\"cd_palette_item\">ชุดสี %1$s</string>\n    <string name=\"play_all\">เล่นทั้งหมด</string>\n    <string name=\"enable_high_refresh_rate\">เปิดใช้งานรีเฟรชเรตสูง</string>\n    <string name=\"enable_high_refresh_rate_desc\">บังคับให้หน้าจอทำงานที่อัตรารีเฟรชสูงสุดที่รองรับ (เช่น 120Hz)</string>\n    <string name=\"recognize_music\">ค้นหาเพลงจากเสียง</string>\n    <string name=\"tap_to_recognize\">แตะเพื่อค้นหาเพลง</string>\n    <string name=\"listening\">กำลังฟัง…</string>\n    <string name=\"processing\">กำลังประมวลผล…</string>\n    <string name=\"no_match_found\">ไม่พบเพลงที่ตรงกัน</string>\n    <string name=\"recognition_error\">เกิดข้อผิดพลาดในการรู้จำเพลง</string>\n    <string name=\"try_again\">ลองอีกครั้ง</string>\n    <string name=\"recognition_history\">ประวัติการค้นหาเพลง</string>\n    <string name=\"clear_recognition_history\">ล้างประวัติการค้นหาเพลง</string>\n    <string name=\"clear_recognition_history_confirm\">คุณแน่ใจหรือไม่ว่าต้องการล้างประวัติการค้นหาเพลงทั้งหมด?</string>\n    <string name=\"delete_from_history\">ออก</string>\n    <string name=\"re_listen\">ฟังอีกครั้ง</string>\n    <string name=\"play_on_app\">เล่นบน Metrolist</string>\n    <string name=\"map_csv_columns\">จับคู่คอลัมน์ CSV</string>\n    <string name=\"first_row_is_header\">แถวแรกเป็นหัวตาราง</string>\n    <string name=\"artist_name_column\">คอลัมน์ชื่อศิลปิน</string>\n    <string name=\"song_title_column\">คอลัมน์ชื่อเพลง</string>\n    <string name=\"youtube_url_column\">คอลัมน์ YouTube URL (ไม่บังคับ)</string>\n    <string name=\"continue_action\">ดำเนินการต่อ</string>\n    <string name=\"importing_csv\">กำลังนำเข้าไฟล์ CSV</string>\n    <string name=\"recently_converted\">แปลงล่าสุด</string>\n    <string name=\"column_label\">คอลัมน์ %d</string>\n    <string name=\"prevent_duplicate_tracks_in_queue\">ป้องกันแทร็กซ้ำในคิว</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">เมื่อเพิ่มแทร็กลงในคิว หากมีอยู่แล้วให้ลบออกจากตำแหน่งเดิม</string>\n    <string name=\"ai_translation_literal_desc\">แปลความหมายเป็นภาษาปลายทาง</string>\n    <string name=\"ai_translation_transcribed_desc\">แปลงเสียงอ่านเป็นอักษรของภาษาปลายทาง</string>\n    <string name=\"ai_provider_help\">รับ API Key</string>\n    <string name=\"ai_provider_openrouter_help\">ไปที่ https://openrouter.ai เพื่อดูโมเดลแบบฟรีและแบบชำระเงิน</string>\n    <string name=\"ai_provider_openai_help\">ไปที่ https://platform.openai.com/api-keys</string>\n    <string name=\"ai_provider_claude_help\">ไปที่ https://console.anthropic.com/settings/keys</string>\n    <string name=\"ai_provider_gemini_help\">ไปที่ https://aistudio.google.com/apikey</string>\n    <string name=\"ai_provider_perplexity_help\">ไปที่ https://perplexity.ai/settings/api</string>\n    <string name=\"ai_provider_xai_help\">ไปที่ https://console.x.ai</string>\n    <string name=\"ai_provider_deepl_help\">ไปที่ https://deepl.com/pro-api เพื่อรับ API Key แบบฟรีหรือแบบชำระเงิน</string>\n    <string name=\"ai_deepl_formality\">ระดับความเป็นทางการ</string>\n    <string name=\"ai_deepl_formality_default\">ค่าเริ่มต้น</string>\n    <string name=\"ai_deepl_formality_more\">ทางการมากขึ้น</string>\n    <string name=\"ai_deepl_formality_less\">ทางการน้อยลง</string>\n    <string name=\"discord_status\">สถานะ</string>\n    <string name=\"discord_status_online\">ออนไลน์</string>\n    <string name=\"discord_status_dnd\">ห้ามรบกวน</string>\n    <string name=\"discord_buttons\">ปุ่ม</string>\n    <string name=\"discord_button_1\">ปุ่มที่ 1</string>\n    <string name=\"discord_button_2\">ปุ่มที่ 2</string>\n    <string name=\"login_successful\">เข้าสู่ระบบสำเร็จ!</string>\n    <string name=\"discord_information_warning\">ฟีเจอร์นี้ใช้ไลบรารี KizzyRPC เพื่อเชื่อมต่อกับ Discord Gateway และตั้งค่า Rich Presence ของคุณ แม้ยังไม่มีรายงานว่าบัญชีถูกระงับจากการใช้งานลักษณะนี้ แต่วิธีนี้ไม่ได้รับการสนับสนุนอย่างเป็นทางการจาก Discord และอาจเข้าข่ายละเมิดข้อกำหนดการให้บริการ โทเคนของคุณจะถูกดึงและใช้งานภายในเครื่องเท่านั้น และจะไม่ถูกส่งไปยังเซิร์ฟเวอร์ของบุคคลที่สาม โปรดพิจารณาและตัดสินใจใช้งานด้วยความระมัดระวัง</string>\n    <string name=\"discord_status_idle\">ไม่อยู่</string>\n    <string name=\"discord_activity_type\">ประเภทกิจกรรม</string>\n    <string name=\"discord_activity_playing\">กำลังเล่น</string>\n    <string name=\"discord_activity_listening\">กำลังฟัง</string>\n    <string name=\"discord_activity_watching\">กำลังรับชม</string>\n    <string name=\"discord_activity_competing\">กำลังแข่งขัน</string>\n    <string name=\"discord_button_text_variables\">ตัวแปร: {song_name}, {artist_name}, {album_name}</string>\n    <string name=\"discord_rpc_preview\">ตัวอย่าง Rich Presence</string>\n    <string name=\"discord_presence\">สถานะการแสดงตัว</string>\n    <string name=\"discord_connect_description\">เข้าสู่ระบบด้วย Discord เพื่อแชร์สิ่งที่คุณกำลังฟัง</string>\n    <string name=\"discord_playing_metrolist\">กำลังเล่น Metrolist</string>\n    <string name=\"discord_watching_metrolist\">กำลังรับชม Metrolist</string>\n    <string name=\"discord_competing_metrolist\">กำลังแข่งขันใน Metrolist</string>\n    <string name=\"discord_activity_name\">ชื่อกิจกรรม</string>\n    <string name=\"discord_activity_name_description\">กำหนดชื่อกิจกรรมเอง (เว้นว่างเพื่อใช้ค่าเริ่มต้น)</string>\n    <string name=\"discord_advanced_mode\">โหมดขั้นสูง</string>\n    <string name=\"discord_advanced_mode_description\">แสดงตัวเลือกการปรับแต่งเพิ่มเติมสำหรับ Rich Presence</string>\n    <string name=\"player_background_solid\">สีทึบ</string>\n    <string name=\"resume_on_bluetooth_connect\">เล่นต่อเมื่อเชื่อมต่อบลูทูธ</string>\n    <string name=\"lyrics_romanize_hindi\">ถอดเสียงเนื้อเพลงภาษาฮินดี</string>\n    <string name=\"lyrics_romanize_punjabi\">ถอดเสียงเนื้อเพลงภาษาปัญจาบ</string>\n    <string name=\"lyrics_romanize_as_main\">แสดงเนื้อเพลงที่ถอดเสียงเป็นหลัก</string>\n    <string name=\"display_density\">ความหนาแน่นการแสดงผล</string>\n    <string name=\"restart\">เริ่มต้นใหม่</string>\n    <string name=\"restart_required\">ต้องรีสตาร์ท</string>\n    <string name=\"density_restart_message\">การเปลี่ยนความหนาแน่นของหน้าจอจะมีผลหลังจากรีสตาร์ทแอป คุณต้องการรีสตาร์ทตอนนี้หรือไม่?</string>\n    <string name=\"enable_lrclib_desc\">ฐานข้อมูลเนื้อเพลงแบบซิงค์ที่ขับเคลื่อนโดยชุมชนผู้ใช้</string>\n    <string name=\"enable_kugou_desc\">ดึงเนื้อเพลงจาก KuGou แพลตฟอร์มเพลงยอดนิยมของจีน</string>\n    <string name=\"youtube_music_lyrics_note\">หมายเหตุ: เนื้อเพลงจาก YouTube Music จะแสดงโดยอัตโนมัติเมื่อไม่พบเนื้อเพลงจากแหล่งอื่น โดยทั่วไปเนื้อเพลงจาก YTM มักจะไม่ซิงค์กับเพลง</string>\n    <string name=\"enable_lyricsplus\">เปิดใช้งาน LyricsPlus</string>\n    <string name=\"enable_lyricsplus_desc\">เนื้อเพลงแบบซิงค์จากหลายแหล่ง</string>\n    <string name=\"lyrics_provider_selection\">การเลือกผู้ให้บริการเนื้อเพลง</string>\n    <string name=\"lyrics_provider_selection_desc\">เลือกแหล่งที่มาของเนื้อเพลงที่ต้องการเปิดใช้งาน</string>\n    <string name=\"lyrics_provider_priority\">ลำดับความสำคัญของแหล่งเนื้อเพลง</string>\n    <string name=\"lyrics_provider_priority_desc\">ลากเพื่อจัดเรียงลำดับผู้ให้บริการตามความต้องการ ตำแหน่งที่อยู่สูงกว่าจะมีลำดับความสำคัญมากกว่า</string>\n    <string name=\"changelog\">บันทึกการเปลี่ยนแปลง</string>\n    <string name=\"changelog_empty\">ยังไม่มีบันทึกการเปลี่ยนแปลง</string>\n    <string name=\"github_releases_url\">https://github.com/MetrolistGroup/Metrolist/releases</string>\n    <string name=\"list_bullet\">•</string>\n    <string name=\"view_on_github\">ดูบน GitHub</string>\n    <string name=\"current_version\">เวอร์ชันปัจจุบัน</string>\n    <string name=\"version_format\">เวอร์ชัน: %s</string>\n    <string name=\"update_settings\">การตั้งค่าการอัปเดต</string>\n    <string name=\"check_for_updates_title\">ตรวจสอบการอัปเดต</string>\n    <string name=\"checking_for_updates\">กำลังตรวจสอบการอัปเดต…</string>\n    <string name=\"latest_version_format\">เวอร์ชันล่าสุด: %s</string>\n    <string name=\"check_for_updates_button\">ตรวจสอบการอัปเดต</string>\n    <string name=\"hide_changelog\">ซ่อนบันทึกการเปลี่ยนแปลง</string>\n    <string name=\"view_changelog\">ดูบันทึกการเปลี่ยนแปลง</string>\n    <string name=\"failed_to_check_updates\">ไม่สามารถตรวจสอบการอัปเดต: %s</string>\n    <string name=\"set_as_default\">ตั้งเป็นค่าเริ่มต้น</string>\n    <string name=\"sleep_timer_default_set\">ตั้งค่าเวลาปิดเพลงอัตโนมัติเริ่มต้นเป็น %d นาที</string>\n    <string name=\"found_in_settings_content\">ดูได้ที่ การตั้งค่า &gt; เนื้อหา</string>\n    <string name=\"plays\">ครั้ง</string>\n    <string name=\"error_episode_save\">เกิดข้อผิดพลาดในการบันทึกตอน</string>\n    <string name=\"error_episode_remove\">เกิดข้อผิดพลาดในการลบตอน</string>\n    <string name=\"error_podcast_subscribe\">เกิดข้อผิดพลาดในการติดตามพอดแคสต์</string>\n    <string name=\"error_podcast_unsubscribe\">เกิดข้อผิดพลาดในการเลิกติดตามพอดแคสต์</string>\n    <string name=\"listen_together_auto_approval_suggestions\">อนุมัติคำแนะนำเพลงอัตโนมัติ</string>\n    <string name=\"listen_together_auto_approval_suggestions_desc\">อนุมัติและเพิ่มเพลงที่ผู้เข้าร่วมแนะนำเข้าสู่คิวโดยอัตโนมัติ</string>\n    <string name=\"importing_playlist\">กำลังนำเข้าเพลย์ลิสต์</string>\n    <string name=\"speed_dial\">ปุ่มลัด</string>\n    <string name=\"pin_to_speed_dial\">ปักหมุดไว้ในปุ่มลัด</string>\n    <string name=\"unpin_from_speed_dial\">เอาหมุดออกจากปุ่มลัด</string>\n    <string name=\"randomize_home_order\">สุ่มลำดับหน้าแรก</string>\n    <string name=\"randomize_home_order_desc\">สุ่มจัดเรียงส่วนต่าง ๆ ในหน้าแรกใหม่ตามลำดับความสำคัญแบบถ่วงน้ำหนัก</string>\n    <string name=\"daily_discover_sounds_like\">ฟังดูคล้ายกับ %1$s</string>\n    <string name=\"daily_discover_because_you_listen_to\">เพราะคุณฟัง %1$s</string>\n    <string name=\"daily_discover_similar_to\">คล้ายกับ %1$s</string>\n    <string name=\"daily_discover_based_on\">อิงจาก %1$s</string>\n    <string name=\"daily_discover_for_fans_of\">สำหรับแฟน ๆ ของ %1$s</string>\n    <string name=\"from_the_community\">จากชุมชนผู้ใช้</string>\n    <string name=\"logout_dialog_title\">เก็บข้อมูลคลังเพลงไว้หรือไม่?</string>\n    <string name=\"logout_dialog_message\">คุณต้องการเก็บเพลย์ลิสต์และข้อมูลคลังเพลงไว้หรือไม่? เพลงที่ดาวน์โหลดไว้จะยังคงอยู่ไม่ว่าคุณจะเลือกแบบใด</string>\n    <string name=\"logout_keep\">เก็บไว้</string>\n    <string name=\"logout_clear\">ล้างข้อมูล</string>\n    <string name=\"credits_lead_developer\">หัวหน้านักพัฒนา</string>\n    <string name=\"credits_collaborator\">ผู้ร่วมพัฒนา</string>\n    <string name=\"credits_collaborators_section\">ผู้ร่วมพัฒนา</string>\n    <string name=\"credits_license_name\">GNU General Public License v3.0</string>\n    <string name=\"credits_license_desc\">ซอฟต์แวร์โอเพนซอร์สฟรี คุณสามารถใช้งาน ศึกษา แจกจ่าย และพัฒนาเพิ่มเติมได้</string>\n    <string name=\"credits_discord\">เซิร์ฟเวอร์ Discord</string>\n    <string name=\"credits_telegram\">ช่อง Telegram</string>\n    <string name=\"credits_website\">เว็บไซต์</string>\n    <string name=\"credits_instagram\">Instagram</string>\n    <string name=\"credits_github\">GitHub</string>\n    <string name=\"credits_view_repo\">ดูที่เก็บโค้ด</string>\n    <string name=\"app_version_info\">%1$s • %2$s</string>\n    <string name=\"like_what_i_do\">ชอบผลงานของฉันไหม?</string>\n    <string name=\"buy_mo_a_coffee\">เลี้ยงกาแฟฉันสักแก้ว</string>\n    <string name=\"community_and_info\">ชุมชนและข้อมูล</string>\n    <string name=\"metrolist\">METROLIST</string>\n    <string name=\"wanna_play_favorite_song\">อยากเปิดเพลงโปรดของพวกเขาไหม?</string>\n    <string name=\"yeah\">ใช่เลย</string>\n    <string name=\"stands_with_palestine\">โปรเจกต์นี้ขอยืนหยัดเคียงข้างปาเลสไตน์ 🇵🇸</string>\n    <string name=\"filter_podcasts\">พอดแคสต์</string>\n    <string name=\"view_podcast\">ดูพอดแคสต์</string>\n    <string name=\"podcast_channels\">ช่องพอดแคสต์</string>\n    <string name=\"latest_episodes\">ตอนล่าสุด</string>\n    <string name=\"your_shows\">รายการของคุณ</string>\n    <string name=\"new_episodes\">ตอนใหม่</string>\n    <string name=\"episodes_for_later\">ตอนที่บันทึกไว้ฟังทีหลัง</string>\n    <string name=\"save_episode_for_later\">บันทึกไว้ฟังทีหลัง</string>\n    <string name=\"save_episode_for_later_desc\">เพิ่มลงในเพลย์ลิสต์ “ฟังทีหลัง”</string>\n    <string name=\"remove_episode_from_saved\">นำออกจากรายการที่บันทึกไว้</string>\n    <string name=\"subscribe_to_podcast\">บันทึกพอดแคสต์ไว้ในคลัง</string>\n    <plurals name=\"n_episode\">\n        <item quantity=\"other\">%d ตอน</item>\n    </plurals>\n    <string name=\"restore_confirm_title\">กู้คืนข้อมูลจากแบ็กอัป?</string>\n    <string name=\"restore_confirm_message\">การดำเนินการนี้จะกู้คืนข้อมูลแอปจากไฟล์แบ็กอัป</string>\n    <string name=\"restore_account_warning\">หลังการกู้คืน คุณจะต้องเข้าสู่ระบบอีกครั้ง บัญชีต่อไปนี้จะถูกออกจากระบบ:</string>\n    <string name=\"restore\">กู้คืน</string>\n    <string name=\"checking_previous_account\">กำลังตรวจสอบบัญชีเดิม…</string>\n    <string name=\"no_account_found\">ไม่พบบัญชี</string>\n    <string name=\"widget_recognizer_name\">ระบบค้นหาเพลง</string>\n    <string name=\"widget_recognizer_description\">ระบุชื่อเพลงที่กำลังเล่นรอบตัวคุณได้จากหน้าจอหลักโดยตรง</string>\n    <string name=\"widget_recognizer_tap_to_search\">แตะเพื่อค้นหาเพลง</string>\n    <string name=\"widget_recognizer_listening\">กำลังฟัง…</string>\n    <string name=\"widget_recognizer_processing\">กำลังระบุเพลง…</string>\n    <string name=\"widget_recognizer_no_match\">ไม่พบเพลงที่ตรงกัน ลองใหม่อีกครั้ง</string>\n    <string name=\"widget_recognizer_error\">การค้นหาเพลงล้มเหลว</string>\n    <string name=\"widget_recognizer_error_generic\">เกิดข้อผิดพลาด กรุณาลองใหม่อีกครั้ง</string>\n    <string name=\"widget_recognizer_unknown_song\">ไม่ทราบชื่อเพลง</string>\n    <string name=\"widget_recognizer_unknown_artist\">ไม่ทราบชื่อศิลปิน</string>\n    <string name=\"widget_recognizer_mic_desc\">ค้นหาเพลง</string>\n    <string name=\"widget_recognizer_channel_name\">การค้นหาเพลง</string>\n    <string name=\"widget_recognizer_channel_desc\">แสดงการแจ้งเตือนขณะกำลังค้นหาเพลงจากวิดเจ็ต</string>\n    <string name=\"widget_recognizer_notification_text\">กำลังบันทึกเสียงเพื่อระบุเพลง…</string>\n    <string name=\"filter_episodes\">ตอน</string>\n    <string name=\"filter_channels\">ช่อง</string>\n    <string name=\"auto_playlist\">เพลย์ลิสต์อัตโนมัติ</string>\n    <string name=\"downloaded_episodes\">ตอนที่ดาวน์โหลดแล้ว</string>\n    <string name=\"no_subscribed_channels\">ยังไม่มีช่องที่ติดตาม</string>\n    <string name=\"no_downloaded_episodes\">ยังไม่มีตอนที่ดาวน์โหลด</string>\n    <plurals name=\"n_channel\">\n        <item quantity=\"other\">%d ช่อง</item>\n    </plurals>\n    <string name=\"view_channel\">ดูช่อง</string>\n    <string name=\"filter_profiles\">โปรไฟล์</string>\n    <string name=\"enable_automatic_sleeptimer\">เปิดตัวตั้งเวลาปิดอัตโนมัติ</string>\n    <string name=\"sleeptimer_description\">เปิดใช้งานตัวตั้งเวลาปิดอัตโนมัติด้วยค่าเริ่มต้นตามเวลาที่กำหนดเอง</string>\n    <string name=\"sleep_timer_repeat_description\">กำหนดวันและเวลาที่ต้องการให้ตัวตั้งเวลาปิดทำงานอัตโนมัติ</string>\n    <string name=\"sleep_timer_repeat\">ทำซ้ำ</string>\n    <string name=\"sleep_timer_daily\">ทุกวัน</string>\n    <string name=\"sleep_timer_weekdays\">วันจันทร์–ศุกร์</string>\n    <string name=\"sleep_timer_weekdays_weekends\">วันธรรมดา / วันหยุดสุดสัปดาห์</string>\n    <string name=\"sleep_timer_weekends\">วันหยุดสุดสัปดาห์ (เสาร์–อาทิตย์)</string>\n    <string name=\"sleep_timer_custom\">กำหนดเอง</string>\n    <string name=\"sleep_timer_start_time\">เวลาเริ่มต้น</string>\n    <string name=\"sleep_timer_end_time\">เวลาสิ้นสุด</string>\n    <string name=\"sleep_timer_monday\">วันจันทร์</string>\n    <string name=\"sleep_timer_tuesday\">วันอังคาร</string>\n    <string name=\"sleep_timer_wednesday\">วันพุธ</string>\n    <string name=\"sleep_timer_thursday\">วันพฤหัสบดี</string>\n    <string name=\"sleep_timer_friday\">วันศุกร์</string>\n    <string name=\"sleep_timer_saturday\">วันเสาร์</string>\n    <string name=\"sleep_timer_sunday\">วันอาทิตย์</string>\n    <string name=\"sleep_timer_stop_after_current_song\">หยุดเล่นเมื่อเพลงปัจจุบันจบเมื่อครบเวลาที่ตั้งไว้</string>\n    <string name=\"sleep_timer_fade_out\">ค่อย ๆ ลดเสียงลงในนาทีสุดท้าย</string>\n    <string name=\"upload_songs\">อัปโหลดเพลง</string>\n    <string name=\"uploading\">กำลังอัปโหลด…</string>\n    <string name=\"upload_progress\">%1$d จาก %2$d</string>\n    <string name=\"upload_complete\">อัปโหลดเสร็จสิ้น</string>\n    <string name=\"upload_failed\">อัปโหลดไม่สำเร็จ</string>\n    <string name=\"upload_file_too_large\">ไฟล์มีขนาดใหญ่เกินไป (สูงสุด 300MB)</string>\n    <string name=\"upload_unsupported_format\">รูปแบบไฟล์ไม่รองรับ กรุณาใช้ mp3, m4a, wma, flac หรือ ogg</string>\n    <string name=\"delete_uploaded_song\">ลบเพลงที่อัปโหลด</string>\n    <string name=\"delete_uploaded_song_confirm\">คุณแน่ใจหรือไม่ว่าต้องการลบเพลงที่อัปโหลดนี้? การดำเนินการนี้ไม่สามารถย้อนกลับได้</string>\n    <string name=\"delete_uploaded_song_success\">ลบเพลงที่อัปโหลดแล้ว</string>\n    <string name=\"delete_uploaded_song_failed\">ไม่สามารถลบเพลงที่อัปโหลดได้</string>\n    <string name=\"delete_uploaded_songs\">ลบเพลงที่อัปโหลด</string>\n    <string name=\"delete_uploaded_songs_confirm\">คุณแน่ใจหรือไม่ว่าต้องการลบเพลงที่อัปโหลด %1$d เพลง? การดำเนินการนี้ไม่สามารถย้อนกลับได้</string>\n    <string name=\"deleted_n_songs\">ลบแล้ว %1$d เพลง</string>\n    <string name=\"deleting\">กำลังลบ…</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-tr/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">Yerel</string>\n    <string name=\"remote_history\">Uzak</string>\n    <string name=\"charts\">Listeler</string>\n    <string name=\"back_button_desc\">Geri</string>\n    <string name=\"album_cover_desc\">Albüm açıklaması</string>\n    <string name=\"top_music_videos\">Popüler Müzik Videoları</string>\n    <string name=\"trending\">Trendler</string>\n    <string name=\"weeks\">Haftalar</string>\n    <string name=\"months\">Aylar</string>\n    <string name=\"years\">Yıllar</string>\n    <string name=\"continuous\">Sürekli</string>\n    <string name=\"liked\">Beğenilen</string>\n    <string name=\"offline\">İndirilen</string>\n    <string name=\"my_top\">En İyilerim</string>\n    <string name=\"cached_playlist\">Önbelleğe Alınan</string>\n    <string name=\"sync_playlist\">Oynatma listesini senkronize et</string>\n    <string name=\"sync_disabled\">Senkronizasyon devre dışı</string>\n    <string name=\"allows_for_sync_witch_youtube\">Not: Bu, YouTube Music ile senkronizasyona izin verir. Sonradan DEĞİŞTİRİLEMEZ.</string>\n    <string name=\"remove_from_cache\">Önbellekten kaldır</string>\n    <string name=\"copy_link\">Bağlantıyı kopyala</string>\n    <string name=\"select\">Tümünü seç</string>\n    <string name=\"like_all\">Tümünü beğen</string>\n    <string name=\"dislike_all\">Tümünü beğenme</string>\n    <string name=\"sort_by_last_updated\">Güncelleme tarihi</string>\n    <string name=\"link_copied\">Bağlantı panoya kopyalandı</string>\n    <string name=\"lyrics\">Şarkı sözleri</string>\n    <string name=\"already_in_playlist\">Zaten oynatma listesinde:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">1 kez</item>\n        <item quantity=\"other\">%d kez</item>\n    </plurals>\n    <string name=\"similar_content\">Benzer içerik</string>\n    <string name=\"player_background_style\">Oynatıcı arka plan stili</string>\n    <string name=\"follow_theme\">Temayı takip et</string>\n    <string name=\"gradient\">Gradyan</string>\n    <string name=\"player_background_blur\">Bulanıklık</string>\n    <string name=\"player_buttons_style\">Oynatıcı düğme renkleri</string>\n    <string name=\"default_style\">Varsayılan</string>\n    <string name=\"enable_swipe_thumbnail\">Şarkıyı değiştirmek için kaydırmayı etkinleştir</string>\n    <string name=\"swipe_song_to_add\">Şarkıyı kuyruğa eklemek için sola, sonra çalmak için sağa kaydırın</string>\n    <string name=\"lyrics_click_change\">Tıklama ile şarkı sözlerini değiştir</string>\n    <string name=\"slim\">İnce</string>\n    <string name=\"slim_navbar\">İnce alt gezinme çubuğu</string>\n    <string name=\"auto_playlists\">Otomatik Oynatma Listeleri</string>\n    <string name=\"show_liked_playlist\">\\\"Beğenilen\\\" Oynatma Listesini Göster</string>\n    <string name=\"show_downloaded_playlist\">\\\"İndirilen\\\" Oynatma Listesini Göster</string>\n    <string name=\"show_top_playlist\">\\\"En İyi\\\" Oynatma Listesini Göster</string>\n    <string name=\"show_cached_playlist\">\\\"Önbelleğe Alınan\\\" oynatma listesini göster</string>\n    <string name=\"advanced_login\">Token ile giriş yap</string>\n    <string name=\"token_hidden\">Tokeni göstermek için dokunun</string>\n    <string name=\"token_shown\">Kopyalamak veya düzenlemek için tekrar dokunun</string>\n    <string name=\"token_adv_login_description\">Bu, GELİŞMİŞ bir giriş yöntemidir. Web portalına alternatif olarak, giriş tokeninizi buraya doğrudan girebilir veya güncelleyebilirsiniz. Örneğin, bu, birden fazla cihazda girişi hızlandırabilir. Lütfen, uygulamanın ayrıştıramadığı geçersiz token formatlarının kabul edilmeyeceğini unutmayın</string>\n    <string name=\"general\">Genel</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"default_lib_chips\">Varsayılan kütüphane çipini değiştir</string>\n    <string name=\"set_quick_picks\">Hızlı seçimleri ayarla</string>\n    <string name=\"last_song_listened\">Son dinlenen şarkıya göre</string>\n    <string name=\"app_language\">Uygulama dili</string>\n    <string name=\"enable_similar_content\">Benzer içeriği etkinleştir</string>\n    <string name=\"similar_content_desc\">Kuyruğun sonuna ulaşıldığında otomatik olarak daha fazla benzer şarkı ekleyin</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"clear_song_cache_dialog\">Tüm önbelleğe alınan şarkıları temizlemek istediğinizden emin misiniz?</string>\n    <string name=\"clear_downloads_dialog\">Tüm indirmeleri temizlemek istediğinizden emin misiniz?</string>\n    <string name=\"not_logged_in_youtube\">YouTube\\'a giriş yapılmadı</string>\n    <string name=\"default_links\">Desteklenen bağlantıları aç</string>\n    <string name=\"open_app_settings_error\">Uygulama ayarları açılamadı</string>\n    <string name=\"release_notes\">Sürüm notları</string>\n    <string name=\"all_time\">Tüm zamanlar</string>\n    <string name=\"past_24_hours\">Son 24 saat</string>\n    <string name=\"past_week\">Son hafta</string>\n    <string name=\"past_month\">Son ay</string>\n    <string name=\"past_year\">Son yıl</string>\n    <string name=\"top_length\">Benim En İyiler listesi uzunluğu</string>\n    <string name=\"history_duration\">Geçmiş süresi</string>\n    <string name=\"information\">Bilgi</string>\n    <string name=\"description\">Açıklama</string>\n    <string name=\"views\">Görüntülemeler</string>\n    <string name=\"likes\">Beğeniler</string>\n    <string name=\"dislikes\">Beğenilmeyenler</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">1 saniye</item>\n        <item quantity=\"other\">%d saniye</item>\n    </plurals>\n    <string name=\"please_wait\">Lütfen bekleyin</string>\n    <string name=\"cancel\">İptal</string>\n    <string name=\"share_lyrics\">Şarkı sözlerini paylaş</string>\n    <string name=\"share_as_text\">Metin olarak paylaş</string>\n    <string name=\"share_as_image\">Görsel olarak paylaş</string>\n    <string name=\"max_selection_limit\">Maksimum seçim sınırı</string>\n    <string name=\"share_selected\">Seçilenleri paylaş</string>\n    <string name=\"customize_colors\">Renkleri düzenle</string>\n    <string name=\"text_color\">Metin rengi</string>\n    <string name=\"secondary_text_color\">İkincil metin rengi</string>\n    <string name=\"background_color\">Arka plan rengi</string>\n    <string name=\"new_player_design\">Yeni oynatıcı tasarımı</string>\n    <string name=\"lyrics_auto_scroll\">Şarkı sözlerini otomatik kaydır</string>\n    <string name=\"lyrics_romanize_japanese\">Japonca şarkı sözlerini Latinleştir</string>\n    <string name=\"lyrics_romanize_korean\">Korece şarkı sözlerini Latinleştir</string>\n    <string name=\"yt_sync\">Hesapla otomatik olarak senkronize et</string>\n    <string name=\"more_content\">Daha fazla içerik</string>\n    <string name=\"import_online\">Bir \\\"m3u\\\" oynatma listesi içe aktar</string>\n    <string name=\"import_csv\">CSV oynatma listelerini içe aktarın</string>\n    <string name=\"playlist_add_local_to_synced_note\">Not: Yerel şarkıların, senkronize edilmiş/uzaktan oynatma listelerine eklenmesi desteklenmemektedir. Bunun dışındaki herhangi bir kombinasyon geçerlidir</string>\n    <string name=\"auto_download_on_like\">Beğendiğimde otomatik indir</string>\n    <string name=\"auto_download_on_like_desc\">Beğenilen şarkıları otomatik indir</string>\n    <string name=\"swipe_sensitivity\">Mini oynatıcı kaydırma hassasiyeti</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"clear_image_cache_dialog\">Önbelleğe alınmış tüm görüntüleri temizlemek istediğinizden emin misiniz?</string>\n    <string name=\"disable\">Devre dışı bırak</string>\n    <string name=\"subscribe\">Abone ol</string>\n    <string name=\"subscribed\">Abone olundu</string>\n    <string name=\"generating_image\">Görüntü oluşturuluyor</string>\n    <string name=\"now_playing\">Şimdi Oynatılıyor</string>\n    <string name=\"new_mini_player_design\">Yeni mini oynatıcı tasarımı</string>\n    <string name=\"close\">Kapat</string>\n    <string name=\"seek_forward_dynamic\">+%1$d saniye ileri</string>\n    <string name=\"seek_backward_dynamic\">-%1$d saniye geri</string>\n    <string name=\"seek_seconds_addup\">Kademeli atlama</string>\n    <string name=\"seek_seconds_addup_description\">Eğer etkinleştirilirse, her ileri/geri sarma atlamasında kademeli olarak fazladan 5 saniye ekler</string>\n    <string name=\"disable_load_more_when_repeat_all\">Tümünü tekrarla açıkken daha fazla yükle seçeneğini devre dışı bırak</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Tümünü tekrarla modu açıkken daha fazla şarkı veya benzer içerik yükleme</string>\n    <string name=\"hide_player_thumbnail\">Oynatıcı Küçük Resmini Gizle</string>\n    <string name=\"hide_player_thumbnail_desc\">Oynatıcıdaki albüm resmini uygulama logosuyla değiştirin</string>\n    <string name=\"settings_section_ui\">Arayüz</string>\n    <string name=\"settings_section_privacy\">Gizlilik ve Güvenlik</string>\n    <string name=\"settings_section_player_content\">Oynatıcı ve İçerik</string>\n    <string name=\"settings_section_storage\">Depolama ve Veri</string>\n    <string name=\"settings_section_system\">Sistem ve Hakkında</string>\n    <string name=\"starting_radio\">Radyo başlatılıyor</string>\n    <string name=\"edit_playlist_cover\">Oynatma listesi kapağını düzenle</string>\n    <string name=\"edit_playlist_cover_note\">Not: Oynatma listesi kapağını değiştirmek için hesabınızın bir telefon numarasına bağlı olması ve YouTube Music\\'te doğrulanmış olması gerekir.</string>\n    <string name=\"edit_playlist_cover_note_wait\">Bir resim seçtikten sonra lütfen yeni kapağın oynatma listenizde görünmesi için bir süre bekleyin.</string>\n    <string name=\"choose_from_library\">Kütüphaneden seç</string>\n    <string name=\"remove_custom_image\">Özelleştirilmiş görseli sil</string>\n    <string name=\"config_proxy\">Proxy ayarla</string>\n    <string name=\"proxy_username\">Proxy kullanıcı adı</string>\n    <string name=\"proxy_password\">Proxy şifresi</string>\n    <string name=\"enable_authentication\">Kimlik doğrulamayı etkinleştir</string>\n    <string name=\"lyrics_romanization_cyrillic\">Kiril</string>\n    <string name=\"lyrics_romanize_title\">Latinleştir</string>\n    <string name=\"lyrics_romanization\">Şarkı sözlerini Latinleştir</string>\n    <string name=\"lyrics_romanize_russian\">Rusça şarkı sözlerini Latinleştir</string>\n    <string name=\"lyrics_romanize_ukrainian\">Ukraynaca şarkı sözlerini Latinleştir</string>\n    <string name=\"lyrics_romanize_belarusian\">Belarusça şarkı sözlerini Latinleştir</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Kırgızca şarkı sözlerini Latinleştir</string>\n    <string name=\"lyrics_romanize_serbian\">Sırpça şarkı sözlerini Latinleştir</string>\n    <string name=\"lyrics_romanize_bulgarian\">Bulgarca şarkı sözlerini Latinleştir</string>\n    <string name=\"line_by_line_option_title\">DENEYSEL: Her satırın dilini ayrı ayrı algıla</string>\n    <string name=\"line_by_line_option_desc\">Kiril dili, şarkının tamamı yerine satır satır algılanacaktır.</string>\n    <string name=\"line_by_line_dialog_title\">Emin misiniz?</string>\n    <string name=\"line_by_line_dialog_desc\">Bu, başarılı veya başarısız olabilecek deneysel bir özelliktir.\\n\\nVarsayılan olarak, dil tüm şarkı üzerinden belirlenir; ancak bu seçenek açık olduğunda, dil satır satır belirlenecektir. Bu sayede çok dilli şarkılar da çalışabilir, ama dil her zaman doğru olmayabilir (örneğin, Ukraynaca bir satır Ukraynaca’ya özgü harf içermiyorsa, yanlışlıkla Rusça olarak Latinleştirilebilir).\\n\\nSorun yaşamıyorsanız, bu seçeneği kapalı bırakmanız önerilir.</string>\n    <string name=\"romanize_current_track\">Geçerli parçayı Latinleştir</string>\n    <string name=\"audio_offload\">Offload\\'u etkinleştir</string>\n    <string name=\"audio_offload_description\">Ses çalmak için offload ses yolunu kullan. Bu seçeneği devre dışı bırakmak güç tüketimini artırabilir, ancak ses çalma veya sonrası işleme ile ilgili sorunlar yaşıyorsanız faydalı olabilir</string>\n    <string name=\"uploaded_playlist\">Yüklendi</string>\n    <string name=\"filter_uploaded\">Yüklendi</string>\n    <string name=\"show_uploaded_playlist\">\\\"Yüklenen\\\" oynatma listesini göster</string>\n    <string name=\"updater\">Güncelleyici</string>\n    <string name=\"check_for_updates\">Güncellemeleri otomatik olarak kontrol et</string>\n    <string name=\"update_notifications\">Güncelleme bildirimlerini etkinleştir</string>\n    <string name=\"update_available_title\">Güncelleme mevcut</string>\n    <string name=\"update_channel_name\">Uygulama güncellemeleri</string>\n    <string name=\"update_channel_desc\">Yeni sürümlerle ilgili bildirimler</string>\n    <string name=\"lyrics_romanize_macedonian\">Makedonca şarkı sözlerini Latinleştir</string>\n    <string name=\"discord_use_details\">Durum yerine ayrıntıları kullan</string>\n    <string name=\"discord_use_details_description\">Sanatçı adları yerine şarkı adını belirgin şekilde göster</string>\n    <string name=\"integrations\">Entegrasyonlar</string>\n    <string name=\"username\">Kullanıcı adı</string>\n    <string name=\"password\">Şifre</string>\n    <string name=\"lastfm_integration\">Last.fm Entegrasyonu</string>\n    <string name=\"enable_scrobbling\">Scrobbling\\'i etkinleştir</string>\n    <string name=\"lastfm_now_playing\">Şimdi çalanı gönder</string>\n    <string name=\"scrobbling_configuration\">Scrobbling Yapılandırması</string>\n    <string name=\"scrobble_min_track_duration\">Şu süreden uzun şarkıları kaydet</string>\n    <string name=\"scrobble_delay_percent\">Scrobble gecikme yüzdesi</string>\n    <string name=\"scrobble_delay_minutes\">Scrobble gecikme dakikaları</string>\n    <string name=\"swipe_song_to_remove\">Şarkıyı oynatma listesinden kaldırmak için kaydırın</string>\n    <string name=\"last_fm_send_likes\">Beğenilenler/Beğenilmeyenleri Gönder</string>\n    <string name=\"last_fm_send_likes_description\">Metrolist\\'te Beğenilen/Beğenilmeyen şarkıları Last.fm\\'de de Beğen/Beğenme</string>\n    <string name=\"primary_color_style\">Ana renk</string>\n    <string name=\"auto_scroll\">Yeniden senkronize et</string>\n    <string name=\"lyrics_romanize_chinese\">Çince şarkı sözlerini Latinleştir</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"google_cast_description\">Chromecast ve diğer Cast özellikli cihazlara ses aktarımını etkinleştirin</string>\n    <string name=\"hide_video_songs\">Video şarkılarını gizle</string>\n    <string name=\"details_desc\">Şarkının bilgilerini görüntüle</string>\n    <string name=\"edit_desc\">Başlığı veya sanatçıyı değiştirme</string>\n    <string name=\"start_radio_desc\">Bu öğeyi temel alarak bir istasyon oluşturun</string>\n    <string name=\"play_next_desc\">Sıranızın başına ekleyin</string>\n    <string name=\"add_to_queue_desc\">Sıranızın en altına ekleyin</string>\n    <string name=\"add_to_library_desc\">Kitaplığınıza kaydedin</string>\n    <string name=\"download_desc\">Çevrimdışı oynatma için kullanılabilir hale getir</string>\n    <string name=\"add_to_playlist_desc\">Oynatma listelerinizden birine ekleyin</string>\n    <string name=\"refetch_desc\">YouTube Music\\'ten en son meta verileri alın</string>\n    <string name=\"share_desc\">Bu öğenin bağlantısını paylaşın</string>\n    <string name=\"delete_desc\">Bu öğeyi kalıcı olarak kaldır</string>\n    <string name=\"advanced_desc\">Şarkının temposunu ve perdesini değiştirin</string>\n    <string name=\"equalizer_desc\">Ses ekolayzırını ayarlayın</string>\n    <string name=\"enable_dynamic_icon\">Dinamik simgeyi etkinleştir</string>\n    <string name=\"mini_player\">Mini oynatıcı</string>\n    <string name=\"pure_black_mini_player\">Saf siyah mini oynatıcı</string>\n    <string name=\"cache_size_warning_title\">Bekle!</string>\n    <string name=\"cache_size_warning_message\">Uygulamanın şu an kullandığı boyuttan (%1$s) daha küçük bir önbellek sınırı seçtiniz. Devam ederseniz uygulama, yeni sınıra uymak için önbelleğe alınmış bazı %2$s içeriklerini silebilir. Yine de devam edilsin mi?</string>\n    <string name=\"cache_size_warning_confirm\">Devam et</string>\n    <string name=\"tertiary_color_style\">Üçüncül renk</string>\n    <string name=\"logging_in\">Giriş yapılıyor…</string>\n    <string name=\"download_playlist_desc\">Çevrimdışı dinlemek için tüm şarkıları indir</string>\n    <string name=\"remove_download_playlist_desc\">İndirilen tüm şarkıları bu oynatma listesinden kaldır</string>\n    <string name=\"download_in_progress_desc\">İndirme işlemi devam ediyor</string>\n    <string name=\"share_playlist_desc\">Bu oynatma listesini başkalarıyla paylaş</string>\n    <string name=\"delete_playlist_desc\">Bu oynatma listesini kalıcı olarak sil</string>\n    <string name=\"sync_playlist_desc\">Oynatma listesini YouTube Music ile senkronize et</string>\n    <string name=\"enable_better_lyrics\">Better Lyrics\\'i Etkinleştir</string>\n    <string name=\"enable_better_lyrics_desc\">Herhangi bir şarkı için karaoke tarzı, hece senkronizasyonlu sözler</string>\n    <string name=\"lyrics_animation_style\">Kelime kelime animasyon stili</string>\n    <string name=\"none\">Hiçbiri</string>\n    <string name=\"fade\">Soluklaşmış</string>\n    <string name=\"glow\">Parıltılı</string>\n    <string name=\"slide\">Kaydır</string>\n    <string name=\"karaoke\">Karaoke</string>\n    <string name=\"apple_music_style\">Apple Müzik</string>\n    <string name=\"lyrics_text_size\">Şarkı sözü metin boyutu</string>\n    <string name=\"lyrics_line_spacing\">Şarkı sözü satır aralığı</string>\n    <string name=\"shuffle_playlist_first\">Oynatma listesini/albümü önce karıştır</string>\n    <string name=\"shuffle_playlist_first_desc\">Şarkıları karıştırırken, önce orijinal oynatma listesindeki/albümdeki tüm şarkıları, ardından benzer içerikteki şarkıları çalın</string>\n    <string name=\"show_wrapped_card\">Özet kartını göster</string>\n    <string name=\"album_art_for\">%s için albüm kapağı</string>\n    <string name=\"wrapped_total_albums_title\">Şunları dinledin</string>\n    <string name=\"wrapped_total_albums_subtitle\">benzersiz albümler</string>\n    <string name=\"wrapped_top_album_title\">En iyi albümünüz</string>\n    <string name=\"wrapped_playlist_ready\">Kişisel oynatma listeniz hazır</string>\n    <string name=\"wrapped_top_5_albums_title\">En iyi 5 albümünüz</string>\n    <string name=\"wrapped_album_listening_time\">Bu albümü %d dakika dinledin</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d dakika</string>\n    <string name=\"wrapped_no_data\">Veri yok</string>\n    <string name=\"wrapped_top_5_artists_title\">Yılın en iyi sanatçıları</string>\n    <string name=\"wrapped_artist_listening_time\">%d dakika</string>\n    <string name=\"wrapped_top_5_songs_title\">Yılın en iyi şarkıları</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">Albüm görseli</string>\n    <string name=\"wrapped_top_artist_title\">Yılın en iyi sanatçısı</string>\n    <string name=\"wrapped_top_artist_image_content_description\">En iyi sanatçı görseli</string>\n    <string name=\"wrapped_top_artist_listening_time\">Onları %d dakika boyunca dinlediniz</string>\n    <string name=\"wrapped_top_song_title\">En çok çaldığınız şarkı</string>\n    <string name=\"wrapped_top_song_listening_time\">%d dakika boyunca dinlediniz</string>\n    <string name=\"wrapped_total_artists_title\">Şunları dinlediniz</string>\n    <string name=\"wrapped_total_artists_subtitle\">benzersiz sanatçılar</string>\n    <string name=\"wrapped_total_songs_title\">Şunları dinlediniz</string>\n    <string name=\"wrapped_total_songs_subtitle\">benzersiz şarkılar</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_intro_subtitle\">Dinlediklerinize göz atmanın zamanı geldi</string>\n    <string name=\"wrapped_intro_button\">hadi başlayalım!</string>\n    <string name=\"wrapped_logo_content_description\">Metrolist Logosu</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"wrapped_ready_title\">ÖZETİNİZ HAZIR!</string>\n    <string name=\"wrapped_ready_subtitle\">Bu yıl neleri sevdiğini görme zamanı.</string>\n    <string name=\"wrapped_thank_you\">Dinlediğin için teşekkürler</string>\n    <string name=\"wrapped_special_thanks\">Metrolist\\'i oluşturduğu için MO Agamy\\'ye özel teşekkürler</string>\n    <string name=\"wrapped_close\">Özeti kapat</string>\n    <string name=\"wrapped_playlist_title\">%s Özetin</string>\n    <string name=\"wrapped_create_playlist\">Oynatma listesi oluştur</string>\n    <string name=\"wrapped_playlist_saved\">Oynatma listesi kaydedildi</string>\n    <string name=\"casting_to\">%s cihazına yansıtılıyor</string>\n    <string name=\"progress_percent\">İlerleme %s%%</string>\n    <string name=\"listening_to_metrolist\">Metrolist dinleniyor</string>\n    <string name=\"open\">Aç</string>\n    <string name=\"failed_to_create_image\">Görsel oluşturulamadı: %s</string>\n    <string name=\"copied_title\">Başlık kopyalandı</string>\n    <string name=\"copied_artist\">Sanatçı kopyalandı</string>\n    <string name=\"error_playing\">Oynatma hatası</string>\n    <string name=\"failed_to_parse_proxy\">Proxy URL\\'si ayrıştırılamadı.</string>\n    <string name=\"lyrics_glow_effect\">Parıltılı şarkı sözü efektini aktif et</string>\n    <string name=\"lyrics_glow_effect_desc\">Aktif şarkı sözlerine parlama animasyonu ve zıplama efekti ekle</string>\n    <string name=\"wavy\">Dalgalı</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"one\">%d Profil</item>\n        <item quantity=\"other\">%d Profiller</item>\n    </plurals>\n    <string name=\"equalizer_header\">Ekolayzer</string>\n    <string name=\"no_profiles\">Ekolayzır profili yok</string>\n    <string name=\"import_profile\">Profili İçe Aktar</string>\n    <string name=\"eq_disabled\">Devre dışı</string>\n    <plurals name=\"band_count\">\n        <item quantity=\"one\">%d Gurup</item>\n        <item quantity=\"other\">%d Guruplar</item>\n    </plurals>\n    <string name=\"delete_profile_desc\">Profili Sil</string>\n    <string name=\"delete_profile_confirmation\">%1$s profilini silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.</string>\n    <string name=\"error_file_read\">Dosya okunamadı</string>\n    <string name=\"error_file_open\">Dosya açılamadı: %1$s</string>\n    <string name=\"import_error_title\">İçe Aktarma Hatası</string>\n    <string name=\"pause_music_when_media_is_muted\">Medya sesi kapatıldığında müziği duraklat</string>\n    <string name=\"enable_simpmusic\">SimpMusic\\'in şarkı sözlerini etkinleştir</string>\n    <string name=\"enable_simpmusic_desc\">Şarkı sözleri Musixmatch ve YouTube Transcript üzerinden otomatik olarak sağlanmıştır</string>\n    <string name=\"remember_shuffle_and_repeat\">Karışık listeyi hatırla ve tekrarla</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Karışık listeyi hatırla ve uygulama yeniden başlatıldığında tekrarla</string>\n    <string name=\"system_equalizer\">Sistem Ekolayzeri</string>\n    <string name=\"album_art\">Albüm kapağı</string>\n    <string name=\"no_song_playing\">Oynatılan şarkı yok</string>\n    <string name=\"tap_to_open\">Metrolist\\'i açmak için bas</string>\n    <string name=\"previous\">Bir önceki</string>\n    <string name=\"play_pause\">Devam Et/Duraklat</string>\n    <string name=\"next\">Bir sonraki</string>\n    <string name=\"like\">Beğen</string>\n    <string name=\"widget_description\">Oynatıcı widgetı için arkaplan kontrolcüsü</string>\n    <string name=\"turntable_widget_description\">Şarkıyı oynatmak ve değiştirmek yapılmış için dairesel müzik widget\\'ı</string>\n    <string name=\"lyrics_offset\">Şarkı sözünün gecikme süresi</string>\n    <string name=\"skip_silence_desc\">Şarkıların sessiz kısımlarını hızlıca atla</string>\n    <string name=\"skip_silence_instant\">Sessiz kısımları hemen atla</string>\n    <string name=\"skip_silence_instant_desc\">Şarkı hızını arttırmadan şarkının sessiz kısımları atla</string>\n    <string name=\"about_artist\">Hakkında</string>\n    <string name=\"show_more\">Daha fazla göster</string>\n    <string name=\"show_less\">Daha az göster</string>\n    <string name=\"artist_page_settings\">Sanatçı sayfası</string>\n    <string name=\"show_artist_description\">Sanatçının açıklamasını göster</string>\n    <string name=\"show_artist_subscriber_count\">Abone sayısını göster</string>\n    <string name=\"show_artist_monthly_listeners\">Aylık dinleyicileri göster</string>\n    <string name=\"persistent_shuffle_title\">Sürekli karıştırma</string>\n    <string name=\"persistent_shuffle_desc\">Yeni şarkılar veya oynatma listeleri başlatırken karışık çalma özelliğini açık tutun</string>\n    <string name=\"error_playback_failed\">Oynatma başarısız</string>\n    <string name=\"error_title\">Hata</string>\n    <string name=\"error_eq_apply_failed\">Ekolayzır profilini kaydederken sorun oluştu: %1$s</string>\n    <string name=\"crop_album_art\">Albüm Resmini Kırp</string>\n    <string name=\"crop_album_art_desc\">Video küçük resimlerini kırparak kare en boy oranını zorlayın</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">Oynatıcı genişletildiğinde ekranı açık tut</string>\n    <string name=\"listen_together\">Birlikte Dinle</string>\n    <string name=\"listen_together_server_url\">Sunucu URL\\'si</string>\n    <string name=\"listen_together_username\">Kullanıcı adı</string>\n    <string name=\"listen_together_connected\">Bağlanıldı</string>\n    <string name=\"listen_together_reconnecting\">Yeniden bağlanılıyor…</string>\n    <string name=\"listen_together_disconnected\">Bağlantı kesildi</string>\n    <string name=\"listen_together_connecting\">Bağlanılıyor…</string>\n    <string name=\"listen_together_error\">Bağlantı hatası</string>\n    <string name=\"listen_together_create_room\">Oda oluştur</string>\n    <string name=\"listen_together_create_room_desc\">Oda oluştur ve kodunu arkadaşlarınla paylaş</string>\n    <string name=\"listen_together_join_room\">Odaya katıl</string>\n    <string name=\"listen_together_room_code\">Oda kodu</string>\n    <string name=\"listen_together_you_are_host\">Sen sunucu sahibisin</string>\n    <string name=\"listen_together_you_are_guest\">Sen misafirsin</string>\n    <string name=\"listen_together_join_requests\">Katılma isteği</string>\n    <string name=\"listen_together_view_logs\">Logları görüntüle</string>\n    <string name=\"listen_together_view_logs_desc\">Bağlantı ve mesajların hatalarını ayıkla</string>\n    <string name=\"listen_together_logs\">Bağlantı logları</string>\n    <string name=\"listen_together_no_logs\">Log bulunmamakta</string>\n    <string name=\"listen_together_description\">Gerçek zamanlı olarak arkadaşlarınızla müzik dinleyin. Sunucu sahibi olarak bir oda oluştur un veya oda kodu ile var olan bir odaya katılın.</string>\n    <string name=\"listen_together_background_disconnect_note\">Not: Müzik çalmazken bir oda oluşturursanız ve başka bir uygulamaya geçerseniz bağlantınız kesilebilir.</string>\n    <string name=\"listen_together_not_configured\">Başkaları ile dinleme ayarlanmadı. Lütfen sunucu URL\\'sini Ayarlar → Entegrasyonlar → Başkalarıyla dinle konumuna giderek ayarlayın.</string>\n    <string name=\"listen_together_suggestion_received\">%1$s, %2$s\\'e talepte bulundu</string>\n    <string name=\"listen_together_suggestion_sent\">Sunucu sahibine öneride bulun!</string>\n    <string name=\"listen_together_join_request_notification\">%1$s sunucuya katılmak istiyor</string>\n    <string name=\"listen_together_notification_channel_name\">Başkaları ile dinle</string>\n    <string name=\"listen_together_notification_channel_desc\">Başkaları ile dinleme etkinliği için bildirimler</string>\n    <string name=\"listen_together_room_created\">Oda oluşturuldu: %s</string>\n    <string name=\"listen_together_cannot_edit_username_in_room\">Bir odadayken kullanıcı adı değiştirilemez</string>\n    <string name=\"waiting_for_approval\">Sunucu sahibi tarafından onay bekleniliyor</string>\n    <string name=\"invalid_room_code\">Geçersiz oda kodu</string>\n    <string name=\"join_request_denied\">Katılma isteği reddedildi</string>\n    <string name=\"join_existing_room\">Var olan bir odaya katıl</string>\n    <string name=\"room_code\">Oda kodu</string>\n    <string name=\"leave_room\">Odadan ayrıl</string>\n    <string name=\"join_room\">Katıl</string>\n    <string name=\"create_room\">Oluştur</string>\n    <string name=\"joining_room\">%s odasına giriliyor…</string>\n    <string name=\"creating_room\">Oda oluşturuluyor…</string>\n    <string name=\"connect\">Bağlan</string>\n    <string name=\"disconnect\">Bağlantıyı kes</string>\n    <string name=\"create\">Oluştur</string>\n    <string name=\"join\">Katıl</string>\n    <string name=\"approve\">Onayla</string>\n    <string name=\"reject\">Reddet</string>\n    <string name=\"clear\">Temizle</string>\n    <string name=\"copy\">Kopyala</string>\n    <string name=\"copied_to_clipboard\">Panoya kopyalandı</string>\n    <string name=\"not_set\">Ayarlanmadı</string>\n    <string name=\"hosting_room\">Paylaşılan oda</string>\n    <string name=\"in_room\">Odada</string>\n    <string name=\"pending_requests\">Beklemedeki istekler</string>\n    <string name=\"pending_suggestions\">Bekleyen öneriler</string>\n    <string name=\"suggest_to_host\">Sunucu sahibine öner</string>\n    <string name=\"kick_user\">At</string>\n    <string name=\"host_label\">Sunucu</string>\n    <string name=\"you_label\">Sen</string>\n    <string name=\"connected_users\">Bağlanmış kullanıcılar</string>\n    <string name=\"enter_username\">Kullanıcı adını gir</string>\n    <string name=\"error_username_empty\">Kullanıcı adı zorunludur.</string>\n    <string name=\"resync\">Yeniden eşitle</string>\n    <string name=\"mute\">Sesi kapat</string>\n    <string name=\"unmute\">Sesi geri aç</string>\n    <string name=\"crash_title\">Uygulamada sorun oluştu</string>\n    <string name=\"crash_description\">Beklenmedik bir hata oluştu. Sorunu çözebilmemiz için lütfen bize oluşan hatayı raporlayın.</string>\n    <string name=\"crash_share_logs\">Logları paylaş</string>\n    <string name=\"crash_share_title\">Hata raporlarını paylaş</string>\n    <string name=\"crash_report_subject\">Metrolist Hata Bildirimi</string>\n    <string name=\"crash_close\">Kapat</string>\n    <string name=\"crash_no_log\">Hata logları bulunmamakta</string>\n    <string name=\"palette_dynamic\">Dinamik</string>\n    <string name=\"palette_crimson\">Kıpkırmızı</string>\n    <string name=\"palette_rose\">Gül Pembesi</string>\n    <string name=\"palette_purple\">Mor</string>\n    <string name=\"palette_deep_purple\">Koyu Mor</string>\n    <string name=\"palette_indigo\">Çivit Mavisi</string>\n    <string name=\"palette_blue\">Mavi</string>\n    <string name=\"palette_sky_blue\">Gök Mavisi</string>\n    <string name=\"palette_cyan\">Camgöbeği</string>\n    <string name=\"palette_teal\">Turkuaz</string>\n    <string name=\"palette_green\">Yeşil</string>\n    <string name=\"palette_light_green\">Açık Yeşil</string>\n    <string name=\"palette_lime\">Limon Sarısı</string>\n    <string name=\"palette_yellow\">Sarı</string>\n    <string name=\"palette_amber\">Kehribar Sarısı</string>\n    <string name=\"palette_orange\">Turuncu</string>\n    <string name=\"palette_deep_orange\">Koyu Turuncu</string>\n    <string name=\"palette_brown\">Kahverengi</string>\n    <string name=\"palette_grey\">Gri</string>\n    <string name=\"palette_blue_grey\">Mavi Gri</string>\n    <string name=\"cd_back\">Geri</string>\n    <string name=\"cd_pure_black_mode\">Saf Siyah modu</string>\n    <string name=\"cd_light_mode\">Aydınlık modu</string>\n    <string name=\"cd_dark_mode\">Karanlık modu</string>\n    <string name=\"cd_system_mode\">Sistem modu</string>\n    <string name=\"cd_palette_item\">%1$s paleti</string>\n    <string name=\"listen_together_sync_volume\">Sunucu sesini eşitle</string>\n    <string name=\"listen_together_use_custom_server\">Özel sunucu kullan</string>\n    <string name=\"listen_together_custom_server\">Özel sunucu</string>\n    <string name=\"listen_together_choose_server\">Sunucu seç</string>\n    <string name=\"listen_together_sync_volume_desc\">Misafirler sunucu sahibinin ses seviyesini takip eder</string>\n    <string name=\"listen_together_auto_approval_joins_desc\">Katılım isteklerini manuel olarak incelemek yerine otomatik olarak onaylayın</string>\n    <string name=\"listen_together_auto_approval_joins\">Katılım isteklerini otomatik olarak onayla</string>\n    <string name=\"copy_code\">Kodu kopyala</string>\n    <string name=\"kick_user_desc\">Bu kişiyi oturumdan kaldır</string>\n    <string name=\"permanently_kick_user\">Kalıcı Olarak Engelle</string>\n    <string name=\"permanently_kick_user_desc\">Bu kişinin katılma isteklerini engelle ve önerilerini gizle</string>\n    <string name=\"listen_together_no_blocked_users\">Engellenen kullanıcı yok</string>\n    <string name=\"listen_together_blocked_users_count\">%d kullanıcı engellendi</string>\n    <string name=\"listen_together_blocked_users\">Engellenen Kullanıcılar</string>\n    <string name=\"manage_user\">Kullanıcıyı Yönet</string>\n    <string name=\"transfer_ownership_desc\">Bu kişiyi odanın sunucu sahibi yapın</string>\n    <string name=\"unblock\">Engellemeyi kaldır</string>\n    <string name=\"user_blocked_by_host\">Kullanıcı sunucu sahibi tarafından engellendi</string>\n    <string name=\"transfer_ownership\">Sahipliği Aktar</string>\n    <string name=\"not_playing\">Herhangi bir şarkı oynatılmıyor</string>\n    <string name=\"tap_to_play\">Metrolist\\'i açmak için tıkla</string>\n    <string name=\"widget_music_player\">Müzik oynatıcı</string>\n    <string name=\"widget_turntable\">Dönen plak</string>\n    <string name=\"together\">Beraber dinle</string>\n    <string name=\"enter_room_code\">Oda kodu gir</string>\n    <string name=\"listen_together_settings_desc\">Sunucuyu, kullanıcı adını, ve daha fazlasını ayarlayın</string>\n    <string name=\"ai_lyrics_translation\">Yapay zeka ile şarkı sözü çevirisi</string>\n    <string name=\"ai_translating_lyrics\">Sözler çeviriliyor...</string>\n    <string name=\"ai_lyrics_translated\">Sözler çevirildi</string>\n    <string name=\"ai_provider\">Sağlayıcı</string>\n    <string name=\"ai_base_url\">Temel URL</string>\n    <string name=\"ai_api_key\">API Anahtarı</string>\n    <string name=\"ai_model\">Model</string>\n    <string name=\"ai_translation_mode\">Çeviri modu</string>\n    <string name=\"ai_target_language\">Hedef Dil</string>\n    <string name=\"ai_setup_guide\">API kimliği</string>\n    <string name=\"ai_translation_literal\">Çeviri</string>\n    <string name=\"ai_translation_transcribed\">Transkripsiyon</string>\n    <string name=\"ai_api_key_required\">API anahtarına ihtiyaç var</string>\n    <string name=\"ai_error_api_key_required\">API anahtarı zorunludur</string>\n    <string name=\"ai_error_no_lyrics\">Çevirilecek söz bulunmamakta</string>\n    <string name=\"ai_error_lyrics_empty\">Şarkı sözü bulunmamakta</string>\n    <string name=\"ai_error_language_required\">Hedef dilin seçilmesi gerekiyor</string>\n    <string name=\"ai_error_unexpected\">Beklenmeyen çeviri sonucu</string>\n    <string name=\"ai_error_unknown\">Bilinmeyen bir hata oluştu</string>\n    <string name=\"ai_error_translation_failed\">Çeviride sorun oluştu</string>\n    <string name=\"play_all\">Tümünü oynat</string>\n    <string name=\"recognize_music\">Müziği tanımla</string>\n    <string name=\"youtube_url_column\">YouTube URL Sütunu (İsteğe bağlı)</string>\n    <string name=\"re_listen\">Yeniden dinle</string>\n    <string name=\"clear_recognition_history_confirm\">Tüm tanımlama geçmişini silmek istediğinize emin misiniz?</string>\n    <string name=\"no_match_found\">Eşleşme bulunamadı</string>\n    <string name=\"delete_from_history\">Geçmişten sil</string>\n    <string name=\"artist_name_column\">Sanatçı adı sütunu</string>\n    <string name=\"processing\">İşleniyor…</string>\n    <string name=\"clear_recognition_history\">Tanımlama geçmişini temizle</string>\n    <string name=\"map_csv_columns\">CSV Sütunlarını Eşleştir</string>\n    <string name=\"column_label\">Sütun %d</string>\n    <string name=\"recognition_error\">Tanımlama hatası</string>\n    <string name=\"enable_high_refresh_rate_desc\">Ekranı desteklenen en yüksek yenileme hızında çalışmaya zorla (örn. 120Hz)</string>\n    <string name=\"first_row_is_header\">İlk satır başlıktır</string>\n    <string name=\"try_again\">Yeniden dene</string>\n    <string name=\"tap_to_recognize\">Tanımlamak için dokunun</string>\n    <string name=\"recognition_history\">Tanımlama geçmişi</string>\n    <string name=\"enable_high_refresh_rate\">Yüksek yenileme hızını etkinleştir</string>\n    <string name=\"song_title_column\">Şarkı adı sütunu</string>\n    <string name=\"recently_converted\">Son Dönüştürülenler</string>\n    <string name=\"importing_csv\">CSV içe aktarılıyor</string>\n    <string name=\"play_on_app\">Metrolist\\'te Oynat</string>\n    <string name=\"listening\">Dinleniliyor…</string>\n    <string name=\"continue_action\">Devam et</string>\n    <string name=\"enable\">Etkinleştir</string>\n    <string name=\"crossfade\">Çapraz geçiş</string>\n    <string name=\"crossfade_desc\">Şarkılar arasında çapraz geçiş</string>\n    <string name=\"crossfade_duration\">Çapraz geçiş süresi</string>\n    <string name=\"crossfade_gapless\">Aralıksız albümler için bunu kapat</string>\n    <string name=\"crossfade_gapless_desc\">Albüm kesintisiz ise çapraz geçişi kullanma</string>\n    <string name=\"crossfade_beta_title\">Beta Özellikleri</string>\n    <string name=\"crossfade_beta_message\">Çapraz geçiş yeni bir özelliktir ve hatalar içerebilir. Bir hata ile karşılaşırsanız lütfen bildirin.\\n\\nBu özellik, bazı teknik kısıtlamalar nedeniyle sesin donanıma devredilmesini devre dışı bırakır (ses, DSP yerine CPU üzerinde işlenir).</string>\n    <string name=\"audio_offload_disabled_by_crossfade\">Çapraz geçiş aktif olduğu için kapatıldı</string>\n    <string name=\"hide_youtube_shorts\">YouTube Shorts\\'u gizle</string>\n    <string name=\"listen_together_in_top_bar\">Üst çubuktan Başkaları İle Dinle</string>\n    <string name=\"listen_together_in_top_bar_desc\">Başkaları ile dinlemeyi navigasyon yerine üst çubukta göster</string>\n    <string name=\"prevent_duplicate_tracks_in_queue\">Sırada birden fazla şarkı olmasını önle</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">Eğer listeye yeni bir şarkı eklenmişse ve daha önceden de listede varsa o şarkıyı kaldır</string>\n    <string name=\"ai_translation_literal_desc\">Hedef dile uygun bir şekilde çevir</string>\n    <string name=\"ai_translation_transcribed_desc\">Belirlenmiş alfabeye göre tellaffuz edilecek şekilde çevir</string>\n    <string name=\"ai_provider_help\">API anahtarı al</string>\n    <string name=\"ai_provider_openrouter_help\">https://openrouter.ai sitesinden ücretli ve ücretsiz modellerini görüntüle</string>\n    <string name=\"ai_provider_openai_help\">https://platform.openai.com/api-keys sitesini ziyaret et</string>\n    <string name=\"ai_provider_claude_help\">https://console.anthropic.com/settings/keys sitesini ziyaret et</string>\n    <string name=\"ai_provider_gemini_help\">https://aistudio.google.com/apikey sitesini ziyaret et</string>\n    <string name=\"ai_provider_perplexity_help\">https://perplexity.ai/settings/api sitesini ziyaret et</string>\n    <string name=\"ai_provider_xai_help\">https://console.x.ai sitesini ziyaret et</string>\n    <string name=\"ai_provider_deepl_help\">https://deepl.com/pro-api sitesini ücretli ve ücretsiz planlarını görmek için ziyaret et</string>\n    <string name=\"ai_deepl_formality\">Formalite</string>\n    <string name=\"ai_deepl_formality_default\">Varsayılan</string>\n    <string name=\"ai_deepl_formality_more\">Daha resmi</string>\n    <string name=\"ai_deepl_formality_less\">Az resmi</string>\n    <string name=\"discord_status\">Durum bilgisi</string>\n    <string name=\"discord_status_online\">Çevrim içi</string>\n    <string name=\"discord_status_idle\">Boşta</string>\n    <string name=\"discord_status_dnd\">Rahatsız etmeyin</string>\n    <string name=\"discord_buttons\">Butonlar</string>\n    <string name=\"discord_button_1\">1. tuş</string>\n    <string name=\"discord_button_2\">2. tuş</string>\n    <string name=\"login_successful\">Giriş başarılı!</string>\n    <string name=\"discord_information_warning\">Bu özellik, Discord Gateway’e bağlanmak ve Rich Presence durumunuzu ayarlamak için KizzyRPC kütüphanesini kullanır.Benzer kullanımlar nedeniyle bilinen bir hesap askıya alma durumu yaşanmamış olsa da, bu yöntem Discord tarafından resmi olarak desteklenmemektedir ve Kullanım Koşulları ihlali sayılabilir.Tokeniniz yalnızca cihazınızda yerel olarak alınır ve üçüncü taraf sunuculara gönderilmez.Kullanım sorumluluğu tamamen size aittir.</string>\n    <string name=\"discord_activity_type\">Etkinlik türü</string>\n    <string name=\"discord_activity_playing\">Oynuyor</string>\n    <string name=\"discord_activity_listening\">Dinliyor</string>\n    <string name=\"discord_activity_watching\">İzliyor</string>\n    <string name=\"discord_activity_competing\">Rekabet</string>\n    <string name=\"discord_button_text_variables\">Değişkenler: {song_name}, {artist_name}, {album_name}</string>\n    <string name=\"discord_rpc_preview\">Rich Presence Önizlemesi</string>\n    <string name=\"discord_presence\">Durum</string>\n    <string name=\"discord_connect_description\">Ne dinlediğini paylaşmak için Discord\\'a giriş yap</string>\n    <string name=\"discord_playing_metrolist\">Metrolist oynuyor</string>\n    <string name=\"discord_watching_metrolist\">Metrolist izliyor</string>\n    <string name=\"discord_competing_metrolist\">Metrolist\\'te yarışıyor</string>\n    <string name=\"discord_activity_name\">Etkinlik adı</string>\n    <string name=\"discord_activity_name_description\">Etkinlik için özel ad (varsayılan için boş bırakın)</string>\n    <string name=\"discord_advanced_mode\">Gelişmiş mod</string>\n    <string name=\"discord_advanced_mode_description\">Rich Presence için ek özelleştirme seçeneklerini göster</string>\n    <string name=\"player_background_solid\">Düz</string>\n    <string name=\"resume_on_bluetooth_connect\">Bluetooth bağlandığında oynatmaya devam et</string>\n    <string name=\"lyrics_romanize_hindi\">Hintçe şarkı sözlerini Latinleştir</string>\n    <string name=\"lyrics_romanize_punjabi\">Pencapça şarkı sözlerini Latinleştir</string>\n    <string name=\"lyrics_romanize_as_main\">Latin harfleriyle yazılmış sözleri ana metin olarak göster</string>\n    <string name=\"display_density\">Ekran yoğunluğu</string>\n    <string name=\"restart\">Yeniden başlat</string>\n    <string name=\"restart_required\">Yeniden başlatma gerekli</string>\n    <string name=\"density_restart_message\">Ekran yoğunluğu değişikliği, uygulamayı yeniden başlattıktan sonra geçerli olacaktır. Şimdi yeniden başlatmak ister misiniz?</string>\n    <string name=\"enable_lrclib_desc\">Topluluk odaklı senkronize şarkı sözü veritabanı</string>\n    <string name=\"enable_kugou_desc\">Şarkı sözleri, popüler Çin müzik platformu KuGou\\'dan alınır</string>\n    <string name=\"youtube_music_lyrics_note\">NOT: Diğer şarkı sözleri mevcut olmadığında YouTube Music\\'ten şarkı sözleri otomatik olarak gösterilecektir. YTM\\'den alınan şarkı sözleri genellikle senkronize değildir.</string>\n    <string name=\"enable_lyricsplus\">LyricsPlus\\'ı etkinleştir</string>\n    <string name=\"enable_lyricsplus_desc\">Birden fazla kaynaktan senkronize edilmiş şarkı sözleri</string>\n    <string name=\"lyrics_provider_selection\">Sağlayıcı seçimi</string>\n    <string name=\"lyrics_provider_selection_desc\">Hangi şarkı sözü sağlayıcılarının etkinleştirileceğini seçin</string>\n    <string name=\"lyrics_provider_priority\">Şarkı sözü sağlayıcı önceliği</string>\n    <string name=\"lyrics_provider_priority_desc\">Tercihinize göre sağlayıcıları yeniden sıralamak için sürükleyin. Daha üst sıra -&gt; daha yüksek öncelik.</string>\n    <string name=\"changelog\">Değişiklik Günlüğü</string>\n    <string name=\"changelog_empty\">Değişiklik günlüğü mevcut değil</string>\n    <string name=\"github_releases_url\">https://github.com/MetrolistGroup/Metrolist/releases</string>\n    <string name=\"list_bullet\">•</string>\n    <string name=\"view_on_github\">GitHub\\'da görüntüle</string>\n    <string name=\"current_version\">Mevcut sürüm</string>\n    <string name=\"version_format\">Sürüm: %s</string>\n    <string name=\"update_settings\">Ayarları güncelle</string>\n    <string name=\"check_for_updates_title\">Güncellemeleri kontrol et</string>\n    <string name=\"checking_for_updates\">Güncellemeler kontrol ediliyor…</string>\n    <string name=\"latest_version_format\">Güncel: %s</string>\n    <string name=\"check_for_updates_button\">Güncellemeleri kontrol et</string>\n    <string name=\"hide_changelog\">Değişiklik günlüğünü gizle</string>\n    <string name=\"view_changelog\">Değişiklik günlüğünü görüntüle</string>\n    <string name=\"failed_to_check_updates\">Güncellemeleri kontrol etme başarısız oldu: %s</string>\n    <string name=\"set_as_default\">Varsayılan olarak ayarla</string>\n    <string name=\"sleep_timer_default_set\">Uyku zamanlayıcısı varsayılan olarak %d dk olarak ayarlanmıştır</string>\n    <string name=\"found_in_settings_content\">Ayarlar &gt; İçerik bölümünde bulunur</string>\n    <string name=\"plays\">oynatılanlar</string>\n    <string name=\"error_episode_save\">Bölüm kaydedilemedi</string>\n    <string name=\"error_episode_remove\">Bölüm kaldırılamadı</string>\n    <string name=\"error_podcast_subscribe\">Podcast\\'e abone olma işlemi başarısız oldu</string>\n    <string name=\"error_podcast_unsubscribe\">Podcast aboneliğinden çıkma işlemi başarısız oldu</string>\n    <string name=\"listen_together_auto_approval_suggestions\">Şarkı önerilerini otomatik olarak onayla</string>\n    <string name=\"listen_together_auto_approval_suggestions_desc\">Konuklardan gelen şarkı önerilerini otomatik olarak onaylayın ve sıraya alın</string>\n    <string name=\"speed_dial\">Hızlı arama</string>\n    <string name=\"pin_to_speed_dial\">Hızlı arama için sabitle</string>\n    <string name=\"unpin_from_speed_dial\">Hızlı aramadan sabitlemeyi kaldır</string>\n    <string name=\"randomize_home_order\">Ana Ekran Sırasını Rastgeleleştir</string>\n    <string name=\"randomize_home_order_desc\">Ana ekran bölümlerinin sırasını ağırlıklı önceliklere göre rastgele değiştirin</string>\n    <string name=\"daily_discover_sounds_like\">%1$s tarzında</string>\n    <string name=\"daily_discover_because_you_listen_to\">%1$s dinlediğin için</string>\n    <string name=\"daily_discover_similar_to\">%1$s benzeri</string>\n    <string name=\"daily_discover_based_on\">%1$s baz alınarak</string>\n    <string name=\"daily_discover_for_fans_of\">%1$s fanları için</string>\n    <string name=\"from_the_community\">Topluluktan</string>\n    <string name=\"logout_dialog_title\">Kütüphane verileri saklansın mı?</string>\n    <string name=\"logout_dialog_message\">Oynatma listelerinizi ve kütüphane verilerinizi saklamak istiyor musunuz? İndirilen şarkılar her durumda saklanacaktır.</string>\n    <string name=\"logout_keep\">Sakla</string>\n    <string name=\"logout_clear\">Temizle</string>\n    <string name=\"credits_lead_developer\">Baş Geliştirici</string>\n    <string name=\"credits_collaborator\">Katkıda bulunan</string>\n    <string name=\"credits_collaborators_section\">Katkıda bulunanlar</string>\n    <string name=\"credits_license_name\">GNU Genel Açık Laynak Lisansı v3.0</string>\n    <string name=\"credits_license_desc\">Ücretsiz, açık kaynaklı yazılım. Kullanabilir, inceleyebilir, paylaşabilir ve geliştirebilirsiniz.</string>\n    <string name=\"credits_discord\">Discord Sunucusu</string>\n    <string name=\"credits_telegram\">Telegram Kanalı</string>\n    <string name=\"credits_website\">Web sitesi</string>\n    <string name=\"credits_instagram\">Instagram</string>\n    <string name=\"credits_github\">GitHub</string>\n    <string name=\"credits_view_repo\">Depoyu Görüntüle</string>\n    <string name=\"app_version_info\">%1$s • %2$s</string>\n    <string name=\"like_what_i_do\">Yaptıklarımdan hoşlanıyor musunuz?</string>\n    <string name=\"buy_mo_a_coffee\">Bana bir kahve ısmarla</string>\n    <string name=\"community_and_info\">Topluluk ve Bilgi</string>\n    <string name=\"metrolist\">METROLIST</string>\n    <string name=\"wanna_play_favorite_song\">Onların en sevdiği şarkıyı çalmak ister misin?</string>\n    <string name=\"yeah\">Evet</string>\n    <string name=\"stands_with_palestine\">Bu proje Filistin\\'in yanındadır 🇵🇸</string>\n    <string name=\"filter_podcasts\">Podcastler</string>\n    <string name=\"view_podcast\">Podcast\\'i görüntüle</string>\n    <string name=\"podcast_channels\">Podcast Kanalları</string>\n    <string name=\"latest_episodes\">Son bölümler</string>\n    <string name=\"your_shows\">Sizin Programlarınız</string>\n    <string name=\"new_episodes\">Yeni bölümler</string>\n    <string name=\"episodes_for_later\">Daha Sonraki Bölümler</string>\n    <string name=\"save_episode_for_later\">Daha sonrası için kaydet</string>\n    <string name=\"save_episode_for_later_desc\">Bölümlerinizi \\'Daha Sonra Dinle\\' listenize ekleyin</string>\n    <string name=\"remove_episode_from_saved\">Kaydedilenlerden kaldır</string>\n    <string name=\"subscribe_to_podcast\">Podcast\\'i kütüphaneye kaydet</string>\n    <plurals name=\"n_episode\">\n        <item quantity=\"one\">%d bölüm</item>\n        <item quantity=\"other\">%d bölüm</item>\n    </plurals>\n    <string name=\"restore_confirm_title\">Yedeklemeyi geri yükle?</string>\n    <string name=\"restore_confirm_message\">Bu seçenekle uygulama verilerinizi yedekten geri yükleyebilirsiniz.</string>\n    <string name=\"restore_account_warning\">Geri yükleme işleminden sonra tekrar giriş yapmanız gerekecek. Aşağıdaki hesaptan çıkış yapılacaktır:</string>\n    <string name=\"restore\">Geri yükle</string>\n    <string name=\"checking_previous_account\">Önceki hesap kontrol ediliyor…</string>\n    <string name=\"no_account_found\">Hesap bulunamadı</string>\n    <string name=\"importing_playlist\">Oynatma listesi içe aktarılıyor</string>\n    <string name=\"widget_recognizer_name\">Müzik Tanıyıcı</string>\n    <string name=\"widget_recognizer_description\">Çevrenizde çalan şarkıları doğrudan ana ekranınızdan tanımlayın</string>\n    <string name=\"widget_recognizer_tap_to_search\">Şarkıyı tanımlamak için dokunun</string>\n    <string name=\"widget_recognizer_listening\">Dinliyor…</string>\n    <string name=\"widget_recognizer_processing\">Tanımlanıyor…</string>\n    <string name=\"widget_recognizer_no_match\">Eşleşme bulunamadı. Tekrar deneyin</string>\n    <string name=\"widget_recognizer_error\">Tanıma başarısız oldu</string>\n    <string name=\"widget_recognizer_error_generic\">Bir hata oluştu. Lütfen tekrar deneyin</string>\n    <string name=\"widget_recognizer_unknown_song\">Bilinmeyen şarkı</string>\n    <string name=\"widget_recognizer_unknown_artist\">Bilinmeyen sanatçı</string>\n    <string name=\"widget_recognizer_mic_desc\">Şarkıyı tanımla</string>\n    <string name=\"widget_recognizer_channel_name\">Müzik Tanıma</string>\n    <string name=\"widget_recognizer_channel_desc\">Widget\\'tan bir şarkı tanımlanırken bildirim gösterilir</string>\n    <string name=\"widget_recognizer_notification_text\">Şarkıyı belirlemek için ses kaydı yapılıyor…</string>\n    <string name=\"filter_episodes\">Bölümler</string>\n    <string name=\"filter_channels\">Kanallar</string>\n    <string name=\"auto_playlist\">Otomatik oynatma listesi</string>\n    <string name=\"downloaded_episodes\">İndirilen bölümler</string>\n    <string name=\"no_subscribed_channels\">Abone olunan kanal yok</string>\n    <string name=\"no_downloaded_episodes\">İndirilen bölüm yok</string>\n    <plurals name=\"n_channel\">\n        <item quantity=\"one\">%d kanal</item>\n        <item quantity=\"other\">%d kanal</item>\n    </plurals>\n    <string name=\"view_channel\">Kanalı görüntüle</string>\n    <string name=\"filter_profiles\">Profiller</string>\n    <string name=\"enable_automatic_sleeptimer\">Uyku zamanlayıcısını otomatikman etkinleştir</string>\n    <string name=\"sleeptimer_description\">Varsayılan değer özel bir zaman ise uyku zamanlayıcısını otomatik olarak aktif eder</string>\n    <string name=\"sleep_timer_repeat_description\">Uyku zamaayıcısının otomatikman aktif edilmesi için özel gün ve zamanı ayarlayın</string>\n    <string name=\"sleep_timer_repeat\">Tekrar</string>\n    <string name=\"sleep_timer_daily\">Günlük</string>\n    <string name=\"sleep_timer_weekdays\">Pazartesiden cumaya kadar</string>\n    <string name=\"sleep_timer_weekdays_weekends\">Hafta içi / Hafta sonu</string>\n    <string name=\"sleep_timer_weekends\">Hafta sonları (Cmt-Pzr)</string>\n    <string name=\"sleep_timer_custom\">Özel</string>\n    <string name=\"sleep_timer_start_time\">Başlangıç vakti</string>\n    <string name=\"sleep_timer_end_time\">Bitiş vakti</string>\n    <string name=\"sleep_timer_monday\">Pazartesi</string>\n    <string name=\"sleep_timer_tuesday\">Salı</string>\n    <string name=\"sleep_timer_wednesday\">Çarşamba</string>\n    <string name=\"sleep_timer_thursday\">Perşembe</string>\n    <string name=\"sleep_timer_friday\">Cuma</string>\n    <string name=\"sleep_timer_saturday\">Cumartesi</string>\n    <string name=\"sleep_timer_sunday\">Pazar</string>\n    <string name=\"sleep_timer_stop_after_current_song\">Zamanlayıcının süresi dolduğunda o sırada çalınan şarkıyı durdur</string>\n    <string name=\"sleep_timer_fade_out\">Son dakikalarda ses yavaşça azaltılır</string>\n    <string name=\"upload_songs\">Şarkıları yükle</string>\n    <string name=\"uploading\">Yükleniyor…</string>\n    <string name=\"upload_progress\">%1$d / %2$d</string>\n    <string name=\"upload_complete\">Yükleme tamamlandı</string>\n    <string name=\"upload_failed\">Yükleme başarısız oldu</string>\n    <string name=\"upload_file_too_large\">Dosya çok büyük (maksimum 300 MB)</string>\n    <string name=\"upload_unsupported_format\">Desteklenmeyen format. Lütfen mp3, m4a, wma, flac veya ogg kullanın</string>\n    <string name=\"delete_uploaded_song\">Yüklenen şarkıyı sil</string>\n    <string name=\"delete_uploaded_song_confirm\">Yüklediğiniz bu şarkıyı silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.</string>\n    <string name=\"delete_uploaded_song_success\">Yüklenen şarkı silindi</string>\n    <string name=\"delete_uploaded_song_failed\">Yüklenen şarkı silinemedi</string>\n    <string name=\"delete_uploaded_songs\">Yüklenen şarkıları sil</string>\n    <string name=\"delete_uploaded_songs_confirm\">Yüklediğiniz %1$d şarkıyı silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.</string>\n    <string name=\"deleted_n_songs\">Silinen %1$d şarkı</string>\n    <string name=\"deleting\">Siliniyor…</string>\n    <string name=\"qs_tile_music_recognizer\">Müziği Tanı</string>\n    <string name=\"export_option_save\">Belgelere Kaydet</string>\n    <string name=\"export_option_share\">Paylaş</string>\n    <string name=\"export_failed\">Oynatma listesi dışa aktarılamadı</string>\n    <string name=\"export_success\">Oynatma listesi başarıyla dışa aktarıldı</string>\n    <string name=\"export_as_m3u\">M3U olarak dışa aktar</string>\n    <string name=\"export_as_csv\">CSV olarak dışa aktar</string>\n    <string name=\"export_playlist\">Oynatma listesini dışa aktar</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-tr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <string name=\"home\">Ana Sayfa</string>\n    <string name=\"songs\">Şarkılar</string>\n    <string name=\"artists\">Sanatçılar</string>\n    <string name=\"albums\">Albümler</string>\n    <string name=\"playlists\">Çalma Listeleri</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d seçildi</item>\n        <item quantity=\"other\">%d seçililer</item>\n    </plurals>\n    <string name=\"history\">Geçmiş</string>\n    <string name=\"stats\">İstatistikler</string>\n    <string name=\"mood_and_genres\">Ruh Hali ve Türler</string>\n    <string name=\"account\">Hesap</string>\n    <string name=\"quick_picks\">Hızlı seçimler</string>\n    <string name=\"quick_picks_empty\">Hızlı seçimlerinizi oluşturmak için birkaç şarkı dinleyin</string>\n    <string name=\"new_release_albums\">Yeni çıkan albümler</string>\n    <string name=\"today\">Bugün</string>\n    <string name=\"yesterday\">Dün</string>\n    <string name=\"this_week\">Bu hafta</string>\n    <string name=\"last_week\">Geçen hafta</string>\n    <string name=\"most_played_songs\">En çok dinlenen şarkılar</string>\n    <string name=\"most_played_artists\">En çok dinlenen sanatçılar</string>\n    <string name=\"most_played_albums\">En çok dinlenen albümler</string>\n    <string name=\"search\">Arama</string>\n    <string name=\"search_yt_music\">YouTube Müzik\\'te Ara…</string>\n    <string name=\"search_library\">Kütüphanede ara…</string>\n    <string name=\"filter_library\">Kütüphane</string>\n    <string name=\"filter_liked\">Beğenilenler</string>\n    <string name=\"filter_downloaded\">İndirilenler</string>\n    <string name=\"filter_all\">Hepsi</string>\n    <string name=\"filter_songs\">Şarkılar</string>\n    <string name=\"filter_videos\">Videolar</string>\n    <string name=\"filter_albums\">Albümler</string>\n    <string name=\"filter_artists\">Sanatçılar</string>\n    <string name=\"filter_playlists\">Çalma Listeleri</string>\n    <string name=\"filter_community_playlists\">Topluluk çalma listeleri</string>\n    <string name=\"filter_featured_playlists\">Öne çıkan listeler</string>\n    <string name=\"filter_bookmarked\">Favoriler</string>\n    <string name=\"no_results_found\">Sonuç bulunamadı</string>\n    <string name=\"from_your_library\">Kütüphanenizden</string>\n    <string name=\"liked_songs\">Beğenilen şarkılar</string>\n    <string name=\"downloaded_songs\">İndirilen şarkılar</string>\n    <string name=\"playlist_is_empty\">Çalma listesi boş</string>\n    <string name=\"retry\">Tekrar dene</string>\n    <string name=\"radio\">Radyo</string>\n    <string name=\"shuffle\">Karıştır</string>\n    <string name=\"reset\">Sıfırla</string>\n    <string name=\"details\">Detaylar</string>\n    <string name=\"edit\">Düzenle</string>\n    <string name=\"start_radio\">Radyo başlat</string>\n    <string name=\"play\">Çal</string>\n    <string name=\"play_next\">Bir sonra çal</string>\n    <string name=\"add_to_queue\">Sıraya ekle</string>\n    <string name=\"add_to_library\">Kütüphaneye ekle</string>\n    <string name=\"remove_from_library\">Kütüphaneden kaldır</string>\n    <string name=\"action_download\">İndir</string>\n    <string name=\"downloading\">İndiriliyor</string>\n    <string name=\"remove_download\">İndirmeyi kaldır</string>\n    <string name=\"import_playlist\">Çalma listesini içe aktar</string>\n    <string name=\"add_to_playlist\">Çalma listesine ekle</string>\n    <string name=\"view_artist\">Sanatçıyı görüntüle</string>\n    <string name=\"view_album\">Albümü görüntüle</string>\n    <string name=\"refetch\">Yenile</string>\n    <string name=\"share\">Paylaş</string>\n    <string name=\"delete\">Sil</string>\n    <string name=\"remove_from_history\">Geçmişten kaldır</string>\n    <string name=\"search_online\">Çevrim içi ara</string>\n    <string name=\"action_sync\">Eşitle</string>\n    <string name=\"advanced\">Gelişmiş</string>\n    <string name=\"sort_by_create_date\">Eklendiği tarih</string>\n    <string name=\"sort_by_name\">Ad</string>\n    <string name=\"sort_by_artist\">Sanatçı</string>\n    <string name=\"sort_by_year\">Yıl</string>\n    <string name=\"sort_by_song_count\">Şarkı sayısı</string>\n    <string name=\"sort_by_length\">Uzunluk</string>\n    <string name=\"sort_by_play_time\">Çalma süresi</string>\n    <string name=\"sort_by_custom\">Özel sıralama</string>\n    <string name=\"media_id\">Medya kimliği</string>\n    <string name=\"mime_type\">MIME türü</string>\n    <string name=\"codecs\">Kodekler</string>\n    <string name=\"bitrate\">Bit hızı</string>\n    <string name=\"sample_rate\">Örnek hızı</string>\n    <string name=\"loudness\">Ses şiddeti</string>\n    <string name=\"volume\">Ses yüksekliği</string>\n    <string name=\"file_size\">Dosya boyutu</string>\n    <string name=\"unknown\">Bilinmiyor</string>\n    <string name=\"copied\">Panoya kopyalandı</string>\n    <string name=\"edit_lyrics\">Sözleri düzenle</string>\n    <string name=\"search_lyrics\">Sözleri ara</string>\n    <string name=\"edit_song\">Şarkıyı düzenle</string>\n    <string name=\"song_title\">Şarkı adı</string>\n    <string name=\"song_artists\">Şarkı sanatçıları</string>\n    <string name=\"error_song_title_empty\">Şarkı adı boş olamaz.</string>\n    <string name=\"error_song_artist_empty\">Şarkı sanatçısı boş olamaz.</string>\n    <string name=\"save\">Kaydet</string>\n    <string name=\"choose_playlist\">Çalma listesi seç</string>\n    <string name=\"edit_playlist\">Çalma listesini düzenle</string>\n    <string name=\"create_playlist\">Çalma listesi oluştur</string>\n    <string name=\"playlist_name\">Çalma listesi adı</string>\n    <string name=\"error_playlist_name_empty\">Çalma listesi adı boş olamaz.</string>\n    <string name=\"edit_artist\">Sanatçıyı düzenle</string>\n    <string name=\"artist_name\">Sanatçı adı</string>\n    <string name=\"error_artist_name_empty\">Sanatçı adı boş olamaz.</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d şarkı</item>\n        <item quantity=\"other\">%d şarkılar</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d sanatçı</item>\n        <item quantity=\"other\">%d sanatçılar</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d albüm</item>\n        <item quantity=\"other\">%d albümler</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d çalma listesi</item>\n        <item quantity=\"other\">%d çalma listeleri</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d hafta</item>\n        <item quantity=\"other\">%d haftalar</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d ay</item>\n        <item quantity=\"other\">%d aylar</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d yıl</item>\n        <item quantity=\"other\">%d yıllar</item>\n    </plurals>\n    <string name=\"playlist_imported\">Çalma listesi içe aktarıldı</string>\n    <string name=\"removed_song_from_playlist\">\\\"%s\\\" çalma listesinden kaldırıldı</string>\n    <string name=\"playlist_synced\">Çalma listesi eşitlendi</string>\n    <string name=\"undo\">Geri al</string>\n    <string name=\"lyrics_not_found\">Şarkı sözleri bulunamadı</string>\n    <string name=\"sleep_timer\">Uyku zamanlayıcısı</string>\n    <string name=\"end_of_song\">Şarkının sonu</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">1 dakika</item>\n        <item quantity=\"other\">%d dakika</item>\n    </plurals>\n    <string name=\"error_no_stream\">Akış yok</string>\n    <string name=\"error_no_internet\">Ağ bağlantısı yok</string>\n    <string name=\"error_timeout\">Zaman aşımı</string>\n    <string name=\"error_unknown\">Bilinmeyen hata</string>\n    <string name=\"action_like\">Beğen</string>\n    <string name=\"action_remove_like\">Beğeniyi kaldır</string>\n    <string name=\"action_shuffle_on\">Karıştırma açık</string>\n    <string name=\"action_shuffle_off\">Karıştırma kapalı</string>\n    <string name=\"repeat_mode_off\">Tekrarlama modu kapalı</string>\n    <string name=\"repeat_mode_one\">Şu anki şarkıyı tekrarla</string>\n    <string name=\"repeat_mode_all\">Sırayı tekrarla</string>\n    <string name=\"queue_all_songs\">Tüm şarkılar</string>\n    <string name=\"queue_searched_songs\">Aranan şarkılar</string>\n    <string name=\"music_player\">Müzik Çalar</string>\n    <string name=\"settings\">Ayarlar</string>\n    <string name=\"appearance\">Görünüm</string>\n    <string name=\"enable_dynamic_theme\">Dinamik temayı etkinleştir</string>\n    <string name=\"dark_theme\">Koyu tema</string>\n    <string name=\"dark_theme_on\">Açık</string>\n    <string name=\"dark_theme_off\">Kapalı</string>\n    <string name=\"dark_theme_follow_system\">Sistemi izle</string>\n    <string name=\"pure_black\">Simsiyah</string>\n    <string name=\"default_open_tab\">Varsayılan giriş sekmesi</string>\n    <string name=\"customize_navigation_tabs\">Gezinti sekmelerini özelleştir</string>\n    <string name=\"lyrics_text_position\">Şarkı sözleri konumu</string>\n    <string name=\"left\">Sol</string>\n    <string name=\"center\">Orta</string>\n    <string name=\"right\">Sağ</string>\n    <string name=\"content\">İçerik</string>\n    <string name=\"login\">Oturum aç</string>\n    <string name=\"content_language\">Varsayılan içerik dili</string>\n    <string name=\"content_country\">Varsayılan ülke içeriği</string>\n    <string name=\"system_default\">Sistem varsayılanı</string>\n    <string name=\"enable_proxy\">Proxy\\'yi etkinleştir</string>\n    <string name=\"proxy_type\">Proxy türü</string>\n    <string name=\"proxy_url\">Proxy URL\\'si</string>\n    <string name=\"restart_to_take_effect\">Etkinleştirmek için yeniden başlatın</string>\n    <string name=\"player_and_audio\">Müzik çalar ve ses</string>\n    <string name=\"audio_quality\">Ses kalitesi</string>\n    <string name=\"audio_quality_auto\">Otomatik</string>\n    <string name=\"audio_quality_high\">Yüksek</string>\n    <string name=\"audio_quality_low\">Düşük</string>\n    <string name=\"persistent_queue\">Kalıcı şarkı sırası</string>\n    <string name=\"skip_silence\">Sessizliği atla</string>\n    <string name=\"audio_normalization\">Ses normalleştirme</string>\n    <string name=\"equalizer\">Ekolayzer</string>\n    <string name=\"storage\">Depolama</string>\n    <string name=\"cache\">Önbellek</string>\n    <string name=\"image_cache\">Görüntü Önbelleği</string>\n    <string name=\"song_cache\">Şarkı Önbelleği</string>\n    <string name=\"max_cache_size\">Maksimum önbellek boyutu</string>\n    <string name=\"unlimited\">Sınırsız</string>\n    <string name=\"clear_all_downloads\">Tüm indirilenleri temizle</string>\n    <string name=\"max_image_cache_size\">Maksimum görüntü önbellek boyutu</string>\n    <string name=\"clear_image_cache\">Görüntü önbelleğini temizle</string>\n    <string name=\"max_song_cache_size\">Maksimum şarkı önbellek boyutu</string>\n    <string name=\"clear_song_cache\">Şarkı önbelleğini temizle</string>\n    <string name=\"size_used\">%s kullanıldı</string>\n    <string name=\"privacy\">Gizlilik</string>\n    <string name=\"pause_listen_history\">Dinleme geçmişini duraklat</string>\n    <string name=\"clear_listen_history\">Dinleme geçmişini temizle</string>\n    <string name=\"clear_listen_history_confirm\">Tüm dinleme geçmişini temizlemekten emin misiniz?</string>\n    <string name=\"pause_search_history\">Arama geçmişini duraklat</string>\n    <string name=\"clear_search_history\">Arama geçmişini temizle</string>\n    <string name=\"clear_search_history_confirm\">Tüm arama geçmişini temizlemekten emin misiniz?</string>\n    <string name=\"enable_lrclib\">LrcLib şarkı sözü sağlayıcısını etkinleştir</string>\n    <string name=\"enable_kugou\">KuGou şarkı sözü sağlayıcısını etkinleştir</string>\n    <string name=\"backup_restore\">Yedekleme ve geri yükleme</string>\n    <string name=\"action_backup\">Yedekle</string>\n    <string name=\"action_restore\">Geri yükle</string>\n    <string name=\"imported_playlist\">Çalma listesi içe aktarıldı</string>\n    <string name=\"backup_create_success\">Yedekleme başarıyla oluşturuldu</string>\n    <string name=\"backup_create_failed\">Yedekleme oluşturulamadı</string>\n    <string name=\"restore_failed\">Yedekleme geri yüklenemedi</string>\n    <string name=\"about\">Hakkında</string>\n    <string name=\"app_version\">Uygulama sürümü</string>\n    <string name=\"new_version_available\">Yeni sürüm mevcut</string>\n    <string name=\"translation_models\">Çeviri Modelleri</string>\n    <string name=\"clear_translation_models\">Çeviri modellerini temizle</string>\n    <string name=\"big\">Büyük</string>\n    <string name=\"options\">Seçenekler</string>\n    <string name=\"dismiss\">İptal</string>\n    <string name=\"auto_load_more\">Daha fazla şarkıyı otomatik ekle</string>\n    <string name=\"preview\">Ön izleme</string>\n    <string name=\"theme\">Tema</string>\n    <string name=\"login_failed\">Oturum açma başarısız</string>\n    <string name=\"disable_screenshot_desc\">Bu özellik etkinleştirildiğinde ekran görüntüleri ve son kullanılanlardaki uygulamalar görünümü devre dışı bırakılır.</string>\n    <string name=\"disable_screenshot\">Ekran görüntüsünü devre dışı bırak</string>\n    <string name=\"remove_from_queue\">Sıradan çıkar</string>\n    <string name=\"auto_load_more_desc\">Sıranın sonuna gelindiğinde mümkünse otomatik olarak daha fazla şarkı ekler</string>\n    <string name=\"remove_all_from_library\">Hepsini kütüphaneden kaldır</string>\n    <string name=\"player\">Oynatıcı</string>\n    <string name=\"duplicates\">Yinelenenler</string>\n    <string name=\"duplicates_description_single\">Şarkı zaten çalma listenizde var</string>\n    <string name=\"your_youtube_playlists\">YouTube çalma listeleriniz</string>\n    <string name=\"remove_from_playlist\">Çalma listesinden kaldır</string>\n    <string name=\"player_text_alignment\">Oynatıcı metin hizalaması</string>\n    <string name=\"player_slider_style\">Oynatıcı kaydırma biçimi</string>\n    <string name=\"queue\">Sıra</string>\n    <string name=\"persistent_queue_desc\">Uygulama başladığında son şarkı sırasını geri getirir</string>\n    <string name=\"auto_skip_next_on_error\">Hata oluştuğunda sonraki şarkıya otomatik atla</string>\n    <string name=\"listen_history\">Dinleme geçmişi</string>\n    <string name=\"forgotten_favorites\">Unutulan favoriler</string>\n    <string name=\"keep_listening\">Dinlemeye devam edin</string>\n    <string name=\"similar_to\">Benzerleri</string>\n    <string name=\"library_song_empty\">Kütüphane şarkıları burada görünecektir</string>\n    <string name=\"library_artist_empty\">Kütüphane sanatçıları burada görünecektir</string>\n    <string name=\"library_album_empty\">Kütüphane albümleri burada görünecektir</string>\n    <string name=\"library_playlist_empty\">Çalma listeleriniz burada görünecektir</string>\n    <string name=\"other_versions\">Diğer versiyonlar</string>\n    <string name=\"remove_download_playlist_confirm\">İndirilen şarkılar deposundaki \\\"%s\\\" şarkının hepsini silmek istiyor musunuz?</string>\n    <string name=\"delete_playlist_confirm\">\\\"%s\\\" çalma listesini silmek istiyor musunuz?</string>\n    <string name=\"add_all_to_library\">Hepsini kütüphaneye ekle</string>\n    <string name=\"tempo_and_pitch\">Tempo ve Perde</string>\n    <string name=\"skip_duplicates\">Yinelemeleri atla</string>\n    <string name=\"add_anyway\">Yine de ekle</string>\n    <string name=\"duplicates_description_multiple\">%d şarkı zaten çalma listenizde</string>\n    <string name=\"action_like_all\">Hepsini beğen</string>\n    <string name=\"action_remove_like_all\">Tüm beğenileri kaldır</string>\n    <string name=\"sided\">Kenarlı</string>\n    <string name=\"default_\">Varsayılan</string>\n    <string name=\"squiggly\">Dalgalı</string>\n    <string name=\"misc\">Çeşitli</string>\n    <string name=\"grid_cell_size\">Izgara boyutu</string>\n    <string name=\"small\">Küçük</string>\n    <string name=\"not_logged_in\">Oturum açılmadı</string>\n    <string name=\"auto_skip_next_on_error_desc\">Kesintisiz dinleme deneyimi sağlar</string>\n    <string name=\"stop_music_on_task_clear\">Arka plandan temizlendiğinde çalmayı durdur</string>\n    <string name=\"search_history\">Arama geçmişi</string>\n    <string name=\"hide_explicit\">Uygunsuz içeriği gizle</string>\n    <string name=\"discord_integration\">Discord Entegrasyonu</string>\n    <string name=\"discord_information\">Metrolist, Discord hesabınızın durumunu ayarlamak için KizzyRPC kütüphanesini kullanır. Bu, Discord\\'un Hizmet Şartları\\'nın ihlali olarak kabul edilebilecek Discord Geçit bağlantısının kullanılmasını gerektirir. Fakat kullanıcı hesaplarının bu nedenle askıya alındığı bilinen bir örnek yoktur. Riski size ait olmak üzere kullanın. \\n \\nMetrolist yalnızca belirtecinizi çıkarır ve diğer her şey yerel olarak saklanır.</string>\n    <string name=\"action_logout\">Oturumu kapat</string>\n    <string name=\"enable_discord_rpc\">Rich Presence\\'ı etkinleştir</string>\n    <string name=\"use_login_for_browse\">İçerikleri görmek için Giriş yap\\'ı kullanın</string>\n    <string name=\"use_login_for_browse_desc\">Bu gördüğünüz içerikleri etkileyebilir ve örnek olarak Premium\\'a özel içerikleri Premium\\'a sahip bir hesapla giriş yaptıysanız size sunabilir</string>\n    <string name=\"action_login\">Giriş Yap</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-uk/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">Локально</string>\n    <string name=\"remote_history\">Віддалено</string>\n    <string name=\"weeks\">Тижні</string>\n    <string name=\"months\">Місяці</string>\n    <string name=\"years\">Роки</string>\n    <string name=\"continuous\">Безперервно</string>\n    <string name=\"liked\">Вподобане</string>\n    <string name=\"offline\">Завантажене</string>\n    <string name=\"my_top\">Мій Топ</string>\n    <string name=\"sync_playlist\">Синхронізувати список відтворення</string>\n    <string name=\"allows_for_sync_witch_youtube\">Зауважте: Це дозволяє синхронізацію з YouTube Music. Це НЕ можна буде змінити пізніше.</string>\n    <string name=\"select\">Обрати все</string>\n    <string name=\"like_all\">Вподобати все</string>\n    <string name=\"dislike_all\">Позначити все як не вподобане</string>\n    <string name=\"sort_by_last_updated\">Нещодавно оновлені</string>\n    <string name=\"lyrics\">Текст</string>\n    <string name=\"already_in_playlist\">Вже в списку відтворення:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"one\">%d раз</item>\n        <item quantity=\"few\">%d рази</item>\n        <item quantity=\"many\">%d разів</item>\n        <item quantity=\"other\">%d разів</item>\n    </plurals>\n    <string name=\"similar_content\">Подібний контент</string>\n    <string name=\"player_background_style\">Стиль фону програвача</string>\n    <string name=\"follow_theme\">Дотримуватись теми</string>\n    <string name=\"gradient\">Градієнт</string>\n    <string name=\"player_background_blur\">Розмиття</string>\n    <string name=\"enable_swipe_thumbnail\">Перемикання пісень проведенням по обкладинці</string>\n    <string name=\"lyrics_click_change\">Змінити текст пісні при натисканні</string>\n    <string name=\"slim_navbar\">Тонка нижня панель навігації</string>\n    <string name=\"advanced_login\">Вхід через токен</string>\n    <string name=\"token_hidden\">Натисніть для показу токену</string>\n    <string name=\"token_shown\">Повторно натисніть, щоб скопіювати або редагувати</string>\n    <string name=\"token_adv_login_description\">Це ПРОСУНУТИЙ метод входу в систему. Альтернативно до веб-порталу, ви можете безпосередньо ввести або оновити свій токен для входу тут. Наприклад, це може прискорити вхід на декількох пристроях. Зверніть увагу, що будь-які недійсні формати токенів, які додаток не зможе розпізнати, не будуть прийняті</string>\n    <string name=\"general\">Загальне</string>\n    <string name=\"default_lib_chips\">Змінити головну сторінку бібліотеки</string>\n    <string name=\"set_quick_picks\">Налаштувати швидкі добірки</string>\n    <string name=\"last_song_listened\">На основі останньої прослуханої пісні</string>\n    <string name=\"app_language\">Мова застосунку</string>\n    <string name=\"enable_similar_content\">Увімкнути подібний контент</string>\n    <string name=\"similar_content_desc\">Автоматично додавати більше схожих пісень, коли буде досягнуто кінця черги</string>\n    <string name=\"default_links\">Відкрити підтримувані посилання</string>\n    <string name=\"open_app_settings_error\">Не вдалося відкрити налаштування застосунку</string>\n    <string name=\"release_notes\">Примітки релізу</string>\n    <string name=\"all_time\">Весь час</string>\n    <string name=\"past_24_hours\">Минулі 24 години</string>\n    <string name=\"past_week\">Минулий тиждень</string>\n    <string name=\"past_month\">Минулий місяць</string>\n    <string name=\"past_year\">Минулий рік</string>\n    <string name=\"top_length\">Довжина мого топ-списку</string>\n    <string name=\"history_duration\">Довжина історії</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"one\">%d секунда</item>\n        <item quantity=\"few\">%d секунди</item>\n        <item quantity=\"many\">%d cекунд</item>\n        <item quantity=\"other\">%d cекунд</item>\n    </plurals>\n    <string name=\"charts\">Чарти</string>\n    <string name=\"back_button_desc\">Назад</string>\n    <string name=\"album_cover_desc\">Обкладинка альбому</string>\n    <string name=\"top_music_videos\">Найкращі музичні відео</string>\n    <string name=\"trending\">Тренди</string>\n    <string name=\"cached_playlist\">Кешовано</string>\n    <string name=\"generating_image\">Генерація зображення</string>\n    <string name=\"sync_disabled\">Синхронізація вимкнена</string>\n    <string name=\"please_wait\">Зачекайте, будь ласка</string>\n    <string name=\"cancel\">Скасувати</string>\n    <string name=\"share_lyrics\">Поділитися текстом пісні</string>\n    <string name=\"share_as_text\">Поділитися у вигляді тексту</string>\n    <string name=\"share_as_image\">Поділитися у вигляді зображення</string>\n    <string name=\"max_selection_limit\">Максимальний ліміт вибору</string>\n    <string name=\"share_selected\">Поділитися вибраним</string>\n    <string name=\"customize_colors\">Налаштувати кольори</string>\n    <string name=\"text_color\">Колір тексту</string>\n    <string name=\"secondary_text_color\">Вторинний колір тексту</string>\n    <string name=\"background_color\">Колір тла</string>\n    <string name=\"remove_from_cache\">Видалити з кешу</string>\n    <string name=\"copy_link\">Скопіювати посилання</string>\n    <string name=\"link_copied\">Посилання скопійовано в буфер обміну</string>\n    <string name=\"player_buttons_style\">Кольори кнопок програвача</string>\n    <string name=\"default_style\">За замовчуванням</string>\n    <string name=\"show_downloaded_playlist\">Показати список відтворення \\\"Завантажене\\\"</string>\n    <string name=\"swipe_song_to_add\">Проведіть пісню вліво, щоб додати її до черги, або вправо — щоб відтворити наступною</string>\n    <string name=\"slim\">Тонкий</string>\n    <string name=\"auto_playlists\">Автоматичні списки відтворення</string>\n    <string name=\"show_top_playlist\">Показати список відтворення \\\"Топ\\\"</string>\n    <string name=\"show_cached_playlist\">Показати кешований список відтворення</string>\n    <string name=\"show_liked_playlist\">Показати список відтворення \\\"Вподобане\\\"</string>\n    <string name=\"proxy\">Проксі</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"auto_download_on_like\">Автоматично завантажувати вподобані пісні</string>\n    <string name=\"auto_download_on_like_desc\">Автоматично завантажувати пісні, які ви вподобали</string>\n    <string name=\"clear_song_cache_dialog\">Ви впевнені, що хочете очистити всі кешовані пісні?</string>\n    <string name=\"clear_downloads_dialog\">Ви впевнені, що хочете очистити всі завантаження?</string>\n    <string name=\"not_logged_in_youtube\">Ви не ввійшли в YouTube</string>\n    <string name=\"information\">Інформація</string>\n    <string name=\"description\">Опис</string>\n    <string name=\"views\">Перегляди</string>\n    <string name=\"likes\">Вподобання</string>\n    <string name=\"dislikes\">Невподобання</string>\n    <string name=\"lyrics_auto_scroll\">Автопрокрутка тексту пісні</string>\n    <string name=\"import_online\">Імпортувати список відтворення у форматі \\\"m3u\\\"</string>\n    <string name=\"import_csv\">Імпортувати список відтворення у форматі \\\"csv\\\"</string>\n    <string name=\"playlist_add_local_to_synced_note\">Примітка: додавання локальних пісень до синхронізованих або віддалених списків відтворення не підтримується. Усі інші комбінації можливі</string>\n    <string name=\"new_player_design\">Новий дизайн програвача</string>\n    <string name=\"lyrics_romanize_japanese\">Романізувати Японські тексти</string>\n    <string name=\"lyrics_romanize_korean\">Романізувати Корейські тексти</string>\n    <string name=\"yt_sync\">Автосинхронізація з акаунтом</string>\n    <string name=\"more_content\">Більше контенту</string>\n    <string name=\"swipe_sensitivity\">Чутливість проведення по мініпрогравачу</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"clear_image_cache_dialog\">Ви дійсно хочете очистити всі кешовані зображення?</string>\n    <string name=\"disable\">Вимкнути</string>\n    <string name=\"subscribe\">Підписатися</string>\n    <string name=\"subscribed\">Підписані</string>\n    <string name=\"new_mini_player_design\">Новий дизайн мініпрогравача</string>\n    <string name=\"now_playing\">Зараз грає</string>\n    <string name=\"seek_forward_dynamic\">+%1$d сек. вперед</string>\n    <string name=\"seek_backward_dynamic\">-%1$d сек. назад</string>\n    <string name=\"seek_seconds_addup\">Прогресивний пошук</string>\n    <string name=\"seek_seconds_addup_description\">Якщо ввімкнено, додає 5 додаткових секунд при кожному пропусканні пошуку</string>\n    <string name=\"close\">Закрити</string>\n    <string name=\"disable_load_more_when_repeat_all\">Вимкнути завантаження додаткових параметрів під час повторення всіх</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Не завантажувати автоматично більше пісень та подібного контенту, коли ввімкнено режим повторення всіх</string>\n    <string name=\"hide_player_thumbnail\">Приховати мініатюру програвача</string>\n    <string name=\"hide_player_thumbnail_desc\">Замінити обкладинку альбому логотипом додатка в програвачі</string>\n    <string name=\"settings_section_ui\">Інтерфейс</string>\n    <string name=\"settings_section_privacy\">Конфіденційність та безпека</string>\n    <string name=\"settings_section_player_content\">Плеєр і контент</string>\n    <string name=\"settings_section_storage\">Зберігання та дані</string>\n    <string name=\"settings_section_system\">Система та про нас</string>\n    <string name=\"starting_radio\">Запуск радіо</string>\n    <string name=\"config_proxy\">Налаштувати проксі-сервер</string>\n    <string name=\"proxy_username\">Ім\\'я користувача проксі-сервера</string>\n    <string name=\"proxy_password\">Пароль проксі-сервера</string>\n    <string name=\"enable_authentication\">Увімкнути автентифікацію</string>\n    <string name=\"lyrics_romanization_cyrillic\">Кирилица</string>\n    <string name=\"lyrics_romanize_title\">Романізація</string>\n    <string name=\"lyrics_romanization\">Романізація текстів пісень</string>\n    <string name=\"lyrics_romanize_russian\">Романізувати Російські тексти</string>\n    <string name=\"lyrics_romanize_ukrainian\">Романізувати Українську лірику</string>\n    <string name=\"lyrics_romanize_belarusian\">Романізувати Білоруські тексти пісень</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Романізувати Киргизькі тексти пісень</string>\n    <string name=\"lyrics_romanize_serbian\">Романізувати Сербські тексти пісень</string>\n    <string name=\"lyrics_romanize_bulgarian\">Романізувати Болгарські тексти пісень</string>\n    <string name=\"line_by_line_option_title\">ЕКСПЕРИМЕНТАЛЬНА ВЕРСІЯ: Визначення мови рядок за рядком</string>\n    <string name=\"line_by_line_option_desc\">Кирилична мова буде розпізнаватися рядок за рядком, а не по всій пісні.</string>\n    <string name=\"line_by_line_dialog_title\">Ви впевнені?</string>\n    <string name=\"line_by_line_dialog_desc\">Це експериментальна функція, яка може працювати як з успіхом, так і з невдачею. \\n\\nЗа замовчуванням мова визначається на основі всієї пісні, але якщо ввімкнути цю опцію, вона буде визначатися по рядках. Це дозволить працювати з багатомовними піснями, АЛЕ мова може бути не завжди правильною (наприклад, якщо в українському тексті немає літер, характерних для української мови, він може бути транслітерований як російський). \\n\\nЯкщо у вас немає проблем, рекомендується залишити цю опцію вимкненою.</string>\n    <string name=\"romanize_current_track\">Романізувати поточний трек</string>\n    <string name=\"edit_playlist_cover\">Редагувати обкладинку списку відтворення</string>\n    <string name=\"edit_playlist_cover_note\">Примітка: Щоб змінити обкладинку списку відтворення, ваш обліковий запис має бути пов’язаний з номером телефону та підтверджений у YouTube Music.</string>\n    <string name=\"edit_playlist_cover_note_wait\">Після вибору зображення зачекайте трохи, поки нова обкладинка з’явиться у вашому списку відтворення.</string>\n    <string name=\"audio_offload\">Увімкнути розвантаження</string>\n    <string name=\"audio_offload_description\">Використовуйте шлях розвантаження аудіо для відтворення аудіо. Вимкнення цієї функції може збільшити споживання енергії, але може бути корисним, якщо у вас виникнуть проблеми з відтворенням аудіо або постобробкою</string>\n    <string name=\"choose_from_library\">Виберіть з бібліотеки</string>\n    <string name=\"remove_custom_image\">Вилучити власне зображення</string>\n    <string name=\"uploaded_playlist\">Завантажено</string>\n    <string name=\"filter_uploaded\">Завантаженні</string>\n    <string name=\"show_uploaded_playlist\">Показати список відтворення \\\"Завантажено\\\"</string>\n    <string name=\"updater\">Оновлення</string>\n    <string name=\"check_for_updates\">Автоматично перевіряти наявність оновлень</string>\n    <string name=\"lyrics_romanize_macedonian\">Романізувати македонську лірику</string>\n    <string name=\"update_notifications\">Увімкнути сповіщення про оновлення</string>\n    <string name=\"update_available_title\">Доступне оновлення</string>\n    <string name=\"update_channel_name\">Оновлення програм</string>\n    <string name=\"update_channel_desc\">Сповіщення про нові версії</string>\n    <string name=\"discord_use_details\">Використовуйте деталі замість штату</string>\n    <string name=\"discord_use_details_description\">Помітно показувати назву пісні замість імен виконавців</string>\n    <string name=\"integrations\">Інтеграції</string>\n    <string name=\"username\">Ім\\'я користувача</string>\n    <string name=\"password\">Пароль</string>\n    <string name=\"lastfm_integration\">Інтеграція з Last.fm</string>\n    <string name=\"enable_scrobbling\">Увімкнути скробблінг</string>\n    <string name=\"lastfm_now_playing\">Надіслати «Зараз відтворюється»</string>\n    <string name=\"scrobbling_configuration\">Конфігурація скроблювання</string>\n    <string name=\"scrobble_min_track_duration\">Пісні Scrobble довші за</string>\n    <string name=\"scrobble_delay_percent\">Відсоток затримки Scrobble</string>\n    <string name=\"scrobble_delay_minutes\">Затримка Scrobble у хвилинах</string>\n    <string name=\"swipe_song_to_remove\">Проведіть пальцем по пісні, щоб вилучити її зі списку відтворення</string>\n    <string name=\"last_fm_send_likes\">Надіслати вподобання/не вподобання</string>\n    <string name=\"last_fm_send_likes_description\">Пісні, які подобається/не подобається, на Last.fm, коли вони вподобані/зняті з Metrolist</string>\n    <string name=\"lyrics_romanize_chinese\">Романізація китайських текстів</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"google_cast_description\">Увімкнути трансляцію аудіо на Chromecast та інші пристрої з підтримкою Cast</string>\n    <string name=\"hide_video_songs\">Приховати відеопісні</string>\n    <string name=\"primary_color_style\">Основний колір</string>\n    <string name=\"auto_scroll\">Повторна синхронізація</string>\n    <string name=\"details_desc\">Переглянути інформацію про пісню</string>\n    <string name=\"edit_desc\">Змінити назву або виконавця</string>\n    <string name=\"start_radio_desc\">Створити станцію на основі цього елемента</string>\n    <string name=\"play_next_desc\">Додати на початок черги</string>\n    <string name=\"add_to_queue_desc\">Додати в кінець черги</string>\n    <string name=\"add_to_library_desc\">Зберегти у своїй бібліотеці</string>\n    <string name=\"download_desc\">Зробити доступним для відтворення офлайн</string>\n    <string name=\"add_to_playlist_desc\">Додати до одного зі своїх плейлистів</string>\n    <string name=\"refetch_desc\">Отримати найновіші метадані з YouTube Music</string>\n    <string name=\"share_desc\">Поділитися посиланням на цей елемент</string>\n    <string name=\"delete_desc\">Вилучити цей елемент назавжди</string>\n    <string name=\"advanced_desc\">Змінити темп і висоту тону пісні</string>\n    <string name=\"equalizer_desc\">Налаштування аудіоеквалайзера</string>\n    <string name=\"enable_dynamic_icon\">Увімкнути динамічний значок</string>\n    <string name=\"mini_player\">Міні-плеєр</string>\n    <string name=\"pure_black_mini_player\">Чисто чорний міні-плеєр</string>\n    <string name=\"cache_size_warning_title\">Тримайся!</string>\n    <string name=\"cache_size_warning_message\">Ви вибрали обмеження розміру кешу, менше за те, що зараз використовує програма (%1$s). Якщо ви продовжите, програма може видалити деякі кешовані %2$s, щоб вони відповідали новому ліміту. Продовжити все одно?</string>\n    <string name=\"cache_size_warning_confirm\">Продовжити</string>\n    <string name=\"tertiary_color_style\">Третинний колір</string>\n    <string name=\"logging_in\">Вхід до…</string>\n    <string name=\"download_playlist_desc\">Завантажте всі пісні для відтворення офлайн</string>\n    <string name=\"remove_download_playlist_desc\">Видалити всі завантажені пісні з цього списку відтворення</string>\n    <string name=\"download_in_progress_desc\">Триває завантаження</string>\n    <string name=\"share_playlist_desc\">Поділіться цим плейлистом з іншими</string>\n    <string name=\"delete_playlist_desc\">Видалити цей плейлист назавжди</string>\n    <string name=\"sync_playlist_desc\">Синхронізуйте плейлист з YouTube Music</string>\n    <string name=\"enable_better_lyrics\">Увімкнути кращі тексти пісень</string>\n    <string name=\"enable_better_lyrics_desc\">Використовуйте постачальника Better Lyrics для синхронізованих текстів пісень слово за словом</string>\n    <string name=\"lyrics_animation_style\">Стиль анімації слово за словом</string>\n    <string name=\"none\">Жоден</string>\n    <string name=\"fade\">Згасання</string>\n    <string name=\"glow\">Сяйво</string>\n    <string name=\"slide\">Слайд</string>\n    <string name=\"karaoke\">Караоке</string>\n    <string name=\"apple_music_style\">Apple Music</string>\n    <string name=\"lyrics_text_size\">Розмір тексту пісні</string>\n    <string name=\"lyrics_line_spacing\">Міжрядковий інтервал тексту пісень</string>\n    <string name=\"shuffle_playlist_first\">Спочатку перемішати відтворення відтворення/альбому</string>\n    <string name=\"shuffle_playlist_first_desc\">Під час перемішування спочатку відтворювати всі пісні з оригінального списку відтворення/альбому, а потім схожий контент</string>\n    <string name=\"lyrics_glow_effect\">Увімкнути ефект сяйва тексту пісень</string>\n    <string name=\"lyrics_glow_effect_desc\">Додайте анімацію сяйва та ефект відскоку до активних текстів пісень</string>\n    <string name=\"show_wrapped_card\">Показати обгорнуту картку</string>\n    <string name=\"album_art_for\">Обкладинка альбому для %s</string>\n    <string name=\"wrapped_total_albums_title\">Ви слухали</string>\n    <string name=\"wrapped_total_albums_subtitle\">унікальні альбоми</string>\n    <string name=\"wrapped_top_album_title\">Ваш найкращий альбом –</string>\n    <string name=\"wrapped_playlist_ready\">Ваш особистий плейлист готовий</string>\n    <string name=\"wrapped_top_5_albums_title\">Ваші 5 найкращих альбомів</string>\n    <string name=\"wrapped_album_listening_time\">Ви слухали цей альбом протягом %d хвилин</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d хвилин</string>\n    <string name=\"wrapped_no_data\">Немає даних</string>\n    <string name=\"wrapped_top_5_artists_title\">Ваші найкращі артисти року</string>\n    <string name=\"wrapped_artist_listening_time\">%d хвилин</string>\n    <string name=\"wrapped_top_5_songs_title\">Ваші найкращі пісні року</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">Обкладинка альбому</string>\n    <string name=\"wrapped_top_artist_title\">Ваш найкращий артист року –</string>\n    <string name=\"wrapped_top_artist_image_content_description\">Найкраще зображення виконавця</string>\n    <string name=\"wrapped_top_artist_listening_time\">Ви слухали їх протягом %d хвилин</string>\n    <string name=\"wrapped_top_song_title\">Ваша найпопулярніша пісня —</string>\n    <string name=\"wrapped_top_song_listening_time\">Ви слухали %d хвилин</string>\n    <string name=\"wrapped_total_artists_title\">Ти слухав/слухала</string>\n    <string name=\"wrapped_total_artists_subtitle\">унікальні митці</string>\n    <string name=\"wrapped_total_songs_title\">Ти слухав/слухала</string>\n    <string name=\"wrapped_total_songs_subtitle\">унікальні пісні</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_intro_subtitle\">час подивитися, що ви слухали</string>\n    <string name=\"wrapped_intro_button\">Ходімо!</string>\n    <string name=\"wrapped_logo_content_description\">Логотип Metrolist</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"wrapped_ready_title\">ВАШ АУДІОПІДСУМОК ЗА РІК ГОТОВИЙ!</string>\n    <string name=\"wrapped_ready_subtitle\">Час подивитися, що вам сподобалося цього року.</string>\n    <string name=\"wrapped_thank_you\">Дякую, що вислухали</string>\n    <string name=\"wrapped_special_thanks\">Особлива подяка MO Agamy за створення Metrolist</string>\n    <string name=\"wrapped_close\">Закрити обгорнуте</string>\n    <string name=\"wrapped_playlist_title\">Ваш %s загорнутий</string>\n    <string name=\"wrapped_create_playlist\">Створити плейлист</string>\n    <string name=\"wrapped_playlist_saved\">Список відтворення збережено</string>\n    <string name=\"casting_to\">Трансляція на %s</string>\n    <string name=\"progress_percent\">Прогрес %s%%</string>\n    <string name=\"listening_to_metrolist\">Слухаючи Metrolist</string>\n    <string name=\"open\">Відкрити</string>\n    <string name=\"failed_to_create_image\">Не вдалося створити зображення: %s</string>\n    <string name=\"copied_title\">Скопійована назва</string>\n    <string name=\"copied_artist\">Скопійований виконавець</string>\n    <string name=\"error_playing\">Помилка відтворення</string>\n    <string name=\"failed_to_parse_proxy\">Не вдалося проаналізувати URL-адресу проксі-сервера.</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"one\">%d профіль</item>\n        <item quantity=\"few\">%d профіли</item>\n        <item quantity=\"many\">%d профілів</item>\n        <item quantity=\"other\">%d профілів</item>\n    </plurals>\n    <string name=\"equalizer_header\">Еквалайзер</string>\n    <string name=\"no_profiles\">Немає профілів еквалайзера</string>\n    <string name=\"import_profile\">Імпортувати профіль</string>\n    <string name=\"eq_disabled\">Вимкнено</string>\n    <plurals name=\"band_count\">\n        <item quantity=\"one\">%d діапазон</item>\n        <item quantity=\"few\">%d діапазони</item>\n        <item quantity=\"many\">%d діапазонів</item>\n        <item quantity=\"other\">%d діапазонів</item>\n    </plurals>\n    <string name=\"delete_profile_desc\">Видалити профіль</string>\n    <string name=\"delete_profile_confirmation\">Ви впевнені, що хочете видалити %1$s? Цю дію не можна скасувати.</string>\n    <string name=\"error_file_read\">Не вдалося прочитати файл</string>\n    <string name=\"error_file_open\">Не вдалося відкрити файл: %1$s</string>\n    <string name=\"import_error_title\">Помилка імпорту</string>\n    <string name=\"wavy\">Хвилястий</string>\n    <string name=\"pause_music_when_media_is_muted\">Призупиняти музику, коли медіа вимкнено</string>\n    <string name=\"enable_simpmusic\">Увімкнути текст пісні SimpMusic</string>\n    <string name=\"enable_simpmusic_desc\">Використовуйте постачальника текстів пісень SimpMusic для синхронізованих текстів пісень</string>\n    <string name=\"system_equalizer\">Системний еквалайзер</string>\n    <string name=\"album_art\">Обкладинка альбому</string>\n    <string name=\"no_song_playing\">Пісня не відтворюється</string>\n    <string name=\"tap_to_open\">Натисніть, щоб відкрити Metrolist</string>\n    <string name=\"previous\">Попередній</string>\n    <string name=\"play_pause\">Відтворення/Пауза</string>\n    <string name=\"next\">Далі</string>\n    <string name=\"like\">Подобається</string>\n    <string name=\"widget_description\">Віджет музичного плеєра з елементами керування відтворенням</string>\n    <string name=\"turntable_widget_description\">Швидкий доступ до останньої відтвореної композиції</string>\n    <string name=\"remember_shuffle_and_repeat\">Пам’ятайте про перемішування та повторення</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Запам\\'ятайте режим випадкового відтворення та повтору під час перезапуску програми</string>\n    <string name=\"lyrics_offset\">Текст пісні Offset</string>\n    <string name=\"about_artist\">Про нас</string>\n    <string name=\"show_more\">Показати більше</string>\n    <string name=\"show_less\">Показати менше</string>\n    <string name=\"artist_page_settings\">Сторінка виконавця</string>\n    <string name=\"show_artist_description\">Показати опис виконавця</string>\n    <string name=\"show_artist_subscriber_count\">Показати кількість підписників</string>\n    <string name=\"show_artist_monthly_listeners\">Показати щомісячні слухачі</string>\n    <string name=\"skip_silence_desc\">Перемотування вперед тихих частин пісень</string>\n    <string name=\"skip_silence_instant\">Миттєво пропустити тишу</string>\n    <string name=\"skip_silence_instant_desc\">Перемотуйте вперед під час тихих моментів замість прискорення відтворення</string>\n    <string name=\"persistent_shuffle_title\">Постійне перемішування</string>\n    <string name=\"persistent_shuffle_desc\">Увімкніть випадкове перемішування під час запуску нових пісень або списків відтворення</string>\n    <string name=\"error_playback_failed\">Помилка відтворення</string>\n    <string name=\"error_title\">Помилка</string>\n    <string name=\"error_eq_apply_failed\">Не вдалося застосувати профіль еквалайзера: %1$s</string>\n    <string name=\"crop_album_art\">Обрізати обкладинку альбому</string>\n    <string name=\"crop_album_art_desc\">Примусове квадратне співвідношення сторін шляхом обрізання мініатюр відео</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">Залишати екран увімкненим, коли програвач розгорнуто</string>\n    <string name=\"enable\">Увімкнути</string>\n    <string name=\"player_background_solid\">Суцільний</string>\n    <string name=\"display_density\">Щільність відображення</string>\n    <string name=\"restart\">Перезапустити</string>\n    <string name=\"density_restart_message\">Зміна щільності відображення почне діяти після перезапуску програми. Ви хочете перезапустити зараз?</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-uk/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <string name=\"home\">Головна</string>\n    <string name=\"songs\">Музика</string>\n    <string name=\"artists\">Виконавці</string>\n    <string name=\"albums\">Альбоми</string>\n    <string name=\"playlists\">Плейлисти</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"one\">%d вибрано</item>\n        <item quantity=\"few\">%d вибране</item>\n        <item quantity=\"many\">%d вибрані</item>\n        <item quantity=\"other\">%d вибрані</item>\n    </plurals>\n    <string name=\"history\">Історія</string>\n    <string name=\"stats\">Статистика</string>\n    <string name=\"mood_and_genres\">Настрій та жанри</string>\n    <string name=\"account\">Акаунт</string>\n    <string name=\"quick_picks\">Швидкий вибір</string>\n    <string name=\"quick_picks_empty\">Послухайте кілька пісень, щоб створити ваш швидкий вибір</string>\n    <string name=\"forgotten_favorites\">Забуті улюблені</string>\n    <string name=\"keep_listening\">Продовжуйте слухати</string>\n    <string name=\"your_youtube_playlists\">Ваші плейлисти YouTube</string>\n    <string name=\"similar_to\">Схожі на</string>\n    <string name=\"new_release_albums\">Нові релізи альбомів</string>\n    <string name=\"today\">Сьогодні</string>\n    <string name=\"yesterday\">Вчора</string>\n    <string name=\"this_week\">Цього тижня</string>\n    <string name=\"last_week\">Минулого тижня</string>\n    <string name=\"most_played_songs\">Найпопулярніші пісні</string>\n    <string name=\"most_played_artists\">Найпопулярніші виконавці</string>\n    <string name=\"most_played_albums\">Найпопулярніші альбоми</string>\n    <string name=\"search\">Пошук</string>\n    <string name=\"search_yt_music\">Пошук в YouTube Music…</string>\n    <string name=\"search_library\">Пошук в бібліотеці…</string>\n    <string name=\"filter_library\">Бібліотека</string>\n    <string name=\"filter_liked\">Вподобані</string>\n    <string name=\"filter_downloaded\">Завантажені</string>\n    <string name=\"filter_all\">Всі</string>\n    <string name=\"filter_songs\">Композиції</string>\n    <string name=\"filter_videos\">Відео</string>\n    <string name=\"filter_albums\">Альбоми</string>\n    <string name=\"filter_artists\">Виконавці</string>\n    <string name=\"filter_playlists\">Плейлисти</string>\n    <string name=\"filter_community_playlists\">Плейлисти спільноти</string>\n    <string name=\"filter_featured_playlists\">Обрані плейлисти</string>\n    <string name=\"filter_bookmarked\">Додано в закладки</string>\n    <string name=\"no_results_found\">Результатів не знайдено</string>\n    <string name=\"library_song_empty\">Тут будуть відображатися пісні з вашої бібліотеки</string>\n    <string name=\"library_artist_empty\">Тут будуть відображатися виконавці з вашої бібліотеки</string>\n    <string name=\"library_album_empty\">Тут будуть відображатися альбоми з вашої бібліотеки</string>\n    <string name=\"library_playlist_empty\">Тут будуть відображатися ваші плейлисти</string>\n    <string name=\"from_your_library\">З вашої бібліотеки</string>\n    <string name=\"other_versions\">Інші версії</string>\n    <string name=\"liked_songs\">Улюблені треки</string>\n    <string name=\"downloaded_songs\">Завантажена музика</string>\n    <string name=\"playlist_is_empty\">Плейлист порожній</string>\n    <string name=\"remove_download_playlist_confirm\">Ви впевнені, що хочете видалити всі пісні з плейлиста «%s» зі сховища завантаженої музики?</string>\n    <string name=\"delete_playlist_confirm\">Ви впевнені, що хочете видалити плейлист «%s»?</string>\n    <string name=\"retry\">Повторювати</string>\n    <string name=\"radio\">Радіо</string>\n    <string name=\"shuffle\">Перемішати</string>\n    <string name=\"reset\">Скинути</string>\n    <string name=\"details\">Детальніше</string>\n    <string name=\"edit\">Редагувати</string>\n    <string name=\"start_radio\">Увімкнути радіо</string>\n    <string name=\"play\">Відтворити</string>\n    <string name=\"play_next\">Відтворити наступним</string>\n    <string name=\"add_to_queue\">Додати в чергу</string>\n    <string name=\"add_to_library\">Додати в бібліотеку</string>\n    <string name=\"add_all_to_library\">Додати все до бібліотеки</string>\n    <string name=\"remove_from_library\">Видалити з бібліотеки</string>\n    <string name=\"remove_all_from_library\">Видалити все з бібліотеки</string>\n    <string name=\"action_download\">Завантаження</string>\n    <string name=\"downloading\">Завантаження</string>\n    <string name=\"remove_download\">Видалити із завантажених</string>\n    <string name=\"import_playlist\">Імпортувати плейлист</string>\n    <string name=\"add_to_playlist\">Додати в плейлист</string>\n    <string name=\"view_artist\">Перейти до виконавця</string>\n    <string name=\"view_album\">Перейти до альбому</string>\n    <string name=\"refetch\">Оновити</string>\n    <string name=\"share\">Поділитися</string>\n    <string name=\"delete\">Видалити</string>\n    <string name=\"remove_from_history\">Видалити з історії</string>\n    <string name=\"remove_from_playlist\">Видалити з плейлиста</string>\n    <string name=\"remove_from_queue\">Видалити з черги</string>\n    <string name=\"search_online\">Пошук в Інтернеті</string>\n    <string name=\"action_sync\">Синхронізація</string>\n    <string name=\"advanced\">Контроль аудіо</string>\n    <string name=\"tempo_and_pitch\">Темп і висота тону</string>\n    <string name=\"sort_by_create_date\">Нещодавно додані</string>\n    <string name=\"sort_by_name\">Назва</string>\n    <string name=\"sort_by_artist\">Виконавець</string>\n    <string name=\"sort_by_year\">Рік</string>\n    <string name=\"sort_by_song_count\">Кількість треків</string>\n    <string name=\"sort_by_length\">Тривалість</string>\n    <string name=\"sort_by_play_time\">Кількість відтворень</string>\n    <string name=\"sort_by_custom\">Корист. порядок</string>\n    <string name=\"media_id\">Ідентифікатор медіа</string>\n    <string name=\"mime_type\">Тип MIME</string>\n    <string name=\"codecs\">Кодеки</string>\n    <string name=\"bitrate\">Бітрейт</string>\n    <string name=\"sample_rate\">Частота дискретизації</string>\n    <string name=\"loudness\">Гучність</string>\n    <string name=\"volume\">Рівень гучності</string>\n    <string name=\"file_size\">Розмір файлу</string>\n    <string name=\"unknown\">Невідомо</string>\n    <string name=\"copied\">Скопійовано</string>\n    <string name=\"edit_lyrics\">Редагувати текст пісні</string>\n    <string name=\"search_lyrics\">Пошук тексту пісні</string>\n    <string name=\"edit_song\">Редагувати композицію</string>\n    <string name=\"song_title\">Назва композиції</string>\n    <string name=\"song_artists\">Виконавці композиції</string>\n    <string name=\"error_song_title_empty\">Назва пісні не може бути порожньою.</string>\n    <string name=\"error_song_artist_empty\">Поле \\\"Виконавець пісні\\\" не може бути порожнім.</string>\n    <string name=\"save\">Зберегти</string>\n    <string name=\"choose_playlist\">Вибрати плейлист</string>\n    <string name=\"edit_playlist\">Редагувати плейлист</string>\n    <string name=\"create_playlist\">Створити плейлист</string>\n    <string name=\"playlist_name\">Назва плейлиста</string>\n    <string name=\"error_playlist_name_empty\">Назва списку відтворення не може бути порожньою.</string>\n    <string name=\"edit_artist\">Редагувати виконавця</string>\n    <string name=\"artist_name\">Ім\\'я виконавця</string>\n    <string name=\"error_artist_name_empty\">Ім\\'я виконавця не може бути порожнім.</string>\n    <string name=\"duplicates\">Дублікати</string>\n    <string name=\"skip_duplicates\">Пропустити дублікати</string>\n    <string name=\"add_anyway\">Додати в будь-якому разі</string>\n    <string name=\"duplicates_description_single\">Ця пісня вже у вашому плейлисті</string>\n    <string name=\"duplicates_description_multiple\">%d пісень вже у вашому плейлисті</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"one\">%d композиція</item>\n        <item quantity=\"few\">%d композиції</item>\n        <item quantity=\"many\">%d композицій</item>\n        <item quantity=\"other\">%d композицій</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"one\">%d виконавець</item>\n        <item quantity=\"few\">%d виконавця</item>\n        <item quantity=\"many\">%d виконавців</item>\n        <item quantity=\"other\">%d виконавців</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"one\">%d альбом</item>\n        <item quantity=\"few\">%d альбоми</item>\n        <item quantity=\"many\">%d альбомів</item>\n        <item quantity=\"other\">%d альбомів</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"one\">%d плейлист</item>\n        <item quantity=\"few\">%d плейлисти</item>\n        <item quantity=\"many\">%d плейлистів</item>\n        <item quantity=\"other\">%d плейлистів</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"one\">%d тиждень</item>\n        <item quantity=\"few\">%d тижні</item>\n        <item quantity=\"many\">%d тижнів</item>\n        <item quantity=\"other\">%d тижнів</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"one\">%d місяць</item>\n        <item quantity=\"few\">%d місяці</item>\n        <item quantity=\"many\">%d місяців</item>\n        <item quantity=\"other\">%d місяців</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"one\">%d рік</item>\n        <item quantity=\"few\">%d роки</item>\n        <item quantity=\"many\">%d років</item>\n        <item quantity=\"other\">%d років</item>\n    </plurals>\n    <string name=\"playlist_imported\">Плейлист імпортовано</string>\n    <string name=\"removed_song_from_playlist\">«%s» видалена з плейлиста</string>\n    <string name=\"playlist_synced\">Плейлист синхронізовано</string>\n    <string name=\"undo\">Скасувати</string>\n    <string name=\"lyrics_not_found\">Текст пісні не знайдено</string>\n    <string name=\"sleep_timer\">Таймер сну</string>\n    <string name=\"end_of_song\">Кінець пісні</string>\n    <plurals name=\"minute\">\n        <item quantity=\"one\">%d хвилина</item>\n        <item quantity=\"few\">%d хвилини</item>\n        <item quantity=\"many\">%d хвилин</item>\n        <item quantity=\"other\">%d хвилин</item>\n    </plurals>\n    <string name=\"error_no_stream\">Немає доступних потоків</string>\n    <string name=\"error_no_internet\">Відсутнє підключення до мережі</string>\n    <string name=\"error_timeout\">Тайм-аут</string>\n    <string name=\"error_unknown\">Невідома помилка</string>\n    <string name=\"action_like\">Поставити «Подобається»</string>\n    <string name=\"action_like_all\">Відзначити все як «Подобається»</string>\n    <string name=\"action_remove_like\">Прибрати «Подобається»</string>\n    <string name=\"action_remove_like_all\">Прибрати всі позначки «Подобається»</string>\n    <string name=\"action_shuffle_on\">Увімкнути перемішування</string>\n    <string name=\"action_shuffle_off\">Вимкнути перемішування</string>\n    <string name=\"repeat_mode_off\">Вимкнути повторення</string>\n    <string name=\"repeat_mode_one\">Повторити поточну пісню</string>\n    <string name=\"repeat_mode_all\">Повторити чергу</string>\n    <string name=\"queue_all_songs\">Всі композиції</string>\n    <string name=\"queue_searched_songs\">Шукані композиції</string>\n    <string name=\"music_player\">Музичний плеєр</string>\n    <string name=\"settings\">Параметри</string>\n    <string name=\"appearance\">Зовнішній вигляд</string>\n    <string name=\"theme\">Тема</string>\n    <string name=\"enable_dynamic_theme\">Увімкнути динамічну тему</string>\n    <string name=\"dark_theme\">Темна тема</string>\n    <string name=\"dark_theme_on\">Увімк.</string>\n    <string name=\"dark_theme_off\">Вимк.</string>\n    <string name=\"dark_theme_follow_system\">Використовувати налаштування системи</string>\n    <string name=\"pure_black\">Режим чистого чорного кольору</string>\n    <string name=\"customize_navigation_tabs\">Налаштування вкладок навігації</string>\n    <string name=\"player\">Плеєр</string>\n    <string name=\"player_text_alignment\">Вирівнювання тексту плеєра</string>\n    <string name=\"lyrics_text_position\">Розташування тексту пісні</string>\n    <string name=\"sided\">Збоку</string>\n    <string name=\"left\">Ліворуч</string>\n    <string name=\"center\">По центру</string>\n    <string name=\"right\">Праворуч</string>\n    <string name=\"player_slider_style\">Стиль повзунка плеєра</string>\n    <string name=\"default_\">Стандартний</string>\n    <string name=\"squiggly\">Хвилястий</string>\n    <string name=\"misc\">Різне</string>\n    <string name=\"default_open_tab\">Основна вкладка навігації</string>\n    <string name=\"grid_cell_size\">Розмір клітинки сітки</string>\n    <string name=\"small\">Малий</string>\n    <string name=\"big\">Великий</string>\n    <string name=\"content\">Контент</string>\n    <string name=\"login\">Логін</string>\n    <string name=\"not_logged_in\">Не авторизовано</string>\n    <string name=\"content_language\">Мова контенту</string>\n    <string name=\"content_country\">Країна контенту</string>\n    <string name=\"system_default\">Використовувати налаштування системи</string>\n    <string name=\"enable_proxy\">Увімкнути проксі</string>\n    <string name=\"proxy_type\">Тип проксі</string>\n    <string name=\"proxy_url\">URL проксі</string>\n    <string name=\"restart_to_take_effect\">Перезапуск програми</string>\n    <string name=\"player_and_audio\">Плеєр та аудіо</string>\n    <string name=\"audio_quality\">Якість аудіо</string>\n    <string name=\"audio_quality_auto\">Авто</string>\n    <string name=\"audio_quality_high\">Висока</string>\n    <string name=\"audio_quality_low\">Низька</string>\n    <string name=\"queue\">Черга</string>\n    <string name=\"persistent_queue\">Постійна черга</string>\n    <string name=\"persistent_queue_desc\">Відновлювати останню чергу після запуску програми</string>\n    <string name=\"auto_load_more\">Автозавантаження більшої кількості пісень</string>\n    <string name=\"auto_load_more_desc\">Автоматично додавати більше пісень при досягненні кінця черги, якщо це можливо</string>\n    <string name=\"skip_silence\">Пропуск тиші в композиціях</string>\n    <string name=\"audio_normalization\">Нормалізація аудіо</string>\n    <string name=\"auto_skip_next_on_error\">Автоперехід до наступної пісні при помилці</string>\n    <string name=\"auto_skip_next_on_error_desc\">Забезпечити безперервне відтворення</string>\n    <string name=\"stop_music_on_task_clear\">Зупиняти музику при очищенні завдань</string>\n    <string name=\"equalizer\">Еквалайзер</string>\n    <string name=\"storage\">Сховище</string>\n    <string name=\"cache\">Кеш</string>\n    <string name=\"image_cache\">Кеш зображень</string>\n    <string name=\"song_cache\">Кеш аудіо</string>\n    <string name=\"max_cache_size\">Максимальний розмір кешу</string>\n    <string name=\"unlimited\">Необмежено</string>\n    <string name=\"clear_all_downloads\">Очистити всі завантаження</string>\n    <string name=\"max_image_cache_size\">Макс. розмір кешу зображень</string>\n    <string name=\"clear_image_cache\">Очистити кеш зображень</string>\n    <string name=\"max_song_cache_size\">Макс. розмір кешу аудіо</string>\n    <string name=\"clear_song_cache\">Очистити кеш аудіо</string>\n    <string name=\"size_used\">%s використано</string>\n    <string name=\"privacy\">Конфіденційність</string>\n    <string name=\"listen_history\">Історія прослуховувань</string>\n    <string name=\"pause_listen_history\">Призупинити історію прослуховування</string>\n    <string name=\"clear_listen_history\">Очистити історію прослуховування</string>\n    <string name=\"clear_listen_history_confirm\">Ви впевнені, що хочете очистити всю історію прослуховування?</string>\n    <string name=\"pause_search_history\">Призупинити історію пошуку</string>\n    <string name=\"search_history\">Історія пошуку</string>\n    <string name=\"clear_search_history\">Очистити історію пошуку</string>\n    <string name=\"clear_search_history_confirm\">Ви впевнені, що хочете очистити всю історію пошуку?</string>\n    <string name=\"disable_screenshot\">Вимкнути знімок екрана</string>\n    <string name=\"disable_screenshot_desc\">Якщо ця опція увімкнена, знімки екрана та відображення програми в списку останніх відключаються.</string>\n    <string name=\"enable_lrclib\">Увімкнути провайдера текстів LrcLib</string>\n    <string name=\"enable_kugou\">Увімкнути провайдера текстів KuGou</string>\n    <string name=\"hide_explicit\">Приховувати контент із нецензурною лексикою</string>\n    <string name=\"backup_restore\">Резервне Копіювання</string>\n    <string name=\"action_backup\">Резервне копіювання</string>\n    <string name=\"action_restore\">Відновити</string>\n    <string name=\"imported_playlist\">Імпортований плейлист</string>\n    <string name=\"backup_create_success\">Резервну копію створено успішно</string>\n    <string name=\"backup_create_failed\">Не вдалося створити резервну копію</string>\n    <string name=\"restore_failed\">Не вдалося відновити з резервної копії</string>\n    <string name=\"discord_integration\">Інтеграція з Discord</string>\n    <string name=\"discord_information\">Metrolist використовує бібліотеку KizzyRPC для встановлення статусу вашого облікового запису Discord. Це передбачає використання підключення через Discord Gateway, що може вважатися порушенням умов використання Discord. Однак, наразі немає відомих випадків блокування облікових записів користувачів з цієї причини. Використовуйте на свій страх і ризик.\\n\\nMetrolist буде зчитувати тільки ваш токен, а все інше зберігається локально.</string>\n    <string name=\"dismiss\">Закрити</string>\n    <string name=\"options\">Налаштування</string>\n    <string name=\"preview\">Попередній перегляд</string>\n    <string name=\"login_failed\">Помилка входу</string>\n    <string name=\"action_logout\">Вийти</string>\n    <string name=\"enable_discord_rpc\">Увімкнути Rich Presenсe</string>\n    <string name=\"about\">Про програму</string>\n    <string name=\"app_version\">Версія застосунку</string>\n    <string name=\"new_version_available\">Доступна нова версія</string>\n    <string name=\"translation_models\">Моделі перекладу</string>\n    <string name=\"clear_translation_models\">Очистити моделі перекладу</string>\n    <string name=\"use_login_for_browse_desc\">Це може вплинути на те, який вміст ви бачите, і, наприклад, показує лише платні альбоми, якщо ви ввійшли в обліковий запис Premium</string>\n    <string name=\"use_login_for_browse\">Використовуйте логін для перегляду вмісту</string>\n    <string name=\"action_login\">Увійти</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-v31/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Widget Theme for Android 12+ with Dynamic Colors -->\n    <style name=\"Theme.Widget.Metrolist\" parent=\"@android:style/Theme.DeviceDefault.DayNight\" />\n\n    <style name=\"Theme.Metrolist.Transparent\" parent=\"@android:style/Theme.Translucent.NoTitleBar\">\n        <item name=\"android:windowBackground\">@android:color/transparent</item>\n        <item name=\"android:windowIsTranslucent\">true</item>\n        <item name=\"android:windowNoTitle\">true</item>\n        <item name=\"android:windowDisablePreview\">true</item>\n        <item name=\"android:windowAnimationStyle\">@null</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-v31/widget_colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <!-- Widget colors for Android 12+ using Material 3 dynamic colors -->\n    <!-- Material 3 Primary Container (Light) -->\n    <color name=\"widget_primary_container\">@android:color/system_accent1_100</color>\n    <color name=\"widget_on_primary_container\">@android:color/system_accent1_900</color>\n\n    <!-- Material 3 Tertiary Container (Light) - For like button -->\n    <color name=\"widget_tertiary_container\">@android:color/system_accent3_100</color>\n    <color name=\"widget_on_tertiary_container\">@android:color/system_accent3_900</color>\n\n    <!-- Play button colors (Low style) -->\n    <color name=\"widget_play_button_low_bg\">@color/widget_primary_container</color>\n    <color name=\"widget_play_button_low_icon\">@color/widget_on_primary_container</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-vi/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">Lịch sử nghe cục bộ</string>\n    <string name=\"remote_history\">Lịch sử nghe từ xa</string>\n    <string name=\"weeks\">Tuần</string>\n    <string name=\"months\">Tháng</string>\n    <string name=\"years\">Năm</string>\n    <string name=\"continuous\">Liên tục</string>\n    <string name=\"liked\">Đã thích</string>\n    <string name=\"offline\">Đã tải xuống</string>\n    <string name=\"my_top\">Top nghe nhiều</string>\n    <string name=\"sync_playlist\">Đồng bộ danh sách phát</string>\n    <string name=\"allows_for_sync_witch_youtube\">Lưu ý: Hành động này sẽ cho phép ứng dụng đồng bộ với YouTube Music, và bạn sẽ không thể hủy bỏ hành động này.</string>\n    <string name=\"select\">Chọn tất cả</string>\n    <string name=\"like_all\">Thích tất cả</string>\n    <string name=\"dislike_all\">Không thích toàn bộ</string>\n    <string name=\"sort_by_last_updated\">Ngày cập nhật</string>\n    <string name=\"lyrics\">Lời bài hát</string>\n    <string name=\"already_in_playlist\">Đã tồn tại trong danh sách phát:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"other\">%d lần</item>\n    </plurals>\n    <string name=\"similar_content\">Nội dung tương tự</string>\n    <string name=\"follow_theme\">Dựa theo chủ đề</string>\n    <string name=\"gradient\">Độ phủ màu</string>\n    <string name=\"player_background_blur\">Làm mờ</string>\n    <string name=\"enable_swipe_thumbnail\">Bật lướt bìa để chuyển bài</string>\n    <string name=\"lyrics_click_change\">Thay đổi lời bài hát khi nhấp</string>\n    <string name=\"slim\">Dạng mỏng</string>\n    <string name=\"slim_navbar\">Thanh điều hướng mỏng phía dưới</string>\n    <string name=\"advanced_login\">Đăng nhập bằng token</string>\n    <string name=\"token_hidden\">Chạm để xem Token</string>\n    <string name=\"token_shown\">Chạm lần nữa để sao chép hoặc chỉnh sửa</string>\n    <string name=\"token_adv_login_description\">Đây là phương thức đăng nhập NÂNG CAO. Để thay thế cho web, bạn có thể trực tiếp nhập hoặc cập nhật mã Token của mình tại đây. Điều này có thể tăng tốc độ đăng nhập trên nhiều thiết bị. Xin lưu ý rằng mọi định dạng mã Token không hợp lệ mà ứng dụng không phân tích được sẽ không được chấp nhận</string>\n    <string name=\"general\">Nâng cao</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"default_lib_chips\">Thay đổi thư viện mặc định</string>\n    <string name=\"set_quick_picks\">Đặt lựa chọn nhanh</string>\n    <string name=\"last_song_listened\">Dựa vào lần cuối bạn đã nghe</string>\n    <string name=\"app_language\">Ngôn ngữ ứng dụng</string>\n    <string name=\"enable_similar_content\">Bật hiển thị nội dung tương tự</string>\n    <string name=\"similar_content_desc\">Tự động thêm bài hát có nội dung tương tự khi hàng chờ của bạn đã hết</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"clear_song_cache_dialog\">Bạn có muốn xóa tất cả bộ nhớ đệm (cache) liên quan đến nhạc?</string>\n    <string name=\"clear_downloads_dialog\">Bạn có muốn xóa tất cả bài hát đã tải xuống?</string>\n    <string name=\"not_logged_in_youtube\">Chưa đăng nhập vào YouTube</string>\n    <string name=\"default_links\">Mở đường liên kết được hỗ trợ</string>\n    <string name=\"open_app_settings_error\">Không thể mở ứng dụng cài đặt</string>\n    <string name=\"release_notes\">Ghi chú của bản phát hành</string>\n    <string name=\"all_time\">Mọi lúc</string>\n    <string name=\"past_24_hours\">24 giờ trước</string>\n    <string name=\"past_week\">Tuần trước</string>\n    <string name=\"past_month\">Tháng trước</string>\n    <string name=\"past_year\">Năm trước</string>\n    <string name=\"top_length\">Độ dài danh sách Top của tôi</string>\n    <string name=\"history_duration\">Thời lượng nghe</string>\n    <string name=\"information\">Thông tin chi tiết</string>\n    <string name=\"description\">Mô tả</string>\n    <string name=\"views\">Lượt xem</string>\n    <string name=\"likes\">Thích</string>\n    <string name=\"dislikes\">Không thích</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"other\">%d giây</item>\n    </plurals>\n    <string name=\"lyrics_auto_scroll\">Tự động cuộn lời bài hát</string>\n    <string name=\"show_top_playlist\">Hiện Top danh sách phát</string>\n    <string name=\"show_cached_playlist\">Hiện danh sách phát tạm thời trong bộ đệm</string>\n    <string name=\"import_csv\">Nhập 1 danh sách phát kiểu định dạng \\\"csv\\\"</string>\n    <string name=\"import_online\">Nhập 1 danh sách phát kiểu định dạng \\\"m3u\\\"</string>\n    <string name=\"charts\">Biểu đồ</string>\n    <string name=\"album_cover_desc\">Ảnh bìa Album</string>\n    <string name=\"top_music_videos\">Top MV</string>\n    <string name=\"sync_disabled\">Đã tắt đồng bộ</string>\n    <string name=\"cancel\">Huỷ bỏ</string>\n    <string name=\"share_as_text\">Chia sẻ dưới dạng văn bản</string>\n    <string name=\"share_as_image\">Chia sẻ dưới dạng hình ảnh</string>\n    <string name=\"max_selection_limit\">Giới hạn chọn tối đa</string>\n    <string name=\"share_selected\">Chia sẻ mục đã chọn</string>\n    <string name=\"customize_colors\">Tùy chỉnh màu sắc</string>\n    <string name=\"remove_from_cache\">Xoá khỏi bộ đệm</string>\n    <string name=\"copy_link\">Sao chép link</string>\n    <string name=\"link_copied\">Link đã được sao chép vào bộ nhớ đệm</string>\n    <string name=\"player_buttons_style\">Màu của các nút trong trình phát</string>\n    <string name=\"default_style\">Mặc định</string>\n    <string name=\"swipe_song_to_add\">Lướt bài nhạc sang bên trái để thêm vào hàng chờ, lướt sang bên phải để phát ngay sau bài hiện đang phát</string>\n    <string name=\"auto_playlists\">Danh sách phát tự động</string>\n    <string name=\"show_liked_playlist\">Hiện danh sách phát đã yêu thích</string>\n    <string name=\"show_downloaded_playlist\">Hiện danh sách phát đã tải về</string>\n    <string name=\"generating_image\">Đang khởi tạo hình ảnh</string>\n    <string name=\"text_color\">Màu của văn bản</string>\n    <string name=\"secondary_text_color\">Màu của văn bản phụ</string>\n    <string name=\"background_color\">Màu nền</string>\n    <string name=\"please_wait\">Vui lòng chờ</string>\n    <string name=\"back_button_desc\">Quay lại</string>\n    <string name=\"player_background_style\">Kiểu nền của trình phát</string>\n    <string name=\"trending\">Đang xu hướng</string>\n    <string name=\"share_lyrics\">Chia sẻ lời bài hát</string>\n    <string name=\"cached_playlist\">Đã lưu vào bộ đệm</string>\n    <string name=\"lyrics_romanize_japanese\">Lời bài hát tiếng Nhật</string>\n    <string name=\"lyrics_romanize_korean\">Lời bài hát tiếng Hàn</string>\n    <string name=\"yt_sync\">Tự động đồng bộ với tài khoản</string>\n    <string name=\"more_content\">Nội dung khác</string>\n    <string name=\"playlist_add_local_to_synced_note\">Lưu ý: Không hỗ trợ thêm bài hát cục bộ vào danh sách phát đồng bộ/từ xa. Bất kỳ kết hợp nào khác đều hợp lệ</string>\n    <string name=\"auto_download_on_like\">Tự động tải xuống khi thích</string>\n    <string name=\"auto_download_on_like_desc\">Tự động tải xuống các bài hát khi bạn thích</string>\n    <string name=\"new_player_design\">Thiết kế trình phát mới</string>\n    <string name=\"swipe_sensitivity\">Độ nhạy vuốt của trình phát mini</string>\n    <string name=\"clear_image_cache_dialog\">Bạn có chắc chắn muốn xóa toàn bộ hình ảnh đã lưu trong bộ nhớ đệm không?</string>\n    <string name=\"disable\">Vô hiệu hóa</string>\n    <string name=\"subscribe\">Đăng ký</string>\n    <string name=\"subscribed\">Đã đăng ký</string>\n    <string name=\"now_playing\">Đang Phát</string>\n    <string name=\"close\">Đóng</string>\n    <string name=\"seek_forward_dynamic\">+%1$d giây chuyển tiếp</string>\n    <string name=\"seek_backward_dynamic\">-%1$d giây ngược lại</string>\n    <string name=\"hide_player_thumbnail\">Ẩn hình thu nhỏ của trình phát</string>\n    <string name=\"hide_player_thumbnail_desc\">Thay thế ảnh bìa album với logo ứng dụng trong trình phát</string>\n    <string name=\"new_mini_player_design\">Thiết kế trình phát mini mới</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"disable_load_more_when_repeat_all\">Tắt tải thêm khi lặp lại tất cả</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">Không tự động tải thêm bài hát và nội dung tương tự khi bật chế độ lặp lại tất cả</string>\n    <string name=\"seek_seconds_addup\">Tua liên tục</string>\n    <string name=\"seek_seconds_addup_description\">Nếu được bật, sẽ cộng thêm 5 giây theo từng lần bỏ qua tua</string>\n    <string name=\"settings_section_ui\">Giao diện</string>\n    <string name=\"settings_section_privacy\">Quyền riêng tư &amp; Bảo mật</string>\n    <string name=\"settings_section_player_content\">Trình phát &amp; Nội dung</string>\n    <string name=\"settings_section_storage\">Lưu trữ &amp; Dữ liệu</string>\n    <string name=\"settings_section_system\">Hệ thống &amp; Thông tin</string>\n    <string name=\"starting_radio\">Radio đang bắt đầu</string>\n    <string name=\"edit_playlist_cover\">Chỉnh sửa ảnh bìa danh sách phát</string>\n    <string name=\"edit_playlist_cover_note\">Lưu ý: Tài khoản của bạn phải được liên kết với số điện thoại và được xác minh trên YouTube Music để thay đổi ảnh bìa danh sách phát.</string>\n    <string name=\"edit_playlist_cover_note_wait\">Sau khi chọn hình ảnh, vui lòng đợi để ảnh bìa mới xuất hiện trong danh sách phát của bạn.</string>\n    <string name=\"config_proxy\">Cấu hình proxy</string>\n    <string name=\"proxy_username\">Tên người dùng proxy</string>\n    <string name=\"proxy_password\">Mật khẩu proxy</string>\n    <string name=\"enable_authentication\">Bật xác thực</string>\n    <string name=\"lyrics_romanization_cyrillic\">Cyrillic</string>\n    <string name=\"lyrics_romanization\">Lời bài hát Latinh</string>\n    <string name=\"lyrics_romanize_title\">Latinh</string>\n    <string name=\"lyrics_romanize_russian\">Lời bài hát tiếng Nga</string>\n    <string name=\"lyrics_romanize_ukrainian\">Lời bài hát tiếng Ukraine</string>\n    <string name=\"lyrics_romanize_belarusian\">Lời bài hát tiếng Belarus</string>\n    <string name=\"lyrics_romanize_kyrgyz\">Lời bài hát tiếng Kyrgyz</string>\n    <string name=\"lyrics_romanize_serbian\">Lời bài hát tiếng Serbia</string>\n    <string name=\"lyrics_romanize_bulgarian\">Lời bài hát tiếng Bulgaria</string>\n    <string name=\"line_by_line_option_title\">THỬ NGHIỆM: Phát hiện ngôn ngữ theo từng dòng</string>\n    <string name=\"line_by_line_option_desc\">Ngôn ngữ Cyrillic sẽ được phát hiện theo từng dòng thay vì toàn bộ bài hát.</string>\n    <string name=\"line_by_line_dialog_title\">Bạn có chắc không?</string>\n    <string name=\"line_by_line_dialog_desc\">Đây là một tính năng thử nghiệm có thể thành công hoặc thất bại.\\n\\nTheo mặc định, ngôn ngữ được xác định dựa trên toàn bộ bài hát, nhưng khi bật tùy chọn này, ngôn ngữ sẽ được xác định theo từng dòng. Điều này cho phép các bài hát đa ngôn ngữ hoạt động NHƯNG ngôn ngữ có thể không phải lúc nào cũng chính xác (ví dụ: nếu lời bài hát tiếng Ukraine không chứa bất kỳ chữ cái nào đặc trưng của tiếng Ukraine, nó có thể được chuyển sang tiếng Nga).\\n\\nNếu bạn không gặp sự cố, bạn nên tắt tùy chọn này.</string>\n    <string name=\"romanize_current_track\">La Mã hóa bài hát hiện tại</string>\n    <string name=\"choose_from_library\">Chọn từ thư viện</string>\n    <string name=\"remove_custom_image\">Xoá ảnh bìa tùy chỉnh</string>\n    <string name=\"audio_offload\">Cho phép giảm tải</string>\n    <string name=\"audio_offload_description\">Sử dụng đường dẫn giảm tải để phát. Tắt tính năng này có thể làm tiêu tốn pin nhưng có thể hữu ích nếu bạn gặp sự cố khi phát hoặc xử lý hậu kỳ</string>\n    <string name=\"uploaded_playlist\">Đã tải lên</string>\n    <string name=\"filter_uploaded\">Đã tải lên</string>\n    <string name=\"download_playlist_desc\">Tải tất cả các bài hát để phát ngoại tuyến</string>\n    <string name=\"remove_download_playlist_desc\">Xóa tất cả các bài hát từ danh sách phát này</string>\n    <string name=\"download_in_progress_desc\">Đang tiến hành tải xuống</string>\n    <string name=\"share_playlist_desc\">Chia sẻ danh sách phát này</string>\n    <string name=\"delete_playlist_desc\">Xóa danh sách phát này vĩnh viễn</string>\n    <string name=\"sync_playlist_desc\">Đồng bộ danh sách phát với YouTube Music</string>\n    <string name=\"primary_color_style\">Kiểu màu chính</string>\n    <string name=\"tertiary_color_style\">Kiểu màu tam cấp (phụ)</string>\n    <string name=\"swipe_song_to_remove\">Lướt bài hát để xóa khỏi danh sách phát</string>\n    <string name=\"lyrics_glow_effect\">Bật hiệu ứng phát sáng cho lời bài hát</string>\n    <string name=\"lyrics_glow_effect_desc\">Thêm hiệu ứng làm nổi bật và hiệu ứng bounce (nảy) cho phần lời bài hát đang được chọn</string>\n    <string name=\"enable_better_lyrics\">Bật Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">Lời bài hát khớp theo từng âm tiết cho mọi bài hát, dùng để hát karaoke</string>\n    <string name=\"auto_scroll\">Đồng bộ lại</string>\n    <string name=\"show_uploaded_playlist\">Hiển thị danh sách phát \\\"Đã tải lên\\\"</string>\n    <string name=\"shuffle_playlist_first\">Phát đảo trộn danh sách phát / Album</string>\n    <string name=\"shuffle_playlist_first_desc\">Khi kích hoạt phát đảo trộn, nó sẽ phát tất cả các bài hát từ danh sách phát / Album trước, sau đó sẽ phát nội dung tương tự</string>\n    <string name=\"show_wrapped_card\">Hiển thị thẻ Wrapped</string>\n    <string name=\"discord_use_details\">Dùng chi tiết thay vì trạng thái</string>\n    <string name=\"discord_use_details_description\">Hiện tên bài hát chính thay vì tên nghệ sĩ</string>\n    <string name=\"lyrics_romanize_chinese\">Lời Romanize Chinese</string>\n    <string name=\"updater\">Trình cập nhật</string>\n    <string name=\"check_for_updates\">Tự động kiểm tra cập nhật</string>\n    <string name=\"update_notifications\">Bật thông báo về cập nhật</string>\n    <string name=\"update_available_title\">Đã có cập nhật mới</string>\n    <string name=\"update_channel_name\">Cập nhật ứng dụng</string>\n    <string name=\"update_channel_desc\">Thông báo về phiên bản mới của ứng dụng</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"google_cast_description\">Bật trình chiếu Audio đến Chromecast và các thiết bị khác có hỗ trợ tính năng trình chiếu (Cast)</string>\n    <string name=\"lyrics_romanize_macedonian\">Phiên âm chữ Ma-xê-đô-ni-a (Macedonia) sang chữ Latinh</string>\n    <string name=\"integrations\">Tích hợp tiện ích</string>\n    <string name=\"username\">Tên người dùng</string>\n    <string name=\"password\">Mật khẩu</string>\n    <string name=\"lastfm_integration\">Tích hợp Last.fm</string>\n    <string name=\"about_artist\">Giới thiệu nghệ sĩ</string>\n    <string name=\"show_more\">Hiển thị thêm</string>\n    <string name=\"show_less\">Hiện ít hơn</string>\n    <string name=\"artist_page_settings\">Trang nghệ sĩ</string>\n    <string name=\"show_artist_description\">Hiển thị mô tả nghệ sĩ</string>\n    <string name=\"show_artist_subscriber_count\">Hiện số lượng người đăng ký</string>\n    <string name=\"show_artist_monthly_listeners\">Hiển thị số lượt nghe hằng tháng</string>\n    <string name=\"wrapped_logo_content_description\">Metrolist Logo</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"listen_together_join_room\">Tham gia phòng</string>\n    <string name=\"listen_together_create_room_desc\">Tạo phòng và chia sẽ mã cho bạn bè</string>\n    <string name=\"listen_together_room_code\">Mã phòng</string>\n    <string name=\"listen_together_you_are_host\">Bạn là chủ phòng</string>\n    <string name=\"listen_together_you_are_guest\">Bạn là khách</string>\n    <string name=\"mute\">Tắt tiếng</string>\n    <string name=\"unmute\">Bật tiếng</string>\n    <string name=\"listen_together_join_requests\">Yêu cầu tham gia</string>\n    <string name=\"listen_together_view_logs\">Xem nhật ký</string>\n    <string name=\"listen_together_disconnected\">Ngắt kết nối</string>\n    <string name=\"listen_together_connecting\">Đang kết nối…</string>\n    <string name=\"listen_together_error\">Lỗi kết nối</string>\n    <string name=\"listen_together_create_room\">Tạo phòng</string>\n    <string name=\"listen_together_username\">Tên người dùng</string>\n    <string name=\"listen_together_connected\">Đã kết nối</string>\n    <string name=\"listen_together_reconnecting\">Đang kết nối lại…</string>\n    <string name=\"listen_together_server_url\">URL máy chủ</string>\n    <string name=\"listen_together_choose_server\">Chọn máy chủ</string>\n    <string name=\"listen_together_custom_server\">Máy chủ tùy chỉnh</string>\n    <string name=\"listen_together_use_custom_server\">Sử dụng máy chủ tùy chỉnh</string>\n    <string name=\"album_art\">Cài đặt widget</string>\n    <string name=\"no_song_playing\">Không có bài hát đang phát</string>\n    <string name=\"tap_to_open\">Chạm để mở Metrolist</string>\n    <string name=\"previous\">Bài trước</string>\n    <string name=\"play_pause\">Phát/Tạm dừng</string>\n    <string name=\"next\">Bài tiếp</string>\n    <string name=\"like\">Thích</string>\n    <string name=\"widget_description\">Widget trình phát nhạc với các nút điều khiển</string>\n    <string name=\"listen_together\">Nghe cùng nhau</string>\n    <string name=\"listen_together_view_logs_desc\">Gỡ lỗi kết nối và tin nhắn</string>\n    <string name=\"listen_together_logs\">Nhật ký kết nối</string>\n    <string name=\"listen_together_no_logs\">Chưa có nhật ký nào</string>\n    <string name=\"listen_together_auto_approval_joins\">Tự động duyệt yêu cầu tham gia</string>\n    <string name=\"listen_together_auto_approval_joins_desc\">Tự động duyệt yêu cầu tham gia thay vì xem xét thủ công</string>\n    <string name=\"listen_together_sync_volume\">Đồng bộ âm lượng của chủ phòng</string>\n    <string name=\"listen_together_sync_volume_desc\">Khách sẽ theo mức âm lượng của chủ phòng</string>\n    <string name=\"listen_together_description\">Nghe nhạc cùng bạn bè theo thời gian thực. Tạo phòng để làm chủ phòng hoặc tham gia phòng có sẵn bằng mã.</string>\n    <string name=\"listen_together_background_disconnect_note\">Lưu ý: Bạn có thể bị ngắt kết nối nếu tạo phòng khi chưa phát nhạc rồi chuyển sang ứng dụng khác.</string>\n    <string name=\"listen_together_not_configured\">Nghe cùng nhau chưa được cấu hình. Vui lòng thiết lập URL máy chủ trong Cài đặt → Tích hợp → Nghe cùng nhau.</string>\n    <string name=\"listen_together_suggestion_received\">%1$s đã yêu cầu %2$s</string>\n    <string name=\"listen_together_suggestion_sent\">Đã gửi gợi ý đến chủ phòng!</string>\n    <string name=\"listen_together_join_request_notification\">%1$s muốn tham gia phòng</string>\n    <string name=\"listen_together_notification_channel_name\">Nghe cùng nhau</string>\n    <string name=\"listen_together_notification_channel_desc\">Thông báo cho các sự kiện Nghe cùng nhau</string>\n    <string name=\"listen_together_room_created\">Phòng đã được tạo: %s</string>\n    <string name=\"listen_together_cannot_edit_username_in_room\">Không thể chỉnh sửa tên người dùng khi đang ở trong phòng</string>\n    <string name=\"waiting_for_approval\">Đang chờ chủ phòng phê duyệt</string>\n    <string name=\"invalid_room_code\">Mã phòng không hợp lệ</string>\n    <string name=\"join_request_denied\">Yêu cầu tham gia bị từ chối</string>\n    <string name=\"join_existing_room\">Tham gia phòng có sẵn</string>\n    <string name=\"room_code\">Mã phòng</string>\n    <string name=\"leave_room\">Rời phòng</string>\n    <string name=\"join_room\">Tham gia</string>\n    <string name=\"create_room\">Tạo</string>\n    <string name=\"joining_room\">Đang tham gia phòng %s…</string>\n    <string name=\"creating_room\">Đang tạo phòng…</string>\n    <string name=\"connect\">Kết nối</string>\n    <string name=\"disconnect\">Ngắt kết nối</string>\n    <string name=\"create\">Tạo</string>\n    <string name=\"join\">Tham gia</string>\n    <string name=\"approve\">Duyệt</string>\n    <string name=\"reject\">Từ chối</string>\n    <string name=\"clear\">Xóa</string>\n    <string name=\"copy\">Sao chép</string>\n    <string name=\"copied_to_clipboard\">Đã sao chép vào bộ nhớ tạm</string>\n    <string name=\"not_set\">Chưa thiết lập</string>\n    <string name=\"hosting_room\">Đang làm chủ phòng</string>\n    <string name=\"in_room\">Trong phòng</string>\n    <string name=\"pending_requests\">Yêu cầu đang chờ</string>\n    <string name=\"pending_suggestions\">Gợi ý chờ duyệt</string>\n    <string name=\"suggest_to_host\">Gợi ý cho chủ phòng</string>\n    <string name=\"kick_user\">Loại khỏi phòng</string>\n    <string name=\"host_label\">Chủ phòng</string>\n    <string name=\"you_label\">Bạn</string>\n    <string name=\"connected_users\">Người dùng đã kết nối</string>\n    <string name=\"enter_username\">Nhập tên người dùng</string>\n    <string name=\"error_username_empty\">Bắt buộc nhập tên người dùng.</string>\n    <string name=\"resync\">Đồng bộ lại</string>\n    <string name=\"copy_code\">Sao chép mã</string>\n    <string name=\"kick_user_desc\">Loại người này khỏi phiên</string>\n    <string name=\"permanently_kick_user\">Chặn vĩnh viễn</string>\n    <string name=\"permanently_kick_user_desc\">Chặn yêu cầu tham gia của người này và ẩn các gợi ý của họ</string>\n    <string name=\"transfer_ownership\">Chuyển quyền sở hữu</string>\n    <string name=\"transfer_ownership_desc\">Chuyển người này làm chủ phòng</string>\n    <string name=\"manage_user\">Quản lý người dùng</string>\n    <string name=\"listen_together_blocked_users\">Người dùng bị chặn</string>\n    <string name=\"listen_together_blocked_users_count\">Đã chặn %d người dùng</string>\n    <string name=\"listen_together_no_blocked_users\">Không có người dùng bị chặn</string>\n    <string name=\"unblock\">Bỏ chặn</string>\n    <string name=\"user_blocked_by_host\">Người dùng bị chặn bởi chủ phòng</string>\n    <string name=\"crash_title\">Ứng dụng bị sập</string>\n    <string name=\"crash_description\">Đã xảy ra lỗi không mong muốn. Vui lòng chia sẻ báo cáo sự cố để giúp chúng tôi khắc phục lỗi.</string>\n    <string name=\"crash_share_logs\">Chia sẻ nhật ký</string>\n    <string name=\"crash_share_title\">Chia sẻ báo cáo sự cố</string>\n    <string name=\"crash_report_subject\">Báo cáo sự cố Metrolist</string>\n    <string name=\"crash_close\">Đóng</string>\n    <string name=\"crash_no_log\">Không có nhật ký sự cố</string>\n    <string name=\"palette_crimson\">Đỏ thẫm</string>\n    <string name=\"palette_rose\">Hồng</string>\n    <string name=\"palette_purple\">Tím</string>\n    <string name=\"palette_deep_purple\">Tím đậm</string>\n    <string name=\"palette_indigo\">Chàm</string>\n    <string name=\"palette_blue\">Xanh dương</string>\n    <string name=\"palette_sky_blue\">Xanh da trời</string>\n    <string name=\"palette_cyan\">Xanh lục lam</string>\n    <string name=\"palette_teal\">Xanh lam</string>\n    <string name=\"palette_green\">Xanh lá cây</string>\n    <string name=\"palette_light_green\">Xanh lá nhạt</string>\n    <string name=\"palette_lime\">Xanh lá mạ</string>\n    <string name=\"palette_yellow\">Vàng</string>\n    <string name=\"palette_amber\">Hổ phách</string>\n    <string name=\"palette_orange\">Cam</string>\n    <string name=\"palette_deep_orange\">Cam đậm</string>\n    <string name=\"palette_brown\">Nâu</string>\n    <string name=\"palette_grey\">Xám</string>\n    <string name=\"palette_blue_grey\">Xanh xám</string>\n    <string name=\"cd_light_mode\">Chế độ sáng</string>\n    <string name=\"cd_dark_mode\">Chế độ tối</string>\n    <string name=\"cd_system_mode\">Chế độ hệ thống</string>\n    <string name=\"cd_palette_item\">%1$s bảng màu</string>\n    <string name=\"crop_album_art\">Cắt ảnh bìa album</string>\n    <string name=\"crop_album_art_desc\">Cắt ảnh thu nhỏ video về tỷ lệ vuông</string>\n    <string name=\"skip_silence_desc\">Bỏ qua đoạn im lặng trong bài hát</string>\n    <string name=\"skip_silence_instant\">Bỏ qua đoạn im lặng tức thì</string>\n    <string name=\"skip_silence_instant_desc\">Tua nhanh qua các đoạn im lặng thay vì tăng tốc độ phát</string>\n    <string name=\"persistent_shuffle_title\">Giữ chế độ phát ngẫu nhiên</string>\n    <string name=\"persistent_shuffle_desc\">Giữ chế độ ngẫu nhiên khi phát bài/playlist mới</string>\n    <string name=\"remember_shuffle_and_repeat\">Nhớ trạng thái ngẫu nhiên và lặp</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">Nhớ chế độ ngẫu nhiên &amp; lặp khi mở lại ứng dụng</string>\n    <string name=\"pause_music_when_media_is_muted\">Tạm dừng nhạc khi tắt tiếng media</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">Giữ màn hình bật khi mở trình phát</string>\n    <string name=\"last_fm_send_likes_description\">Đánh dấu yêu thích/bỏ yêu thích bài hát trên Last.fm khi chúng được thích/bỏ thích trong Metrolist</string>\n    <string name=\"logging_in\">Đang đăng nhập…</string>\n    <string name=\"hide_video_songs\">Ẩn bài hát video</string>\n    <string name=\"details_desc\">Xem thông tin bài hát</string>\n    <string name=\"edit_desc\">Sửa tên bài hát hoặc nghệ sĩ</string>\n    <string name=\"none\">Không có</string>\n    <string name=\"fade\">Mờ dần</string>\n    <string name=\"glow\">Phát sáng</string>\n    <string name=\"slide\">Trượt</string>\n    <string name=\"karaoke\">Karaoke</string>\n    <string name=\"apple_music_style\">Apple Music</string>\n    <string name=\"lyrics_text_size\">Cỡ chữ lời bài hát</string>\n    <string name=\"lyrics_line_spacing\">Giãn dòng lời bài hát</string>\n    <string name=\"wavy\">Lượn sóng</string>\n    <string name=\"enable_simpmusic\">Kiếm lời nhạc từ SimpMusic</string>\n    <string name=\"enable_simpmusic_desc\">Tự động lấy lời bài hát từ Musixmatch và Bản ghi YouTube</string>\n    <string name=\"lyrics_offset\">Độ trễ của lời</string>\n    <string name=\"play_next_desc\">Thêm vào đầu danh sách phát nhạc</string>\n    <string name=\"add_to_queue_desc\">Thêm vào cuối danh sách phát nhạc</string>\n    <string name=\"add_to_library_desc\">Lưu vào thư viện</string>\n    <string name=\"download_desc\">Cho phép phát lại ngoại tuyến</string>\n    <string name=\"add_to_playlist_desc\">Lưu vào danh sách nhạc</string>\n    <string name=\"refetch_desc\">Lấy dữ liệu mới nhất từ Youtube Music</string>\n    <string name=\"share_desc\">Chia sẻ liên kết cho bài hát này</string>\n    <string name=\"delete_desc\">Xóa vĩnh viễn bài hát này</string>\n    <string name=\"advanced_desc\">Đổi nhịp độ và cao độ của bài hát</string>\n    <string name=\"equalizer_desc\">Chỉnh bộ cân bằng âm thanh</string>\n    <string name=\"enable\">Cho phép</string>\n    <string name=\"credits_instagram\">Instagram</string>\n    <string name=\"save_episode_for_later\">Lưu để nghe sau</string>\n    <string name=\"metrolist\">METROLIST</string>\n    <string name=\"credits_license_name\">GNU General Public License v3.0</string>\n    <string name=\"daily_discover_based_on\">Dựa trên %1$s</string>\n    <string name=\"logout_keep\">Giữ lại</string>\n    <string name=\"logout_clear\">Xoá bỏ</string>\n    <string name=\"credits_telegram\">Kênh Telegram</string>\n    <string name=\"credits_website\">Website</string>\n    <string name=\"credits_github\">GitHub</string>\n    <string name=\"filter_podcasts\">Podcasts</string>\n    <string name=\"view_podcast\">Xem podcast</string>\n    <string name=\"no_account_found\">Không tìm thấy tài khoản</string>\n    <string name=\"enter_room_code\">Nhập mã phòng</string>\n    <string name=\"listen_together_settings_desc\">Cấu hình server, username, và thêm nữa</string>\n    <string name=\"ai_lyrics_translation\">AI dịch lời bài hát</string>\n    <string name=\"ai_translating_lyrics\">Đang dịch lời...</string>\n    <string name=\"ai_lyrics_translated\">Lời đã dịch xong</string>\n    <string name=\"player_background_solid\">Tĩnh</string>\n    <string name=\"display_density\">Mật độ hiển thị</string>\n    <string name=\"restart\">Khởi động lại</string>\n    <string name=\"restart_required\">Khởi động lại được yêu cầu</string>\n    <string name=\"density_restart_message\">Thay đổi mật độ hiển thị sẽ có hiệu lực sau khi khởi động lại ứng dụng. Bạn có muốn khởi động lại ngay không?</string>\n    <string name=\"enable_lrclib_desc\">Cơ sở dữ liệu lời bài hát khớp thời gian do cộng đồng đóng góp</string>\n    <string name=\"enable_kugou_desc\">Lời bài hát lấy từ KuGou, một nền tảng âm nhạc phổ biến của Trung Quốc</string>\n    <string name=\"youtube_music_lyrics_note\">LƯU Ý: Lời bài hát từ YouTube Music sẽ tự động được hiển thị khi không lời bài hát nào khác có sẵn. Lời bài hát từ YTM thường không đồng bộ.</string>\n    <string name=\"enable_lyricsplus\">Bật LyricsPlus</string>\n    <string name=\"enable_lyricsplus_desc\">Đồng bộ lời bài hát từ nhiều nguồn</string>\n    <string name=\"lyrics_provider_selection\">Chọn bên cung cấp</string>\n    <string name=\"lyrics_provider_selection_desc\">Chọn các nguồn cung cấp lời bài hát được kích hoạt</string>\n    <string name=\"prevent_duplicate_tracks_in_queue\">Tránh lặp lại bài hát trong hàng</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">Khi thêm bài hát vào hàng đợi, xóa nó ở vị trí trước đó nếu đã có sẵn</string>\n    <string name=\"lyrics_provider_priority\">Ưu tiên nhà cung cấp lời bài hát</string>\n    <string name=\"lyrics_provider_priority_desc\">Kéo để xắp xếp lại nhà cung cấp theo ý muốn. Vị trí càng cao -&gt; Uư tiên càng cao.</string>\n    <string name=\"changelog\">Lịch sử cập nhật</string>\n    <string name=\"changelog_empty\">Không có lịch sử cập nhật có sẵn</string>\n    <string name=\"github_releases_url\">https://github.com/MetrolistGroup/Metrolist/releases</string>\n    <string name=\"list_bullet\">•</string>\n    <string name=\"view_on_github\">Xem trong GitHub</string>\n    <string name=\"current_version\">Phiên bản hiện tại</string>\n    <string name=\"version_format\">Phiên bản: %s</string>\n    <string name=\"update_settings\">Cài đặt cập nhật</string>\n    <string name=\"check_for_updates_title\">Kiểm tra cập nhật</string>\n    <string name=\"checking_for_updates\">Đang kiểm tra cập nhật…</string>\n    <string name=\"latest_version_format\">Mới nhất: %s</string>\n    <string name=\"check_for_updates_button\">Kiểm tra cập nhật</string>\n    <string name=\"hide_changelog\">Ẩn lịch sử cập nhật</string>\n    <string name=\"view_changelog\">Hiện lịch sử cập nhật</string>\n    <string name=\"failed_to_check_updates\">Kiểm tra cập nhật không thành công: %s</string>\n    <string name=\"set_as_default\">Đặt làm mặc định</string>\n    <string name=\"sleep_timer_default_set\">Hẹn giờ ngủ mặc định đặt là %d phút</string>\n    <string name=\"resume_on_bluetooth_connect\">Tiếp tục khi kết nối Bluetooth</string>\n    <string name=\"crossfade\">Đan xen nhạc</string>\n    <string name=\"crossfade_desc\">Đan xen giữa các bài hát</string>\n    <string name=\"crossfade_duration\">Thời lượng đan xen</string>\n    <string name=\"crossfade_gapless\">Tắt cho album gapless</string>\n    <string name=\"crossfade_gapless_desc\">Không đan xen nếu album là gapless</string>\n    <string name=\"crossfade_beta_title\">Tính năng Beta</string>\n    <string name=\"crossfade_beta_message\">Đan xen là tính năng mới và có thể sẽ có lỗi. Nếu bạn gặp bất kì vấn đề gì, vui lòng báo cáo.\\n\\nTính năng này sẽ tắt Xử lý âm thanh độc lập vì giới hạn kĩ thuật.</string>\n    <string name=\"lyrics_romanize_hindi\">La-tinh hóa lời bài hát Hindi</string>\n    <string name=\"lyrics_romanize_punjabi\">La-tinh hóa lời bài hát Punjabi</string>\n    <string name=\"audio_offload_disabled_by_crossfade\">Vô hiệu hóa vì Đan xen đang kích hoạt</string>\n    <string name=\"lyrics_romanize_as_main\">Hiện lời bài hát La-tinh hóa là chính</string>\n    <string name=\"enable_scrobbling\">Kích hoạt scrobbling</string>\n    <string name=\"lastfm_now_playing\">Gửi Hiện Đang Phát</string>\n    <string name=\"last_fm_send_likes\">Gửi Thích/Không thích</string>\n    <string name=\"scrobbling_configuration\">Cấu hình Scrobbling</string>\n    <string name=\"scrobble_min_track_duration\">Scrobble các bài hát dài hơn</string>\n    <string name=\"scrobble_delay_percent\">Tỉ lệ phần trăm chờ Scrobble</string>\n    <string name=\"scrobble_delay_minutes\">Phút chờ Scrobble</string>\n    <string name=\"hide_youtube_shorts\">Ẩn YouTube Shorts</string>\n    <string name=\"start_radio_desc\">Tạo đài phát từ nội dung này</string>\n    <string name=\"enable_dynamic_icon\">Kích hoạt biểu tượng động</string>\n    <string name=\"mini_player\">Trình phát mini</string>\n    <string name=\"pure_black_mini_player\">Trình phát mini đen thuần</string>\n    <string name=\"cache_size_warning_title\">Từ từ đã!</string>\n    <string name=\"cache_size_warning_message\">Bạn đã chọn giới hạn kích cỡ bộ nhớ đệm nhỏ hơn hiện tại đang sử dụng (%1$s). Nếu bạn tiếp tục, ứng dụng sẽ xóa bớt một số bộ nhớ đệm %2$s để khớp giới hạn mới. Vẫn tiếp tục?</string>\n    <string name=\"cache_size_warning_confirm\">Tiếp tục</string>\n    <string name=\"lyrics_animation_style\">Phong cách hiệu ứng chạy từng từ</string>\n    <string name=\"album_art_for\">Ảnh album cho %s</string>\n    <string name=\"wrapped_total_albums_title\">Bạn đã nghe</string>\n    <string name=\"wrapped_total_albums_subtitle\">Album độc đáo</string>\n    <string name=\"wrapped_top_album_title\">Album nghe nhiều nhất là</string>\n    <string name=\"wrapped_playlist_ready\">Danh sách phát cá nhân hóa của bạn đã sẵn sàng</string>\n    <string name=\"wrapped_top_5_albums_title\">Top 5 album của bạn</string>\n    <string name=\"wrapped_album_listening_time\">Bạn đã nghe album này được %d phút</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d phút</string>\n    <string name=\"wrapped_no_data\">Không có dữ liệu</string>\n    <string name=\"credits_license_desc\">Phần mềm miễn phí, mã nguồn mở. Bạn có thể sử dụng, học hỏi, chia sẻ và cải thiện nó.</string>\n    <string name=\"credits_discord\">Máy chủ Discord</string>\n    <string name=\"credits_view_repo\">Xem Repository</string>\n    <string name=\"app_version_info\">%1$s • %2$s</string>\n    <string name=\"like_what_i_do\">Thích những gì tôi làm?</string>\n    <string name=\"buy_mo_a_coffee\">Ủng hộ tôi một ly cà phê</string>\n    <string name=\"community_and_info\">Cộng đồng &amp; Thông tin</string>\n    <string name=\"stands_with_palestine\">Dự án này đứng về Palestine 🇵🇸</string>\n    <string name=\"podcast_channels\">Kênh Podcast</string>\n    <string name=\"latest_episodes\">Tập Mới Nhất</string>\n    <string name=\"your_shows\">Chương Trình Của Bạn</string>\n    <string name=\"new_episodes\">Tập Mới</string>\n    <string name=\"episodes_for_later\">Tập Để Sau</string>\n    <string name=\"save_episode_for_later_desc\">Thêm vào danh sách phát Tập xem sau</string>\n    <string name=\"remove_episode_from_saved\">Đã xóa khỏi lưu</string>\n    <string name=\"subscribe_to_podcast\">Lưu podcast vào thư viện</string>\n    <plurals name=\"n_episode\">\n        <item quantity=\"other\">%d tập</item>\n    </plurals>\n    <string name=\"filter_episodes\">Tập</string>\n    <string name=\"filter_profiles\">Hồ sơ</string>\n    <string name=\"filter_channels\">Kênh</string>\n    <string name=\"auto_playlist\">Danh sách phát tự động</string>\n    <string name=\"downloaded_episodes\">Tập đã tải về</string>\n    <string name=\"no_subscribed_channels\">Chưa có kênh được đăng kí</string>\n    <string name=\"no_downloaded_episodes\">Chưa có tập được tải về</string>\n    <plurals name=\"n_channel\">\n        <item quantity=\"other\">%d kênh</item>\n    </plurals>\n    <string name=\"restore_confirm_title\">Phục hồi sao lưu?</string>\n    <string name=\"restore_confirm_message\">Cái này sẽ phục hồi dữ liệu ứng dụng của bạn từ bản sao lưu.</string>\n    <string name=\"restore_account_warning\">Bạn sẽ phải đăng nhập sau khi phục hồi. Những tài khoản sau sẽ bị đăng xuất:</string>\n    <string name=\"restore\">Phục hồi</string>\n    <string name=\"checking_previous_account\">Kiểm tra tài khoản trước đây…</string>\n    <string name=\"enable_automatic_sleeptimer\">Kích hoạt hẹn giờ ngủ tự động</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-vi/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Trang chủ</string>\n    <string name=\"songs\">Bài hát</string>\n    <string name=\"artists\">Nghệ sĩ</string>\n    <string name=\"albums\">Album</string>\n    <string name=\"playlists\">Danh sách phát</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"other\">Đã chọn %d</item>\n    </plurals>\n    <string name=\"history\">Lịch sử</string>\n    <string name=\"stats\">Thống kê</string>\n    <string name=\"mood_and_genres\">Tâm trạng và thể loại</string>\n    <string name=\"account\">Tài Khoản</string>\n    <string name=\"quick_picks\">Lựa chọn nhanh</string>\n    <string name=\"quick_picks_empty\">Hãy nghe các bài hát để tạo danh sách chọn nhanh của bạn</string>\n    <string name=\"new_release_albums\">Các album mới phát hành</string>\n    <string name=\"today\">Hôm nay</string>\n    <string name=\"yesterday\">Hôm qua</string>\n    <string name=\"this_week\">Tuần này</string>\n    <string name=\"last_week\">Tuần trước</string>\n    <string name=\"most_played_songs\">Bài hát được phát nhiều nhất</string>\n    <string name=\"most_played_artists\">Nghệ sĩ được phát nhiều nhất</string>\n    <string name=\"most_played_albums\">Album được phát nhiều nhất</string>\n    <string name=\"search\">Tìm kiếm</string>\n    <string name=\"search_yt_music\">Tìm kiếm trên YouTube Music…</string>\n    <string name=\"search_library\">Tìm kiếm trong thư viện…</string>\n    <string name=\"filter_library\">Thư viện</string>\n    <string name=\"filter_liked\">Đã thích</string>\n    <string name=\"filter_downloaded\">Đã tải xuống</string>\n    <string name=\"filter_all\">Tất cả</string>\n    <string name=\"filter_songs\">Bài hát</string>\n    <string name=\"filter_videos\">Video</string>\n    <string name=\"filter_albums\">Album</string>\n    <string name=\"filter_artists\">Nghệ sĩ</string>\n    <string name=\"filter_playlists\">Danh sách phát</string>\n    <string name=\"filter_community_playlists\">Danh sách phát cộng đồng</string>\n    <string name=\"filter_featured_playlists\">Danh sách phát nổi bật</string>\n    <string name=\"filter_bookmarked\">Đã đánh dấu</string>\n    <string name=\"no_results_found\">Không tìm thấy kết quả nào</string>\n    <string name=\"from_your_library\">Từ thư viện của bạn</string>\n    <string name=\"liked_songs\">Bài hát được yêu thích</string>\n    <string name=\"downloaded_songs\">Bài hát được tải xuống</string>\n    <string name=\"playlist_is_empty\">Danh sách phát đang trống</string>\n    <string name=\"retry\">Thử lại</string>\n    <string name=\"radio\">Đài phát</string>\n    <string name=\"shuffle\">Trộn bài</string>\n    <string name=\"reset\">Cài lại</string>\n    <string name=\"details\">Chi tiết</string>\n    <string name=\"edit\">Chỉnh sửa</string>\n    <string name=\"start_radio\">Bắt đầu đài phát</string>\n    <string name=\"play\">Phát</string>\n    <string name=\"play_next\">Phát bài tiếp theo</string>\n    <string name=\"add_to_queue\">Thêm vào hàng chờ</string>\n    <string name=\"add_to_library\">Thêm vào thư viện</string>\n    <string name=\"remove_from_library\">Xoá khỏi thư viện</string>\n    <string name=\"action_download\">Tải xuống</string>\n    <string name=\"downloading\">Đang tải xuống</string>\n    <string name=\"remove_download\">Xoá mục đã tải xuống</string>\n    <string name=\"import_playlist\">Nhập danh sách phát</string>\n    <string name=\"add_to_playlist\">Thêm vào danh sách phát</string>\n    <string name=\"view_artist\">Xem nghệ sĩ</string>\n    <string name=\"view_album\">Xem album</string>\n    <string name=\"refetch\">Làm mới</string>\n    <string name=\"share\">Chia sẻ</string>\n    <string name=\"delete\">Xoá</string>\n    <string name=\"remove_from_history\">Xoá khỏi lịch sử</string>\n    <string name=\"search_online\">Tìm kiếm trực tuyến</string>\n    <string name=\"action_sync\">Đồng bộ</string>\n    <string name=\"advanced\">Nâng cao</string>\n    <string name=\"sort_by_create_date\">Ngày thêm vào</string>\n    <string name=\"sort_by_name\">Tên</string>\n    <string name=\"sort_by_artist\">Nghệ sĩ</string>\n    <string name=\"sort_by_year\">Năm</string>\n    <string name=\"sort_by_song_count\">Số lượng bài hát</string>\n    <string name=\"sort_by_length\">Độ dài</string>\n    <string name=\"sort_by_play_time\">Thời gian phát</string>\n    <string name=\"sort_by_custom\">Tuỳ chỉnh bộ lọc</string>\n    <string name=\"media_id\">ID phương tiện</string>\n    <string name=\"mime_type\">Định dạng</string>\n    <string name=\"codecs\">Bộ giải mã</string>\n    <string name=\"bitrate\">Tốc độ bit</string>\n    <string name=\"sample_rate\">Tỷ lệ mẫu</string>\n    <string name=\"loudness\">Độ ồn</string>\n    <string name=\"volume\">Âm lượng</string>\n    <string name=\"file_size\">Kích thước tệp tin</string>\n    <string name=\"unknown\">Không rõ</string>\n    <string name=\"copied\">Đã sao chép vào clipboard</string>\n    <string name=\"edit_lyrics\">Sửa lời bài hát</string>\n    <string name=\"search_lyrics\">Tìm kiếm lời bài hát</string>\n    <string name=\"edit_song\">Sửa bài hát</string>\n    <string name=\"song_title\">Tên bài hát</string>\n    <string name=\"song_artists\">Nghệ sĩ bài hát</string>\n    <string name=\"error_song_title_empty\">Tên bài hát không được để trống.</string>\n    <string name=\"error_song_artist_empty\">Nghệ sĩ bài hát không được để trống.</string>\n    <string name=\"save\">Lưu</string>\n    <string name=\"choose_playlist\">Chọn danh sách phát</string>\n    <string name=\"edit_playlist\">Chỉnh sửa danh sách phát</string>\n    <string name=\"create_playlist\">Tạo danh sách phát</string>\n    <string name=\"playlist_name\">Tên danh sách phát</string>\n    <string name=\"error_playlist_name_empty\">Tên danh sách phát không được để trống.</string>\n    <string name=\"edit_artist\">Chỉnh sửa nghệ sĩ</string>\n    <string name=\"artist_name\">Tên nghệ sĩ</string>\n    <string name=\"error_artist_name_empty\">Tên nghệ sĩ không được để trống.</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"other\">%d bài hát</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"other\">%d nghệ sĩ</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"other\">%d album</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"other\">%d danh sách phát</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"other\">%d tuần</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"other\">%d tháng</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"other\">%d năm</item>\n    </plurals>\n    <string name=\"playlist_imported\">Danh sách phát đã được nhập vào</string>\n    <string name=\"removed_song_from_playlist\">Đã xoá \\\"%s\\\" khỏi danh sách phát</string>\n    <string name=\"playlist_synced\">Danh sách phát được đồng bộ</string>\n    <string name=\"undo\">Hoàn tác</string>\n    <string name=\"lyrics_not_found\">Không tìm thấy lời bài hát</string>\n    <string name=\"sleep_timer\">Hẹn giờ ngủ</string>\n    <string name=\"end_of_song\">Kết thúc bài hát</string>\n    <plurals name=\"minute\">\n        <item quantity=\"other\">%d phút</item>\n    </plurals>\n    <string name=\"error_no_stream\">Không có luồng nào khả dụng</string>\n    <string name=\"error_no_internet\">Không có kết nối mạng</string>\n    <string name=\"error_timeout\">Hết giờ</string>\n    <string name=\"error_unknown\">Lỗi không xác định</string>\n    <string name=\"action_like\">Thích</string>\n    <string name=\"action_remove_like\">Bỏ thích</string>\n    <string name=\"action_shuffle_on\">Bật trộn bài</string>\n    <string name=\"action_shuffle_off\">Tắt trộn bài</string>\n    <string name=\"repeat_mode_off\">Tắt lặp lại</string>\n    <string name=\"repeat_mode_one\">Lặp lại bài hát này</string>\n    <string name=\"repeat_mode_all\">Lặp lại hàng đợi</string>\n    <string name=\"queue_all_songs\">Tất cả bài hát</string>\n    <string name=\"queue_searched_songs\">Những bài hát được tìm kiếm</string>\n    <string name=\"music_player\">Trình phát nhạc</string>\n    <string name=\"settings\">Cài đặt</string>\n    <string name=\"appearance\">Hiển thị</string>\n    <string name=\"enable_dynamic_theme\">Bật chủ đề theo màu sắc</string>\n    <string name=\"dark_theme\">Chủ đề tối</string>\n    <string name=\"dark_theme_on\">Bật</string>\n    <string name=\"dark_theme_off\">Tắt</string>\n    <string name=\"dark_theme_follow_system\">Theo hệ thống</string>\n    <string name=\"pure_black\">Tối hoàn toàn</string>\n    <string name=\"default_open_tab\">Tab mặc định</string>\n    <string name=\"customize_navigation_tabs\">Tùy chỉnh thanh tab điều hướng</string>\n    <string name=\"lyrics_text_position\">Vị trí lời bài hát</string>\n    <string name=\"left\">Trái</string>\n    <string name=\"center\">Giữa</string>\n    <string name=\"right\">Phải</string>\n    <string name=\"content\">Nội dung</string>\n    <string name=\"login\">Sự đăng nhập</string>\n    <string name=\"content_language\">Nội dung ngôn ngữ mặc định</string>\n    <string name=\"content_country\">Nội dung quốc gia mặc định</string>\n    <string name=\"system_default\">Mặc định hệ thống</string>\n    <string name=\"enable_proxy\">Bật Proxy</string>\n    <string name=\"proxy_type\">Loại Proxy</string>\n    <string name=\"proxy_url\">Liên kết Proxy</string>\n    <string name=\"restart_to_take_effect\">Khởi động lại để áp dụng</string>\n    <string name=\"player_and_audio\">Trình phát và âm thanh</string>\n    <string name=\"audio_quality\">Chất lượng âm thanh</string>\n    <string name=\"audio_quality_auto\">Tự động</string>\n    <string name=\"audio_quality_high\">Cao</string>\n    <string name=\"audio_quality_low\">Thấp</string>\n    <string name=\"persistent_queue\">Cố định hàng đợi</string>\n    <string name=\"skip_silence\">Bỏ qua khoảng lặng</string>\n    <string name=\"audio_normalization\">Chuẩn hoá âm lượng</string>\n    <string name=\"equalizer\">Bộ chỉnh âm</string>\n    <string name=\"storage\">Bộ nhớ</string>\n    <string name=\"cache\">Bộ nhớ đệm</string>\n    <string name=\"image_cache\">Bộ nhớ đệm hình ảnh</string>\n    <string name=\"song_cache\">Bộ nhớ đệm bài hát</string>\n    <string name=\"max_cache_size\">Kích thước bộ nhớ đệm tối đa</string>\n    <string name=\"unlimited\">Không giới hạn</string>\n    <string name=\"clear_all_downloads\">Xoá tất cả các mục đã tải xuống</string>\n    <string name=\"max_image_cache_size\">Kích thước bộ nhớ đệm hình ảnh tối đa</string>\n    <string name=\"clear_image_cache\">Xóa bộ nhớ đệm hình ảnh</string>\n    <string name=\"max_song_cache_size\">Kích thước bộ nhớ đệm bài hát tối đa</string>\n    <string name=\"clear_song_cache\">Xóa bộ nhớ đệm bài hát</string>\n    <string name=\"size_used\">Đã sử dụng %s</string>\n    <string name=\"privacy\">Quyền riêng tư</string>\n    <string name=\"pause_listen_history\">Tạm dừng lịch sử nghe</string>\n    <string name=\"clear_listen_history\">Xóa lịch sử nghe</string>\n    <string name=\"clear_listen_history_confirm\">Bạn có chắc muốn xoá tất cả lịch sử nghe không?</string>\n    <string name=\"pause_search_history\">Tạm dừng lịch sử tìm kiếm</string>\n    <string name=\"clear_search_history\">Xóa lịch sử tìm kiếm</string>\n    <string name=\"clear_search_history_confirm\">Bạn có chắc muốn xoá tất cả lịch sử tìm kiếm?</string>\n    <string name=\"enable_kugou\">Bật nhà cung cấp lời bài hát KuGou</string>\n    <string name=\"backup_restore\">Sao lưu và khôi phục</string>\n    <string name=\"action_backup\">Sao lưu</string>\n    <string name=\"action_restore\">Khôi phục</string>\n    <string name=\"imported_playlist\">Đã nhập danh sách phát</string>\n    <string name=\"backup_create_success\">Đã tạo bản sao lưu thành công</string>\n    <string name=\"backup_create_failed\">Không thể tạo bản sao lưu</string>\n    <string name=\"restore_failed\">Không thể khôi phục bản sao lưu</string>\n    <string name=\"about\">Giới thiệu</string>\n    <string name=\"app_version\">Phiên bản ứng dụng</string>\n    <string name=\"new_version_available\">Phiên bản mới có sẵn</string>\n    <string name=\"translation_models\">Mô hình dịch thuật</string>\n    <string name=\"clear_translation_models\">Xoá mô hình dịch thuật</string>\n    <string name=\"remove_from_playlist\">Xoá khỏi danh sách phát</string>\n    <string name=\"duplicates\">Bản sao</string>\n    <string name=\"skip_duplicates\">Bỏ qua các bản sao</string>\n    <string name=\"add_anyway\">Vẫn cứ thêm vào</string>\n    <string name=\"duplicates_description_single\">Bài hát đã có trong danh sách phát của bạn</string>\n    <string name=\"duplicates_description_multiple\">%d bài hát đã có trong danh sách phát của bạn</string>\n    <string name=\"remove_download_playlist_confirm\">Bạn có thực sự muốn xóa tất cả \\\"%s\\\" bài hát trong danh sách phát khỏi bộ nhớ Bài hát được tải xuống không?</string>\n    <string name=\"delete_playlist_confirm\">Bạn có thực sự muốn xóa \\\"%s\\\" danh sách phát không?</string>\n    <string name=\"not_logged_in\">Chưa đăng nhập</string>\n    <string name=\"enable_lrclib\">Bật nhà cung cấp lời bài hát LrcLib</string>\n    <string name=\"discord_integration\">Tích hợp Discord</string>\n    <string name=\"dismiss\">Bỏ qua</string>\n    <string name=\"options\">Tùy chọn</string>\n    <string name=\"preview\">Xem trước</string>\n    <string name=\"login_failed\">Đăng nhập không thành công</string>\n    <string name=\"action_logout\">Đăng xuất</string>\n    <string name=\"enable_discord_rpc\">Kích hoạt Rich Presence</string>\n    <string name=\"sided\">Cạnh bên</string>\n    <string name=\"player_text_alignment\">Căn chỉnh văn bản của trình phát</string>\n    <string name=\"hide_explicit\">Ẩn nội dung phản cảm</string>\n    <string name=\"forgotten_favorites\">Những mục yêu thích bị lãng quên</string>\n    <string name=\"keep_listening\">Tiếp tục nghe</string>\n    <string name=\"library_song_empty\">Bài hát trong thư viện sẽ hiển thị ở đây</string>\n    <string name=\"library_artist_empty\">Nghệ sĩ thư viện sẽ xuất hiện ở đây</string>\n    <string name=\"library_album_empty\">Album thư viện sẽ hiển thị ở đây</string>\n    <string name=\"library_playlist_empty\">Danh sách phát của bạn sẽ hiển thị ở đây</string>\n    <string name=\"other_versions\">Các phiên bản khác</string>\n    <string name=\"tempo_and_pitch\">Nhịp độ và Cao độ</string>\n    <string name=\"player\">Trình phát</string>\n    <string name=\"player_slider_style\">Kiểu thanh trượt của trình phát</string>\n    <string name=\"default_\">Mặc định</string>\n    <string name=\"squiggly\">Lượn sóng</string>\n    <string name=\"grid_cell_size\">Kích thước ô lưới</string>\n    <string name=\"small\">Nhỏ</string>\n    <string name=\"misc\">Khác</string>\n    <string name=\"big\">Lớn</string>\n    <string name=\"persistent_queue_desc\">Khôi phục hàng đợi cuối cùng của bạn khi ứng dụng bắt đầu</string>\n    <string name=\"queue\">Hàng đợi</string>\n    <string name=\"auto_load_more\">Tự động tải thêm bài hát</string>\n    <string name=\"stop_music_on_task_clear\">Dừng nhạc khi đóng ứng dụng</string>\n    <string name=\"disable_screenshot\">Vô hiệu ảnh chụp màn hình</string>\n    <string name=\"auto_skip_next_on_error\">Tự động chuyển sang bài hát tiếp theo khi xảy ra lỗi</string>\n    <string name=\"your_youtube_playlists\">Danh sách phát YouTube của bạn</string>\n    <string name=\"similar_to\">Tương tự</string>\n    <string name=\"auto_load_more_desc\">Tự động thêm nhiều bài hát hơn khi hàng đợi kết thúc, nếu có thể</string>\n    <string name=\"add_all_to_library\">Thêm tất cả vào thư viện</string>\n    <string name=\"remove_all_from_library\">Xóa tất cả khỏi thư viện</string>\n    <string name=\"remove_from_queue\">Xóa khỏi hàng đợi</string>\n    <string name=\"action_remove_like_all\">Xóa tất cả lượt thích</string>\n    <string name=\"action_like_all\">Thích tất cả</string>\n    <string name=\"listen_history\">Lịch sử nghe</string>\n    <string name=\"theme\">Chủ đề</string>\n    <string name=\"auto_skip_next_on_error_desc\">Đảm bảo trải nghiệm phát lại liên tục của bạn</string>\n    <string name=\"search_history\">Lịch sử tìm kiếm</string>\n    <string name=\"disable_screenshot_desc\">Khi tùy chọn này được bật, ảnh chụp màn hình và chế độ xem ứng dụng trong mục Gần đây sẽ bị vô hiệu hóa.</string>\n    <string name=\"discord_information\">Metrolist sử dụng thư viện KizzyRPC để thiết lập trạng thái tài khoản Discord của bạn. Điều này liên quan đến việc sử dụng kết nối Discord Gateway, có thể được coi là vi phạm TOS của Discord. Tuy nhiên, không có trường hợp nào được biết đến về việc tài khoản người dùng bị đình chỉ vì lý do này. Sử dụng theo rủi ro của riêng bạn. \\n \\nMetrolist sẽ chỉ trích xuất mã thông báo của bạn và mọi thứ khác được lưu trữ cục bộ.</string>\n    <string name=\"use_login_for_browse\">Đăng nhập để xem nội dung</string>\n    <string name=\"use_login_for_browse_desc\">Điều này có thể ảnh hưởng đến nội dung bạn nhìn thấy, ví dụ như hiện những album dành riêng cho premium nếu bạn đăng nhập với tài khoản Premium</string>\n    <string name=\"action_login\">Đăng nhập</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-wae/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    </resources>\n"
  },
  {
    "path": "app/src/main/res/values-zh-rCN/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">本地</string>\n    <string name=\"remote_history\">远程</string>\n    <string name=\"charts\">排行榜</string>\n    <string name=\"back_button_desc\">返回</string>\n    <string name=\"album_cover_desc\">专辑封面</string>\n    <string name=\"top_music_videos\">热门音乐视频</string>\n    <string name=\"trending\">热门</string>\n    <string name=\"weeks\">周</string>\n    <string name=\"months\">月</string>\n    <string name=\"years\">年</string>\n    <string name=\"liked\">已点赞</string>\n    <string name=\"offline\">已下载</string>\n    <string name=\"my_top\">我的最爱</string>\n    <string name=\"cached_playlist\">已缓存</string>\n    <string name=\"sync_playlist\">同步播放列表</string>\n    <string name=\"generating_image\">生成图片</string>\n    <string name=\"please_wait\">请稍候</string>\n    <string name=\"cancel\">取消</string>\n    <string name=\"share_as_text\">以文本形式分享</string>\n    <string name=\"share_as_image\">以图片形式分享</string>\n    <string name=\"max_selection_limit\">最大选择限制</string>\n    <string name=\"share_selected\">分享已选内容</string>\n    <string name=\"customize_colors\">自定义颜色</string>\n    <string name=\"text_color\">文本颜色</string>\n    <string name=\"secondary_text_color\">次要文本颜色</string>\n    <string name=\"background_color\">背景颜色</string>\n    <string name=\"remove_from_cache\">从缓存中删除</string>\n    <string name=\"copy_link\">复制链接</string>\n    <string name=\"select\">全选</string>\n    <string name=\"like_all\">全部点赞</string>\n    <string name=\"link_copied\">链接已复制到剪贴板</string>\n    <string name=\"lyrics\">歌词</string>\n    <string name=\"already_in_playlist\">已在播放列表中：</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"other\">%d 次</item>\n    </plurals>\n    <string name=\"similar_content\">相似内容</string>\n    <string name=\"player_background_style\">播放器背景样式</string>\n    <string name=\"follow_theme\">跟随主题</string>\n    <string name=\"gradient\">渐变</string>\n    <string name=\"player_background_blur\">模糊</string>\n    <string name=\"player_buttons_style\">播放器按钮颜色</string>\n    <string name=\"default_style\">默认</string>\n    <string name=\"enable_swipe_thumbnail\">启用滑动手势切换歌曲</string>\n    <string name=\"swipe_song_to_add\">向左滑动以添加到待播列表，向右滑动歌曲以下一首播放</string>\n    <string name=\"slim\">纤细</string>\n    <string name=\"slim_navbar\">更细的底部导航栏</string>\n    <string name=\"show_cached_playlist\">显示“已缓存”播放列表</string>\n    <string name=\"advanced_login\">使用令牌登录</string>\n    <string name=\"token_hidden\">点击显示令牌</string>\n    <string name=\"token_shown\">再次点击以复制或编辑</string>\n    <string name=\"token_adv_login_description\">这是高级登录方法。作为网页门户的替代方案，您可以直接在此处输入或更新您的登录令牌。例如，这可加快在多台设备上的登录速度。请注意，应用无法解析的任何无效令牌格式均不会被接受</string>\n    <string name=\"general\">常规</string>\n    <string name=\"proxy\">代理</string>\n    <string name=\"set_quick_picks\">设置猜你喜欢</string>\n    <string name=\"last_song_listened\">基于上次听的歌曲</string>\n    <string name=\"enable_similar_content\">启用相似内容</string>\n    <string name=\"similar_content_desc\">在待播列表快结束时，自动添加更多相似歌曲</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"app_language\">应用语言</string>\n    <string name=\"auto_download_on_like\">自动下载已点赞的歌曲</string>\n    <string name=\"auto_download_on_like_desc\">当你点赞某首歌曲时，系统会自动为你下载该歌曲</string>\n    <string name=\"clear_song_cache_dialog\">您确定要清除所有缓存的歌曲吗？</string>\n    <string name=\"clear_downloads_dialog\">您确定要清除所有下载内容吗？</string>\n    <string name=\"not_logged_in_youtube\">未登录 YouTube</string>\n    <string name=\"default_links\">打开支持的链接</string>\n    <string name=\"release_notes\">发行说明</string>\n    <string name=\"all_time\">所有时间</string>\n    <string name=\"past_week\">过去一周</string>\n    <string name=\"past_month\">过去一个月</string>\n    <string name=\"past_year\">过去一年</string>\n    <string name=\"top_length\">我的热门列表长度</string>\n    <string name=\"description\">描述</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"other\">%d 秒</item>\n    </plurals>\n    <string name=\"continuous\">连续</string>\n    <string name=\"allows_for_sync_witch_youtube\">注意：此功能可与 YouTube Music 进行同步。此设置后续无法更改。</string>\n    <string name=\"sync_disabled\">同步已禁用</string>\n    <string name=\"share_lyrics\">分享歌词</string>\n    <string name=\"views\">浏览量</string>\n    <string name=\"show_downloaded_playlist\">显示“已下载”播放列表</string>\n    <string name=\"past_24_hours\">过去 24 小时</string>\n    <string name=\"show_top_playlist\">显示“我的最爱”播放列表</string>\n    <string name=\"open_app_settings_error\">无法打开应用设置</string>\n    <string name=\"information\">信息</string>\n    <string name=\"dislike_all\">倒赞所有</string>\n    <string name=\"sort_by_last_updated\">更新日期</string>\n    <string name=\"lyrics_click_change\">点击更换歌词</string>\n    <string name=\"auto_playlists\">自动播放列表</string>\n    <string name=\"show_liked_playlist\">显示“已点赞”播放列表</string>\n    <string name=\"default_lib_chips\">更改默认库标签</string>\n    <string name=\"history_duration\">历史持续长度</string>\n    <string name=\"likes\">点赞</string>\n    <string name=\"dislikes\">倒赞</string>\n    <string name=\"now_playing\">正在播放</string>\n    <string name=\"new_player_design\">新的播放器外观</string>\n    <string name=\"new_mini_player_design\">新的播放控件外观</string>\n    <string name=\"lyrics_auto_scroll\">歌词自动滚动</string>\n    <string name=\"lyrics_romanize_japanese\">使用日文（罗马音）</string>\n    <string name=\"lyrics_romanize_korean\">使用韩文（罗马音）</string>\n    <string name=\"yt_sync\">自动与账号同步</string>\n    <string name=\"more_content\">更多内容</string>\n    <string name=\"import_online\">导入 M3U 格式的播放列表</string>\n    <string name=\"import_csv\">导入 CSV 格式的播放列表</string>\n    <string name=\"playlist_add_local_to_synced_note\">注意：不支持将本地歌曲添加到同步/远程播放列表，其他组合均有效</string>\n    <string name=\"swipe_sensitivity\">播放控件灵敏度</string>\n    <string name=\"clear_image_cache_dialog\">您确定要清除所有缓存的图片吗？</string>\n    <string name=\"disable\">禁用</string>\n    <string name=\"subscribe\">订阅</string>\n    <string name=\"subscribed\">已订阅</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"close\">关闭</string>\n    <string name=\"hide_player_thumbnail\">隐藏专辑封面</string>\n    <string name=\"hide_player_thumbnail_desc\">在播放器中将专辑封面替换为应用图标</string>\n    <string name=\"seek_forward_dynamic\">快进+%1$d 秒</string>\n    <string name=\"seek_backward_dynamic\">快退-%1$d 秒</string>\n    <string name=\"seek_seconds_addup\">渐进式快进/快退</string>\n    <string name=\"disable_load_more_when_repeat_all\">如果循环播放则停止加载更多歌曲</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">循环播放模式启用后，不会自动加载更多歌曲与相似内容</string>\n    <string name=\"settings_section_ui\">用户界面</string>\n    <string name=\"settings_section_privacy\">安全与隐私</string>\n    <string name=\"settings_section_player_content\">播放器与内容</string>\n    <string name=\"settings_section_storage\">储存与数据</string>\n    <string name=\"settings_section_system\">系统与关于</string>\n    <string name=\"seek_seconds_addup_description\">启用后，每次快进/快退都额外增加5秒</string>\n    <string name=\"starting_radio\">正在打开电台</string>\n    <string name=\"config_proxy\">配置代理</string>\n    <string name=\"proxy_username\">代理用户名</string>\n    <string name=\"proxy_password\">代理密码</string>\n    <string name=\"enable_authentication\">启用身份验证</string>\n    <string name=\"lyrics_romanization_cyrillic\">罗马音设置</string>\n    <string name=\"lyrics_romanize_title\">罗马化</string>\n    <string name=\"lyrics_romanization\">歌词罗马化</string>\n    <string name=\"lyrics_romanize_russian\">使用俄文（罗马音）</string>\n    <string name=\"lyrics_romanize_ukrainian\">使用乌克兰语（罗马音）</string>\n    <string name=\"lyrics_romanize_belarusian\">使用白俄罗斯语（罗马音）</string>\n    <string name=\"lyrics_romanize_kyrgyz\">使用柯尔克孜语（罗马音）</string>\n    <string name=\"lyrics_romanize_serbian\">使用塞尔维亚语（罗马音）</string>\n    <string name=\"lyrics_romanize_bulgarian\">使用保加利亚语（罗马音）</string>\n    <string name=\"line_by_line_option_title\">实验性：逐行检测语言</string>\n    <string name=\"line_by_line_option_desc\">罗马音将会按行检测，而不是检测整首歌曲。</string>\n    <string name=\"line_by_line_dialog_title\">您确定吗？</string>\n    <string name=\"line_by_line_dialog_desc\">这是一个不稳定的实验性功能。\\n\\n默认情况下，语言会根据整首歌曲来判断，但启用该选项后，将会按行检测语言。这样可以支持多语言歌曲，但检测出的语言可能并非总是正确（例如，如果一行乌克兰语歌词没有包含任何乌克兰语特有字母，可能会被识别并罗马化为俄语）。\\n\\n如果你没有遇到相关问题，建议保持该选项关闭。</string>\n    <string name=\"romanize_current_track\">罗马化当前曲目</string>\n    <string name=\"edit_playlist_cover\">编辑播放列表封面</string>\n    <string name=\"edit_playlist_cover_note\">提示：您的 YouTube Music 账号必须绑定电话号码并通过验证才能更改播放列表封面。</string>\n    <string name=\"edit_playlist_cover_note_wait\">选择图片后，请等待片刻，新封面就会出现在您的播放列表中。</string>\n    <string name=\"choose_from_library\">从库中选择</string>\n    <string name=\"remove_custom_image\">删除自定义图片</string>\n    <string name=\"uploaded_playlist\">已上传</string>\n    <string name=\"filter_uploaded\">已上传</string>\n    <string name=\"show_uploaded_playlist\">显示“已上传”播放列表</string>\n    <string name=\"discord_use_details\">使用详细信息代替状态</string>\n    <string name=\"discord_use_details_description\">突出显示歌曲标题，而非音乐人名称</string>\n    <string name=\"updater\">更新器</string>\n    <string name=\"check_for_updates\">自动检查更新</string>\n    <string name=\"update_notifications\">启用更新通知</string>\n    <string name=\"update_available_title\">有可用更新</string>\n    <string name=\"update_channel_desc\">新版本通知</string>\n    <string name=\"audio_offload_description\">开启省电播放模式。关闭后可能会增加耗电量，如果您遇到音频播放或后处理相关的问题，关闭它可能会有所帮助</string>\n    <string name=\"update_channel_name\">应用更新</string>\n    <string name=\"audio_offload\">开启省电播放</string>\n    <string name=\"integrations\">集成</string>\n    <string name=\"username\">用户名</string>\n    <string name=\"password\">密码</string>\n    <string name=\"lastfm_integration\">Last.fm 集成</string>\n    <string name=\"lyrics_romanize_macedonian\">使用马其顿语（罗马音）</string>\n    <string name=\"enable_scrobbling\">启用记录</string>\n    <string name=\"lastfm_now_playing\">发送正在播放的歌曲</string>\n    <string name=\"scrobbling_configuration\">Scrobbling记录设置</string>\n    <string name=\"scrobble_min_track_duration\">记录比…长的歌曲</string>\n    <string name=\"scrobble_delay_minutes\">记录所需已听时间（分钟）</string>\n    <string name=\"scrobble_delay_percent\">记录所需已听占比</string>\n    <string name=\"swipe_song_to_remove\">滑动歌曲将其从播放列表中删除</string>\n    <string name=\"download_playlist_desc\">下载所有歌曲以离线播放</string>\n    <string name=\"remove_download_playlist_desc\">从此播放列表中删除所有已下载的歌曲</string>\n    <string name=\"download_in_progress_desc\">下载正在进行</string>\n    <string name=\"share_playlist_desc\">分享此播放列表</string>\n    <string name=\"delete_playlist_desc\">永久删除此播放列表</string>\n    <string name=\"sync_playlist_desc\">将播放列表与 YouTube Music 同步</string>\n    <string name=\"primary_color_style\">主色</string>\n    <string name=\"tertiary_color_style\">第三色</string>\n    <string name=\"lyrics_glow_effect\">启用歌词发光效果</string>\n    <string name=\"lyrics_glow_effect_desc\">为动态歌词添加发光动画和弹跳效果</string>\n    <string name=\"enable_better_lyrics\">启用 Better Lyrics 提供歌词</string>\n    <string name=\"enable_better_lyrics_desc\">支持任意歌曲的逐音节同步歌词，适用于卡拉OK</string>\n    <string name=\"auto_scroll\">对齐歌词</string>\n    <string name=\"shuffle_playlist_first\">随机播放列表或专辑</string>\n    <string name=\"shuffle_playlist_first_desc\">随机播放时，先播放原播放列表/专辑中的所有歌曲，然后再播放内容相似的歌曲</string>\n    <string name=\"show_wrapped_card\">显示年度总结卡片</string>\n    <string name=\"lyrics_romanize_chinese\">使用中文（罗马音）</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"google_cast_description\">启用向 Chromecast 和其他支持投屏功能的设备投屏音频</string>\n    <string name=\"last_fm_send_likes\">发送点赞/取消点赞</string>\n    <string name=\"last_fm_send_likes_description\">Last.fm上的歌曲在Metrolist上被点赞/取消点赞时,也会被标记为喜欢/不喜欢</string>\n    <string name=\"logging_in\">正在登录…</string>\n    <string name=\"hide_video_songs\">隐藏带视频歌曲</string>\n    <string name=\"details_desc\">查看歌曲信息</string>\n    <string name=\"edit_desc\">更改标题或音乐人</string>\n    <string name=\"start_radio_desc\">基于此歌曲创建一个电台</string>\n    <string name=\"play_next_desc\">添加到待播列表顶部</string>\n    <string name=\"add_to_queue_desc\">添加到待播列表底部</string>\n    <string name=\"add_to_library_desc\">保存到您的媒体库</string>\n    <string name=\"download_desc\">提供离线播放功能</string>\n    <string name=\"add_to_playlist_desc\">添加到您的播放列表</string>\n    <string name=\"refetch_desc\">从 YouTube Music 获取最新元数据</string>\n    <string name=\"share_desc\">展示此歌曲的链接</string>\n    <string name=\"delete_desc\">完全移除此歌曲</string>\n    <string name=\"advanced_desc\">改变歌曲的节奏和音调</string>\n    <string name=\"equalizer_desc\">调整音频均衡器</string>\n    <string name=\"enable_dynamic_icon\">开启动态图标</string>\n    <string name=\"mini_player\">迷你播放器</string>\n    <string name=\"pure_black_mini_player\">纯黑迷你播放器</string>\n    <string name=\"cache_size_warning_title\">且慢!</string>\n    <string name=\"cache_size_warning_message\">您选择的缓存大小限制小于应用当前使用的大小 (%1$s)。如果继续，应用可能会删除一些缓存的 %2$s 以匹配新的限制。是否仍然继续？</string>\n    <string name=\"cache_size_warning_confirm\">继续</string>\n    <string name=\"lyrics_animation_style\">逐字动画风格</string>\n    <string name=\"none\">无</string>\n    <string name=\"fade\">褪色</string>\n    <string name=\"glow\">生长</string>\n    <string name=\"slide\">滑动</string>\n    <string name=\"karaoke\">卡拉OK</string>\n    <string name=\"apple_music_style\">Apple Music</string>\n    <string name=\"lyrics_text_size\">歌词文字大小</string>\n    <string name=\"lyrics_line_spacing\">歌词行间距</string>\n    <string name=\"album_art_for\">%s 的专辑封面</string>\n    <string name=\"wrapped_total_albums_title\">你已经听过了</string>\n    <string name=\"wrapped_total_albums_subtitle\">独特的专辑</string>\n    <string name=\"wrapped_top_album_title\">您最喜欢的专辑是</string>\n    <string name=\"wrapped_playlist_ready\">您的个人播放列表已准备就绪</string>\n    <string name=\"wrapped_top_5_albums_title\">您最喜欢的五张专辑是</string>\n    <string name=\"wrapped_album_listening_time\">您已收听此专辑 %d 分钟</string>\n    <string name=\"wrapped_album_listening_time_minutes\">%d 分钟</string>\n    <string name=\"wrapped_no_data\">没有数据</string>\n    <string name=\"wrapped_top_5_artists_title\">您今年最喜欢的音乐人</string>\n    <string name=\"wrapped_artist_listening_time\">%d 分钟</string>\n    <string name=\"wrapped_top_5_songs_title\">您今年最喜欢的歌曲</string>\n    <string name=\"wrapped_top_song_album_art_content_description\">专辑封面</string>\n    <string name=\"wrapped_top_artist_title\">您今年心中的最佳音乐人是</string>\n    <string name=\"wrapped_top_artist_image_content_description\">最喜欢的音乐人图片</string>\n    <string name=\"wrapped_top_artist_listening_time\">您听了他们%d分钟</string>\n    <string name=\"wrapped_top_song_title\">您播放次数最多的歌曲是</string>\n    <string name=\"wrapped_top_song_listening_time\">您已经听了%d分钟</string>\n    <string name=\"wrapped_total_artists_title\">您听了</string>\n    <string name=\"wrapped_total_artists_subtitle\">独特的音乐人</string>\n    <string name=\"wrapped_total_songs_title\">您听了</string>\n    <string name=\"wrapped_total_songs_subtitle\">独特的歌曲</string>\n    <string name=\"wrapped_intro_title\">METROLIST</string>\n    <string name=\"wrapped_intro_subtitle\">是时候看看您最近听了什么了</string>\n    <string name=\"wrapped_intro_button\">让我们开始吧!</string>\n    <string name=\"wrapped_logo_content_description\">Metrolist 徽标</string>\n    <string name=\"wrapped_year\">2025</string>\n    <string name=\"wrapped_ready_title\">您的年度总结已准备好！</string>\n    <string name=\"wrapped_ready_subtitle\">是时候看看您今年最喜欢什么了.</string>\n    <string name=\"wrapped_thank_you\">谢谢您的聆听</string>\n    <string name=\"wrapped_special_thanks\">特别感谢 MO Agamy 创建了 Metrolist</string>\n    <string name=\"wrapped_close\">关闭年度总结</string>\n    <string name=\"wrapped_playlist_title\">您的 %s 年度总结</string>\n    <string name=\"wrapped_create_playlist\">创建播放列表</string>\n    <string name=\"wrapped_playlist_saved\">播放列表已保存</string>\n    <string name=\"casting_to\">转换为 %s</string>\n    <string name=\"progress_percent\">进度 %s%%</string>\n    <string name=\"listening_to_metrolist\">收听 Metrolist</string>\n    <string name=\"open\">打开</string>\n    <string name=\"failed_to_create_image\">无法创建图片：%s</string>\n    <string name=\"copied_title\">已复制标题</string>\n    <string name=\"copied_artist\">已复制音乐人</string>\n    <string name=\"error_playing\">播放错误</string>\n    <string name=\"failed_to_parse_proxy\">无法解析代理网址。</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"other\">%d 个配置文件</item>\n    </plurals>\n    <string name=\"equalizer_header\">均衡器</string>\n    <string name=\"no_profiles\">无均衡器配置文件</string>\n    <string name=\"import_profile\">导入配置</string>\n    <string name=\"eq_disabled\">已禁用</string>\n    <string name=\"delete_profile_desc\">删除配置</string>\n    <string name=\"delete_profile_confirmation\">您确定要删除 %1$s 吗？此操作无法撤销。</string>\n    <string name=\"error_file_read\">无法读取文件</string>\n    <string name=\"error_file_open\">无法打开文件：%1$s</string>\n    <string name=\"import_error_title\">导入错误</string>\n    <plurals name=\"band_count\">\n        <item quantity=\"other\">%d 乐队</item>\n    </plurals>\n    <string name=\"wavy\">波浪</string>\n    <string name=\"pause_music_when_media_is_muted\">在静音时暂停播放</string>\n    <string name=\"enable_simpmusic\">启用 SimpMusic 提供歌词</string>\n    <string name=\"enable_simpmusic_desc\">自动从 Musixmatch 和 YouTube Transcript 获取歌词</string>\n    <string name=\"system_equalizer\">系统均衡器</string>\n    <string name=\"album_art\">专辑封面</string>\n    <string name=\"no_song_playing\">没有音乐正在播放</string>\n    <string name=\"tap_to_open\">点击打开 Metrolist</string>\n    <string name=\"previous\">上一曲</string>\n    <string name=\"play_pause\">播放/暂停</string>\n    <string name=\"next\">下一曲</string>\n    <string name=\"like\">喜欢</string>\n    <string name=\"widget_description\">有播放控制的音乐播放器微件</string>\n    <string name=\"turntable_widget_description\">带有播放和点赞控制按钮的播放控件</string>\n    <string name=\"about_artist\">关于</string>\n    <string name=\"show_more\">展开</string>\n    <string name=\"show_less\">收起</string>\n    <string name=\"artist_page_settings\">音乐人页面</string>\n    <string name=\"show_artist_description\">显示音乐人简介</string>\n    <string name=\"show_artist_subscriber_count\">显示订阅者数量</string>\n    <string name=\"show_artist_monthly_listeners\">每月听众人数</string>\n    <string name=\"skip_silence_desc\">快进跳过歌曲的静音部分</string>\n    <string name=\"skip_silence_instant\">立即跳过静音</string>\n    <string name=\"skip_silence_instant_desc\">在静音片段快进而非加速播放</string>\n    <string name=\"remember_shuffle_and_repeat\">记住随机与循环设置</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">重启应用时，记住随机播放和重复播放模式</string>\n    <string name=\"lyrics_offset\">歌词偏移</string>\n    <string name=\"persistent_shuffle_title\">持续随机播放</string>\n    <string name=\"persistent_shuffle_desc\">开始播放新歌曲或播放列表时，保持随机播放功能开启</string>\n    <string name=\"error_playback_failed\">播放失败</string>\n    <string name=\"error_eq_apply_failed\">无法应用均衡器配置：%1$s</string>\n    <string name=\"error_title\">错误</string>\n    <string name=\"crop_album_art\">裁剪专辑封面</string>\n    <string name=\"crop_album_art_desc\">通过裁剪视频缩略图强制使用正方形宽高比</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">当播放页面最大化时保持屏幕打开</string>\n    <string name=\"listen_together\">一起听</string>\n    <string name=\"listen_together_server_url\">服务器网址</string>\n    <string name=\"listen_together_username\">用户名</string>\n    <string name=\"listen_together_connected\">已连接</string>\n    <string name=\"listen_together_reconnecting\">正在重新连接…</string>\n    <string name=\"listen_together_disconnected\">已断开连接</string>\n    <string name=\"listen_together_connecting\">正在连接…</string>\n    <string name=\"listen_together_error\">连接错误</string>\n    <string name=\"listen_together_create_room\">创建房间</string>\n    <string name=\"listen_together_create_room_desc\">创建房间并与朋友分享邀请码</string>\n    <string name=\"listen_together_join_room\">加入房间</string>\n    <string name=\"listen_together_room_code\">房间邀请码</string>\n    <string name=\"listen_together_you_are_host\">您是房主</string>\n    <string name=\"listen_together_you_are_guest\">您是访客</string>\n    <string name=\"listen_together_join_requests\">加入请求</string>\n    <string name=\"listen_together_view_logs\">查看日志</string>\n    <string name=\"listen_together_view_logs_desc\">调试连接和消息</string>\n    <string name=\"listen_together_logs\">连接日志</string>\n    <string name=\"listen_together_no_logs\">还没有日志</string>\n    <string name=\"listen_together_description\">跨越距离，与好友实时共赏音乐。创建房间成为房主,或使用邀请码加入现有房间。</string>\n    <string name=\"listen_together_background_disconnect_note\">注意:如果在房间内没有播放音乐的情况下创建房间,然后切换到其他应用程序,则可能会断开连接.</string>\n    <string name=\"listen_together_not_configured\">一起听尚未配置。请在“设置”→“集成”→“一起听”中设置服务器网址。</string>\n    <string name=\"listen_together_suggestion_received\">%1$s 请求 %2$s</string>\n    <string name=\"listen_together_suggestion_sent\">推荐已发送给房主!</string>\n    <string name=\"listen_together_join_request_notification\">%1$s 人想要加入房间</string>\n    <string name=\"listen_together_notification_channel_name\">一起听</string>\n    <string name=\"listen_together_notification_channel_desc\">“一起听”活动通知</string>\n    <string name=\"listen_together_room_created\">房间创建成功 编号:%s</string>\n    <string name=\"listen_together_cannot_edit_username_in_room\">在房间内无法编辑用户名</string>\n    <string name=\"waiting_for_approval\">等待房主批准</string>\n    <string name=\"invalid_room_code\">无效的房间邀请码</string>\n    <string name=\"join_request_denied\">加入请求被拒绝</string>\n    <string name=\"join_existing_room\">加入现有房间</string>\n    <string name=\"room_code\">房间邀请码</string>\n    <string name=\"leave_room\">离开房间</string>\n    <string name=\"join_room\">加入</string>\n    <string name=\"create_room\">创建</string>\n    <string name=\"joining_room\">正在加入房间 %s…</string>\n    <string name=\"creating_room\">正在创建房间…</string>\n    <string name=\"connect\">连接</string>\n    <string name=\"disconnect\">断开连接</string>\n    <string name=\"create\">创建</string>\n    <string name=\"join\">加入</string>\n    <string name=\"approve\">批准</string>\n    <string name=\"reject\">拒绝</string>\n    <string name=\"clear\">清除</string>\n    <string name=\"copy\">复制</string>\n    <string name=\"copied_to_clipboard\">已复制到剪贴板</string>\n    <string name=\"not_set\">未设置</string>\n    <string name=\"hosting_room\">正主持房间</string>\n    <string name=\"in_room\">在房间内</string>\n    <string name=\"pending_requests\">待处理请求</string>\n    <string name=\"pending_suggestions\">待处理推荐</string>\n    <string name=\"suggest_to_host\">推荐给房主</string>\n    <string name=\"kick_user\">踢出</string>\n    <string name=\"host_label\">房主</string>\n    <string name=\"you_label\">你</string>\n    <string name=\"connected_users\">连接的用户</string>\n    <string name=\"enter_username\">输入用户名</string>\n    <string name=\"error_username_empty\">需要用户名。</string>\n    <string name=\"resync\">重新同步</string>\n    <string name=\"mute\">静音</string>\n    <string name=\"unmute\">取消静音</string>\n    <string name=\"crash_title\">应用崩溃了</string>\n    <string name=\"crash_share_logs\">分享日志</string>\n    <string name=\"crash_description\">发生意外错误。请提供崩溃报告以帮助我们修复问题。</string>\n    <string name=\"crash_share_title\">分享崩溃报告</string>\n    <string name=\"crash_report_subject\">Metrolist 崩溃报告</string>\n    <string name=\"crash_close\">关闭</string>\n    <string name=\"crash_no_log\">没有可用的崩溃日志</string>\n    <string name=\"palette_dynamic\">灵动</string>\n    <string name=\"palette_crimson\">赤红</string>\n    <string name=\"palette_rose\">玫瑰</string>\n    <string name=\"palette_purple\">紫色</string>\n    <string name=\"palette_deep_purple\">深紫</string>\n    <string name=\"palette_indigo\">靛青</string>\n    <string name=\"palette_blue\">蓝色</string>\n    <string name=\"palette_sky_blue\">天蓝</string>\n    <string name=\"palette_cyan\">青色</string>\n    <string name=\"palette_green\">绿色</string>\n    <string name=\"palette_teal\">蓝绿</string>\n    <string name=\"palette_light_green\">亮绿</string>\n    <string name=\"palette_lime\">柠檬</string>\n    <string name=\"palette_yellow\">黄色</string>\n    <string name=\"palette_amber\">琥珀</string>\n    <string name=\"palette_orange\">橙色</string>\n    <string name=\"palette_deep_orange\">深橙</string>\n    <string name=\"palette_brown\">棕色</string>\n    <string name=\"palette_grey\">灰色</string>\n    <string name=\"cd_back\">返回</string>\n    <string name=\"palette_blue_grey\">蓝灰</string>\n    <string name=\"cd_dark_mode\">深色模式</string>\n    <string name=\"cd_system_mode\">系统模式</string>\n    <string name=\"cd_pure_black_mode\">纯黑模式</string>\n    <string name=\"cd_light_mode\">浅色模式</string>\n    <string name=\"cd_palette_item\">样式 %1$s</string>\n    <string name=\"listen_together_choose_server\">选择服务器</string>\n    <string name=\"listen_together_custom_server\">自定义服务器</string>\n    <string name=\"listen_together_use_custom_server\">使用自定义服务器</string>\n    <string name=\"listen_together_auto_approval_joins\">自动批准加入请求</string>\n    <string name=\"listen_together_auto_approval_joins_desc\">自动批准加入请求，无需手动审核</string>\n    <string name=\"copy_code\">复制邀请码</string>\n    <string name=\"manage_user\">管理用户</string>\n    <string name=\"permanently_kick_user\">永久屏蔽</string>\n    <string name=\"permanently_kick_user_desc\">屏蔽此人的加入请求并隐藏其推荐</string>\n    <string name=\"listen_together_blocked_users\">已屏蔽用户</string>\n    <string name=\"listen_together_blocked_users_count\">已屏蔽 %d 位用户</string>\n    <string name=\"listen_together_no_blocked_users\">无已屏蔽的用户</string>\n    <string name=\"unblock\">取消屏蔽</string>\n    <string name=\"not_playing\">未在播放歌曲</string>\n    <string name=\"tap_to_play\">点击打开 Metrolist</string>\n    <string name=\"widget_music_player\">播放器</string>\n    <string name=\"listen_together_sync_volume\">与房主音量保持一致</string>\n    <string name=\"listen_together_sync_volume_desc\">访客音量随房主同步</string>\n    <string name=\"kick_user_desc\">从此会话中移除该人</string>\n    <string name=\"transfer_ownership_desc\">将此人设为房主</string>\n    <string name=\"user_blocked_by_host\">你已被房主拉黑</string>\n    <string name=\"transfer_ownership\">移交所有权</string>\n    <string name=\"widget_turntable\">虚拟打碟台</string>\n    <string name=\"enter_room_code\">输入邀请码</string>\n    <string name=\"listen_together_settings_desc\">配置服务器、用户名等</string>\n    <string name=\"ai_lyrics_translation\">AI歌词翻译</string>\n    <string name=\"ai_translating_lyrics\">正在翻译歌词…</string>\n    <string name=\"ai_lyrics_translated\">歌词已翻译</string>\n    <string name=\"ai_provider\">提供商</string>\n    <string name=\"ai_base_url\">基础URL</string>\n    <string name=\"ai_api_key\">API 密钥</string>\n    <string name=\"ai_model\">模型</string>\n    <string name=\"ai_translation_mode\">翻译模式</string>\n    <string name=\"ai_target_language\">目标语言</string>\n    <string name=\"ai_setup_guide\">API 凭据</string>\n    <string name=\"ai_translation_literal\">翻译</string>\n    <string name=\"ai_api_key_required\">需要 API 密钥</string>\n    <string name=\"ai_error_api_key_required\">需要 API 密钥</string>\n    <string name=\"ai_error_no_lyrics\">没有可翻译的歌词</string>\n    <string name=\"ai_error_lyrics_empty\">无歌词</string>\n    <string name=\"ai_error_language_required\">请选择目标语言</string>\n    <string name=\"ai_error_unexpected\">翻译出现意外错误</string>\n    <string name=\"ai_error_unknown\">发生未知错误</string>\n    <string name=\"ai_error_translation_failed\">翻译失败</string>\n    <string name=\"play_all\">播放全部</string>\n    <string name=\"together\">一起听</string>\n    <string name=\"ai_translation_transcribed\">转录</string>\n    <string name=\"recognize_music\">识别音乐</string>\n    <string name=\"youtube_url_column\">YouTube 网址列（可选）</string>\n    <string name=\"re_listen\">重听</string>\n    <string name=\"clear_recognition_history_confirm\">您确定要清除所有识别历史吗？</string>\n    <string name=\"no_match_found\">未找到匹配</string>\n    <string name=\"delete_from_history\">从历史中删除</string>\n    <string name=\"artist_name_column\">音乐人名称列</string>\n    <string name=\"processing\">正在识别…</string>\n    <string name=\"clear_recognition_history\">清除识别历史</string>\n    <string name=\"map_csv_columns\">映射 CSV 列</string>\n    <string name=\"column_label\">列 %d</string>\n    <string name=\"recognition_error\">识别错误</string>\n    <string name=\"enable_high_refresh_rate_desc\">强制屏幕以最高支持的刷新率运行（例如 120Hz）</string>\n    <string name=\"first_row_is_header\">首行是标题</string>\n    <string name=\"try_again\">重试</string>\n    <string name=\"tap_to_recognize\">点击识别</string>\n    <string name=\"recognition_history\">识别历史</string>\n    <string name=\"enable_high_refresh_rate\">启用高刷新率</string>\n    <string name=\"song_title_column\">歌曲标题列</string>\n    <string name=\"recently_converted\">最近转换</string>\n    <string name=\"importing_csv\">正在导入 CSV</string>\n    <string name=\"play_on_app\">在 Metrolist 上播放</string>\n    <string name=\"listening\">正在听…</string>\n    <string name=\"continue_action\">继续</string>\n    <string name=\"enable\">启用</string>\n    <string name=\"crossfade\">淡入淡出</string>\n    <string name=\"crossfade_desc\">歌曲间淡入淡出</string>\n    <string name=\"crossfade_duration\">淡入淡出时长</string>\n    <string name=\"crossfade_gapless\">对无缝专辑禁用</string>\n    <string name=\"crossfade_gapless_desc\">如果专辑为无缝格式则不淡入淡出</string>\n    <string name=\"crossfade_beta_title\">测试功能</string>\n    <string name=\"crossfade_beta_message\">淡入淡出是一项新功能，可能存在错误。如果您遇到任何问题，请及时反馈。\\n\\n由于技术限制，此功能会禁用省电播放。</string>\n    <string name=\"audio_offload_disabled_by_crossfade\">因淡入淡出已启用而禁用</string>\n    <string name=\"hide_youtube_shorts\">隐藏 YouTube Shorts 短视频</string>\n    <string name=\"listen_together_in_top_bar\">在顶部栏显示“一起听”</string>\n    <string name=\"listen_together_in_top_bar_desc\">在顶部应用栏显示“一起听”，而非在导航栏显示</string>\n    <string name=\"prevent_duplicate_tracks_in_queue\">防止待播列表中出现重复曲目</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">将曲目添加到待播列表时，如果曲目已存在待播列表中，则将其从先前的位置移除</string>\n    <string name=\"ai_translation_literal_desc\">将含义翻译成目标语言</string>\n    <string name=\"ai_translation_transcribed_desc\">将发音转换为目标文字</string>\n    <string name=\"ai_provider_help\">获取 API 密钥</string>\n    <string name=\"ai_provider_openrouter_help\">访问 https://openrouter.ai 获取免费和付费模型</string>\n    <string name=\"ai_provider_openai_help\">访问 https://platform.openai.com/api-keys</string>\n    <string name=\"ai_provider_claude_help\">访问 https://console.anthropic.com/settings/keys</string>\n    <string name=\"ai_provider_gemini_help\">访问 https://aistudio.google.com/apikey</string>\n    <string name=\"ai_provider_perplexity_help\">访问 https://perplexity.ai/settings/api</string>\n    <string name=\"ai_provider_xai_help\">访问 https://console.x.ai</string>\n    <string name=\"ai_provider_deepl_help\">访问 https://deepl.com/pro-api 获取免费和付费密钥</string>\n    <string name=\"ai_deepl_formality\">语气功能</string>\n    <string name=\"ai_deepl_formality_default\">默认</string>\n    <string name=\"ai_deepl_formality_more\">更正式一些</string>\n    <string name=\"ai_deepl_formality_less\">不太正式</string>\n    <string name=\"discord_status\">状态</string>\n    <string name=\"discord_status_online\">在线</string>\n    <string name=\"discord_status_idle\">空闲</string>\n    <string name=\"discord_status_dnd\">请勿打扰</string>\n    <string name=\"discord_buttons\">按钮</string>\n    <string name=\"discord_button_1\">按钮 1</string>\n    <string name=\"discord_button_2\">按钮 2</string>\n    <string name=\"login_successful\">登录成功！</string>\n    <string name=\"discord_information_warning\">此功能使用 KizzyRPC 库连接到 Discord 网关并设置您的 Rich Presence 状态。虽然目前尚未发现因类似使用而导致账号被封禁的情况，但此方法并未获得 Discord 官方支持，并且可能会被视为违反服务条款。您的令牌将在本地提取，绝不会发送至第三方服务器。请自行承担使用风险。</string>\n    <string name=\"discord_activity_type\">活动类型</string>\n    <string name=\"discord_activity_playing\">正在播放</string>\n    <string name=\"discord_activity_listening\">正在听</string>\n    <string name=\"discord_activity_watching\">正在看</string>\n    <string name=\"discord_activity_competing\">正在竞争</string>\n    <string name=\"discord_button_text_variables\">变量：{song_name},{artist_name},{album_name}</string>\n    <string name=\"discord_rpc_preview\">Rich Presence 预览</string>\n    <string name=\"discord_presence\">在场</string>\n    <string name=\"discord_connect_description\">使用 Discord 登录来分享你正在收听的内容</string>\n    <string name=\"discord_playing_metrolist\">播放 Metrolist</string>\n    <string name=\"discord_watching_metrolist\">观看Metrolist</string>\n    <string name=\"discord_competing_metrolist\">参与Metrolist评选</string>\n    <string name=\"discord_activity_name\">活动名称</string>\n    <string name=\"discord_activity_name_description\">自定义活动名称（留空则使用默认值）</string>\n    <string name=\"discord_advanced_mode\">高级模式</string>\n    <string name=\"discord_advanced_mode_description\">显示 Rich Presence 的更多自定义选项</string>\n    <string name=\"resume_on_bluetooth_connect\">蓝牙连接后恢复播放</string>\n    <string name=\"lyrics_romanize_hindi\">使用印地语（罗马音）</string>\n    <string name=\"lyrics_romanize_punjabi\">使用旁遮普语（罗马音）</string>\n    <string name=\"lyrics_romanize_as_main\">将罗马化歌词设为主要显示</string>\n    <string name=\"player_background_solid\">纯色</string>\n    <string name=\"display_density\">显示密度</string>\n    <string name=\"restart\">重启</string>\n    <string name=\"restart_required\">需要重启</string>\n    <string name=\"density_restart_message\">显示密度更改将在重启应用后生效。是否现在要重启？</string>\n    <string name=\"enable_lrclib_desc\">社区驱动的同步歌词数据库</string>\n    <string name=\"enable_kugou_desc\">歌词来自中国热门音乐平台酷狗</string>\n    <string name=\"youtube_music_lyrics_note\">注意：当其他歌词不可用时，会自动显示来自 YouTube Music 的歌词，该来源歌词通常不同步。</string>\n    <string name=\"enable_lyricsplus\">启用 LyricsPlus 提供歌词</string>\n    <string name=\"enable_lyricsplus_desc\">多源同步歌词支持</string>\n    <string name=\"lyrics_provider_selection\">提供商选择</string>\n    <string name=\"lyrics_provider_selection_desc\">选择要启用的歌词提供商</string>\n    <string name=\"lyrics_provider_priority\">歌词提供商优先级</string>\n    <string name=\"lyrics_provider_priority_desc\">拖动以按偏好重新排序提供商。位置越高 -&gt; 优先级越高。</string>\n    <string name=\"changelog\">更新日志</string>\n    <string name=\"changelog_empty\">暂无更新日志</string>\n    <string name=\"github_releases_url\">https://github.com/MetrolistGroup/Metrolist/releases</string>\n    <string name=\"list_bullet\">•</string>\n    <string name=\"view_on_github\">在 GitHub 上查看</string>\n    <string name=\"current_version\">当前版本</string>\n    <string name=\"version_format\">版本：%s</string>\n    <string name=\"update_settings\">更新设置</string>\n    <string name=\"check_for_updates_title\">检查更新</string>\n    <string name=\"check_for_updates_button\">检查更新</string>\n    <string name=\"checking_for_updates\">正在检查更新…</string>\n    <string name=\"latest_version_format\">最新版本：%s</string>\n    <string name=\"hide_changelog\">隐藏更新日志</string>\n    <string name=\"view_changelog\">查看更新日志</string>\n    <string name=\"failed_to_check_updates\">无法检查更新：%s</string>\n    <string name=\"set_as_default\">设为默认</string>\n    <string name=\"sleep_timer_default_set\">睡眠定时器默认设置为 %d 分钟</string>\n    <string name=\"found_in_settings_content\">可在“设置”&gt;“内容”中找到</string>\n    <string name=\"error_episode_save\">无法保存分集</string>\n    <string name=\"error_episode_remove\">无法移除分集</string>\n    <string name=\"error_podcast_subscribe\">无法订阅播客</string>\n    <string name=\"error_podcast_unsubscribe\">无法取消订阅播客</string>\n    <string name=\"widget_recognizer_name\">音乐识别器</string>\n    <string name=\"widget_recognizer_description\">直接从主屏幕识别周围播放的歌曲</string>\n    <string name=\"widget_recognizer_tap_to_search\">点击识别歌曲</string>\n    <string name=\"widget_recognizer_listening\">正在听…</string>\n    <string name=\"widget_recognizer_processing\">正在识别…</string>\n    <string name=\"widget_recognizer_no_match\">未找到匹配，请重试</string>\n    <string name=\"widget_recognizer_error\">识别失败</string>\n    <string name=\"widget_recognizer_error_generic\">发生错误，请重试</string>\n    <string name=\"widget_recognizer_unknown_song\">未知歌曲</string>\n    <string name=\"widget_recognizer_unknown_artist\">未知音乐人</string>\n    <string name=\"widget_recognizer_mic_desc\">识别歌曲</string>\n    <string name=\"widget_recognizer_channel_name\">音乐识别</string>\n    <string name=\"widget_recognizer_channel_desc\">在微件中识别歌曲时显示通知</string>\n    <string name=\"widget_recognizer_notification_text\">正在录音以识别歌曲…</string>\n    <string name=\"importing_playlist\">正在导入播放列表</string>\n    <string name=\"credits_instagram\">Instagram</string>\n    <string name=\"credits_github\">GitHub</string>\n    <string name=\"app_version_info\">%1$s • %2$s</string>\n    <string name=\"credits_view_repo\">查看仓库</string>\n    <string name=\"logout_dialog_title\">保留媒体库数据？</string>\n    <string name=\"logout_dialog_message\">是否要保留您的播放列表和媒体库数据？已下载的歌曲将始终保留。</string>\n    <string name=\"logout_keep\">保留</string>\n    <string name=\"logout_clear\">清除</string>\n    <string name=\"credits_lead_developer\">首席开发者</string>\n    <string name=\"credits_collaborator\">协作者</string>\n    <string name=\"credits_collaborators_section\">协作者</string>\n    <string name=\"credits_license_name\">GNU General Public License v3.0</string>\n    <string name=\"credits_license_desc\">自由开源软件。您可以使用、研究、分享和改进它。</string>\n    <string name=\"credits_discord\">Discord 服务器</string>\n    <string name=\"credits_telegram\">Telegram 频道</string>\n    <string name=\"credits_website\">网站</string>\n    <string name=\"like_what_i_do\">喜欢我的作品？</string>\n    <string name=\"buy_mo_a_coffee\">请我喝杯咖啡</string>\n    <string name=\"community_and_info\">社区与信息</string>\n    <string name=\"metrolist\">METROLIST</string>\n    <string name=\"wanna_play_favorite_song\">想播放他们最喜欢的歌曲吗？</string>\n    <string name=\"yeah\">是的</string>\n    <string name=\"stands_with_palestine\">本项目声援巴勒斯坦 🇵🇸</string>\n    <string name=\"filter_podcasts\">播客</string>\n    <string name=\"view_podcast\">查看播客</string>\n    <string name=\"podcast_channels\">播客频道</string>\n    <string name=\"latest_episodes\">最新分集</string>\n    <string name=\"your_shows\">您的节目</string>\n    <string name=\"new_episodes\">新分集</string>\n    <string name=\"episodes_for_later\">待播分集</string>\n    <string name=\"save_episode_for_later\">保存以稍后收听</string>\n    <string name=\"save_episode_for_later_desc\">添加到“待播分集”播放列表</string>\n    <string name=\"remove_episode_from_saved\">从已保存中移除</string>\n    <string name=\"subscribe_to_podcast\">将播客保存到媒体库</string>\n    <plurals name=\"n_episode\">\n        <item quantity=\"other\">%d 个分集</item>\n    </plurals>\n    <string name=\"restore_confirm_title\">恢复备份？</string>\n    <string name=\"restore_confirm_message\">这将从备份中恢复您的应用数据。</string>\n    <string name=\"restore_account_warning\">恢复后您需要重新登录。以下账号将退出登录：</string>\n    <string name=\"restore\">恢复</string>\n    <string name=\"checking_previous_account\">正在检查先前账号…</string>\n    <string name=\"no_account_found\">未找到账号</string>\n    <string name=\"listen_together_auto_approval_suggestions\">自动批准歌曲推荐</string>\n    <string name=\"listen_together_auto_approval_suggestions_desc\">自动批准访客的歌曲推荐并加入待播列表</string>\n    <string name=\"plays\">播放量</string>\n    <string name=\"speed_dial\">快速访问</string>\n    <string name=\"pin_to_speed_dial\">固定到快速访问</string>\n    <string name=\"unpin_from_speed_dial\">从快速访问取消固定</string>\n    <string name=\"randomize_home_order\">随机排列首页顺序</string>\n    <string name=\"randomize_home_order_desc\">根据优先级随机重新排列首页板块</string>\n    <string name=\"daily_discover_sounds_like\">%1$s 相似风格</string>\n    <string name=\"daily_discover_because_you_listen_to\">因您收听 %1$s</string>\n    <string name=\"daily_discover_similar_to\">%1$s 相似内容</string>\n    <string name=\"daily_discover_based_on\">基于 %1$s</string>\n    <string name=\"daily_discover_for_fans_of\">%1$s 粉丝专享</string>\n    <string name=\"from_the_community\">来自社区</string>\n    <string name=\"filter_episodes\">分集</string>\n    <string name=\"filter_channels\">频道</string>\n    <string name=\"auto_playlist\">自动播放列表</string>\n    <string name=\"downloaded_episodes\">已下载的分集</string>\n    <string name=\"no_subscribed_channels\">无已订阅的频道</string>\n    <string name=\"no_downloaded_episodes\">无已下载的分集</string>\n    <plurals name=\"n_channel\">\n        <item quantity=\"other\">%d 个频道</item>\n    </plurals>\n    <string name=\"view_channel\">查看频道</string>\n    <string name=\"filter_profiles\">个人资料</string>\n    <string name=\"enable_automatic_sleeptimer\">启用自动睡眠定时器</string>\n    <string name=\"sleeptimer_description\">通过自定义时间自动启用默认值的睡眠定时器</string>\n    <string name=\"sleep_timer_repeat_description\">设置睡眠定时器应自动激活的自定义日期和时间</string>\n    <string name=\"sleep_timer_repeat\">重复</string>\n    <string name=\"sleep_timer_daily\">每日</string>\n    <string name=\"sleep_timer_weekdays\">周一至周五</string>\n    <string name=\"sleep_timer_weekdays_weekends\">工作日/周末</string>\n    <string name=\"sleep_timer_weekends\">周末（周六至周日）</string>\n    <string name=\"sleep_timer_custom\">自定义</string>\n    <string name=\"sleep_timer_start_time\">开始时间</string>\n    <string name=\"sleep_timer_end_time\">结束时间</string>\n    <string name=\"sleep_timer_monday\">周一</string>\n    <string name=\"sleep_timer_tuesday\">周二</string>\n    <string name=\"sleep_timer_wednesday\">周三</string>\n    <string name=\"sleep_timer_thursday\">周四</string>\n    <string name=\"sleep_timer_friday\">周五</string>\n    <string name=\"sleep_timer_saturday\">周六</string>\n    <string name=\"sleep_timer_sunday\">周日</string>\n    <string name=\"sleep_timer_stop_after_current_song\">定时器结束时，当前歌曲播放完毕后停止</string>\n    <string name=\"sleep_timer_fade_out\">在最后一分钟淡出</string>\n    <string name=\"upload_songs\">上传歌曲</string>\n    <string name=\"uploading\">正在上传…</string>\n    <string name=\"upload_progress\">%1$d / %2$d</string>\n    <string name=\"upload_complete\">上传完成</string>\n    <string name=\"upload_failed\">上传失败</string>\n    <string name=\"upload_file_too_large\">文件太大（最大 300MB）</string>\n    <string name=\"upload_unsupported_format\">不支持的格式。请使用 mp3、m4a、wma、flac 或 ogg 格式</string>\n    <string name=\"delete_uploaded_song\">删除已上传的歌曲</string>\n    <string name=\"delete_uploaded_song_confirm\">您确定要删除这首已上传的歌曲吗？此操作无法撤销。</string>\n    <string name=\"delete_uploaded_songs_confirm\">您确定要删除 %1$d 首已上传的歌曲吗？此操作无法撤销。</string>\n    <string name=\"delete_uploaded_song_success\">上传的歌曲已删除</string>\n    <string name=\"delete_uploaded_song_failed\">无法删除上传的歌曲</string>\n    <string name=\"delete_uploaded_songs\">删除上传的歌曲</string>\n    <string name=\"deleted_n_songs\">已删除 %1$d 首歌曲</string>\n    <string name=\"deleting\">正在删除…</string>\n    <string name=\"export_playlist\">导出播放列表</string>\n    <string name=\"export_as_csv\">导出为 CSV</string>\n    <string name=\"export_as_m3u\">导出为 M3U</string>\n    <string name=\"export_success\">播放列表导出成功</string>\n    <string name=\"export_failed\">无法导出播放列表</string>\n    <string name=\"export_option_share\">分享</string>\n    <string name=\"export_option_save\">保存到文档</string>\n    <string name=\"qs_tile_music_recognizer\">识别音乐</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-zh-rCN/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <string name=\"home\">首页</string>\n    <string name=\"songs\">歌曲</string>\n    <string name=\"artists\">音乐人</string>\n    <string name=\"albums\">专辑</string>\n    <string name=\"playlists\">播放列表</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"other\">已选择 %d 项</item>\n    </plurals>\n    <string name=\"history\">历史</string>\n    <string name=\"stats\">统计</string>\n    <string name=\"mood_and_genres\">曲调和流派</string>\n    <string name=\"account\">账号</string>\n    <string name=\"quick_picks\">歌曲快选</string>\n    <string name=\"quick_picks_empty\">听一些歌曲来生成歌曲快选</string>\n    <string name=\"new_release_albums\">新专辑</string>\n    <string name=\"today\">今天</string>\n    <string name=\"yesterday\">昨天</string>\n    <string name=\"this_week\">本周</string>\n    <string name=\"last_week\">上周</string>\n    <string name=\"most_played_songs\">最常播放的歌曲</string>\n    <string name=\"most_played_artists\">最常播放的音乐人</string>\n    <string name=\"most_played_albums\">最常播放的专辑</string>\n    <string name=\"search\">搜索</string>\n    <string name=\"search_yt_music\">搜索 YouTube Music…</string>\n    <string name=\"search_library\">搜索媒体库…</string>\n    <string name=\"filter_library\">媒体库</string>\n    <string name=\"filter_liked\">喜欢</string>\n    <string name=\"filter_downloaded\">已下载</string>\n    <string name=\"filter_all\">全部</string>\n    <string name=\"filter_songs\">歌曲</string>\n    <string name=\"filter_videos\">视频</string>\n    <string name=\"filter_albums\">专辑</string>\n    <string name=\"filter_artists\">音乐人</string>\n    <string name=\"filter_playlists\">播放列表</string>\n    <string name=\"filter_community_playlists\">社区播放列表</string>\n    <string name=\"filter_featured_playlists\">精选播放列表</string>\n    <string name=\"filter_bookmarked\">收藏</string>\n    <string name=\"no_results_found\">未找到结果</string>\n    <string name=\"from_your_library\">来自您的媒体库</string>\n    <string name=\"liked_songs\">喜欢的歌曲</string>\n    <string name=\"downloaded_songs\">已下载的歌曲</string>\n    <string name=\"playlist_is_empty\">播放列表为空</string>\n    <string name=\"retry\">重试</string>\n    <string name=\"radio\">电台</string>\n    <string name=\"shuffle\">随机播放</string>\n    <string name=\"reset\">重置</string>\n    <string name=\"details\">详情</string>\n    <string name=\"edit\">编辑</string>\n    <string name=\"start_radio\">收听电台</string>\n    <string name=\"play\">播放</string>\n    <string name=\"play_next\">接下来播放</string>\n    <string name=\"add_to_queue\">加入播放队列</string>\n    <string name=\"add_to_library\">加入媒体库</string>\n    <string name=\"remove_from_library\">从媒体库中移除</string>\n    <string name=\"action_download\">下载</string>\n    <string name=\"downloading\">正在下载</string>\n    <string name=\"remove_download\">移除下载</string>\n    <string name=\"import_playlist\">导入播放列表</string>\n    <string name=\"add_to_playlist\">加入播放列表</string>\n    <string name=\"view_artist\">浏览音乐人</string>\n    <string name=\"view_album\">浏览专辑</string>\n    <string name=\"refetch\">刷新</string>\n    <string name=\"share\">分享</string>\n    <string name=\"delete\">删除</string>\n    <string name=\"remove_from_history\">从历史中移除</string>\n    <string name=\"search_online\">在线搜索</string>\n    <string name=\"action_sync\">同步</string>\n    <string name=\"advanced\">高级</string>\n    <string name=\"sort_by_create_date\">添加日期</string>\n    <string name=\"sort_by_name\">名称</string>\n    <string name=\"sort_by_artist\">音乐人</string>\n    <string name=\"sort_by_year\">年份</string>\n    <string name=\"sort_by_song_count\">歌曲总数</string>\n    <string name=\"sort_by_length\">长度</string>\n    <string name=\"sort_by_play_time\">播放时间</string>\n    <string name=\"sort_by_custom\">自定义顺序</string>\n    <string name=\"media_id\">媒体 ID</string>\n    <string name=\"mime_type\">MIME 类型</string>\n    <string name=\"codecs\">编码</string>\n    <string name=\"bitrate\">比特率</string>\n    <string name=\"sample_rate\">采样率</string>\n    <string name=\"loudness\">响度</string>\n    <string name=\"volume\">音量</string>\n    <string name=\"file_size\">文件大小</string>\n    <string name=\"unknown\">未知</string>\n    <string name=\"copied\">已复制到剪贴板</string>\n    <string name=\"edit_lyrics\">编辑歌词</string>\n    <string name=\"search_lyrics\">搜索歌词</string>\n    <string name=\"edit_song\">编辑歌曲</string>\n    <string name=\"song_title\">歌名</string>\n    <string name=\"song_artists\">音乐人</string>\n    <string name=\"error_song_title_empty\">歌名不能为空。</string>\n    <string name=\"error_song_artist_empty\">音乐人不能为空。</string>\n    <string name=\"save\">保存</string>\n    <string name=\"choose_playlist\">选择播放列表</string>\n    <string name=\"edit_playlist\">编辑播放列表</string>\n    <string name=\"create_playlist\">新建播放列表</string>\n    <string name=\"playlist_name\">名称</string>\n    <string name=\"error_playlist_name_empty\">播放列表名称不能为空。</string>\n    <string name=\"edit_artist\">编辑音乐人</string>\n    <string name=\"artist_name\">音乐人名称</string>\n    <string name=\"error_artist_name_empty\">音乐人名称不能为空。</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"other\">%d 首歌曲</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"other\">%d 位音乐人</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"other\">%d 张专辑</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"other\">%d 个播放列表</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"other\">%d 周</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"other\">%d 月</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"other\">%d 年</item>\n    </plurals>\n    <string name=\"playlist_imported\">播放列表已导入</string>\n    <string name=\"removed_song_from_playlist\">已从播放列表中移除“%s”</string>\n    <string name=\"playlist_synced\">播放列表已同步</string>\n    <string name=\"undo\">撤销</string>\n    <string name=\"lyrics_not_found\">未找到歌词</string>\n    <string name=\"sleep_timer\">睡眠定时器</string>\n    <string name=\"end_of_song\">这首歌曲播放完毕</string>\n    <plurals name=\"minute\">\n        <item quantity=\"other\">%d 分钟</item>\n    </plurals>\n    <string name=\"error_no_stream\">没有可用音源</string>\n    <string name=\"error_no_internet\">没有网络连接</string>\n    <string name=\"error_timeout\">连接超时</string>\n    <string name=\"error_unknown\">未知错误</string>\n    <string name=\"action_like\">喜欢</string>\n    <string name=\"action_remove_like\">取消喜欢</string>\n    <string name=\"action_shuffle_on\">随机播放打开</string>\n    <string name=\"action_shuffle_off\">随机播放关闭</string>\n    <string name=\"repeat_mode_off\">循环播放关闭</string>\n    <string name=\"repeat_mode_one\">循环播放当前歌曲</string>\n    <string name=\"repeat_mode_all\">循环播放队列</string>\n    <string name=\"queue_all_songs\">全部歌曲</string>\n    <string name=\"queue_searched_songs\">搜索的歌曲</string>\n    <string name=\"music_player\">音乐播放器</string>\n    <string name=\"settings\">设置</string>\n    <string name=\"appearance\">外观</string>\n    <string name=\"enable_dynamic_theme\">启用动态主题</string>\n    <string name=\"dark_theme\">深色主题</string>\n    <string name=\"dark_theme_on\">开</string>\n    <string name=\"dark_theme_off\">关</string>\n    <string name=\"dark_theme_follow_system\">跟随系统</string>\n    <string name=\"pure_black\">纯黑</string>\n    <string name=\"default_open_tab\">默认启动标签页</string>\n    <string name=\"customize_navigation_tabs\">自定义导航标签页</string>\n    <string name=\"lyrics_text_position\">歌词文本位置</string>\n    <string name=\"left\">靠左</string>\n    <string name=\"center\">居中</string>\n    <string name=\"right\">靠右</string>\n    <string name=\"content\">内容</string>\n    <string name=\"login\">登录</string>\n    <string name=\"content_language\">默认内容语言</string>\n    <string name=\"content_country\">默认内容国家/地区</string>\n    <string name=\"system_default\">系统默认</string>\n    <string name=\"enable_proxy\">启用代理</string>\n    <string name=\"proxy_type\">代理类型</string>\n    <string name=\"proxy_url\">代理 URL</string>\n    <string name=\"restart_to_take_effect\">重启以应用变更</string>\n    <string name=\"player_and_audio\">播放器与音频</string>\n    <string name=\"audio_quality\">音质</string>\n    <string name=\"audio_quality_auto\">自动</string>\n    <string name=\"audio_quality_high\">高</string>\n    <string name=\"audio_quality_low\">低</string>\n    <string name=\"persistent_queue\">保留播放队列</string>\n    <string name=\"skip_silence\">跳过无声片段</string>\n    <string name=\"audio_normalization\">标准化音量</string>\n    <string name=\"equalizer\">均衡器</string>\n    <string name=\"storage\">储存</string>\n    <string name=\"cache\">缓存</string>\n    <string name=\"image_cache\">图像缓存</string>\n    <string name=\"song_cache\">歌曲缓存</string>\n    <string name=\"max_cache_size\">最大缓存大小</string>\n    <string name=\"unlimited\">无限制</string>\n    <string name=\"clear_all_downloads\">清除所有下载</string>\n    <string name=\"max_image_cache_size\">图像缓存大小</string>\n    <string name=\"clear_image_cache\">清除图像缓存</string>\n    <string name=\"max_song_cache_size\">歌曲缓存大小</string>\n    <string name=\"clear_song_cache\">清除歌曲缓存</string>\n    <string name=\"size_used\">已使用 %s</string>\n    <string name=\"privacy\">隐私</string>\n    <string name=\"pause_listen_history\">暂停听歌历史</string>\n    <string name=\"clear_listen_history\">清除听歌历史</string>\n    <string name=\"clear_listen_history_confirm\">是否确定要清除所有听歌历史？</string>\n    <string name=\"pause_search_history\">暂停搜索历史</string>\n    <string name=\"clear_search_history\">清除搜索历史</string>\n    <string name=\"clear_search_history_confirm\">是否确定要清除所有搜索历史？</string>\n    <string name=\"enable_kugou\">使用酷狗音乐提供歌词</string>\n    <string name=\"backup_restore\">备份与还原</string>\n    <string name=\"action_backup\">备份</string>\n    <string name=\"action_restore\">还原</string>\n    <string name=\"imported_playlist\">已导入的播放列表</string>\n    <string name=\"backup_create_success\">成功新建备份</string>\n    <string name=\"backup_create_failed\">无法新建备份</string>\n    <string name=\"restore_failed\">无法还原备份</string>\n    <string name=\"about\">关于</string>\n    <string name=\"app_version\">应用版本</string>\n    <string name=\"new_version_available\">有新版本</string>\n    <string name=\"translation_models\">翻译模型</string>\n    <string name=\"clear_translation_models\">清除翻译模型</string>\n    <string name=\"delete_playlist_confirm\">是否确定要删除播放列表“%s”？</string>\n    <string name=\"remove_download_playlist_confirm\">是否确定要从已下载的歌曲存储中移除所有“%s”播放列表歌曲？</string>\n    <string name=\"your_youtube_playlists\">您的 YouTube 播放列表</string>\n    <string name=\"other_versions\">其他版本</string>\n    <string name=\"library_artist_empty\">媒体库音乐人将显示在此处</string>\n    <string name=\"library_album_empty\">媒体库专辑将显示在此处</string>\n    <string name=\"library_playlist_empty\">您的播放列表将显示在此处</string>\n    <string name=\"add_anyway\">仍要添加</string>\n    <string name=\"duplicates_description_multiple\">%d 首歌曲已在您的播放列表中</string>\n    <string name=\"action_remove_like_all\">移除全部喜欢</string>\n    <string name=\"theme\">主题</string>\n    <string name=\"player\">播放器</string>\n    <string name=\"player_text_alignment\">播放器文本对齐</string>\n    <string name=\"default_\">默认</string>\n    <string name=\"misc\">杂项</string>\n    <string name=\"grid_cell_size\">网格大小</string>\n    <string name=\"small\">小</string>\n    <string name=\"big\">大</string>\n    <string name=\"not_logged_in\">未登录</string>\n    <string name=\"queue\">播放队列</string>\n    <string name=\"hide_explicit\">隐藏不适宜内容</string>\n    <string name=\"enable_lrclib\">使用 LrcLib 提供歌词</string>\n    <string name=\"options\">选项</string>\n    <string name=\"preview\">预览</string>\n    <string name=\"login_failed\">登录失败</string>\n    <string name=\"action_logout\">登出</string>\n    <string name=\"discord_integration\">Discord 集成</string>\n    <string name=\"enable_discord_rpc\">启用 Rich Presence</string>\n    <string name=\"action_like_all\">喜欢全部</string>\n    <string name=\"library_song_empty\">媒体库歌曲将显示在此处</string>\n    <string name=\"duplicates_description_single\">这首歌曲已在您的播放列表中</string>\n    <string name=\"dismiss\">关闭</string>\n    <string name=\"player_slider_style\">播放器滑块样式</string>\n    <string name=\"keep_listening\">继续听</string>\n    <string name=\"add_all_to_library\">全部加入媒体库</string>\n    <string name=\"remove_all_from_library\">全部从媒体库中移除</string>\n    <string name=\"remove_from_playlist\">从播放列表中移除</string>\n    <string name=\"tempo_and_pitch\">节奏和音调</string>\n    <string name=\"duplicates\">重复项</string>\n    <string name=\"skip_duplicates\">跳过重复项</string>\n    <string name=\"sided\">靠边</string>\n    <string name=\"squiggly\">波浪</string>\n    <string name=\"listen_history\">听歌历史</string>\n    <string name=\"search_history\">搜索历史</string>\n    <string name=\"disable_screenshot\">禁用截屏</string>\n    <string name=\"disable_screenshot_desc\">启用此选项后， 您无法截屏，也无法在“最近用过”中看到此应用的内容。</string>\n    <string name=\"remove_from_queue\">从播放队列中移除</string>\n    <string name=\"auto_skip_next_on_error_desc\">确保您的连续播放体验</string>\n    <string name=\"forgotten_favorites\">重温最爱</string>\n    <string name=\"similar_to\">类似风格</string>\n    <string name=\"persistent_queue_desc\">应用启动时还原上次的播放队列</string>\n    <string name=\"auto_load_more\">自动加载更多歌曲</string>\n    <string name=\"auto_load_more_desc\">如果可能，在播放队列快结束时自动添加更多歌曲</string>\n    <string name=\"auto_skip_next_on_error\">发生错误时自动跳到下一首歌曲</string>\n    <string name=\"stop_music_on_task_clear\">任务清除时停止音乐</string>\n    <string name=\"discord_information\">Metrolist 使用 KizzyRPC 库来设置您的 Discord 账号的状态。这会用到 Discord 网关连接，可能会违反 Discord 的服务条款。不过，目前还没有因此原因暂停用户账号的情况。使用风险自负。 \\n \\nMetrolist 只提取您的令牌，其他内容都存储在本地。</string>\n    <string name=\"use_login_for_browse_desc\">这可能会影响您看到的内容，例如，如果您使用会员账号登录，则会显示仅会员专辑</string>\n    <string name=\"use_login_for_browse\">登录账号浏览内容</string>\n    <string name=\"action_login\">登录</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-zh-rTW/metrolist_strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"local_history\">本地</string>\n    <string name=\"remote_history\">雲端</string>\n    <string name=\"weeks\">週</string>\n    <string name=\"months\">月</string>\n    <string name=\"years\">年</string>\n    <string name=\"continuous\">連續</string>\n    <string name=\"liked\">喜歡的歌曲</string>\n    <string name=\"offline\">已下載</string>\n    <string name=\"my_top\">我的Top</string>\n    <string name=\"sync_playlist\">同步播放清單</string>\n    <string name=\"allows_for_sync_witch_youtube\">注意：這允許與 Youtube Music 同步，之後無法更改。</string>\n    <string name=\"copy_link\">複製連結</string>\n    <string name=\"select\">全選</string>\n    <string name=\"like_all\">全部喜歡</string>\n    <string name=\"dislike_all\">全部不喜歡</string>\n    <string name=\"sort_by_last_updated\">上次更新</string>\n    <string name=\"link_copied\">連結已複製至剪貼簿</string>\n    <string name=\"lyrics\">歌詞</string>\n    <string name=\"already_in_playlist\">已在播放清單中:</string>\n    <plurals name=\"n_time\">\n        <item quantity=\"other\">%d 次</item>\n    </plurals>\n    <string name=\"similar_content\">相似內容</string>\n    <string name=\"player_background_style\">播放器背景樣式</string>\n    <string name=\"follow_theme\">跟隨主題</string>\n    <string name=\"gradient\">漸層</string>\n    <string name=\"player_background_blur\">模糊</string>\n    <string name=\"player_buttons_style\">播放器按鈕顏色</string>\n    <string name=\"default_style\">預設</string>\n    <string name=\"enable_swipe_thumbnail\">啟用滑動縮圖切換歌曲</string>\n    <string name=\"lyrics_click_change\">點擊以切換歌詞</string>\n    <string name=\"slim\">細</string>\n    <string name=\"slim_navbar\">隱藏底部導覽列標籤</string>\n    <string name=\"advanced_login\">透過 token 登入</string>\n    <string name=\"token_hidden\">點擊以顯示 token</string>\n    <string name=\"token_shown\">再次點擊以複製或編輯</string>\n    <string name=\"token_adv_login_description\">這是作為網頁登錄的替代方案的進階登入模式一。你可以在此直接輸入或更新您的登入權杖。這可以用於加速在多台裝置上的登入流程。請注意，若權杖格式無效導致程式無法解析，權杖將不予採用</string>\n    <string name=\"general\">一般</string>\n    <string name=\"proxy\">Proxy</string>\n    <string name=\"default_lib_chips\">更改預設媒體庫標籤</string>\n    <string name=\"set_quick_picks\">設定快速選取</string>\n    <string name=\"last_song_listened\">根據上次聆聽的歌曲</string>\n    <string name=\"app_language\">應用程式語言</string>\n    <string name=\"enable_similar_content\">啟用相似內容</string>\n    <string name=\"similar_content_desc\">當播放佇列快結束時，自動加入更多相似歌曲</string>\n    <string name=\"percentage_format\">%d%%</string>\n    <string name=\"default_links\">開啟支援的連結</string>\n    <string name=\"open_app_settings_error\">無法開啟應用設定</string>\n    <string name=\"release_notes\">更新記錄</string>\n    <string name=\"all_time\">所有時間</string>\n    <string name=\"past_24_hours\">過去 24 小時</string>\n    <string name=\"past_week\">過去一週</string>\n    <string name=\"past_month\">過去一個月</string>\n    <string name=\"past_year\">過去一年</string>\n    <string name=\"top_length\">我的熱門清單長度</string>\n    <string name=\"history_duration\">加入歷史紀錄的播放時長</string>\n    <plurals name=\"seconds\">\n        <item quantity=\"other\">%d 秒</item>\n    </plurals>\n    <string name=\"lyrics_auto_scroll\">自動滾動歌詞</string>\n    <string name=\"show_liked_playlist\">顯示「喜歡的歌曲」播放清單</string>\n    <string name=\"show_downloaded_playlist\">顯示「已下載」播放清單</string>\n    <string name=\"show_cached_playlist\">顯示「快取」播放清單</string>\n    <string name=\"import_csv\">匯入「csv」播放清單</string>\n    <string name=\"import_online\">匯入「m3u」播放清單</string>\n    <string name=\"playlist_add_local_to_synced_note\">Note: 不支援新增本地歌曲到同步/雲端播放清單。任何其他組合才有效</string>\n    <string name=\"show_top_playlist\">顯示「Top」播放清單</string>\n    <string name=\"charts\">排行榜</string>\n    <string name=\"back_button_desc\">返回</string>\n    <string name=\"album_cover_desc\">專輯封面</string>\n    <string name=\"top_music_videos\">熱門音樂影片</string>\n    <string name=\"trending\">發燒</string>\n    <string name=\"cached_playlist\">快取</string>\n    <string name=\"sync_disabled\">關閉同步</string>\n    <string name=\"generating_image\">生成圖片</string>\n    <string name=\"please_wait\">請稍候</string>\n    <string name=\"cancel\">取消</string>\n    <string name=\"share_lyrics\">分享歌詞</string>\n    <string name=\"share_as_text\">以文字分享</string>\n    <string name=\"share_as_image\">以圖片分享</string>\n    <string name=\"max_selection_limit\">最大選擇上限</string>\n    <string name=\"share_selected\">分享已選項目</string>\n    <string name=\"customize_colors\">自訂顏色</string>\n    <string name=\"secondary_text_color\">次要文字顏色</string>\n    <string name=\"background_color\">背景顏色</string>\n    <string name=\"text_color\">文字顏色</string>\n    <string name=\"remove_from_cache\">從快取中移除</string>\n    <string name=\"auto_playlists\">自動生成播放清單</string>\n    <string name=\"swipe_song_to_add\">向左滑動歌曲加入播放佇列，或向右滑動加入下首播放</string>\n    <string name=\"auto_download_on_like\">自動下載喜歡的歌曲</string>\n    <string name=\"auto_download_on_like_desc\">當你點擊喜歡某首歌曲時，將自動下載該歌曲</string>\n    <string name=\"clear_song_cache_dialog\">確定要清除所有快取歌曲嗎?</string>\n    <string name=\"clear_downloads_dialog\">確定要清除所有下載內容嗎?</string>\n    <string name=\"not_logged_in_youtube\">未登入 YouTube</string>\n    <string name=\"description\">說明</string>\n    <string name=\"views\">播放次數</string>\n    <string name=\"likes\">喜歡</string>\n    <string name=\"information\">資訊</string>\n    <string name=\"dislikes\">不喜歡</string>\n    <string name=\"new_player_design\">新播放器設計</string>\n    <string name=\"lyrics_romanize_japanese\">羅馬化日文歌詞</string>\n    <string name=\"lyrics_romanize_korean\">羅馬化韓文歌詞</string>\n    <string name=\"yt_sync\">自動與帳號同步</string>\n    <string name=\"more_content\">更多內容</string>\n    <string name=\"swipe_sensitivity\">迷你播放器滑動靈敏度</string>\n    <string name=\"sensitivity_percentage\">%1$d%%</string>\n    <string name=\"clear_image_cache_dialog\">確定要清除所有快取的圖片嗎?</string>\n    <string name=\"disable\">禁用</string>\n    <string name=\"subscribe\">訂閱</string>\n    <string name=\"subscribed\">已訂閱</string>\n    <string name=\"starting_radio\">電台開始中</string>\n    <string name=\"now_playing\">現在播放</string>\n    <string name=\"close\">關閉</string>\n    <string name=\"hide_player_thumbnail\">隱藏播放器封面</string>\n    <string name=\"hide_player_thumbnail_desc\">播放器中以應用程式圖像代替歌曲封面</string>\n    <string name=\"seek_forward_dynamic\">前進%1$d秒</string>\n    <string name=\"seek_backward_dynamic\">後退%1$d秒</string>\n    <string name=\"seek_seconds_addup\">漸進式時間跳躍</string>\n    <string name=\"seek_seconds_addup_description\">啟用此選項後，每次快進轉或倒轉時將遞增 5 秒的跳轉長度</string>\n    <string name=\"new_mini_player_design\">使用全新迷你播放器設計</string>\n    <string name=\"edit_playlist_cover\">更改播放清單圖片</string>\n    <string name=\"edit_playlist_cover_note\">注意：你的帳戶必須連結到電話號碼並在 YouTube Music 上進行驗證才能更改播放清單封面。</string>\n    <string name=\"edit_playlist_cover_note_wait\">選擇圖片後，請等待片刻，新的封面就會出現在你的播放清單中。</string>\n    <string name=\"config_proxy\">設定代理伺服器</string>\n    <string name=\"proxy_username\">代理伺服器名稱</string>\n    <string name=\"proxy_password\">代理伺服器密碼</string>\n    <string name=\"enable_authentication\">開啟身份驗證</string>\n    <string name=\"disable_load_more_when_repeat_all\">開啟「重複全部」時停用「載入更多」</string>\n    <string name=\"disable_load_more_when_repeat_all_desc\">啟用「重複播放」模式時，停用「自動載入」更多歌曲和類似內容</string>\n    <string name=\"lyrics_romanization_cyrillic\">西里爾</string>\n    <string name=\"lyrics_romanize_title\">羅馬化</string>\n    <string name=\"lyrics_romanization\">羅馬化歌詞</string>\n    <string name=\"lyrics_romanize_russian\">羅馬化俄文歌詞</string>\n    <string name=\"lyrics_romanize_ukrainian\">羅馬化烏克蘭文歌詞</string>\n    <string name=\"lyrics_romanize_belarusian\">羅馬化白俄羅斯文歌詞</string>\n    <string name=\"lyrics_romanize_kyrgyz\">羅馬化吉爾吉斯語歌詞</string>\n    <string name=\"lyrics_romanize_serbian\">羅馬化塞爾維亞語歌詞</string>\n    <string name=\"lyrics_romanize_bulgarian\">羅馬化白俄羅斯文歌詞</string>\n    <string name=\"line_by_line_option_title\">實驗性：逐行偵測語言</string>\n    <string name=\"line_by_line_option_desc\">西里爾語將逐行偵測，而不是整首歌曲。</string>\n    <string name=\"line_by_line_dialog_title\">確定嗎？</string>\n    <string name=\"line_by_line_dialog_desc\">這是一項不保證穩定性的實驗性功能。\\n\\n在預設情況下，系統會根據整首歌來判斷語言；但開啟此選項後，將改為逐行判斷。這可以支援多語言歌曲，但判斷結果未必完全準確（例如：如果一段烏克蘭語歌詞中不含該語言特有的字母，系統可能會將其誤判並以俄語羅馬化呈現）。\\n\\n如果目前使用沒有問題，建議維持關閉狀態。</string>\n    <string name=\"romanize_current_track\">將當前曲目羅馬化</string>\n    <string name=\"settings_section_ui\">介面</string>\n    <string name=\"settings_section_privacy\">私隱與安全性</string>\n    <string name=\"settings_section_player_content\">播放器和內容</string>\n    <string name=\"settings_section_storage\">儲存和數據</string>\n    <string name=\"settings_section_system\">系統及關於</string>\n    <string name=\"uploaded_playlist\">已上傳</string>\n    <string name=\"update_available_title\">可用更新</string>\n    <string name=\"swipe_song_to_remove\">滑動歌曲即可將其從播放清單刪除</string>\n    <string name=\"show_uploaded_playlist\">顯示「已上傳」播放清單</string>\n    <string name=\"filter_uploaded\">已上傳</string>\n    <string name=\"choose_from_library\">從媒體庫選擇</string>\n    <string name=\"remove_custom_image\">移除自定義圖片</string>\n    <string name=\"discord_use_details\">使用詳細內容而非狀態</string>\n    <string name=\"discord_use_details_description\">強化顯示曲名而非作曲家</string>\n    <string name=\"updater\">更新器</string>\n    <string name=\"check_for_updates\">自動檢查更新</string>\n    <string name=\"update_notifications\">開啟更新通知</string>\n    <string name=\"update_channel_name\">應用程式更新</string>\n    <string name=\"update_channel_desc\">新版本更新通知</string>\n    <string name=\"audio_offload\">啟動音訊卸載</string>\n    <string name=\"audio_offload_description\">使用卸載音訊路徑進行播放。停用此功能可能會增加耗電量，但若遇到音訊播放或後處理問題，建議將其關閉</string>\n    <string name=\"lyrics_romanize_macedonian\">羅馬化馬其頓語歌詞</string>\n    <string name=\"integrations\">整合</string>\n    <string name=\"username\">使用者名稱</string>\n    <string name=\"password\">密碼</string>\n    <string name=\"lastfm_integration\">Last.fm 整合功能</string>\n    <string name=\"enable_scrobbling\">開啟紀錄播放歌曲</string>\n    <string name=\"lastfm_now_playing\">傳送正在播放歌曲</string>\n    <string name=\"last_fm_send_likes\">傳送喜歡/不喜歡歌曲</string>\n    <string name=\"last_fm_send_likes_description\">同步 Metrolist 喜歡/不喜歡歌曲到 Last.fm</string>\n    <string name=\"scrobbling_configuration\">歌曲紀錄設定</string>\n    <string name=\"scrobble_min_track_duration\">紀錄播放長度超過以下時間的歌曲</string>\n    <string name=\"scrobble_delay_percent\">紀錄延遲比例</string>\n    <string name=\"scrobble_delay_minutes\">紀錄延遲時間</string>\n    <string name=\"download_playlist_desc\">下載所有歌曲以供離線播放</string>\n    <string name=\"remove_download_playlist_desc\">從此播放列表中刪除所有已下載的歌曲</string>\n    <string name=\"download_in_progress_desc\">下載正在進行</string>\n    <string name=\"share_playlist_desc\">與他人分享此播放列表</string>\n    <string name=\"delete_playlist_desc\">永久刪除此播放列表</string>\n    <string name=\"sync_playlist_desc\">將播放列表與YouTube Music同步</string>\n    <string name=\"primary_color_style\">主色調</string>\n    <string name=\"tertiary_color_style\">三級顏色</string>\n    <string name=\"lyrics_glow_effect\">啟用歌詞發光效果</string>\n    <string name=\"lyrics_glow_effect_desc\">為動態歌詞添加發光動畫與彈跳效果</string>\n    <string name=\"enable_better_lyrics\">啟用 Better Lyrics</string>\n    <string name=\"enable_better_lyrics_desc\">適用於任何歌曲的音節同步歌詞，可用於卡拉OK</string>\n    <string name=\"auto_scroll\">重新同步</string>\n    <string name=\"shuffle_playlist_first\">隨機播放清單/專輯</string>\n    <string name=\"shuffle_playlist_first_desc\">隨機播放時，先播放原播放清單/專輯中的所有歌曲，然後再播放內容相似的歌曲</string>\n    <string name=\"show_wrapped_card\">展示「年度播放」卡片</string>\n    <string name=\"lyrics_romanize_chinese\">中文歌詞羅馬化</string>\n    <string name=\"google_cast_description\">啟用向 Chromecast 和其他支援投影功能的裝置投屏音頻</string>\n    <string name=\"logging_in\">正在登錄…</string>\n    <string name=\"hide_video_songs\">隱藏帶影片的音樂</string>\n    <string name=\"details_desc\">瀏覽歌曲信息</string>\n    <string name=\"edit_desc\">更改標題或藝術家</string>\n    <string name=\"start_radio_desc\">基於此歌曲創造一個電台</string>\n    <string name=\"play_next_desc\">添加到列表頂部</string>\n    <string name=\"show_more\">顯示更多</string>\n    <string name=\"show_less\">顯示更少</string>\n    <string name=\"about_artist\">關於</string>\n    <string name=\"artist_page_settings\">藝人頁面</string>\n    <string name=\"show_artist_description\">顯示藝人的資訊</string>\n    <string name=\"show_artist_subscriber_count\">顯示訂閱人數</string>\n    <string name=\"show_artist_monthly_listeners\">顯示每月聽眾人數</string>\n    <string name=\"download_desc\">設定為可離線播放</string>\n    <string name=\"add_to_library_desc\">新增至媒體庫</string>\n    <string name=\"delete_desc\">永久刪除此項目</string>\n    <string name=\"import_error_title\">匯入時發生錯誤</string>\n    <string name=\"copied_artist\">複製藝人</string>\n    <string name=\"error_playing\">撥放時發生錯誤</string>\n    <string name=\"failed_to_parse_proxy\">無法解析 proxy</string>\n    <string name=\"like\">喜歡</string>\n    <string name=\"karaoke\">卡拉 OK</string>\n    <string name=\"google_cast\">Google Cast</string>\n    <string name=\"failed_to_create_image\">無法創建影像：%s</string>\n    <string name=\"copied_title\">複製標題</string>\n    <string name=\"import_profile\">匯入設定檔</string>\n    <plurals name=\"profiles_count\">\n        <item quantity=\"other\">%d 設定檔</item>\n    </plurals>\n    <string name=\"equalizer_header\">等化器</string>\n    <string name=\"no_profiles\">無等化器設定檔</string>\n    <string name=\"system_equalizer\">系統等化器</string>\n    <string name=\"eq_disabled\">關閉</string>\n    <string name=\"cache_size_warning_confirm\">繼續</string>\n    <string name=\"delete_profile_desc\">刪除設定檔</string>\n    <string name=\"delete_profile_confirmation\">確定要刪除 %1$s 嗎？刪除後將無法恢復</string>\n    <string name=\"wavy\">Wavy</string>\n    <string name=\"enable_dynamic_icon\">啟用動態 icon</string>\n    <string name=\"enable_simpmusic\">啟用 SimpMusic 歌詞</string>\n    <string name=\"enable_simpmusic_desc\">使用 SimpMusic 以同步歌詞</string>\n    <string name=\"skip_silence_instant\">跳過靜音片段</string>\n    <string name=\"skip_silence_desc\">快轉歌曲中的靜音片段</string>\n    <string name=\"skip_silence_instant_desc\">在靜音片段時直接跳轉，而非加速快轉</string>\n    <string name=\"equalizer_desc\">調整等化器</string>\n    <string name=\"error_file_open\">開啟檔案時發生錯誤：%1$s</string>\n    <string name=\"progress_percent\">進度 %s%%</string>\n    <string name=\"open\">開啟</string>\n    <string name=\"fade\">淡出</string>\n    <string name=\"error_file_read\">無法閱讀檔案</string>\n    <string name=\"glow\">高亮效果</string>\n    <string name=\"slide\">滑動</string>\n    <string name=\"pause_music_when_media_is_muted\">靜音時暫停音樂播放</string>\n    <string name=\"turntable_widget_description\">快速存取最近播放的曲目</string>\n    <string name=\"tap_to_open\">點擊開啟 Metrolist</string>\n    <string name=\"persistent_shuffle_desc\">在開始播放新歌曲或播放清單時，仍維持隨機播放狀態</string>\n    <string name=\"persistent_shuffle_title\">固定隨機播放</string>\n    <string name=\"enable\">啟用</string>\n    <string name=\"crop_album_art\">裁切專輯封面</string>\n    <string name=\"crop_album_art_desc\">透過裁切影片縮圖強制使用正方形寬高比</string>\n    <string name=\"player_background_solid\">純色</string>\n    <string name=\"display_density\">顯示密度</string>\n    <string name=\"restart\">重新啟動</string>\n    <string name=\"restart_required\">需要重啟</string>\n    <string name=\"density_restart_message\">顯示密度變更將在重新啟動應用後生效。您現在要重啟嗎？</string>\n    <string name=\"enable_lrclib_desc\">社群驅動的同步歌詞資料庫</string>\n    <string name=\"enable_kugou_desc\">歌詞來自中國最大的線上音樂平台</string>\n    <string name=\"youtube_music_lyrics_note\">注意：當其他歌詞無法使用時，會自動顯示來自 YouTube Music 的歌詞，來源歌詞通常不同步。</string>\n    <string name=\"prevent_duplicate_tracks_in_queue_desc\">將曲目新增至佇列時，如果該曲目已存在於佇列中，則將其從先前的位置移除</string>\n    <string name=\"lyrics_provider_priority\">歌詞提供者優先權</string>\n    <string name=\"lyrics_provider_priority_desc\">拖曳即可依優先順序重新排列供應商。位置越高，優先權越高。</string>\n    <string name=\"remember_shuffle_and_repeat\">記住隨機與循環設置</string>\n    <string name=\"remember_shuffle_and_repeat_desc\">重啟應用程式時，請記住隨機播放和重複播放模式</string>\n    <string name=\"export_playlist\">匯出播放列表</string>\n    <string name=\"export_as_csv\">匯出為 CSV 文件</string>\n    <string name=\"export_as_m3u\">導出為 M3U</string>\n    <string name=\"export_success\">播放清單已成功匯出</string>\n    <string name=\"export_failed\">匯出播放清單失敗</string>\n    <string name=\"export_option_share\">分享</string>\n    <string name=\"export_option_save\">儲存到文檔</string>\n    <string name=\"changelog\">更新日誌</string>\n    <string name=\"changelog_empty\">暫無更新日誌</string>\n    <string name=\"list_bullet\">•</string>\n    <string name=\"view_on_github\">在 GitHub 上查看</string>\n    <string name=\"current_version\">目前版本</string>\n    <string name=\"update_settings\">更新設定</string>\n    <string name=\"check_for_updates_title\">請檢查更新</string>\n    <string name=\"checking_for_updates\">正在檢查更新…</string>\n    <string name=\"check_for_updates_button\">請檢查更新</string>\n    <string name=\"hide_changelog\">隱藏更新日誌</string>\n    <string name=\"view_changelog\">查看更新日誌</string>\n    <string name=\"set_as_default\">設定為預設值</string>\n    <string name=\"enable_automatic_sleeptimer\">啟用自動睡眠定時器</string>\n    <string name=\"sleeptimer_description\">透過自訂時間自動啟用預設值的睡眠定時器</string>\n    <string name=\"sleep_timer_repeat_description\">設定睡眠定時器應自動啟動的自訂日期和時間</string>\n    <string name=\"sleep_timer_repeat\">重複</string>\n    <string name=\"sleep_timer_daily\">每天</string>\n    <string name=\"sleep_timer_weekdays\">週一至週五</string>\n    <string name=\"sleep_timer_weekdays_weekends\">平日/週末</string>\n    <string name=\"sleep_timer_weekends\">週末（週六到週日）</string>\n    <string name=\"sleep_timer_custom\">自訂</string>\n    <string name=\"sleep_timer_start_time\">開始時間</string>\n    <string name=\"sleep_timer_end_time\">結束時間</string>\n    <string name=\"sleep_timer_monday\">週一</string>\n    <string name=\"sleep_timer_tuesday\">週二</string>\n    <string name=\"sleep_timer_wednesday\">週三</string>\n    <string name=\"sleep_timer_thursday\">週四</string>\n    <string name=\"sleep_timer_friday\">星期五</string>\n    <string name=\"sleep_timer_saturday\">週六</string>\n    <string name=\"sleep_timer_sunday\">星期日</string>\n    <string name=\"sleep_timer_stop_after_current_song\">當計時器結束時，在當前歌曲結束時停止播放</string>\n    <string name=\"sleep_timer_fade_out\">最後幾分鐘淡出</string>\n    <string name=\"resume_on_bluetooth_connect\">藍牙連線恢復</string>\n    <string name=\"keep_screen_on_when_player_is_expanded\">播放器最大化時保持螢幕常亮</string>\n    <string name=\"crossfade\">歌曲淡入淡出</string>\n    <string name=\"crossfade_desc\">歌曲之間的淡入淡出</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-zh-rTW/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">首頁</string>\n    <string name=\"songs\">歌曲</string>\n    <string name=\"artists\">藝人</string>\n    <string name=\"albums\">專輯</string>\n    <string name=\"playlists\">播放清單</string>\n    <plurals name=\"n_selected\">\n        <item quantity=\"other\">已選取 %d 個項目</item>\n    </plurals>\n    <string name=\"history\">歷史記錄</string>\n    <string name=\"stats\">統計</string>\n    <string name=\"mood_and_genres\">情境與類型</string>\n    <string name=\"account\">帳號</string>\n    <string name=\"quick_picks\">歌曲快選</string>\n    <string name=\"quick_picks_empty\">聽一些音樂讓我們知道您的喜好</string>\n    <string name=\"forgotten_favorites\">重溫舊愛</string>\n    <string name=\"keep_listening\">再聽一次</string>\n    <string name=\"your_youtube_playlists\">你的 YouTube 播放清單</string>\n    <string name=\"similar_to\">風格近似</string>\n    <string name=\"new_release_albums\">新專輯</string>\n    <string name=\"today\">今天</string>\n    <string name=\"yesterday\">昨天</string>\n    <string name=\"this_week\">這週</string>\n    <string name=\"last_week\">上週</string>\n    <string name=\"most_played_songs\">最常播放的歌曲</string>\n    <string name=\"most_played_artists\">最常播放的藝人</string>\n    <string name=\"most_played_albums\">最常播放的專輯</string>\n    <string name=\"search\">搜尋</string>\n    <string name=\"search_yt_music\">搜尋 YouTube Music…</string>\n    <string name=\"search_library\">搜尋媒體庫…</string>\n    <string name=\"filter_library\">媒體庫</string>\n    <string name=\"filter_liked\">已按讚</string>\n    <string name=\"filter_downloaded\">已下載</string>\n    <string name=\"filter_all\">全部</string>\n    <string name=\"filter_songs\">歌曲</string>\n    <string name=\"filter_videos\">影片</string>\n    <string name=\"filter_albums\">專輯</string>\n    <string name=\"filter_artists\">藝人</string>\n    <string name=\"filter_playlists\">播放清單</string>\n    <string name=\"filter_community_playlists\">社群播放清單</string>\n    <string name=\"filter_featured_playlists\">精選播放清單</string>\n    <string name=\"filter_bookmarked\">收藏</string>\n    <string name=\"no_results_found\">找不到結果</string>\n    <string name=\"library_song_empty\">媒體庫的歌曲會顯示在這裡</string>\n    <string name=\"library_artist_empty\">媒體庫的藝人會顯示在這裡</string>\n    <string name=\"library_album_empty\">媒體庫的專輯會顯示在這裡</string>\n    <string name=\"library_playlist_empty\">你的播放清單會顯示在這裡</string>\n    <string name=\"from_your_library\">來自你的媒體庫</string>\n    <string name=\"other_versions\">其他版本</string>\n    <string name=\"liked_songs\">喜歡的歌曲</string>\n    <string name=\"downloaded_songs\">已下載的歌曲</string>\n    <string name=\"playlist_is_empty\">播放清單為空</string>\n    <string name=\"remove_download_playlist_confirm\">確定要刪除「%s」的下載嗎？</string>\n    <string name=\"delete_playlist_confirm\">確定要刪除播放清單「%s」嗎?</string>\n    <string name=\"retry\">重試</string>\n    <string name=\"radio\">電台</string>\n    <string name=\"shuffle\">隨機播放</string>\n    <string name=\"reset\">重設</string>\n    <string name=\"details\">詳細資訊</string>\n    <string name=\"edit\">編輯</string>\n    <string name=\"start_radio\">開啟電台</string>\n    <string name=\"play\">播放</string>\n    <string name=\"play_next\">接著播放</string>\n    <string name=\"add_to_queue\">加入待播清單</string>\n    <string name=\"add_to_library\">加入媒體庫</string>\n    <string name=\"add_all_to_library\">全部加入媒體庫</string>\n    <string name=\"remove_from_library\">從音樂庫中移除</string>\n    <string name=\"remove_all_from_library\">全部從音樂庫中移除</string>\n    <string name=\"action_download\">下載</string>\n    <string name=\"downloading\">下載中</string>\n    <string name=\"remove_download\">刪除下載</string>\n    <string name=\"import_playlist\">匯入播放清單</string>\n    <string name=\"add_to_playlist\">加入播放清單</string>\n    <string name=\"view_artist\">瀏覽藝人</string>\n    <string name=\"view_album\">瀏覽專輯</string>\n    <string name=\"refetch\">更新資料</string>\n    <string name=\"share\">分享</string>\n    <string name=\"delete\">移除</string>\n    <string name=\"remove_from_history\">從記錄中移除</string>\n    <string name=\"remove_from_playlist\">從播放清單中移除</string>\n    <string name=\"remove_from_queue\">從播放佇列中移除</string>\n    <string name=\"search_online\">線上搜尋</string>\n    <string name=\"action_sync\">同步</string>\n    <string name=\"advanced\">進階</string>\n    <string name=\"tempo_and_pitch\">速度和音調</string>\n    <string name=\"sort_by_create_date\">新增時間</string>\n    <string name=\"sort_by_name\">名稱</string>\n    <string name=\"sort_by_artist\">藝人</string>\n    <string name=\"sort_by_year\">年份</string>\n    <string name=\"sort_by_song_count\">歌曲總數</string>\n    <string name=\"sort_by_length\">長度</string>\n    <string name=\"sort_by_play_time\">播放時間</string>\n    <string name=\"sort_by_custom\">自訂順序</string>\n    <string name=\"media_id\">Id</string>\n    <string name=\"mime_type\">MIME 類型</string>\n    <string name=\"codecs\">編碼</string>\n    <string name=\"bitrate\">位元速率</string>\n    <string name=\"sample_rate\">採樣率</string>\n    <string name=\"loudness\">響度</string>\n    <string name=\"volume\">音量</string>\n    <string name=\"file_size\">檔案大小</string>\n    <string name=\"unknown\">未知</string>\n    <string name=\"copied\">已複製至剪貼簿</string>\n    <string name=\"edit_lyrics\">編輯歌詞</string>\n    <string name=\"search_lyrics\">搜尋歌詞</string>\n    <string name=\"edit_song\">編輯歌曲</string>\n    <string name=\"song_title\">歌名</string>\n    <string name=\"song_artists\">藝人</string>\n    <string name=\"error_song_title_empty\">歌名不能為空</string>\n    <string name=\"error_song_artist_empty\">藝人不能為空</string>\n    <string name=\"save\">儲存</string>\n    <string name=\"choose_playlist\">選擇播放清單</string>\n    <string name=\"edit_playlist\">編輯播放清單</string>\n    <string name=\"create_playlist\">新增播放清單</string>\n    <string name=\"playlist_name\">名稱</string>\n    <string name=\"error_playlist_name_empty\">播放清單名稱不能為空</string>\n    <string name=\"edit_artist\">編輯藝人</string>\n    <string name=\"artist_name\">藝人名稱</string>\n    <string name=\"error_artist_name_empty\">藝人名稱不能為空</string>\n    <string name=\"duplicates\">重複項目</string>\n    <string name=\"skip_duplicates\">略過重複項目</string>\n    <string name=\"add_anyway\">仍要新增</string>\n    <string name=\"duplicates_description_single\">播放清單已有此曲目</string>\n    <string name=\"duplicates_description_multiple\">播放清單已有 %d 首相同曲目</string>\n    <plurals name=\"n_song\">\n        <item quantity=\"other\">%d 首歌曲</item>\n    </plurals>\n    <plurals name=\"n_artist\">\n        <item quantity=\"other\">%d 位藝人</item>\n    </plurals>\n    <plurals name=\"n_album\">\n        <item quantity=\"other\">%d 張專輯</item>\n    </plurals>\n    <plurals name=\"n_playlist\">\n        <item quantity=\"other\">%d 個播放清單</item>\n    </plurals>\n    <plurals name=\"n_week\">\n        <item quantity=\"other\">%d 週</item>\n    </plurals>\n    <plurals name=\"n_month\">\n        <item quantity=\"other\">%d 個月</item>\n    </plurals>\n    <plurals name=\"n_year\">\n        <item quantity=\"other\">%d 年</item>\n    </plurals>\n    <string name=\"playlist_imported\">已匯入此播放清單</string>\n    <string name=\"removed_song_from_playlist\">已將「%s」從播放清單移除</string>\n    <string name=\"playlist_synced\">同步完成</string>\n    <string name=\"undo\">復原</string>\n    <string name=\"lyrics_not_found\">沒有歌詞</string>\n    <string name=\"sleep_timer\">睡眠定時器</string>\n    <string name=\"end_of_song\">這首歌曲播放完畢</string>\n    <plurals name=\"minute\">\n        <item quantity=\"other\">%d 分鐘</item>\n    </plurals>\n    <string name=\"error_no_stream\">沒有可用的音源</string>\n    <string name=\"error_no_internet\">沒有網路連線</string>\n    <string name=\"error_timeout\">連線逾時</string>\n    <string name=\"error_unknown\">未知的錯誤</string>\n    <string name=\"action_like\">喜歡</string>\n    <string name=\"action_like_all\">全部喜歡</string>\n    <string name=\"action_remove_like\">取消喜歡</string>\n    <string name=\"action_remove_like_all\">全部取消喜歡</string>\n    <string name=\"action_shuffle_on\">隨機播放開啟</string>\n    <string name=\"action_shuffle_off\">隨機播放關閉</string>\n    <string name=\"repeat_mode_off\">重複播放關閉</string>\n    <string name=\"repeat_mode_one\">重複播放此歌曲</string>\n    <string name=\"repeat_mode_all\">重複播放佇列</string>\n    <string name=\"queue_all_songs\">全部歌曲</string>\n    <string name=\"queue_searched_songs\">搜尋的歌曲</string>\n    <string name=\"music_player\">音樂播放器</string>\n    <string name=\"settings\">設定</string>\n    <string name=\"appearance\">外觀</string>\n    <string name=\"theme\">主題</string>\n    <string name=\"enable_dynamic_theme\">使用動態主題</string>\n    <string name=\"dark_theme\">深色主題</string>\n    <string name=\"dark_theme_on\">開</string>\n    <string name=\"dark_theme_off\">關</string>\n    <string name=\"dark_theme_follow_system\">跟隨系統</string>\n    <string name=\"pure_black\">純黑</string>\n    <string name=\"customize_navigation_tabs\">自訂導覽列</string>\n    <string name=\"player\">播放器</string>\n    <string name=\"player_text_alignment\">播放器文字對齊</string>\n    <string name=\"lyrics_text_position\">歌詞文字位置</string>\n    <string name=\"sided\">靠邊</string>\n    <string name=\"left\">靠左</string>\n    <string name=\"center\">置中</string>\n    <string name=\"right\">靠右</string>\n    <string name=\"player_slider_style\">播放器滑桿樣式</string>\n    <string name=\"default_\">預設</string>\n    <string name=\"squiggly\">波浪</string>\n    <string name=\"misc\">其他</string>\n    <string name=\"default_open_tab\">預設啟動標籤</string>\n    <string name=\"grid_cell_size\">網格大小</string>\n    <string name=\"small\">小</string>\n    <string name=\"big\">大</string>\n    <string name=\"content\">內容</string>\n    <string name=\"login\">登入</string>\n    <string name=\"not_logged_in\">尚未登入</string>\n    <string name=\"content_language\">預設內容語言</string>\n    <string name=\"content_country\">預設內容國家</string>\n    <string name=\"system_default\">系統預設</string>\n    <string name=\"enable_proxy\">啟用 Proxy</string>\n    <string name=\"proxy_type\">Proxy 種類</string>\n    <string name=\"proxy_url\">Proxy URL</string>\n    <string name=\"restart_to_take_effect\">重啟以套用變更</string>\n    <string name=\"player_and_audio\">播放與音訊</string>\n    <string name=\"audio_quality\">音質</string>\n    <string name=\"audio_quality_auto\">自動</string>\n    <string name=\"audio_quality_high\">高</string>\n    <string name=\"audio_quality_low\">低</string>\n    <string name=\"queue\">播放佇列</string>\n    <string name=\"persistent_queue\">保留播放佇列</string>\n    <string name=\"persistent_queue_desc\">開啟應用程式時還原上次的播放佇列</string>\n    <string name=\"auto_load_more\">自動載入更多歌曲</string>\n    <string name=\"auto_load_more_desc\">當播放佇列快結束時，自動加入更多歌曲，如果可以的話</string>\n    <string name=\"skip_silence\">跳過無聲片段</string>\n    <string name=\"audio_normalization\">標準化音量</string>\n    <string name=\"auto_skip_next_on_error\">發生錯誤時自動跳到下一首</string>\n    <string name=\"auto_skip_next_on_error_desc\">讓你享受音樂不中斷</string>\n    <string name=\"stop_music_on_task_clear\">將音樂在清除任務時停止</string>\n    <string name=\"equalizer\">等化器</string>\n    <string name=\"storage\">儲存</string>\n    <string name=\"cache\">快取</string>\n    <string name=\"image_cache\">圖片快取</string>\n    <string name=\"song_cache\">歌曲快取</string>\n    <string name=\"max_cache_size\">最大快取大小</string>\n    <string name=\"unlimited\">無限制</string>\n    <string name=\"clear_all_downloads\">清除所有下載</string>\n    <string name=\"max_image_cache_size\">圖片快取大小</string>\n    <string name=\"clear_image_cache\">清除圖片快取</string>\n    <string name=\"max_song_cache_size\">歌曲快取大小</string>\n    <string name=\"clear_song_cache\">清除歌曲快取</string>\n    <string name=\"size_used\">已使用 %s</string>\n    <string name=\"privacy\">隱私</string>\n    <string name=\"listen_history\">觀看記錄</string>\n    <string name=\"pause_listen_history\">暫停觀看記錄</string>\n    <string name=\"clear_listen_history\">清除觀看記錄</string>\n    <string name=\"clear_listen_history_confirm\">您確定要清除所有觀看記錄嗎？</string>\n    <string name=\"search_history\">搜尋記錄</string>\n    <string name=\"pause_search_history\">暫停搜尋記錄</string>\n    <string name=\"clear_search_history\">清除搜尋記錄</string>\n    <string name=\"clear_search_history_confirm\">您確定要清除所有搜尋記錄嗎？</string>\n    <string name=\"disable_screenshot\">禁用截圖</string>\n    <string name=\"disable_screenshot_desc\">當此選項開啟時，您無法截圖，也無法在「最近使用」中看到此應用程式的畫面。</string>\n    <string name=\"enable_lrclib\">使用 LrcLib 提供歌詞</string>\n    <string name=\"enable_kugou\">使用酷狗音樂提供歌詞</string>\n    <string name=\"hide_explicit\">移除不適當內容</string>\n    <string name=\"backup_restore\">備份與還原</string>\n    <string name=\"action_backup\">備份</string>\n    <string name=\"action_restore\">還原</string>\n    <string name=\"imported_playlist\">已匯入的播放清單</string>\n    <string name=\"backup_create_success\">成功建立備份</string>\n    <string name=\"backup_create_failed\">無法建立備份</string>\n    <string name=\"restore_failed\">無法還原備份</string>\n    <string name=\"discord_integration\">Discord 整合</string>\n    <string name=\"discord_information\">Metrolist使用KizzyRPC函式庫來設定您的Discord狀態。這會用到Discord Gateway連線，可能會違反Discord服務條款，但是目前沒有使用者為此被停用帳號。使用此功能需自行承擔此風險。\\n\\nMetrolist只會提取你的token，所有東西都存在本機。</string>\n    <string name=\"dismiss\">了解</string>\n    <string name=\"options\">選項</string>\n    <string name=\"preview\">預覽</string>\n    <string name=\"login_failed\">登入失敗</string>\n    <string name=\"action_logout\">登出</string>\n    <string name=\"enable_discord_rpc\">啟用 Rich Presence</string>\n    <string name=\"about\">關於</string>\n    <string name=\"app_version\">應用程式版本</string>\n    <string name=\"new_version_available\">發現新版本</string>\n    <string name=\"translation_models\">翻譯模型</string>\n    <string name=\"clear_translation_models\">清除翻譯模型</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/xml/automotive_app_desc.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<automotiveApp>\n    <uses name=\"media\" />\n</automotiveApp>"
  },
  {
    "path": "app/src/main/res/xml/backup_rules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<full-backup-content>\n    <exclude\n        domain=\"file\"\n        path=\"./exoplayer\" />\n    <exclude\n        domain=\"file\"\n        path=\"./download\" />\n    <exclude\n        domain=\"database\"\n        path=\"exoplayer_internal.db\" />\n</full-backup-content>"
  },
  {
    "path": "app/src/main/res/xml/data_extraction_rules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n   Sample data extraction rules file; uncomment and customize as necessary.\n   See https://developer.android.com/about/versions/12/backup-restore#xml-changes\n   for details.\n-->\n<data-extraction-rules>\n    <cloud-backup>\n        <exclude\n            domain=\"file\"\n            path=\"./exoplayer\" />\n        <exclude\n            domain=\"file\"\n            path=\"./download\" />\n        <exclude\n            domain=\"database\"\n            path=\"exoplayer_internal.db\" />\n    </cloud-backup>\n    <device-transfer>\n        <exclude\n            domain=\"file\"\n            path=\"./exoplayer\" />\n        <exclude\n            domain=\"file\"\n            path=\"./download\" />\n        <exclude\n            domain=\"database\"\n            path=\"exoplayer_internal.db\" />\n    </device-transfer>\n</data-extraction-rules>"
  },
  {
    "path": "app/src/main/res/xml/music_widget_info.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<appwidget-provider xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:minWidth=\"110dp\"\n    android:minHeight=\"40dp\"\n    android:minResizeWidth=\"110dp\"\n    android:minResizeHeight=\"40dp\"\n    android:updatePeriodMillis=\"1800000\"\n    android:previewLayout=\"@layout/widget_music_player\"\n    android:initialLayout=\"@layout/widget_music_player\"\n    android:description=\"@string/widget_description\"\n    android:widgetCategory=\"home_screen\"\n    android:resizeMode=\"horizontal|vertical\" />\n"
  },
  {
    "path": "app/src/main/res/xml/network_security_config.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<network-security-config>\n    <!-- Allow cleartext traffic for Listen Together local servers -->\n    <!-- We're already using WSS for the prod server -->\n    <base-config cleartextTrafficPermitted=\"true\">\n        <trust-anchors>\n            <certificates src=\"system\" />\n        </trust-anchors>\n    </base-config>\n</network-security-config>\n"
  },
  {
    "path": "app/src/main/res/xml/provider_paths.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths>\n    <external-path\n        name=\"external_files\"\n        path=\".\" />\n    <cache-path\n        name=\"cache\"\n        path=\".\" />\n    <external-cache-path\n        name=\"external_cache\"\n        path=\".\" />\n    <external-files-path\n        name=\"external_files_documents\"\n        path=\"Documents/MetrolistExports/\" />\n</paths>"
  },
  {
    "path": "app/src/main/res/xml/recognizer_widget_info.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<appwidget-provider xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:minWidth=\"40dp\"\n    android:minHeight=\"40dp\"\n    android:minResizeWidth=\"40dp\"\n    android:minResizeHeight=\"40dp\"\n    android:updatePeriodMillis=\"0\"\n    android:previewLayout=\"@layout/widget_recognizer_wide\"\n    android:initialLayout=\"@layout/widget_recognizer_wide\"\n    android:description=\"@string/widget_recognizer_description\"\n    android:widgetCategory=\"home_screen\"\n    android:resizeMode=\"horizontal|vertical\" />\n"
  },
  {
    "path": "app/src/main/res/xml/shortcuts.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shortcuts xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <shortcut\n        android:enabled=\"true\"\n        android:icon=\"@drawable/shortcut_search\"\n        android:shortcutId=\"search\"\n        android:shortcutShortLabel=\"@string/search\">\n        <intent\n            android:action=\"com.metrolist.music.action.SEARCH\"\n            android:targetClass=\"com.metrolist.music.MainActivity\"\n            android:targetPackage=\"com.metrolist.music\" />\n    </shortcut>\n    <shortcut\n        android:enabled=\"true\"\n        android:icon=\"@drawable/shortcut_library\"\n        android:shortcutId=\"library\"\n        android:shortcutShortLabel=\"@string/filter_library\">\n        <intent\n            android:action=\"com.metrolist.music.action.LIBRARY\"\n            android:targetClass=\"com.metrolist.music.MainActivity\"\n            android:targetPackage=\"com.metrolist.music\" />\n    </shortcut>\n</shortcuts>\n"
  },
  {
    "path": "app/src/main/res/xml/turntable_widget_info.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<appwidget-provider xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:minWidth=\"110dp\"\n    android:minHeight=\"110dp\"\n    android:updatePeriodMillis=\"1800000\"\n    android:previewLayout=\"@layout/widget_turntable\"\n    android:initialLayout=\"@layout/widget_turntable\"\n    android:description=\"@string/turntable_widget_description\"\n    android:widgetCategory=\"home_screen\"\n    android:resizeMode=\"none\" />\n"
  },
  {
    "path": "app/src/main/res/xml-v31/music_widget_info.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<appwidget-provider xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:minWidth=\"110dp\"\n    android:minHeight=\"40dp\"\n    android:minResizeWidth=\"110dp\"\n    android:minResizeHeight=\"40dp\"\n    android:targetCellWidth=\"4\"\n    android:targetCellHeight=\"2\"\n    android:updatePeriodMillis=\"1800000\"\n    android:previewImage=\"@mipmap/ic_launcher\"\n    android:previewLayout=\"@layout/widget_music_player\"\n    android:initialLayout=\"@layout/widget_music_player\"\n    android:description=\"@string/widget_description\"\n    android:widgetCategory=\"home_screen\"\n    android:resizeMode=\"horizontal|vertical\" />\n"
  },
  {
    "path": "app/src/main/res/xml-v31/recognizer_widget_info.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<appwidget-provider xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:minWidth=\"40dp\"\n    android:minHeight=\"40dp\"\n    android:minResizeWidth=\"40dp\"\n    android:minResizeHeight=\"40dp\"\n    android:targetCellWidth=\"4\"\n    android:targetCellHeight=\"1\"\n    android:updatePeriodMillis=\"0\"\n    android:previewImage=\"@mipmap/ic_launcher\"\n    android:previewLayout=\"@layout/widget_recognizer_wide\"\n    android:initialLayout=\"@layout/widget_recognizer_wide\"\n    android:description=\"@string/widget_recognizer_description\"\n    android:widgetCategory=\"home_screen\"\n    android:resizeMode=\"horizontal|vertical\" />\n"
  },
  {
    "path": "app/src/main/res/xml-v31/turntable_widget_info.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<appwidget-provider xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:minWidth=\"110dp\"\n    android:minHeight=\"110dp\"\n    android:targetCellWidth=\"2\"\n    android:targetCellHeight=\"2\"\n    android:updatePeriodMillis=\"1800000\"\n    android:previewImage=\"@mipmap/ic_launcher\"\n    android:previewLayout=\"@layout/widget_turntable\"\n    android:initialLayout=\"@layout/widget_turntable\"\n    android:description=\"@string/turntable_widget_description\"\n    android:widgetCategory=\"home_screen\"\n    android:resizeMode=\"none\" />\n"
  },
  {
    "path": "betterlyrics/build.gradle.kts",
    "content": "plugins {\n    id(\"com.android.library\")\n    alias(libs.plugins.kotlin.serialization)\n}\n\nandroid {\n    namespace = \"com.metrolist.betterlyrics\"\n    compileSdk = 36\n\n    defaultConfig {\n        minSdk = 26\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_21\n        targetCompatibility = JavaVersion.VERSION_21\n    }\n}\n\nkotlin {\n    jvmToolchain(21)\n}\n\ndependencies {\n    implementation(libs.ktor.client.core)\n    implementation(libs.ktor.client.cio)\n    implementation(libs.ktor.client.content.negotiation)\n    implementation(libs.ktor.serialization.json)\n\n    coreLibraryDesugaring(libs.desugaring)\n}\n"
  },
  {
    "path": "betterlyrics/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n</manifest>\n"
  },
  {
    "path": "betterlyrics/src/main/kotlin/com/metrolist/music/betterlyrics/BetterLyrics.kt",
    "content": "package com.metrolist.music.betterlyrics\n\nimport com.metrolist.music.betterlyrics.models.TTMLResponse\nimport io.ktor.client.HttpClient\nimport io.ktor.client.call.body\nimport io.ktor.client.engine.cio.CIO\nimport io.ktor.client.plugins.contentnegotiation.ContentNegotiation\nimport io.ktor.client.plugins.defaultRequest\nimport io.ktor.client.plugins.HttpTimeout\nimport io.ktor.client.request.get\nimport io.ktor.client.request.parameter\nimport io.ktor.http.HttpStatusCode\nimport io.ktor.serialization.kotlinx.json.json\nimport kotlinx.serialization.json.Json\n\nobject BetterLyrics {\n    private val client by lazy {\n        HttpClient(CIO) {\n            install(ContentNegotiation) {\n                json(\n                    Json {\n                        isLenient = true\n                        ignoreUnknownKeys = true\n                    },\n                )\n            }\n\n            install(HttpTimeout) {\n                requestTimeoutMillis = 15000\n                connectTimeoutMillis = 10000\n                socketTimeoutMillis = 15000\n            }\n\n            defaultRequest {\n                url(\"https://lyrics-api.boidu.dev\")\n                headers {\n                    append(\"User-Agent\", \"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\")\n                    append(\"Accept\", \"application/json\")\n                }\n            }\n\n            expectSuccess = false\n        }\n    }\n\n    private suspend fun fetchTTML(\n        artist: String,\n        title: String,\n        duration: Int = -1,\n        album: String? = null,\n    ): String? = runCatching {\n        val response = client.get(\"/getLyrics\") {\n            parameter(\"s\", title)\n            parameter(\"a\", artist)\n            if (duration > 0) {\n                parameter(\"d\", duration)\n            }\n            if (!album.isNullOrBlank()) {\n                parameter(\"al\", album)\n            }\n        }\n        if (response.status == HttpStatusCode.OK) {\n            response.body<TTMLResponse>().ttml\n        } else {\n            null\n        }\n    }.getOrNull()\n\n    suspend fun getLyrics(\n        title: String,\n        artist: String,\n        duration: Int,\n        album: String? = null,\n    ) = runCatching {\n        // Use exact title and artist - no normalization to ensure correct sync\n        // Normalizing can return wrong lyrics (e.g., radio edit vs original)\n        val ttml = fetchTTML(artist, title, duration, album)\n            ?: throw IllegalStateException(\"Lyrics unavailable\")\n        \n        val parsedLines = TTMLParser.parseTTML(ttml)\n        if (parsedLines.isEmpty()) {\n            throw IllegalStateException(\"Failed to parse lyrics\")\n        }\n        \n        TTMLParser.toLRC(parsedLines)\n    }\n\n    suspend fun getAllLyrics(\n        title: String,\n        artist: String,\n        duration: Int,\n        album: String? = null,\n        callback: (String) -> Unit,\n    ) {\n        getLyrics(title, artist, duration, album)\n            .onSuccess { lrcString ->\n                callback(lrcString)\n            }\n    }\n}\n"
  },
  {
    "path": "betterlyrics/src/main/kotlin/com/metrolist/music/betterlyrics/TTMLParser.kt",
    "content": "package com.metrolist.music.betterlyrics\n\nimport org.w3c.dom.Element\nimport org.w3c.dom.Node\nimport javax.xml.parsers.DocumentBuilderFactory\n\nobject TTMLParser {\n    \n    data class ParsedLine(\n        val text: String,\n        val startTime: Double,\n        val words: List<ParsedWord>,\n        val agent: String? = null,\n        val isBackground: Boolean = false,\n        val backgroundLines: List<ParsedLine> = emptyList()\n    )\n    \n    data class ParsedWord(\n        val text: String,\n        val startTime: Double,\n        val endTime: Double\n    )\n    \n    private data class SpanInfo(\n        val text: String,\n        val startTime: Double,\n        val endTime: Double,\n        val hasTrailingSpace: Boolean\n    )\n    \n    // Helper function to get attribute by local name (handles namespace prefixes)\n    private fun Element.getAttributeByLocalName(localName: String): String {\n        // First try namespace-aware lookup\n        val nsValue = getAttributeNS(\"http://www.w3.org/ns/ttml#metadata\", localName)\n        if (nsValue.isNotEmpty()) return nsValue\n        \n        // Then try with common prefixes\n        val prefixedValue = getAttribute(\"ttm:$localName\")\n        if (prefixedValue.isNotEmpty()) return prefixedValue\n        \n        // Finally, search through all attributes\n        val attrs = attributes\n        for (i in 0 until attrs.length) {\n            val attr = attrs.item(i)\n            val attrName = attr.nodeName ?: continue\n            if (attrName == localName || attrName.endsWith(\":$localName\")) {\n                return attr.nodeValue ?: \"\"\n            }\n        }\n        return \"\"\n    }\n    \n    fun parseTTML(ttml: String): List<ParsedLine> {\n        val lines = mutableListOf<ParsedLine>()\n        \n        try {\n            val factory = DocumentBuilderFactory.newInstance()\n            factory.isNamespaceAware = true\n            val builder = factory.newDocumentBuilder()\n            val doc = builder.parse(ttml.byteInputStream())\n            \n            val pElements = doc.getElementsByTagName(\"p\")\n            \n            for (i in 0 until pElements.length) {\n                val pElement = pElements.item(i) as? Element ?: continue\n                \n                val begin = pElement.getAttribute(\"begin\")\n                if (begin.isNullOrEmpty()) continue\n                \n                val startTime = parseTime(begin)\n                val spanInfos = mutableListOf<SpanInfo>()\n                val backgroundLines = mutableListOf<ParsedLine>()\n                \n                // Get agent/vocalist info (ttm:agent attribute)\n                val agent = pElement.getAttributeByLocalName(\"agent\").ifEmpty { null }\n                \n                // Parse child nodes to preserve whitespace between spans\n                val childNodes = pElement.childNodes\n                for (j in 0 until childNodes.length) {\n                    val node = childNodes.item(j)\n                    \n                    when (node.nodeType) {\n                        Node.ELEMENT_NODE -> {\n                            val span = node as? Element\n                            if (span?.tagName?.lowercase() == \"span\") {\n                                // Check for background vocal role (ttm:role=\"x-bg\")\n                                val role = span.getAttributeByLocalName(\"role\")\n                                \n                                when (role) {\n                                    \"x-bg\" -> {\n                                        // Parse background vocal line\n                                        val bgLine = parseBackgroundSpan(span, startTime)\n                                        if (bgLine != null) {\n                                            backgroundLines.add(bgLine)\n                                        }\n                                    }\n                                    \"x-translation\", \"x-roman\" -> {\n                                        // Skip translation and romanization spans\n                                    }\n                                    else -> {\n                                        // Regular word span\n                                        val wordBegin = span.getAttribute(\"begin\")\n                                        val wordEnd = span.getAttribute(\"end\")\n                                        val wordText = span.textContent?.trim() ?: \"\"\n                                        \n                                        if (wordText.isNotEmpty() && wordBegin.isNotEmpty() && wordEnd.isNotEmpty()) {\n                                            val nextSibling = node.nextSibling\n                                            val hasTrailingSpace = nextSibling?.nodeType == Node.TEXT_NODE && \n                                                nextSibling.textContent?.contains(Regex(\"\\\\s\")) == true\n                                            \n                                            spanInfos.add(\n                                                SpanInfo(\n                                                    text = wordText,\n                                                    startTime = parseTime(wordBegin),\n                                                    endTime = parseTime(wordEnd),\n                                                    hasTrailingSpace = hasTrailingSpace\n                                                )\n                                            )\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n                \n                // Merge consecutive spans without whitespace between them into single words\n                val words = mergeSpansIntoWords(spanInfos)\n                val lineText = words.joinToString(\" \") { it.text }\n                \n                // If no spans found, use text content directly (excluding background text)\n                val finalText = if (lineText.isEmpty()) {\n                    getDirectTextContent(pElement).trim()\n                } else {\n                    lineText\n                }\n                \n                if (finalText.isNotEmpty()) {\n                    lines.add(\n                        ParsedLine(\n                            text = finalText,\n                            startTime = startTime,\n                            words = words,\n                            agent = agent,\n                            isBackground = false,\n                            backgroundLines = backgroundLines\n                        )\n                    )\n                }\n            }\n        } catch (e: Exception) {\n            return emptyList()\n        }\n        \n        return lines\n    }\n    \n    private fun parseBackgroundSpan(span: Element, parentStartTime: Double): ParsedLine? {\n        val bgBegin = span.getAttribute(\"begin\")\n        val bgEnd = span.getAttribute(\"end\")\n        val bgStartTime = if (bgBegin.isNotEmpty()) parseTime(bgBegin) else parentStartTime\n        \n        val spanInfos = mutableListOf<SpanInfo>()\n        val childNodes = span.childNodes\n        \n        for (j in 0 until childNodes.length) {\n            val node = childNodes.item(j)\n            if (node.nodeType == Node.ELEMENT_NODE) {\n                val innerSpan = node as? Element\n                if (innerSpan?.tagName?.lowercase() == \"span\") {\n                    val role = innerSpan.getAttributeByLocalName(\"role\")\n                    \n                    // Skip translation and romanization spans\n                    if (role == \"x-translation\" || role == \"x-roman\") continue\n                    \n                    val wordBegin = innerSpan.getAttribute(\"begin\")\n                    val wordEnd = innerSpan.getAttribute(\"end\")\n                    val wordText = innerSpan.textContent?.trim() ?: \"\"\n                    \n                    if (wordText.isNotEmpty() && wordBegin.isNotEmpty() && wordEnd.isNotEmpty()) {\n                        val nextSibling = node.nextSibling\n                        val hasTrailingSpace = nextSibling?.nodeType == Node.TEXT_NODE && \n                            nextSibling.textContent?.contains(Regex(\"\\\\s\")) == true\n                        \n                        spanInfos.add(\n                            SpanInfo(\n                                text = wordText,\n                                startTime = parseTime(wordBegin),\n                                endTime = parseTime(wordEnd),\n                                hasTrailingSpace = hasTrailingSpace\n                            )\n                        )\n                    }\n                }\n            }\n        }\n        \n        val words = mergeSpansIntoWords(spanInfos)\n        val lineText = words.joinToString(\" \") { it.text }\n        \n        val finalText = if (lineText.isEmpty()) {\n            getDirectTextContent(span).trim()\n        } else {\n            lineText\n        }\n        \n        return if (finalText.isNotEmpty()) {\n            ParsedLine(\n                text = finalText,\n                startTime = bgStartTime,\n                words = words,\n                agent = null,\n                isBackground = true,\n                backgroundLines = emptyList()\n            )\n        } else null\n    }\n    \n    private fun getDirectTextContent(element: Element): String {\n        val sb = StringBuilder()\n        val childNodes = element.childNodes\n        for (i in 0 until childNodes.length) {\n            val node = childNodes.item(i)\n            if (node.nodeType == Node.TEXT_NODE) {\n                sb.append(node.textContent)\n            } else if (node.nodeType == Node.ELEMENT_NODE) {\n                val el = node as? Element\n                val role = el?.getAttributeByLocalName(\"role\") ?: \"\"\n                // Skip background, translation, and romanization spans\n                if (role != \"x-bg\" && role != \"x-translation\" && role != \"x-roman\") {\n                    if (el?.tagName?.lowercase() == \"span\") {\n                        sb.append(el.textContent ?: \"\")\n                    }\n                }\n            }\n        }\n        return sb.toString()\n    }\n    \n    private fun mergeSpansIntoWords(spanInfos: List<SpanInfo>): List<ParsedWord> {\n        if (spanInfos.isEmpty()) return emptyList()\n        \n        val words = mutableListOf<ParsedWord>()\n        var currentText = StringBuilder()\n        var currentStartTime = spanInfos[0].startTime\n        var currentEndTime = spanInfos[0].endTime\n        \n        for ((index, span) in spanInfos.withIndex()) {\n            if (index == 0) {\n                currentText.append(span.text)\n                currentStartTime = span.startTime\n                currentEndTime = span.endTime\n            } else {\n                // Check if previous span had trailing space (word boundary)\n                val prevSpan = spanInfos[index - 1]\n                if (prevSpan.hasTrailingSpace) {\n                    // Save current word and start new one\n                    if (currentText.isNotEmpty()) {\n                        words.add(\n                            ParsedWord(\n                                text = currentText.toString().trim(),\n                                startTime = currentStartTime,\n                                endTime = currentEndTime\n                            )\n                        )\n                    }\n                    currentText = StringBuilder(span.text)\n                    currentStartTime = span.startTime\n                    currentEndTime = span.endTime\n                } else {\n                    // No space between spans - merge into same word (syllables)\n                    currentText.append(span.text)\n                    currentEndTime = span.endTime\n                }\n            }\n        }\n        \n        // Add the last word\n        if (currentText.isNotEmpty()) {\n            words.add(\n                ParsedWord(\n                    text = currentText.toString().trim(),\n                    startTime = currentStartTime,\n                    endTime = currentEndTime\n                )\n            )\n        }\n        \n        return words\n    }\n    \n    fun toLRC(lines: List<ParsedLine>): String {\n        return buildString {\n            lines.forEach { line ->\n                val timeMs = (line.startTime * 1000).toLong()\n                val minutes = timeMs / 60000\n                val seconds = (timeMs % 60000) / 1000\n                val centiseconds = (timeMs % 1000) / 10\n                \n                // Add agent info if present\n                val agentPrefix = if (!line.agent.isNullOrEmpty()) \"{agent:${line.agent}}\" else \"\"\n                \n                appendLine(String.format(\"[%02d:%02d.%02d]%s%s\", minutes, seconds, centiseconds, agentPrefix, line.text))\n                \n                if (line.words.isNotEmpty()) {\n                    val wordsData = line.words.joinToString(\"|\") { word ->\n                        \"${word.text}:${word.startTime}:${word.endTime}\"\n                    }\n                    appendLine(\"<$wordsData>\")\n                }\n                \n                // Add background vocals as separate lines\n                line.backgroundLines.forEach { bgLine ->\n                    val bgTimeMs = (bgLine.startTime * 1000).toLong()\n                    val bgMinutes = bgTimeMs / 60000\n                    val bgSeconds = (bgTimeMs % 60000) / 1000\n                    val bgCentiseconds = (bgTimeMs % 1000) / 10\n                    \n                    appendLine(String.format(\"[%02d:%02d.%02d]{bg}%s\", bgMinutes, bgSeconds, bgCentiseconds, bgLine.text))\n                    \n                    if (bgLine.words.isNotEmpty()) {\n                        val bgWordsData = bgLine.words.joinToString(\"|\") { word ->\n                            \"${word.text}:${word.startTime}:${word.endTime}\"\n                        }\n                        appendLine(\"<$bgWordsData>\")\n                    }\n                }\n            }\n        }\n    }\n    \n    private fun parseTime(timeStr: String): Double {\n        return try {\n            when {\n                timeStr.contains(\":\") -> {\n                    val parts = timeStr.split(\":\")\n                    when (parts.size) {\n                        2 -> {\n                            val minutes = parts[0].toDouble()\n                            val seconds = parts[1].toDouble()\n                            minutes * 60 + seconds\n                        }\n                        3 -> {\n                            val hours = parts[0].toDouble()\n                            val minutes = parts[1].toDouble()\n                            val seconds = parts[2].toDouble()\n                            hours * 3600 + minutes * 60 + seconds\n                        }\n                        else -> timeStr.toDoubleOrNull() ?: 0.0\n                    }\n                }\n                else -> timeStr.toDoubleOrNull() ?: 0.0\n            }\n        } catch (e: Exception) {\n            0.0\n        }\n    }\n}\n"
  },
  {
    "path": "betterlyrics/src/main/kotlin/com/metrolist/music/betterlyrics/models/Track.kt",
    "content": "package com.metrolist.music.betterlyrics.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class TTMLResponse(\n    val ttml: String\n)\n\n@Serializable\ndata class SearchResponse(\n    val results: List<Track>\n)\n\n@Serializable\ndata class Track(\n    val title: String,\n    val artist: String,\n    val album: String? = null,\n    val duration: Double,\n    val lyrics: Lyrics? = null\n)\n\n@Serializable\ndata class Lyrics(\n    val lines: List<Line>\n)\n\n@Serializable\ndata class Line(\n    val text: String,\n    val startTime: Double,\n    val words: List<Word>? = null\n)\n\n@Serializable\ndata class Word(\n    val text: String,\n    val startTime: Double,\n    val endTime: Double\n)\n"
  },
  {
    "path": "build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.hilt) apply (false)\n    alias(libs.plugins.kotlin.ksp) apply (false)\n    alias(libs.plugins.kotlin.serialization) apply false\n}\n\nbuildscript {\n    repositories {\n        google()\n        mavenCentral()\n        maven { setUrl(\"https://jitpack.io\") }\n        maven { setUrl(\"https://maven.aliyun.com/repository/public\") }\n    }\n    dependencies {\n        classpath(libs.gradle)\n        classpath(kotlin(\"gradle-plugin\", libs.versions.kotlin.get()))\n    }\n}\n\ntasks.register<Delete>(\"clean\") {\n    delete(rootProject.layout.buildDirectory)\n}\n\nsubprojects {\n    tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {\n        compilerOptions {\n            if (project.findProperty(\"enableComposeCompilerReports\") == \"true\") {\n                arrayOf(\"reports\", \"metrics\").forEach {\n                    freeCompilerArgs.add(\"-P\")\n                    freeCompilerArgs.add(\"plugin:androidx.compose.compiler.plugins.kotlin:${it}Destination=${project.layout.buildDirectory}/compose_metrics\")\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "changelog.md",
    "content": "---v13.3.0\n# Major changes\n- Implemented song upload and delete functionality (@alltechdev)\n- Multiple playback fixes and reliability improvements (@alltechdev, @mostafaalagamy)\n- Fixed proguard rules causing issues with Reproducible Builds (@nyxiereal)\n- Fixed proguard rules removing Listen Together protobuf classes (@mostafaalagamy)\n- Added a playlist export option to the playlist context menu (@nyxiereal)\n\n## Notable new features\n- Added a Play all action for the stats page (@isotjs)\n- Added a quick settings tile for recognizing music (@nyxiereal)\n- Added automatic sleep timer options and integrated fade-out volume handling (@isotjs)\n- Added a profile search filter (@alltechdev)\n- Added channel subscriptions for podcasts and artists (@alltechdev)\n\n## Other improvements\n- Fixed cached images not clearing properly and cached covers not showing when offline (@nyxiereal)\n- Removed useless and stale strings from the codebase (@nyxiereal)\n- Refined the song details view (@omardotdev)\n- Added support for Mistral AI models (@nyxiereal)\n- Redesigned the lastfm integration settings (@omardotdev)\n- Fixed importing csv files crashing the app (@nyxiereal)\n- Prevent guest playback while in listen together (@nyxiereal)\n- Fixed podcasts not working for logged-out users (@alltechdev)\n- Updated dependencies (@nyxiereal)\n\n## New Contributors\n* @isotjs made their first contribution in https://github.com/MetrolistGroup/Metrolist/pull/3090\n\n**Full Changelog**: https://github.com/MetrolistGroup/Metrolist/compare/v13.2.1...v13.3.0\n---v13.2.1\n>[!WARNING]\n>Listen Together doesn't work in v13.2.1! Use v13.2.0 if you need it.\n\n## Hot Fixes\n- Fix interface lag issue\n- Fix navigate local playlists pinned in speed dial\n- Removed \"cache songs only after playback has started\" option\n\n**Full Changelog**: https://github.com/MetrolistGroup/Metrolist/compare/v13.2.0...v13.2.1\n---v13.2.0\n# Major changes\n- Fixed playback breaking due to YouTube's February 2026 n-transform changes (@alltechdev)\n- Added full podcast library support (@mostafaalagamy & @alltechdev)\n- Redesigned loading, Changelog, and About screens (@adrielGGmotion)\n- Improved app startup time via parallelized home screen loading (@mostafaalagamy)\n\n## Notable new features\n- Added an option to cache songs only after playback has started (@kairosci)\n- Added a music recognizer home screen widget (@mostafaalagamy)\n- Rewrote music recognizer in pure Kotlin, removing NDK dependency and reducing APK size (@mostafaalagamy)\n- Overhauled lyrics: added LyricsPlus provider, AI lyric fixes, untranslation support, and provider priority settings (@nyxiereal)\n- Changed listen together to use protobuf, lowering latency and improving reliability (@nyxiereal)\n- Added auto-approve setting for listen together song requests (@nyxiereal)\n- Added an option to persist the sleep timer default value (@johannesbrauer)\n- Added a dialog on logout to keep or clear library data (@alltechdev)\n\n## Other improvements\n- Fixed backup restore causing playback errors due to stale auth credentials (@alltechdev)\n- The CSV import dialog is now scrollable (@kairosci)\n- Fixed Android 15 foreground service crashes (@kairosci)\n- Fixed a crash on the About screen on some devices (@mostafaalagamy)\n- Fixed home screen playlist navigation routing to wrong screen (@mostafaalagamy)\n- Fixed crash when creating local playlists (@mostafaalagamy)\n\n## New Contributors\n* @johannesbrauer made their first contribution in https://github.com/MetrolistGroup/Metrolist/pull/2991\n\n**Full Changelog**: https://github.com/MetrolistGroup/Metrolist/compare/v13.1.1...v13.2.0"
  },
  {
    "path": "crowdin.yml",
    "content": "files:\n  - source: /app/src/main/res/values/strings.xml\n    translation: /app/src/main/res/values-%android_code%/strings.xml\n"
  },
  {
    "path": "development_guide.md",
    "content": "# Metrolist Dev Guide\n\nThis file outlines the process of setting up a local dev environment for Metrolist.\n\n## Prerequisites\n\n- JDK 21\n- Android platform tools (if you don't have a keystore already)\n- protobuf-compiler v3.21 or newer\n\n## Basic setup\n\nThis has been tested on Linux, but should work on other platforms with some adjustments.\n\n```bash\ngit clone https://github.com/MetrolistGroup/Metrolist\ncd Metrolist\ngit submodule update --init --recursive\ncd app\nbash generate_proto.sh\ncd ..\n[ ! -f \"app/persistent-debug.keystore\" ] && keytool -genkeypair -v -keystore app/persistent-debug.keystore -storepass android -keypass android -alias androiddebugkey -keyalg RSA -keysize 2048 -validity 10000 -dname \"CN=Android Debug,O=Android,C=US\" || echo \"Keystore already exists.\"\n./gradlew :app:assembleFossDebug\nls app/build/outputs/apk/universalFoss/debug/app-universal-foss-debug.apk\n```\n\n### GitHub Secrets Configuration\n\nThis project uses GitHub Secrets to securely store API keys for building releases. To set up the secrets:\n\n1. Go to your GitHub repository settings\n2. Navigate to **Settings** → **Secrets and variables** → **Actions**\n3. Add the following repository secrets:\n   - `LASTFM_API_KEY`: Your LastFM API key\n   - `LASTFM_SECRET`: Your LastFM secret key\n\n4. Get your LastFM API credentials from: https://www.last.fm/api/account/create\n\n**Note:** These secrets are automatically injected into the build process via GitHub Actions and are not visible in the source code.\n"
  },
  {
    "path": "fastlane/metadata/android/ar/full_description.txt",
    "content": "عميل YouTube Music Material 3 لأندرويد\n\nالميزات:\n\n- تشغيل أي أغنية أو فيديو من YouTube Music\n- التشغيل في الخلفية\n- اقتراحات سريعة مخصصة\n- إدارة المكتبة\n- تنزيل الأغاني وتخزينها للتشغيل دون اتصال بالإنترنت\n- البحث عن الأغاني، الألبومات، الفنانين، الفيديوهات، وقوائم التشغيل\n- كلمات الأغاني الحية\n- دعم تسجيل الدخول إلى حساب YouTube Music\n- مزامنة الأغاني، الفنانين، الألبومات، وقوائم التشغيل بين حسابك وجهازك\n- تخطي الصمت\n- استيراد قوائم التشغيل\n- تطبيع الصوت\n- تعديل السرعة/الدرجة الصوتية\n- إدارة قوائم التشغيل المحلية\n- إعادة ترتيب الأغاني في قائمة التشغيل\n- السمة الفاتحة - الداكنة - السوداء - الديناميكية\n- مؤقت النوم\n- Material 3\n- وغيرها\n"
  },
  {
    "path": "fastlane/metadata/android/ar/short_description.txt",
    "content": "عميل YouTube Music Material 3 لأندرويد\n"
  },
  {
    "path": "fastlane/metadata/android/az-AZ/full_description.txt",
    "content": "Material 3 Android üçün YouTube Musiqi müştərisi\n\nXüsusiyyətlər:\n\n- YT Music-dən istənilən mahnı və ya videonu səsləndirin\n- Fon oxutma\n- Fərdiləşdirilmiş sürətli seçimlər\n- Kitabxananın idarə edilməsi\n- Oflayn oxutmaq üçün mahnıları yükləyin və yaddaşda saxlamaq imkanı\n- Mahnıları, albomları, sənətçiləri, videoları və çalğı siyahılarını axtarışı\n- Canlı mahnı sözləri\n- YouTube Music hesabına giriş dəstəyi\n- Hesabınızdan mahnıların, sənətçilərin, albomların və çalğı siyahılarının sinxronizasiyası\n- Səssiz hissələri ötürmək imkanı\n- Pleylistləri idxal edin\n- Audio normallaşdırma\n- Tempi/pitchi tənzimləyin\n- Yerli pleylist idarə edilməsi\n- Pleylistdə və ya növbədə mahnıları yenidən sıralayın\n- Açıq - Tünd - qara - Dinamik mövzu\n- Yuxu taymeri - Material 3\n- və s.\n"
  },
  {
    "path": "fastlane/metadata/android/az-AZ/short_description.txt",
    "content": "Android üçün Material 3 YouTube Music müştəri tətbiqi\n"
  },
  {
    "path": "fastlane/metadata/android/bg/short_description.txt",
    "content": "Material 3 YouTube Music клиент за Android\n"
  },
  {
    "path": "fastlane/metadata/android/ca/full_description.txt",
    "content": "Material 3 client de YouTube Music per a Android\n\nCaracterístiques:\n\n- Reprodueix qualsevol cançó o vídeo de YT Music\n- Reproducció en segon pla\n- Seleccions ràpides personalitzades\n- Gestió de la biblioteca\n- Descarrega i desa cançons a la memòria cau per a la reproducció sense connexió\n- Cerca cançons, àlbums, artistes, vídeos i llistes de reproducció\n- Lletres en temps real\n- Suport per a l'inici de sessió del compte de YouTube Music\n- Sincronització de cançons, artistes, àlbums i llistes de reproducció, des del teu compte i cap a ell\n- Omet els silencis\n- Importa llistes de reproducció\n- Normalització d'àudio\n- Ajusta el tempo/el to\n- Gestió de llistes de reproducció locals\n- Reordena les cançons de les llistes de reproducció o la cua\n- Tema clar - fosc - negre - dinàmic\n- Temporitzador de son\n- Material 3\n- etc.\n"
  },
  {
    "path": "fastlane/metadata/android/ca/short_description.txt",
    "content": "Client de YouTube Music amb Material 3 per Android\n"
  },
  {
    "path": "fastlane/metadata/android/cs-CZ/full_description.txt",
    "content": "Material 3 YouTube Music klient pro Android\n\nFunkce:\n\n- Přehrání jakékoli skladby nebo videa z YT Music\n- Přehrávání na pozadí\n- Personalizovaný rychlý výběr\n- Správa knihovny\n- Stahování a ukládání skladeb do mezipaměti pro offline přehrávání\n- Hledání skladeb, alb, umělců, videí a playlistů\n- Živé texty\n- Podpora přihlášení k účtu YouTube Music\n- Oboustranná synchronizace skladeb, umělců, alb a playlistů s účtem\n- Přeskočení ticha\n- Import playlistů\n- Normalizace zvuku\n- Úprava tempa/výšky\n- Místní správa playlistů\n- Změna pořadí skladeb v playlistu nebo ve frontě\n- Světlý / tmavý / černý / dynamický motiv\n- Časovač spánku\n- Material 3\n- atd.\n"
  },
  {
    "path": "fastlane/metadata/android/cs-CZ/short_description.txt",
    "content": "Material 3 YouTube Music klient pro Android\n"
  },
  {
    "path": "fastlane/metadata/android/de-DE/full_description.txt",
    "content": "Material 3 YouTube Music Client für Android\n\nFunktionen:\n\n- Songs und Videos von YT Music abspielen\n- Hintergrund-Wiedergabe\n- Personalisierte Schnellauswahl\n- Bibliotheksverwaltung\n- Songs für die Offline-Wiedergabe herunterladen und cachen\n- Suche nach Songs, Alben, Künstlern, Videos und Playlists\n- Live-Songtexte\n- YouTube Music Konto Anmeldung unterstützt\n- Synchronisierung von Songs, Künstlern, Alben und Playlists zwischen Gerät und Konto\n- Stille überspringen\n- Playlists importieren\n- Audio-Normalisierung\n- Tempo und Tonhöhe anpassen\n- Lokale Playlist-Verwaltung\n- Songs in Playlist und Warteschlange umordnen\n- Hell - Dunkel - Schwarz - Dynamisches Thema\n- Schlaf-Timer\n- Material 3\n- usw.\n"
  },
  {
    "path": "fastlane/metadata/android/de-DE/short_description.txt",
    "content": "Material 3 Youtube Music Client für Android\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/full_description.txt",
    "content": "Material 3 YouTube Music client for Android\n\nFeatures:\n\n- Play any song or video from YT Music\n- Background playback \n- Personalized quick picks \n- Library management \n- Download and cache songs for offline playback\n- Search for songs, albums, artists, videos and playlists\n- Live lyrics \n- YouTube Music account login support\n- Syncing of songs, artists, albums and playlists, from and to your account\n- Skip silence \n- Import playlists \n- Audio normalization \n- Adjust tempo/pitch \n- Local playlist management\n- Reorder songs in playlist or queue \n- Light - Dark - black - Dynamic theme\n- Sleep timer\n- Material 3 \n- etc.\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/short_description.txt",
    "content": "Material 3 YouTube Music client for Android\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/title.txt",
    "content": "Metrolist\n"
  },
  {
    "path": "fastlane/metadata/android/es/full_description.txt",
    "content": "Cliente de YouTube Music Material 3 para Android\n\nCaracterísticas:\n\n- Reproducir cualquier canción o video de YouTube Music\n- Reproducción en segundo plano\n- Sugerencias personalizadas rápidas\n- Gestión de la biblioteca\n- Descargar y almacenar canciones para reproducción sin conexión\n- Buscar canciones, álbumes, artistas, videos y listas de reproducción\n- Letras en vivo\n- Soporte para inicio de sesión en cuenta de YouTube Music\n- Sincronización de canciones, artistas, álbumes y listas de reproducción, desde y hacia tu cuenta\n- Saltar silencios\n- Importar listas de reproducción\n- Normalización de audio\n- Ajustar el tempo/timbre\n- Gestión de listas de reproducción locales\n- Reordenar canciones en la lista de reproducción o cola\n- Tema claro - oscuro - negro - dinámico\n- Temporizador de sueño\n- Material 3\n- etc.\n"
  },
  {
    "path": "fastlane/metadata/android/es/short_description.txt",
    "content": "Cliente de YouTube Music en Material 3 para Android\n"
  },
  {
    "path": "fastlane/metadata/android/et/full_description.txt",
    "content": "Material 3 põhine YouTube Musicu klient Androidile\n\nFunktsionaalsused:\n\n- Kuula või vaata kõiki lugusid YT Musicust\n- Taasesitus taustal \n- Isiklikud kiirvalikud \n- Muusikakogu haldus \n- Laadi lugusid alla hilisemaks esitamiseks ilma võrguühendust kasutamata\n- Otsi lugusid, albumeid, esitajaid, videoid ja esitusloendeid\n- Laulusõnad kuulamise ajal \n- YouTube Musicu kasutajakonto pruukimise võimalus\n- Lugude, esitajate, albumite ja esitusloendite sünkroonimine sinu kasutajakontoga\n- Vaikuse vahelejätmine \n- Esitusloendite import \n- Heli normaliseerimine \n- Kohenda tempot ja helikõrgust\n- Esitusloendite kohaliku haldamise võimalus\n- Võimalus muuta lugude järjestust esitusloendis või esitusjärjekorras\n- Hele, tume, must ja dünaamiline kujundus\n- Unetaimer\n- Material 3 kujunduskeel \n- ja palju muud.\n"
  },
  {
    "path": "fastlane/metadata/android/et/short_description.txt",
    "content": "Material 3 põhine YouTube Musicu klient Androidile\n"
  },
  {
    "path": "fastlane/metadata/android/eu-ES/full_description.txt",
    "content": "Material 3 YouTube Music aplikazioa Android-erako\n\nEzaugarriak:\n\n- YT Music-eko edozein abesti edo bideo erreproduzitu\n- Atzeko planoan erreprodukzioa\n- Aukera azkarrak pertsonalizatuta\n- Liburutegiaren kudeaketa\n- Abestiak deskargatu eta cachean gorde lineaz kanpoko erreprodukziorako\n- Abestiak, albumak, artistak, bideoak eta zerrenda erreproduzigarriak bilatu\n- Letrak zuzenean\n- YouTube Music kontuarekin saioa hasteko euskarria\n- Abestiak, artistak, albumak eta zerrenda erreproduzigarriak zure kontuarekin sinkronizatzea (eta alderantziz)\n- Isiltasuna saltatu\n- Zerrenda erreproduzigarriak inportatu\n- Audioa normalizatzea\n- Tenpoa/tinbrea doitzea\n- Tokiko zerrenda erreproduzigarriak kudeatzea\n- Zerrenda edo ilarako abestiak berrantolatzea\n- Argi - Ilun - Beltz - Dinamiko gaiak\n- Lo-ordutegia\n- Material 3\n- eta abar.\n"
  },
  {
    "path": "fastlane/metadata/android/eu-ES/short_description.txt",
    "content": "Material 3 Youtube Music Android-erako bezeroa\n"
  },
  {
    "path": "fastlane/metadata/android/fil/full_description.txt",
    "content": "Materyal 3 Youtube Music na kliyente na para sa Android\n\nMga Katangian:\n\n- Magpatugtog ng kanta o bidyo na mula sa YT Music\n- Magpatugtog ng kanta na kahit wala sa aplikasyon\n- Personalisadong listahan ng mabilisang pagpilihan\n- Pamamahala ng Aklatan\n- Magdownload at magcache ng mga kanta para makapagpatugtog ng mga kanta na kahit walang internet\n- Maghanap ng mga kanta, mga album, mga artista at mga playlist\n- Mga Liriko\n- Suporta para sa paglogin sa iyong YouTube Music account\n- Singkronisadong mga kanta, mga artista, mga album, at mga playlist, mula at sa iyong account\n- Laktawan ang tahimik na parte ng kanta\n- Magimport ng mga playlist\n- Normalisasyon ng audio\n- I-adjust ang tempo/tinis ng kanta\n- Lokal na pamamahala sa iyong playlist\n- Iayos ang pagkakasunod-sunod ng mga kanta sa playlist o sa listahan ng mga kanta\n- Maliwanag - Madilim - Itim - Paiba-ibang tema\n- Orasan ang pagpatugtog ng mga kanta\n- Materyal 3\n- iba pa.\n"
  },
  {
    "path": "fastlane/metadata/android/fil/short_description.txt",
    "content": "Materyal 3 Youtube Music na kliyente na para sa Android\n"
  },
  {
    "path": "fastlane/metadata/android/fr-FR/full_description.txt",
    "content": "Client YouTube Music Material 3 pour Android\n\nFonctionnalités :\n\n- Lecture de n'importe quel morceau ou vidéo depuis YouTube Music\n- Lecture en arrière-plan\n- Sélection rapide personnalisée\n- Gestion de la bibliothèque\n- Téléchargement et mise en cache des morceaux pour une écoute hors ligne\n- Recherche de morceaux, albums, artistes, vidéos et playlists\n- Affichage des paroles en direct\n- Connexion à votre compte YouTube Music\n- Synchronisation des morceaux, artistes, albums et playlists avec votre compte\n- Suppression des silences\n- Importation de playlists\n- Normalisation audio\n- Réglage du tempo et de la tonalité\n- Gestion des playlists locales\n- Réorganisation des morceaux dans les playlists et la file d'attente\n- Thèmes : Clair, Sombre, Noir, Dynamique\n- Minuterie de mise en veille\n- Material 3\n- Etc.\n"
  },
  {
    "path": "fastlane/metadata/android/fr-FR/short_description.txt",
    "content": "Client YouTube Music Material 3 pour Android\n"
  },
  {
    "path": "fastlane/metadata/android/id/full_description.txt",
    "content": "Klien YouTube Music berbasis Material 3 untuk Android\n\nFitur:\n\n- Putar lagu atau video apa pun dari YouTube Music\n- Pemutaran di latar belakang\n- Pilihan cepat yang dipersonalisasi\n- Pengelolaan pustaka\n- Unduh dan simpan lagu untuk diputar luring\n- Cari lagu, album, artis, video, dan daftar putar\n- Lirik langsung\n- Dukungan masuk akun YouTube Music\n- Sinkronisasi lagu, artis, album, dan daftar putar dari dan ke akun Anda\n- Lewati bagian hening\n- Impor daftar putar\n- Normalisasi audio\n- Atur tempo/tinggi nada\n- Pengelolaan daftar putar lokal\n- Ubah urutan lagu di daftar putar atau antrean\n- Tema terang, gelap, hitam, dan dinamis\n- Pengatur waktu tidur\n- Material 3\n- dan lainnya.\n"
  },
  {
    "path": "fastlane/metadata/android/id/short_description.txt",
    "content": "Klien YouTube Music berbasis Material 3 untuk Android\n"
  },
  {
    "path": "fastlane/metadata/android/it/full_description.txt",
    "content": "Client di YouTube Music in Material 3 per Android\n\nCaratteristiche:\n\n- Riproduzione di qualsiasi canzone o video da YouTube Music\n- Riproduzione in background\n- Suggerimenti rapidi personalizzati\n- Gestione della libreria\n- Scarica e memorizza canzoni per la riproduzione offline\n- Ricerca di canzoni, album, artisti, video e playlist\n- Testo in tempo reale\n- Supporto per l'accesso all'account di YouTube Music\n- Sincronizzazione di canzoni, artisti, album e playlist con il tuo account\n- Rimozione dei silenzi\n- Importazione di playlist\n- Normalizzazione dell'audio\n- Regolazione del tempo/timbro\n- Gestione delle playlist locali\n- Riorganizza le canzoni nella playlist o nella coda\n- Tema chiaro, scuro, nero o dinamico\n- Timer per il sonno\n- Material 3\n- ecc.\n"
  },
  {
    "path": "fastlane/metadata/android/it/short_description.txt",
    "content": "Client di YouTube Music in Material 3 per Android\n"
  },
  {
    "path": "fastlane/metadata/android/it/title.txt",
    "content": "Metrolist\n"
  },
  {
    "path": "fastlane/metadata/android/ko-KR/full_description.txt",
    "content": "머티리얼 3 안드로이드 유튜브 뮤직 클라이언트\n\n특징:\n\n- 유튜브 뮤직의 모든 곡 재생\n- 백그라운드 재생\n- 개인화된 추천\n- 라이브러리 관리\n- 오프라인 재생을 위한 캐시/다운로드\n- 곡, 아티스트, 동영상, 재생목록 검색\n- 살아있는 가사\n- 유튜브 뮤직 로그인\n- 계정과 동기화\n- 무음 건너뛰기\n- 재생목록 가져오기\n- 음량 정규화\n- 템포/피치 조절\n- 로컬 재생목록 관리\n- 재생목록/큐에서 곡 재정렬\n- 라이트 - 다크 - 블랙 - 다이나믹 테마\n- 수면 타이머\n- Material 3\n- 등등.\n"
  },
  {
    "path": "fastlane/metadata/android/ko-KR/short_description.txt",
    "content": "머티리얼 3 안드로이드 유튜브 뮤직 클라이언트\n"
  },
  {
    "path": "fastlane/metadata/android/lt/full_description.txt",
    "content": "„YouTube Music” grotuvas, naudojantis „Material 3”, skirtas „Android”\n\nFunkcijos:\n\n- Grok bet kokią dainą ar klipą iš „YT Music” \n- Grojimas fone \n- Suasmeninti greitieji pasiūlymai\n- Bibliotekos tvarkymas.\n- Atsisiųsk arba išsaugok podėlyje dainas klausymui be ryšio\n- Ieškok dainų, albumų, kūrėjų ir grojaraščių\n- Dainų tekstas tiesiogiai.\n- „YouTube Music” paskyros prijungimo palaikymas\n- Dainų, albumų, kūrėjų ir grojaraščių sinchronizacija iš ir į tavo paskyrą\n- Tylos praleidimas.\n- Grojaraščių įkėlimas.\n- Garso normalizacija.\n- Tempo ir garso aukščio reguliavimas.\n- Vietinių grojaraščių tvarkymas\n- Dainų tvarkos keitimas grojaraštyje arba grojimo eilėje.\n- Šviesioji - Tamsioji - Juodoji - Prisitaikanti tema\n- Miego laikmatis\n- „Material 3”.\n- ir daugiau.\n"
  },
  {
    "path": "fastlane/metadata/android/lt/short_description.txt",
    "content": "Material 3 YouTube Muzikos klientas skirtas Android\n"
  },
  {
    "path": "fastlane/metadata/android/mfe/full_description.txt",
    "content": "Client Material 3 YouTube Music pu Android\n\nFonksyonaliyé:\n\n- Zwé lamizik ek vidéo ki lor YT Music\n- Zwé dan background.\n- Choix rapide personalizé.\n- Zere ou librairie.\n- Download bann lamizik pu zwé offline\n- Rod bann lamizik, album, artist, vidéo ek playlist\n- Bann parole en temps réel.\n- Login ek compte YouTube Music\n- Senkroniz bann lamizik, artist, album ek artist depi ek vers ou compte\n- Sote bann silans.\n- Import bann playlist.\n- Normalizasyon audio.\n- Aziste tempo ek pitch.\n- Zere ou playlist lokal\n- Aranz lorde ou lamizik dan playlist ek queue.\n- Thème Clair - Som - Nwar - Dynamik \n- Minitere pu somey\n- Material 3.\n- etc.\n"
  },
  {
    "path": "fastlane/metadata/android/mfe/short_description.txt",
    "content": "Client Material 3 YouTube Music pu Android\n"
  },
  {
    "path": "fastlane/metadata/android/pt/full_description.txt",
    "content": "Cliente em Material 3 do YouTube Music para Android\n\nRecursos:\n\n- Toque qualquer música ou vídeo do YT Music\n- Reprodução em segundo plano\n- Escolhas rápidas personalizadas\n- Gestão da biblioteca\n- Descarga e cache de músicas para a reprodução off-line\n- Pesquise por músicas, álbuns, artistas, vídeos e listas\n- Letra sincronizada\n- Apoia à login com a conta do YouTube Music\n- Sincronização de músicas, artistas, álbuns e listas, de e para a sua conta\n- Pular silêncio\n- Importar listas\n- Normalização de áudio\n- Ajuste a velocidade e tonalidade do áudio\n- Gestão de listas locais\n- Reordene as músicas numa lista ou fila\n- Temas claro, escuro, preto e dinâmico\n- Timer para dormir\n- Material 3\n- entre outros.\n"
  },
  {
    "path": "fastlane/metadata/android/pt/short_description.txt",
    "content": "Cliente em Material 3 do YouTube Music para Android\n"
  },
  {
    "path": "fastlane/metadata/android/pt-BR/full_description.txt",
    "content": "Cliente em Material 3 do YouTube Music para Android\n\nRecursos:\n\n- Toque qualquer música ou vídeo do YT Music\n- Reprodução em segundo plano\n- Escolhas rápidas personalizadas\n- Gerenciamento da biblioteca\n- Download e cache de músicas para reprodução off-line\n- Pesquise por músicas, álbuns, artistas, vídeos e playlists\n- Letra sincronizada\n- Suporte a login com a conta do YouTube Music\n- Sincronização de músicas, artistas, álbuns, e playlists, de e para a sua conta\n- Pular silêncio\n- Importar playlists\n- Normalização de áudio\n- Ajuste a velocidade e tonalidade do áudio\n- Gerenciamento de playlists locais\n- Reordene as músicas em uma playlist ou fila\n- Temas claro, escuro, preto, e dinâmico\n- Timer para soneca\n- Material 3\n- entre outros.\n"
  },
  {
    "path": "fastlane/metadata/android/pt-BR/short_description.txt",
    "content": "Cliente em Material 3 do YouTube Music para Android\n"
  },
  {
    "path": "fastlane/metadata/android/ro/full_description.txt",
    "content": "Un client Material 3 de YouTube Music pentru Android \n\nCaracteristici:\n\n- Redă orice melodie sau videoclip de pe YT Music\n- Redare în fundal \n- Alegeri rapide personalizate \n- Gestionarea bibliotecii \n- Descarcă și pune în cache melodii pentru redare offline\n- Caută melodii, albume, artiști, videoclipuri și playlisturi\n- Versuri live \n- Suport pentru autentificarea la contul de YouTube Music\n- Sincronizarea melodiilor, artiștilor, albumelor și playlisturilor de pe contul tău și pe contul tău\n- Omite momentele de liniște \n- Importă playlisturi \n- Normalizare audio \n- Ajustează tempo-ul/înălțimea \n- Gestionarea locală a playlisturilor\n- Reordonează melodiile în playlist sau coadă \n- Temă luminoasă, întunecată, neagră și dinamică\n- Temporizator de somn\n- Material 3 \n- etc.\n"
  },
  {
    "path": "fastlane/metadata/android/ro/short_description.txt",
    "content": "Un client Material 3 de Youtube Music pentru Android\n"
  },
  {
    "path": "fastlane/metadata/android/ru-RU/full_description.txt",
    "content": "Клиент YouTube Music в стиле Material 3 для Android\n\nВозможности:\n\n- Воспроизведение любых треков и видео из YouTube Music\n- Фоновое воспроизведение\n- Персонализированный быстрый выбор\n- Управление библиотекой\n- Скачивание и кэширование треков для офлайн-воспроизведения\n- Поиск треков, альбомов, исполнителей, видео и плейлистов\n- Тексты песен в реальном времени\n- Поддержка входа в аккаунт YouTube Music\n- Синхронизация треков, исполнителей, альбомов и плейлистов с вашим аккаунтом и обратно\n- Пропуск тишины\n- Импорт плейлистов\n- Нормализация аудио\n- Регулировка темпа и высоты тона\n- Управление локальными плейлистами\n- Изменение порядка треков в плейлисте или очереди\n- Светлая, тёмная, чёрная и динамическая темы оформления\n- Таймер сна\n- Material 3\n- и др.\n"
  },
  {
    "path": "fastlane/metadata/android/ru-RU/short_description.txt",
    "content": "Клиент YouTube Music в стиле Material 3 для Android\n"
  },
  {
    "path": "fastlane/metadata/android/ru-RU/title.txt",
    "content": "Metrolist\n"
  },
  {
    "path": "fastlane/metadata/android/sk/full_description.txt",
    "content": "Material 3 YouTube Music klient pre Android \n\nFunkcie:\n\n- Prehrajte si akúkoľvek skladbu alebo video z YT Music\n- Prehrávanie na pozadí \n- Personalizované rýchle výbery \n- Správa knižnice \n- Sťahovanie a ukladanie skladieb do vyrovnávacej pamäte pre prehrávanie offline\n- Vyhľadávanie skladieb, albumov, interpretov, videí a zoznamov skladieb\n- Živé texty \n- Podpora prihlásenia do účtu YouTube Music\n- Synchronizácia skladieb, interpretov, albumov a zoznamov skladieb z vášho účtu a do neho\n- Preskočiť tiché momenty v skladbách \n- Importovať zoznamy skladieb \n- Normalizácia zvuku \n- Úprava tempa/výšky tónu \n- Správa lokálnych playlistov\n- Zmena poradia skladieb v zozname skladieb alebo v rade \n- Svetlá - Tmavá - Čierna - Dynamická téma\n- Časovač vypnutia\n- Materiál 3 \n- atd.\n"
  },
  {
    "path": "fastlane/metadata/android/sk/short_description.txt",
    "content": "Materiál 3 YouTube Music klient pre Android\n"
  },
  {
    "path": "fastlane/metadata/android/sl/full_description.txt",
    "content": "Material 3 odjemalec YouTube Music za Android\n\nLastnosti:\n\n- Predvajajte katerokoli skladbo ali videoposnetek iz YT Music.\n- Predvajanje v ozadju\n- Osebne prilagojene hitre izbire\n- Upravljanje knjižnice\n- Prenos in predpomnjenje skladb za predvajanje brez povezave\n- Iskanje skladb, albumov, izvajalcev, videoposnetkov in seznamov predvajanja\n- Besedila v živo\n- Podpora za prijavo v račun YouTube Music\n- Sinhronizacija skladb, izvajalcev, albumov in seznamov predvajanja iz računa in v račun\n- Preskakanje tišine\n- Uvoz seznamov predvajanja\n- Normalizacija zvoka\n- Nastavitev tempa/višine tona\n- Upravljanje lokalnih seznamov predvajanja\n- Spreminjanje vrstnega reda skladb na seznamu ali v vrsti predvajanja\n- Svetla, temna, črna in dinamična tema\n- Časovnik za spanje\n- Material 3\n- itd.\n"
  },
  {
    "path": "fastlane/metadata/android/sl/short_description.txt",
    "content": "Material 3 odjemalec YouTube Music za Android\n"
  },
  {
    "path": "fastlane/metadata/android/te-IN/full_description.txt",
    "content": "ఆండ్రాయిడ్ కోసం మెటీరియల్ 3 పదార్థం రూపకల్పన యూట్యూబ్ మ్యూజిక్ అనువర్తనం\n\nలక్షణాలు:\n\n-యూట్యూబ్ మ్యూజిక్ నుండి ఏ పాట లేదా వీడియోనైనా వినగలగడం\n-వెనుకన కూడా పాట వినడం (పాట తెర మూసినా కూడా పాటలు కొనసాగుతుంది).\n-వ్యక్తిగతీకరించిన త్వరిత ఎంపికలు.\n-సంగీత గ్రంథాలయ నిర్వహణ.\n-అంతర్జాలం లేకుండా వినడానికి పాటలను దిగుమతి చేసుకోవడం మరియు తాత్కాలికంగా నిల్వ చేయడం\n-పాటలు, ఆల్బమ్‌లు, కళాకారులు, చలనచిత్రాలు మరియు పాటల జాబితాల కోసం శోధించడం\n-ప్రత్యక్ష గీతంలోని పద్యాలు చూపించడం\n-యూట్యూబ్ మ్యూజిక్ ఖాతా ప్రవేశం సౌకర్యం\n-మీ యూట్యూబ్ మ్యూజిక్ ఖాతా నుండి మరియు ఖాతాకు పాటలు, కళాకారులు, ఆల్బమ్‌లు మరియు పాటల జాబితాల సమకాలీకరించడం (సింక్ చేయడం)\n-నిశ్శబ్ద భాగాలను దాటేయడం.\n-జాబితాలను దిగుమతి చేసుకోవడం.\n-ధ్వని సాధారణీకరణ ( సమాన స్థాయికి తేవడం).\n-వేగం/స్వరాన్ని ( శృతిని) మార్చగలగడం.\n-స్థానిక జాబితా నిర్వహణ\n-జాబితా లేదా క్యూలో మీకు నచ్చినట్టుగా పాటల క్రమాన్ని మార్చగలగడం\n- అప్ లో వెలుతురు - ముదురు రంగు - నలుపు - మార్చుకునే రంగు రూపకల్పన\n-సమయాలలో పాటలు వీనే అప్పుడు మనం పెట్టిన సమయసూచిక సమయం కి పాటలు ఆగిపోతాయి.\n-మెటీరియల్ 3 పదార్థం రూపకల్పన.\n-ఇంకా మరెన్నో.\n"
  },
  {
    "path": "fastlane/metadata/android/te-IN/short_description.txt",
    "content": "ఆండ్రాయిడ్ కోసం మెటీరియల్ 3 యూట్యూబ్ మ్యూజిక్ క్లయింట్\n"
  },
  {
    "path": "fastlane/metadata/android/tr/full_description.txt",
    "content": "YouTube Music Material 3 İstemcisi (Android için)\n\nÖzellikler:\n\n- YouTube Music'ten her türlü şarkı veya video çalma\n- Arka planda çalma\n- Kişiselleştirilmiş hızlı öneriler\n- Kütüphane yönetimi\n- Çevrimdışı çalma için şarkıları indirip önbelleğe alma\n- Şarkılar, albümler, sanatçılar, videolar ve çalma listeleri arama\n- Canlı şarkı sözleri\n- YouTube Music hesabı ile giriş desteği\n- Hesabınızla şarkı, sanatçı, albüm ve çalma listelerinin senkronizasyonu\n- Sessizlik atlama\n- Çalma listelerini içe aktarma\n- Ses normalizasyonu\n- Tempo/ton ayarı\n- Yerel çalma listesi yönetimi\n- Çalma listesinde veya sırada şarkıları yeniden düzenleme\n- Açık - Koyu - Siyah - Dinamik tema\n- Uyku zamanlayıcı\n- Material 3\n- vb.\n"
  },
  {
    "path": "fastlane/metadata/android/tr/short_description.txt",
    "content": "Android için Material 3 YouTube Müzik istemcisi\n"
  },
  {
    "path": "fastlane/metadata/android/uk-UA/full_description.txt",
    "content": "Клієнт YouTube Music у стилі Material 3 для Android\n\nМожливості:\n\n- Відтворення будь-якої пісні або відео з YouTube Music\n- Фонове відтворення\n- Персоналізовані швидкі добірки\n- Керування бібліотекою\n- Завантаження та кешування пісень для офлайн-прослуховування\n- Пошук пісень, альбомів, виконавців, відео та списків відтворення\n- Живі тексти пісень\n- Підтримка входу через акаунт YouTube Music\n- Синхронізація пісень, виконавців, альбомів і списків відтворення між пристроєм та акаунтом\n- Пропуск тиші\n- Імпорт списків відтворення\n- Нормалізація гучності\n- Налаштування темпу й тональності\n- Керування локальними списками відтворення\n- Зміна порядку пісень у списках відтворення або черзі\n- Світла, темна, чорна та динамічна теми\n- Таймер сну\n- Інтерфейс Material 3\n- Та інше\n"
  },
  {
    "path": "fastlane/metadata/android/uk-UA/short_description.txt",
    "content": "Неофіційний додаток YouTube Music з дизайном Material 3 для Android\n"
  },
  {
    "path": "gradle/gradle-daemon-jvm.properties",
    "content": "#This file is generated by updateDaemonJvm\ntoolchainUrl.FREE_BSD.AARCH64=https\\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect\ntoolchainUrl.FREE_BSD.X86_64=https\\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect\ntoolchainUrl.LINUX.AARCH64=https\\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect\ntoolchainUrl.LINUX.X86_64=https\\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect\ntoolchainUrl.MAC_OS.AARCH64=https\\://api.foojay.io/disco/v3.0/ids/73bcfb608d1fde9fb62e462f834a3299/redirect\ntoolchainUrl.MAC_OS.X86_64=https\\://api.foojay.io/disco/v3.0/ids/846ee0d876d26a26f37aa1ce8de73224/redirect\ntoolchainUrl.UNIX.AARCH64=https\\://api.foojay.io/disco/v3.0/ids/ec7520a1e057cd116f9544c42142a16b/redirect\ntoolchainUrl.UNIX.X86_64=https\\://api.foojay.io/disco/v3.0/ids/4c4f879899012ff0a8b2e2117df03b0e/redirect\ntoolchainUrl.WINDOWS.AARCH64=https\\://api.foojay.io/disco/v3.0/ids/9482ddec596298c84656d31d16652665/redirect\ntoolchainUrl.WINDOWS.X86_64=https\\://api.foojay.io/disco/v3.0/ids/39701d92e1756bb2f141eb67cd4c660e/redirect\ntoolchainVersion=21\n"
  },
  {
    "path": "gradle/libs.versions.toml",
    "content": "[versions]\nandroidGradlePlugin = \"9.1.0\"\njson = \"20251224\"\nkotlin = \"2.3.10\"\ncompose = \"1.10.4\"\nlifecycle = \"2.10.0\"\nmaterial3 = \"1.5.0-alpha15\"\nappcompat = \"1.7.1\"\nmedia3 = \"1.7.1\"\nmediarouter = \"1.8.1\"\ncastFramework = \"22.2.0\"\nroom = \"2.8.4\"\nhilt = \"2.59.2\"\nktor = \"3.4.1\"\nksp = \"2.3.5\"\njsoup = \"1.22.1\"\ncoil = \"3.4.0\"\nucrop = \"2.2.11\"\nguava = \"33.5.0-jre\"\ncoroutinesGuava = \"1.10.2\"\nconcurrentFutures = \"1.3.0\"\nactivity = \"1.12.4\"\nhiltNavigation = \"1.3.0\"\ndatastore = \"1.2.0\"\ncomposeReorderable = \"3.0.0\"\nshimmer = \"1.3.3\"\npalette = \"1.0.0\"\napacheLang3 = \"3.20.0\"\nbrotli = \"0.1.2\"\ndesugaring = \"2.1.5\"\njunit = \"4.13.2\"\ntimber = \"5.0.1\"\nmaterialKolor = \"4.1.1\"\nkuromojiIpadic = \"0.9.0\"\nnewpipeextractor = \"v0.26.0\"\ntinypinyin = \"2.0.3\"\nprotobuf = \"4.34.0\"\n\n[libraries]\nguava = { module = \"com.google.guava:guava\", version.ref = \"guava\" }\ncoroutines-guava = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-guava\", version.ref = \"coroutinesGuava\" }\nconcurrent-futures = { module = \"androidx.concurrent:concurrent-futures-ktx\", version.ref = \"concurrentFutures\" }\n\ngradle = { module = \"com.android.tools.build:gradle\", version.ref = \"androidGradlePlugin\" }\nactivity = { module = \"androidx.activity:activity-compose\", version.ref = \"activity\" }\nhilt-navigation = { module = \"androidx.hilt:hilt-navigation-compose\", version.ref = \"hiltNavigation\" }\ndatastore = { module = \"androidx.datastore:datastore-preferences\", version.ref = \"datastore\" }\n\ncompose-runtime = { module = \"androidx.compose.runtime:runtime\", version.ref = \"compose\" }\ncompose-foundation = { module = \"androidx.compose.foundation:foundation\", version.ref = \"compose\" }\ncompose-ui = { module = \"androidx.compose.ui:ui\", version.ref = \"compose\" }\ncompose-ui-util = { module = \"androidx.compose.ui:ui-util\", version.ref = \"compose\" }\ncompose-ui-tooling = { module = \"androidx.compose.ui:ui-tooling\", version.ref = \"compose\" }\ncompose-animation = { module = \"androidx.compose.animation:animation-graphics\", version.ref = \"compose\" }\ncompose-reorderable = { module = \"sh.calvin.reorderable:reorderable\", version.ref = \"composeReorderable\" }\n\nviewmodel = { module = \"androidx.lifecycle:lifecycle-viewmodel-ktx\", version.ref = \"lifecycle\" }\nviewmodel-compose = { module = \"androidx.lifecycle:lifecycle-viewmodel-compose\", version.ref = \"lifecycle\" }\n\nmaterial3 = { module = \"androidx.compose.material3:material3\", version.ref = \"material3\" }\n\nmaterialKolor = { module = \"com.materialkolor:material-kolor\", version.ref = \"materialKolor\" }\n\nappcompat = { module = \"androidx.appcompat:appcompat\", version.ref = \"appcompat\" }\n\ncoil = { module = \"io.coil-kt.coil3:coil-compose\", version.ref = \"coil\" }\ncoil-network-okhttp = { module = \"io.coil-kt.coil3:coil-network-okhttp\", version.ref = \"coil\" }\nucrop = { module = \"com.github.yalantis:ucrop\", version.ref = \"ucrop\" }\n\nshimmer = { module = \"com.valentinilk.shimmer:compose-shimmer\", version.ref = \"shimmer\" }\n\npalette = { module = \"androidx.palette:palette-ktx\", version.ref = \"palette\" }\n\nmedia3 = { module = \"androidx.media3:media3-exoplayer\", version.ref = \"media3\" }\nmedia3-okhttp = { module = \"androidx.media3:media3-datasource-okhttp\", version.ref = \"media3\" }\nmedia3-session = { module = \"androidx.media3:media3-session\", version.ref = \"media3\" }\nmedia3-cast = { module = \"androidx.media3:media3-cast\", version.ref = \"media3\" }\n\nmediarouter = { module = \"androidx.mediarouter:mediarouter\", version.ref = \"mediarouter\" }\ncast-framework = { module = \"com.google.android.gms:play-services-cast-framework\", version.ref = \"castFramework\" }\n\nroom-runtime = { module = \"androidx.room:room-runtime\", version.ref = \"room\" }\nroom-compiler = { module = \"androidx.room:room-compiler\", version.ref = \"room\" }\nroom-ktx = { module = \"androidx.room:room-ktx\", version.ref = \"room\" }\n\napache-lang3 = { module = \"org.apache.commons:commons-lang3\", version.ref = \"apacheLang3\" }\n\nhilt = { module = \"com.google.dagger:hilt-android\", version.ref = \"hilt\" }\nhilt-compiler = { module = \"com.google.dagger:hilt-android-compiler\", version.ref = \"hilt\" }\n\nktor-client-core = { module = \"io.ktor:ktor-client-core\", version.ref = \"ktor\" }\nktor-client-cio = { module = \"io.ktor:ktor-client-cio\", version.ref = \"ktor\" }\nktor-client-okhttp = { module = \"io.ktor:ktor-client-okhttp\", version.ref = \"ktor\" }\nktor-client-content-negotiation = { module = \"io.ktor:ktor-client-content-negotiation\", version.ref = \"ktor\" }\nktor-client-encoding = { module = \"io.ktor:ktor-client-encoding\", version.ref = \"ktor\" }\nktor-serialization-json = { module = \"io.ktor:ktor-serialization-kotlinx-json\", version.ref = \"ktor\" }\n\njsoup = { module = \"org.jsoup:jsoup\", version.ref = \"jsoup\" }\n\njson = { module = \"org.json:json\", version.ref = \"json\" }\n\nbrotli = { module = \"org.brotli:dec\", version.ref = \"brotli\" }\n\ndesugaring = { module = \"com.android.tools:desugar_jdk_libs_nio\", version.ref = \"desugaring\" }\n\njunit = { module = \"junit:junit\", version.ref = \"junit\" }\n\ntimber = { module = \"com.jakewharton.timber:timber\", version.ref = \"timber\" }\n\nnewpipeextractor = { module = \"com.github.TeamNewPipe:NewPipeExtractor\", version.ref = \"newpipeextractor\" }\n\nkuromoji-ipadic = { module = \"com.atilika.kuromoji:kuromoji-ipadic\", version.ref = \"kuromojiIpadic\" }\ntinypinyin = { module = \"com.github.promeG:tinypinyin\", version.ref = \"tinypinyin\" }\n\nprotobuf-javalite = { module = \"com.google.protobuf:protobuf-javalite\", version.ref = \"protobuf\" }\nprotobuf-kotlin-lite = { module = \"com.google.protobuf:protobuf-kotlin-lite\", version.ref = \"protobuf\" }\n\n[plugins]\ncompose-compiler = { id = \"org.jetbrains.kotlin.plugin.compose\", version.ref = \"kotlin\" }\nhilt = { id = \"com.google.dagger.hilt.android\", version.ref = \"hilt\" }\nkotlin-ksp = { id = \"com.google.devtools.ksp\", version.ref = \"ksp\" }\nkotlin-serialization = { id = \"org.jetbrains.kotlin.plugin.serialization\", version.ref = \"kotlin\" }\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-9.3.1-bin.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "## For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n#\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\n# Default value: -Xmx1024m -XX:MaxPermSize=256m\n# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8\n#\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n#Sat Nov 19 15:59:34 CST 2022\n\norg.gradle.jvmargs=-Xmx4096M -Dkotlin.daemon.jvm.options\\=\"-Xmx4096M\" -XX:+UseParallelGC\nandroid.useAndroidX=true\nandroid.enableJetifier=false\norg.gradle.unsafe.configuration-cache=true\nandroid.nonTransitiveRClass=false\n\n# Jetifier is disabled - no need for ignorelist\n\n# Performance improvements\norg.gradle.parallel=true\norg.gradle.daemon=true\norg.gradle.configureondemand=false\n\n# Suppress deprecated warnings\nandroid.suppressUnsupportedOptionWarnings=android.suppressUnsupportedOptionWarnings,android.nonFinalResIds\n\n# Increase timeouts for JitPack downloads (fixes timeout issues with NewPipeExtractor)\nsystemProp.org.gradle.internal.http.connectionTimeout=180000\nsystemProp.org.gradle.internal.http.socketTimeout=180000\n\n# Disable caching for SNAPSHOT dependencies to avoid timeout issues\norg.gradle.caching=false\n"
  },
  {
    "path": "gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# SPDX-License-Identifier: Apache-2.0\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd -P \"${APP_HOME:-./}\" > /dev/null && printf '%s\\n' \"$PWD\" ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    if ! command -v java >/dev/null 2>&1\n    then\n        die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command:\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n#     and any embedded shellness will be escaped.\n#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n#     treated as '${Hostname}' itself on the command line.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -jar \"$APP_HOME/gradle/wrapper/gradle-wrapper.jar\" \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n@rem SPDX-License-Identifier: Apache-2.0\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -jar \"%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\" %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "innertube/.gitignore",
    "content": "/build"
  },
  {
    "path": "innertube/build.gradle.kts",
    "content": "plugins {\n    id(\"com.android.library\")\n    alias(libs.plugins.kotlin.serialization)\n}\n\nandroid {\n    namespace = \"com.metrolist.innertube\"\n    compileSdk = 36\n\n    defaultConfig {\n        minSdk = 26\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_21\n        targetCompatibility = JavaVersion.VERSION_21\n    }\n}\n\nkotlin {\n    jvmToolchain(21)\n}\n\ndependencies {\n    implementation(libs.ktor.client.core)\n    implementation(libs.ktor.client.okhttp)\n    implementation(libs.ktor.client.content.negotiation)\n    implementation(libs.ktor.serialization.json)\n    implementation(libs.ktor.client.encoding)\n    implementation(libs.brotli)\n    implementation(libs.newpipeextractor)\n    implementation(libs.timber)\n    testImplementation(libs.junit)\n\n    coreLibraryDesugaring(libs.desugaring)\n}\n"
  },
  {
    "path": "innertube/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n</manifest>\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/InnerTube.kt",
    "content": "package com.metrolist.innertube\n\nimport com.metrolist.innertube.models.Context\nimport com.metrolist.innertube.models.MediaInfo\nimport com.metrolist.innertube.models.ReturnYouTubeDislikeResponse\nimport com.metrolist.innertube.models.YouTubeClient\nimport com.metrolist.innertube.models.YouTubeLocale\nimport com.metrolist.innertube.models.body.*\nimport com.metrolist.innertube.models.response.NextResponse\nimport com.metrolist.innertube.utils.parseCookieString\nimport com.metrolist.innertube.utils.sha1\nimport io.ktor.client.*\nimport io.ktor.client.call.body\nimport io.ktor.client.engine.okhttp.*\nimport io.ktor.client.plugins.*\nimport io.ktor.client.plugins.compression.*\nimport io.ktor.client.plugins.contentnegotiation.*\nimport io.ktor.client.plugins.HttpTimeout\nimport io.ktor.client.request.*\nimport io.ktor.client.statement.bodyAsText\nimport io.ktor.http.*\nimport io.ktor.serialization.kotlinx.json.*\nimport kotlinx.serialization.ExperimentalSerializationApi\nimport kotlinx.serialization.json.Json\nimport java.net.Proxy\nimport java.io.IOException\nimport kotlinx.coroutines.delay\nimport java.util.*\nimport kotlin.io.encoding.Base64\nimport timber.log.Timber\nimport kotlin.io.encoding.ExperimentalEncodingApi\n\n/**\n * Provide access to InnerTube endpoints.\n * For making HTTP requests, not parsing response.\n */\n@OptIn(ExperimentalEncodingApi::class)\nclass InnerTube {\n    private var httpClient = createClient()\n\n    var locale = YouTubeLocale(\n        gl = Locale.getDefault().country,\n        hl = Locale.getDefault().toLanguageTag()\n    )\n    var visitorData: String? = null\n    var dataSyncId: String? = null\n    var cookie: String? = null\n        set(value) {\n            field = value\n            cookieMap = if (value == null) emptyMap() else parseCookieString(value)\n        }\n    private var cookieMap = emptyMap<String, String>()\n\n    var proxy: Proxy? = null\n        set(value) {\n            field = value\n            httpClient.close()\n            httpClient = createClient()\n        }\n    \n    var proxyAuth: String? = null\n\n    var useLoginForBrowse: Boolean = false\n\n    @OptIn(ExperimentalSerializationApi::class)\n    private fun createClient() = HttpClient(OkHttp) {\n        expectSuccess = true\n\n        install(ContentNegotiation) {\n            json(Json {\n                ignoreUnknownKeys = true\n                explicitNulls = false\n                encodeDefaults = true\n            })\n        }\n\n        install(ContentEncoding) {\n            gzip(0.9F)\n            deflate(0.8F)\n        }\n\n        // Enhanced network configuration for better performance\n        engine {\n            config {\n                // Connection pool settings for better connection reuse\n                connectionPool(\n                    okhttp3.ConnectionPool(\n                        10, // maxIdleConnections\n                        5, // keepAliveDuration\n                        java.util.concurrent.TimeUnit.MINUTES\n                    )\n                )\n                \n                // Timeout configurations\n                connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)\n                readTimeout(60, java.util.concurrent.TimeUnit.SECONDS)\n                writeTimeout(60, java.util.concurrent.TimeUnit.SECONDS)\n                \n                // Enable HTTP/2 for better performance\n                protocols(listOf(okhttp3.Protocol.HTTP_2, okhttp3.Protocol.HTTP_1_1))\n                \n                // Retry on connection failure\n                retryOnConnectionFailure(true)\n                \n                // Cache configuration for better performance\n                cache(\n                    okhttp3.Cache(\n                        directory = java.io.File(System.getProperty(\"java.io.tmpdir\"), \"http_cache\"),\n                        maxSize = 50L * 1024L * 1024L // 50 MB\n                    )\n                )\n                \n                // Apply proxy configuration\n                this@InnerTube.proxy?.let { proxyConfig ->\n                    proxy(proxyConfig)\n                }\n                \n                // Apply proxy authentication\n                this@InnerTube.proxyAuth?.let { auth ->\n                    proxyAuthenticator { _, response ->\n                        response.request.newBuilder()\n                            .header(\"Proxy-Authorization\", auth)\n                            .build()\n                    }\n                }\n            }\n        }\n\n        // Request timeout configuration\n        install(HttpTimeout) {\n            requestTimeoutMillis = 60000\n            connectTimeoutMillis = 30000\n            socketTimeoutMillis = 60000\n        }\n\n        defaultRequest {\n            url(YouTubeClient.API_URL_YOUTUBE_MUSIC)\n            // Add common headers for better compatibility\n            header(\"Accept\", \"application/json\")\n            header(\"Accept-Language\", \"en-US,en;q=0.9\")\n            header(\"Cache-Control\", \"no-cache\")\n        }\n    }\n\n    private fun HttpRequestBuilder.ytClient(client: YouTubeClient, setLogin: Boolean = false) {\n        contentType(ContentType.Application.Json)\n        headers {\n            append(\"X-Goog-Api-Format-Version\", \"1\")\n            append(\"X-YouTube-Client-Name\", client.clientId /* Not a typo. The Client-Name header does contain the client id. */)\n            append(\"X-YouTube-Client-Version\", client.clientVersion)\n            append(\"X-Origin\", YouTubeClient.ORIGIN_YOUTUBE_MUSIC)\n            append(\"Referer\", YouTubeClient.REFERER_YOUTUBE_MUSIC)\n            visitorData?.let { append(\"X-Goog-Visitor-Id\", it) }\n            if (setLogin && client.loginSupported) {\n                cookie?.let { cookie ->\n                    append(\"cookie\", cookie)\n                    if (\"SAPISID\" !in cookieMap) return@let\n                    val currentTime = System.currentTimeMillis() / 1000\n                    val sapisidHash = sha1(\"$currentTime ${cookieMap[\"SAPISID\"]} ${YouTubeClient.ORIGIN_YOUTUBE_MUSIC}\")\n                    append(\"Authorization\", \"SAPISIDHASH ${currentTime}_${sapisidHash}\")\n                }\n            }\n        }\n        userAgent(client.userAgent)\n        parameter(\"prettyPrint\", false)\n    }\n\n    /**\n     * Simple retry wrapper for transient IO errors (socket aborts, timeouts).\n     * Retries the given block up to [maxAttempts] times with exponential backoff.\n     * Cancellation is respected since [delay] will throw if the coroutine is cancelled.\n     */\n    private suspend fun <T> withRetry(\n        maxAttempts: Int = 3,\n        initialDelay: Long = 500L,\n        factor: Double = 2.0,\n        block: suspend () -> T,\n    ): T {\n        var currentDelay = initialDelay\n        var attempt = 0\n        while (true) {\n            try {\n                return block()\n            } catch (e: IOException) {\n                attempt++\n                if (attempt >= maxAttempts) throw e\n                delay(currentDelay)\n                currentDelay = (currentDelay * factor).toLong()\n            }\n        }\n    }\n\n    suspend fun search(\n        client: YouTubeClient,\n        query: String? = null,\n        params: String? = null,\n        continuation: String? = null,\n    ) = withRetry {\n        httpClient.post(\"search\") {\n            ytClient(client, setLogin = useLoginForBrowse)\n            setBody(\n                SearchBody(\n                    context = client.toContext(\n                        locale,\n                        visitorData,\n                        if (useLoginForBrowse) dataSyncId else null\n                    ),\n                    query = query,\n                    params = params\n                )\n            )\n            parameter(\"continuation\", continuation)\n            parameter(\"ctoken\", continuation)\n        }\n    }\n\n    suspend fun player(\n        client: YouTubeClient,\n        videoId: String,\n        playlistId: String?,\n        signatureTimestamp: Int?,\n        poToken: String? = null,\n    ) = withRetry {\n        httpClient.post(\"player\") {\n            ytClient(client, setLogin = true)\n            setBody(\n                PlayerBody(\n                    context = client.toContext(locale, visitorData, dataSyncId).let {\n                        if (client.isEmbedded) {\n                            it.copy(\n                                thirdParty = Context.ThirdParty(\n                                    embedUrl = \"https://www.youtube.com/watch?v=${videoId}\"\n                                )\n                            )\n                        } else it\n                    },\n                    videoId = videoId,\n                    playlistId = playlistId,\n                    playbackContext = if (client.useSignatureTimestamp && signatureTimestamp != null) {\n                        PlayerBody.PlaybackContext(\n                            PlayerBody.PlaybackContext.ContentPlaybackContext(\n                                signatureTimestamp\n                            )\n                        )\n                    } else null,\n                    serviceIntegrityDimensions = if (client.useWebPoTokens && poToken != null) {\n                        PlayerBody.ServiceIntegrityDimensions(poToken)\n                    } else null,\n                )\n            )\n        }\n    }\n\n    suspend fun registerPlayback(\n        url: String,\n        cpn: String,\n        playlistId: String?,\n        client: YouTubeClient = YouTubeClient.WEB_REMIX,\n    ) = withRetry {\n        httpClient.get(url) {\n            ytClient(client, true)\n            parameter(\"ver\", \"2\")\n            parameter(\"c\", client.clientName)\n            parameter(\"cpn\", cpn)\n\n            if (playlistId != null) {\n                parameter(\"list\", playlistId)\n                parameter(\"referrer\", \"https://music.youtube.com/playlist?list=$playlistId\")\n            }\n        }\n    }\n\n    suspend fun browse(\n        client: YouTubeClient,\n        browseId: String? = null,\n        params: String? = null,\n        continuation: String? = null,\n        setLogin: Boolean = false,\n    ) = withRetry {\n        httpClient.post(\"browse\") {\n            ytClient(client, setLogin = setLogin || useLoginForBrowse)\n            setBody(\n                BrowseBody(\n                    context = client.toContext(\n                        locale,\n                        visitorData,\n                        if (setLogin || useLoginForBrowse) dataSyncId else null\n                    ),\n                    browseId = browseId,\n                    params = params,\n                    continuation = continuation\n                )\n            )\n        }\n    }\n\n    suspend fun next(\n        client: YouTubeClient,\n        videoId: String?,\n        playlistId: String?,\n        playlistSetVideoId: String?,\n        index: Int?,\n        params: String?,\n        continuation: String? = null,\n    ) = withRetry {\n        httpClient.post(\"next\") {\n            ytClient(client, setLogin = true)\n            setBody(\n                NextBody(\n                    context = client.toContext(locale, visitorData, dataSyncId),\n                    videoId = videoId,\n                    playlistId = playlistId,\n                    playlistSetVideoId = playlistSetVideoId,\n                    index = index,\n                    params = params,\n                    continuation = continuation\n                )\n            )\n        }\n    }\n\n    suspend fun feedback(\n        client: YouTubeClient,\n        tokens: List<String>\n    ) = withRetry {\n        httpClient.post(\"feedback\") {\n            ytClient(client, setLogin = true)\n            setBody(\n                FeedbackBody(\n                    context = client.toContext(locale, visitorData, dataSyncId),\n                    feedbackTokens = tokens\n                )\n            )\n        }\n    }\n\n    suspend fun getSearchSuggestions(\n        client: YouTubeClient,\n        input: String,\n    ) = withRetry {\n        httpClient.post(\"music/get_search_suggestions\") {\n            ytClient(client)\n            setBody(\n                GetSearchSuggestionsBody(\n                    context = client.toContext(locale, visitorData, null),\n                    input = input\n                )\n            )\n        }\n    }\n\n    suspend fun getQueue(\n        client: YouTubeClient,\n        videoIds: List<String>?,\n        playlistId: String?,\n    ) = withRetry {\n        httpClient.post(\"music/get_queue\") {\n            ytClient(client)\n            setBody(\n                GetQueueBody(\n                    context = client.toContext(locale, visitorData, null),\n                    videoIds = videoIds,\n                    playlistId = playlistId\n                )\n            )\n        }\n    }\n\n    suspend fun getTranscript(\n        client: YouTubeClient,\n        videoId: String,\n    ) = withRetry {\n        httpClient.post(\"https://music.youtube.com/youtubei/v1/get_transcript\") {\n            parameter(\"key\", \"AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX3\")\n            headers {\n                append(\"Content-Type\", \"application/json\")\n            }\n            setBody(\n                GetTranscriptBody(\n                    context = client.toContext(locale, null, null),\n                    params = Base64.Default.encode(\n                        \"\\n${11.toChar()}$videoId\".encodeToByteArray()\n                    )\n                )\n            )\n        }\n    }\n\n    suspend fun getSwJsData() = withRetry { httpClient.get(\"https://music.youtube.com/sw.js_data\") }\n\n    suspend fun accountMenu(client: YouTubeClient) = withRetry {\n        httpClient.post(\"account/account_menu\") {\n            ytClient(client, setLogin = true)\n            setBody(AccountMenuBody(client.toContext(locale, visitorData, dataSyncId)))\n        }\n    }\n\n    suspend fun likeVideo(\n        client: YouTubeClient,\n        videoId: String,\n    ) = withRetry {\n        httpClient.post(\"like/like\") {\n            ytClient(client, setLogin = true)\n            setBody(\n                LikeBody(\n                    context = client.toContext(locale, visitorData, dataSyncId),\n                    target = LikeBody.Target.video(videoId)\n                )\n            )\n        }\n    }\n\n    suspend fun unlikeVideo(\n        client: YouTubeClient,\n        videoId: String,\n    ) = withRetry {\n        httpClient.post(\"like/removelike\") {\n            ytClient(client, setLogin = true)\n            setBody(\n                LikeBody(\n                    context = client.toContext(locale, visitorData, dataSyncId),\n                    target = LikeBody.Target.video(videoId)\n                )\n            )\n        }\n    }\n\n    suspend fun subscribeChannel(\n        client: YouTubeClient,\n        channelId: String,\n        params: String? = null,\n    ) = withRetry {\n        httpClient.post(\"subscription/subscribe\") {\n            ytClient(client, setLogin = true)\n            setBody(\n                SubscribeBody(\n                    context = client.toContext(locale, visitorData, dataSyncId),\n                    channelIds = listOf(channelId),\n                    params = params\n                )\n            )\n        }\n    }\n\n    suspend fun unsubscribeChannel(\n        client: YouTubeClient,\n        channelId: String,\n        params: String? = null,\n    ) = withRetry {\n        httpClient.post(\"subscription/unsubscribe\") {\n            ytClient(client, setLogin = true)\n            setBody(\n                SubscribeBody(\n                    context = client.toContext(locale, visitorData, dataSyncId),\n                    channelIds = listOf(channelId),\n                    params = params\n                )\n            )\n        }\n    }\n\n    suspend fun likePlaylist(\n        client: YouTubeClient,\n        playlistId: String,\n    ) = withRetry {\n        httpClient.post(\"like/like\") {\n            ytClient(client, setLogin = true)\n            setBody(\n                LikeBody(\n                    context = client.toContext(locale, visitorData, dataSyncId),\n                    target = LikeBody.Target.playlist(playlistId)\n                )\n            )\n        }\n    }\n\n    suspend fun unlikePlaylist(\n        client: YouTubeClient,\n        playlistId: String,\n    ) = withRetry {\n        httpClient.post(\"like/removelike\") {\n            ytClient(client, setLogin = true)\n            setBody(\n                LikeBody(\n                    context = client.toContext(locale, visitorData, dataSyncId),\n                    target = LikeBody.Target.playlist(playlistId)\n                )\n            )\n        }\n    }\n\n    suspend fun addToPlaylist(\n        client: YouTubeClient,\n        playlistId: String,\n        videoId: String,\n    ) = withRetry {\n        httpClient.post(\"browse/edit_playlist\") {\n            ytClient(client, setLogin = true)\n            setBody(\n                EditPlaylistBody(\n                    context = client.toContext(locale, visitorData, dataSyncId),\n                    playlistId = playlistId.removePrefix(\"VL\"),\n                    actions = listOf(\n                        Action.AddVideoAction(addedVideoId = videoId)\n                    )\n                )\n            )\n        }\n    }\n\n    suspend fun addPlaylistToPlaylist(\n        client: YouTubeClient,\n        playlistId: String,\n        addPlaylistId: String,\n    ) = withRetry {\n        httpClient.post(\"browse/edit_playlist\") {\n            ytClient(client, setLogin = true)\n            setBody(\n                EditPlaylistBody(\n                    context = client.toContext(locale, visitorData, dataSyncId),\n                    playlistId = playlistId.removePrefix(\"VL\"),\n                    actions = listOf(\n                        Action.AddPlaylistAction(addedFullListId = addPlaylistId)\n                    )\n                )\n            )\n        }\n    }\n\n    suspend fun removeFromPlaylist(\n        client: YouTubeClient,\n        playlistId: String,\n        videoId: String,\n        setVideoId: String,\n    ) = withRetry {\n        httpClient.post(\"browse/edit_playlist\") {\n            ytClient(client, setLogin = true)\n            setBody(\n                EditPlaylistBody(\n                    context = client.toContext(locale, visitorData, dataSyncId),\n                    playlistId = playlistId.removePrefix(\"VL\"),\n                    actions = listOf(\n                        Action.RemoveVideoAction(\n                            removedVideoId = videoId,\n                            setVideoId = setVideoId,\n                        )\n                    )\n                )\n            )\n        }\n    }\n\n    suspend fun moveSongPlaylist(\n        client: YouTubeClient,\n        playlistId: String,\n        setVideoId: String,\n        successorSetVideoId: String?,\n    ) = withRetry {\n        httpClient.post(\"browse/edit_playlist\") {\n            ytClient(client, setLogin = true)\n            setBody(\n                EditPlaylistBody(\n                    context = client.toContext(locale, visitorData, dataSyncId),\n                    playlistId = playlistId,\n                    actions = listOf(\n                        Action.MoveVideoAction(\n                            movedSetVideoIdSuccessor = successorSetVideoId,\n                            setVideoId = setVideoId,\n                        )\n                    )\n                )\n            )\n        }\n    }\n\n    suspend fun createPlaylist(\n        client: YouTubeClient,\n        title: String,\n    ) = withRetry {\n        httpClient.post(\"playlist/create\") {\n            ytClient(client, true)\n            setBody(\n                CreatePlaylistBody(\n                    context = client.toContext(locale, visitorData, dataSyncId),\n                    title = title\n                )\n            )\n        }\n    }\n\n    suspend fun renamePlaylist(\n        client: YouTubeClient,\n        playlistId: String,\n        name: String,\n    ) = withRetry {\n        httpClient.post(\"browse/edit_playlist\") {\n            ytClient(client, setLogin = true)\n            setBody(\n                EditPlaylistBody(\n                    context = client.toContext(locale, visitorData, dataSyncId),\n                    playlistId = playlistId,\n                    actions = listOf(\n                        Action.RenamePlaylistAction(\n                            playlistName = name\n                        )\n                    )\n                )\n            )\n        }\n    }\n    \n    suspend fun getUploadCustomThumbnailLink(\n        client: YouTubeClient,\n        contentLength: Int\n    ) = withRetry {\n        httpClient.post(\"https://music.youtube.com/playlist_image_upload/playlist_custom_thumbnail\") {\n            ytClient(client, setLogin = true)\n            headers {\n                append(\"X-Goog-Upload-Command\", \"start\")\n                append(\"X-Goog-Upload-Protocol\", \"resumable\")\n                append(\"X-Goog-Upload-Header-Content-Length\", contentLength.toString())\n            }\n        }\n    }\n\n    suspend fun uploadCustomThumbnail(\n        client: YouTubeClient,\n        uploadId: String,\n        image: ByteArray,\n    ) = withRetry {\n        httpClient.post(\"https://music.youtube.com/playlist_image_upload/playlist_custom_thumbnail\") {\n            ytClient(client, setLogin = true)\n            parameter(\"upload_id\", uploadId)\n            parameter(\"upload_protocol\", \"resumable\")\n            headers {\n                append(\"X-Goog-Upload-Command\", \"upload, finalize\")\n                append(\"X-Goog-Upload-Offset\", \"0\")\n            }\n            setBody(image)\n        }\n    }\n\n    suspend fun setThumbnailPlaylist(\n        client: YouTubeClient,\n        playlistId: String,\n        blobId: String,\n    ) = withRetry {\n        httpClient.post(\"browse/edit_playlist\") {\n            ytClient(client, setLogin = true)\n            setBody(\n                EditPlaylistBody(\n                    context = client.toContext(locale, visitorData, dataSyncId),\n                    playlistId = playlistId,\n                    actions = listOf(\n                        Action.SetCustomThumbnailAction(\n                            addedCustomThumbnail = Action.SetCustomThumbnailAction.AddedCustomThumbnail(\n                                playlistScottyEncryptedBlobId = blobId\n                            )\n                        )\n                    )\n                )\n            )\n        }\n    }\n\n    suspend fun removeThumbnailPlaylist(\n        client: YouTubeClient,\n        playlistId: String\n    ) = withRetry {\n        httpClient.post(\"browse/edit_playlist\") {\n            ytClient(client, setLogin = true)\n            setBody(\n                EditPlaylistBody(\n                    context = client.toContext(locale, visitorData, dataSyncId),\n                    playlistId = playlistId,\n                    actions = listOf(\n                        Action.RemoveCustomThumbnailAction()\n                    )\n                )\n            )\n        }\n    }\n\n    suspend fun deletePlaylist(\n        client: YouTubeClient,\n        playlistId: String,\n    ) = withRetry {\n        httpClient.post(\"playlist/delete\") {\n            println(\"deleting $playlistId\")\n            ytClient(client, setLogin = true)\n            setBody(\n                PlaylistDeleteBody(\n                    context = client.toContext(locale, visitorData, dataSyncId),\n                    playlistId = playlistId\n                )\n            )\n        }\n    }\n\n    private suspend fun returnYouTubeDislike(videoId: String) = withRetry {\n        httpClient.get(\"https://returnyoutubedislikeapi.com/Votes?videoId=$videoId\") {\n            contentType(ContentType.Application.Json)\n        }\n    }\n\n\n    /**\n     * Initialize a song upload to YouTube Music.\n     * Returns the upload URL in the X-Goog-Upload-URL header.\n     */\n    suspend fun initSongUpload(\n        filename: String,\n        contentLength: Long\n    ) = withRetry {\n        val authUser = \"0\"\n        httpClient.post(\"https://upload.youtube.com/upload/usermusic/http?authuser=$authUser\") {\n            headers {\n                append(\"X-Goog-Upload-Command\", \"start\")\n                append(\"X-Goog-Upload-Protocol\", \"resumable\")\n                append(\"X-Goog-Upload-Header-Content-Length\", contentLength.toString())\n                append(\"X-Goog-AuthUser\", authUser)\n                append(\"Origin\", YouTubeClient.ORIGIN_YOUTUBE_MUSIC)\n                cookie?.let { cookie ->\n                    append(\"cookie\", cookie)\n                    if (\"SAPISID\" !in cookieMap) return@let\n                    val currentTime = System.currentTimeMillis() / 1000\n                    val sapisidHash = sha1(\"$currentTime ${cookieMap[\"SAPISID\"]} ${YouTubeClient.ORIGIN_YOUTUBE_MUSIC}\")\n                    append(\"Authorization\", \"SAPISIDHASH ${currentTime}_${sapisidHash}\")\n                }\n            }\n            contentType(ContentType.Application.FormUrlEncoded)\n            setBody(\"filename=$filename\")\n        }\n    }\n\n    /**\n     * Upload song data to the provided upload URL.\n     */\n    suspend fun uploadSongData(\n        uploadUrl: String,\n        data: ByteArray,\n        onProgress: ((Float) -> Unit)? = null\n    ) = withRetry {\n        httpClient.post(uploadUrl) {\n            headers {\n                append(\"X-Goog-Upload-Command\", \"upload, finalize\")\n                append(\"X-Goog-Upload-Offset\", \"0\")\n                append(\"X-Goog-AuthUser\", \"0\")\n                append(\"Origin\", YouTubeClient.ORIGIN_YOUTUBE_MUSIC)\n                cookie?.let { cookie ->\n                    append(\"cookie\", cookie)\n                    if (\"SAPISID\" !in cookieMap) return@let\n                    val currentTime = System.currentTimeMillis() / 1000\n                    val sapisidHash = sha1(\"$currentTime ${cookieMap[\"SAPISID\"]} ${YouTubeClient.ORIGIN_YOUTUBE_MUSIC}\")\n                    append(\"Authorization\", \"SAPISIDHASH ${currentTime}_${sapisidHash}\")\n                }\n            }\n            contentType(ContentType.Application.FormUrlEncoded)\n            setBody(data)\n            onUpload { bytesSentTotal, contentLength ->\n                contentLength?.let {\n                    onProgress?.invoke(bytesSentTotal.toFloat() / it.toFloat())\n                }\n            }\n        }\n    }\n\n    /**\n     * Delete a privately owned (uploaded) song from YouTube Music.\n     */\n    suspend fun deletePrivatelyOwnedEntity(entityId: String) = withRetry {\n        val context = YouTubeClient.WEB_REMIX.toContext(locale, visitorData, null)\n        val requestBody = \"\"\"{\"context\":${Json.encodeToString(context)},\"entityId\":\"$entityId\"}\"\"\"\n        httpClient.post(\"https://music.youtube.com/youtubei/v1/music/delete_privately_owned_entity\") {\n            contentType(ContentType.Application.Json)\n            headers {\n                append(\"Referer\", YouTubeClient.REFERER_YOUTUBE_MUSIC)\n                append(\"Origin\", YouTubeClient.ORIGIN_YOUTUBE_MUSIC)\n                cookie?.let { cookie ->\n                    append(\"cookie\", cookie)\n                    if (\"SAPISID\" !in cookieMap) return@let\n                    val currentTime = System.currentTimeMillis() / 1000\n                    val sapisidHash = sha1(\"$currentTime ${cookieMap[\"SAPISID\"]} ${YouTubeClient.ORIGIN_YOUTUBE_MUSIC}\")\n                    append(\"Authorization\", \"SAPISIDHASH ${currentTime}_${sapisidHash}\")\n                }\n            }\n            parameter(\"key\", \"AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX3\")\n            parameter(\"prettyPrint\", false)\n            setBody(requestBody)\n        }\n    }\n\n    suspend fun getMediaInfo(videoId: String): Result<MediaInfo> =\n        runCatching {\n            val response = next(client = YouTubeClient.WEB, videoId, null, null, null, null, null).body<NextResponse>()\n\n            val baseForInfo =\n                response.contents.twoColumnWatchNextResults\n                    ?.results\n                    ?.results\n                    ?.content\n                    ?.find {\n                        it?.videoSecondaryInfoRenderer != null\n                    }?.videoSecondaryInfoRenderer\n\n            val baseForTitle =\n                response.contents.twoColumnWatchNextResults\n                    ?.results\n                    ?.results\n                    ?.content\n                    ?.find {\n                        it?.videoPrimaryInfoRenderer != null\n                    }?.videoPrimaryInfoRenderer\n\n            val returnYouTubeDislikeResponse =\n                returnYouTubeDislike(videoId).body<ReturnYouTubeDislikeResponse>()\n\n            return@runCatching MediaInfo(\n                videoId = videoId,\n                title = baseForTitle\n                    ?.title\n                    ?.runs\n                    ?.firstOrNull()\n                    ?.text,\n                author = baseForInfo\n                    ?.owner\n                    ?.videoOwnerRenderer\n                    ?.title\n                    ?.runs\n                    ?.firstOrNull()\n                    ?.text,\n                authorId =\n                    baseForInfo\n                        ?.owner\n                        ?.videoOwnerRenderer\n                        ?.navigationEndpoint\n                        ?.browseEndpoint\n                        ?.browseId,\n                authorThumbnail =\n                    baseForInfo\n                        ?.owner\n                        ?.videoOwnerRenderer\n                        ?.thumbnail\n                        ?.thumbnails\n                        ?.find {\n                            it.height == 48\n                        }?.url\n                        ?.replace(\"s48\", \"s960\"),\n                description = baseForInfo?.attributedDescription?.content,\n                subscribers =\n                    baseForInfo\n                        ?.owner\n                        ?.videoOwnerRenderer\n                        ?.subscriberCountText\n                        ?.simpleText?.split(\" \")?.firstOrNull(),\n                uploadDate = baseForTitle?.dateText?.simpleText,\n                viewCount = returnYouTubeDislikeResponse.viewCount,\n                like = returnYouTubeDislikeResponse.likes,\n                dislike = returnYouTubeDislikeResponse.dislikes,\n            )\n\n        }\n\n\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/NetworkConfig.kt",
    "content": "package com.metrolist.innertube\n\nimport io.ktor.client.HttpClient\nimport io.ktor.client.engine.okhttp.OkHttp\nimport io.ktor.client.plugins.HttpTimeout\nimport io.ktor.client.plugins.compression.ContentEncoding\nimport io.ktor.client.plugins.contentnegotiation.ContentNegotiation\nimport io.ktor.serialization.kotlinx.json.json\nimport kotlinx.serialization.ExperimentalSerializationApi\nimport kotlinx.serialization.json.Json\nimport java.io.File\nimport java.util.concurrent.TimeUnit\n\n/**\n * Enhanced network configuration for better performance and reliability\n * Inspired by ArchiveTune optimizations\n */\nobject NetworkConfig {\n    \n    // Timeout settings\n    private const val CONNECT_TIMEOUT_SECONDS = 30L\n    private const val READ_TIMEOUT_SECONDS = 60L\n    private const val WRITE_TIMEOUT_SECONDS = 60L\n    private const val REQUEST_TIMEOUT_MILLIS = 60000L\n    \n    // Cache settings\n    private const val CACHE_SIZE_MB = 50L * 1024L * 1024L // 50 MB\n    \n    @OptIn(ExperimentalSerializationApi::class)\n    fun createOptimizedHttpClient(\n        cacheDir: File? = null,\n        enableCache: Boolean = true\n    ): HttpClient = HttpClient(OkHttp) {\n        expectSuccess = true\n\n        install(ContentNegotiation) {\n            json(Json {\n                ignoreUnknownKeys = true\n                explicitNulls = false\n                encodeDefaults = true\n                isLenient = true\n            })\n        }\n\n        install(ContentEncoding) {\n            gzip(0.9F)\n            deflate(0.8F)\n        }\n\n        install(HttpTimeout) {\n            requestTimeoutMillis = REQUEST_TIMEOUT_MILLIS\n            connectTimeoutMillis = CONNECT_TIMEOUT_SECONDS * 1000\n            socketTimeoutMillis = READ_TIMEOUT_SECONDS * 1000\n        }\n\n        engine {\n            config {\n                // Timeout configurations\n                connectTimeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS)\n                readTimeout(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS)\n                writeTimeout(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS)\n                \n                // Retry configuration\n                retryOnConnectionFailure(true)\n                \n                // Cache configuration\n                if (enableCache) {\n                    val cacheDirectory = cacheDir ?: File(System.getProperty(\"java.io.tmpdir\"), \"metrolist_http_cache\")\n                    cache(okhttp3.Cache(cacheDirectory, CACHE_SIZE_MB))\n                }\n            }\n        }\n    }\n    \n    /**\n     * Create a client specifically optimized for YouTube Music API\n     */\n    @OptIn(ExperimentalSerializationApi::class)\n    fun createYouTubeMusicClient(\n        cacheDir: File? = null\n    ): HttpClient {\n        val baseClient = createOptimizedHttpClient(cacheDir)\n        return baseClient.config {\n            // Additional configuration can be added here if needed\n        }\n    }\n    \n    /**\n     * Network quality detection and adaptive configuration\n     */\n    fun getAdaptiveTimeouts(networkQuality: NetworkQuality): TimeoutConfig {\n        return when (networkQuality) {\n            NetworkQuality.EXCELLENT -> TimeoutConfig(\n                connectTimeout = 10000L,\n                readTimeout = 30000L,\n                requestTimeout = 45000L\n            )\n            NetworkQuality.GOOD -> TimeoutConfig(\n                connectTimeout = 20000L,\n                readTimeout = 45000L,\n                requestTimeout = 60000L\n            )\n            NetworkQuality.POOR -> TimeoutConfig(\n                connectTimeout = 30000L,\n                readTimeout = 60000L,\n                requestTimeout = 90000L\n            )\n            NetworkQuality.UNKNOWN -> TimeoutConfig(\n                connectTimeout = CONNECT_TIMEOUT_SECONDS * 1000,\n                readTimeout = READ_TIMEOUT_SECONDS * 1000,\n                requestTimeout = REQUEST_TIMEOUT_MILLIS\n            )\n        }\n    }\n    \n    enum class NetworkQuality {\n        EXCELLENT, GOOD, POOR, UNKNOWN\n    }\n    \n    data class TimeoutConfig(\n        val connectTimeout: Long,\n        val readTimeout: Long,\n        val requestTimeout: Long\n    )\n}"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/YouTube.kt",
    "content": "package com.metrolist.innertube\n\nimport com.metrolist.innertube.models.AccountInfo\nimport com.metrolist.innertube.models.YTItem\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.Artist\nimport com.metrolist.innertube.models.ArtistItem\nimport com.metrolist.innertube.models.BrowseEndpoint\nimport com.metrolist.innertube.models.GridRenderer\nimport com.metrolist.innertube.models.MediaInfo\nimport com.metrolist.innertube.models.MusicResponsiveListItemRenderer\nimport com.metrolist.innertube.models.MusicTwoRowItemRenderer\nimport com.metrolist.innertube.models.MusicCarouselShelfRenderer\nimport com.metrolist.innertube.models.MusicShelfRenderer\nimport com.metrolist.innertube.models.SectionListRenderer\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.PodcastItem\nimport com.metrolist.innertube.models.EpisodeItem\nimport com.metrolist.innertube.models.SearchSuggestions\nimport com.metrolist.innertube.models.Run\nimport com.metrolist.innertube.models.Runs\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.TasteArtist\nimport com.metrolist.innertube.models.TasteProfile\nimport com.metrolist.innertube.models.WatchEndpoint\nimport com.metrolist.innertube.models.WatchEndpoint.WatchEndpointMusicSupportedConfigs.WatchEndpointMusicConfig.Companion.MUSIC_VIDEO_TYPE_ATV\nimport com.metrolist.innertube.models.YouTubeClient\nimport com.metrolist.innertube.models.YouTubeClient.Companion.WEB\nimport com.metrolist.innertube.models.YouTubeClient.Companion.WEB_REMIX\nimport com.metrolist.innertube.models.YouTubeLocale\nimport com.metrolist.innertube.models.getContinuation\nimport com.metrolist.innertube.models.getItems\nimport com.metrolist.innertube.models.oddElements\nimport com.metrolist.innertube.models.response.AccountMenuResponse\nimport com.metrolist.innertube.models.response.BrowseResponse\nimport com.metrolist.innertube.models.response.CreatePlaylistResponse\nimport com.metrolist.innertube.models.response.EditPlaylistResponse\nimport com.metrolist.innertube.models.response.FeedbackResponse\nimport com.metrolist.innertube.models.response.GetQueueResponse\nimport com.metrolist.innertube.models.response.GetSearchSuggestionsResponse\nimport com.metrolist.innertube.models.response.GetTranscriptResponse\nimport com.metrolist.innertube.models.response.ImageUploadResponse\nimport com.metrolist.innertube.models.response.NextResponse\nimport com.metrolist.innertube.models.response.PlayerResponse\nimport com.metrolist.innertube.models.response.SearchResponse\nimport com.metrolist.innertube.pages.AlbumPage\nimport com.metrolist.innertube.pages.ArtistItemsContinuationPage\nimport com.metrolist.innertube.pages.ArtistItemsPage\nimport com.metrolist.innertube.pages.ArtistPage\nimport com.metrolist.innertube.pages.ChartsPage\nimport com.metrolist.innertube.pages.BrowseResult\nimport com.metrolist.innertube.pages.ExplorePage\nimport com.metrolist.innertube.pages.HistoryPage\nimport com.metrolist.innertube.pages.HomePage\nimport com.metrolist.innertube.pages.LibraryContinuationPage\nimport com.metrolist.innertube.pages.LibraryPage\nimport com.metrolist.innertube.pages.MoodAndGenres\nimport com.metrolist.innertube.pages.NewReleaseAlbumPage\nimport com.metrolist.innertube.pages.NextPage\nimport com.metrolist.innertube.pages.NextResult\nimport com.metrolist.innertube.pages.PageHelper\nimport com.metrolist.innertube.pages.PlaylistContinuationPage\nimport com.metrolist.innertube.pages.PlaylistPage\nimport com.metrolist.innertube.pages.PodcastPage\nimport com.metrolist.innertube.pages.RelatedPage\nimport com.metrolist.innertube.pages.SearchPage\nimport com.metrolist.innertube.pages.SearchResult\nimport com.metrolist.innertube.pages.SearchSuggestionPage\nimport com.metrolist.innertube.pages.SearchSummary\nimport com.metrolist.innertube.pages.SearchSummaryPage\nimport io.ktor.client.call.body\nimport io.ktor.client.statement.bodyAsText\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.serialization.encodeToString\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.json.JsonPrimitive\nimport kotlinx.serialization.json.contentOrNull\nimport kotlinx.serialization.json.jsonArray\nimport kotlinx.serialization.json.jsonPrimitive\nimport java.net.Proxy\nimport kotlin.random.Random\nimport timber.log.Timber\n\n/**\n * Parse useful data with [InnerTube] sending requests.\n * Modified from [ViMusic](https://github.com/vfsfitvnm/ViMusic)\n */\nobject YouTube {\n    private val innerTube = InnerTube()\n\n    var locale: YouTubeLocale\n        get() = innerTube.locale\n        set(value) {\n            innerTube.locale = value\n        }\n    var visitorData: String?\n        get() = innerTube.visitorData\n        set(value) {\n            innerTube.visitorData = value\n        }\n    var dataSyncId: String?\n        get() = innerTube.dataSyncId\n        set(value) {\n            innerTube.dataSyncId = value\n        }\n    var cookie: String?\n        get() = innerTube.cookie\n        set(value) {\n            innerTube.cookie = value\n        }\n    var proxy: Proxy?\n        get() = innerTube.proxy\n        set(value) {\n            innerTube.proxy = value\n        }\n\n    var proxyAuth: String?\n        get() = innerTube.proxyAuth\n        set(value) {\n            innerTube.proxyAuth = value\n        }\n    var useLoginForBrowse: Boolean\n        get() = innerTube.useLoginForBrowse\n        set(value) {\n            innerTube.useLoginForBrowse = value\n        }\n\n    suspend fun searchSuggestions(query: String): Result<SearchSuggestions> = runCatching {\n        val response = innerTube.getSearchSuggestions(WEB_REMIX, query).body<GetSearchSuggestionsResponse>()\n        SearchSuggestions(\n            queries = response.contents?.getOrNull(0)?.searchSuggestionsSectionRenderer?.contents?.mapNotNull { content ->\n                content.searchSuggestionRenderer?.suggestion?.runs?.joinToString(separator = \"\") { it.text }\n            }.orEmpty(),\n            recommendedItems = response.contents?.getOrNull(1)?.searchSuggestionsSectionRenderer?.contents?.mapNotNull {\n                it.musicResponsiveListItemRenderer?.let { renderer ->\n                    SearchSuggestionPage.fromMusicResponsiveListItemRenderer(renderer)\n                }\n            }.orEmpty()\n        )\n    }\n\n    suspend fun searchSummary(query: String): Result<SearchSummaryPage> = runCatching {\n        val response = innerTube.search(WEB_REMIX, query).body<SearchResponse>()\n        val allSummaries = mutableListOf<SearchSummary>()\n\n        response.contents?.tabbedSearchResultsRenderer?.tabs?.firstOrNull()\n            ?.tabRenderer?.content?.sectionListRenderer?.contents?.forEach { section ->\n                if (section.musicCardShelfRenderer != null) {\n                    // Top result card - keep as single section\n                    val items = listOfNotNull(SearchSummaryPage.fromMusicCardShelfRenderer(section.musicCardShelfRenderer))\n                        .plus(\n                            section.musicCardShelfRenderer.contents\n                                ?.mapNotNull { it.musicResponsiveListItemRenderer }\n                                ?.mapNotNull(SearchSummaryPage.Companion::fromMusicResponsiveListItemRenderer)\n                                .orEmpty()\n                        )\n                        .distinctBy { it.id }\n\n                    if (items.isNotEmpty()) {\n                        allSummaries.add(SearchSummary(\n                            title = section.musicCardShelfRenderer.header?.musicCardShelfHeaderBasicRenderer?.title?.runs?.firstOrNull()?.text\n                                ?: YouTubeConstants.DEFAULT_TOP_RESULT,\n                            items = items\n                        ))\n                    }\n                } else if (section.musicShelfRenderer != null) {\n                    val items = section.musicShelfRenderer.contents?.getItems()\n                        ?.mapNotNull { SearchSummaryPage.fromMusicResponsiveListItemRenderer(it) }\n                        ?.distinctBy { it.id }\n                        ?: emptyList()\n\n                    if (items.isEmpty()) return@forEach\n\n                    val apiTitle = section.musicShelfRenderer.title?.runs?.firstOrNull()?.text\n\n                    if (apiTitle != null) {\n                        // API provided a title, use single section\n                        allSummaries.add(SearchSummary(title = apiTitle, items = items))\n                    } else {\n                        // No title - group items by type into separate sections\n                        val grouped = items.groupBy { item ->\n                            when (item) {\n                                is EpisodeItem -> \"Episodes\"\n                                is PodcastItem -> \"Podcasts\"\n                                is AlbumItem -> \"Albums\"\n                                is ArtistItem -> if (item.isProfile) \"Profiles\" else \"Artists\"\n                                is PlaylistItem -> \"Playlists\"\n                                is SongItem -> when {\n                                    item.isEpisode -> \"Episodes\"\n                                    item.isVideoSong -> \"Videos\"\n                                    else -> \"Songs\"\n                                }\n                            }\n                        }\n\n                        // Add each group as a separate section in a logical order\n                        val sectionOrder = listOf(\"Songs\", \"Videos\", \"Albums\", \"Artists\", \"Playlists\", \"Podcasts\", \"Episodes\", \"Profiles\", YouTubeConstants.DEFAULT_OTHER_RESULTS)\n                        sectionOrder.forEach { sectionName ->\n                            grouped[sectionName]?.let { groupItems ->\n                                if (groupItems.isNotEmpty()) {\n                                    allSummaries.add(SearchSummary(title = sectionName, items = groupItems))\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n\n        // Merge sections with the same title\n        val mergedSummaries = allSummaries\n            .groupBy { it.title }\n            .map { (title, sections) ->\n                SearchSummary(\n                    title = title,\n                    items = sections.flatMap { it.items }.distinctBy { it.id }\n                )\n            }\n            // Reorder to maintain logical order\n            .sortedBy { summary ->\n                when (summary.title) {\n                    YouTubeConstants.DEFAULT_TOP_RESULT -> 0\n                    \"Songs\" -> 1\n                    \"Videos\" -> 2\n                    \"Albums\" -> 3\n                    \"Artists\" -> 4\n                    \"Playlists\" -> 5\n                    \"Podcasts\" -> 6\n                    \"Episodes\" -> 7\n                    \"Profiles\" -> 8\n                    else -> 9\n                }\n            }\n\n        SearchSummaryPage(summaries = mergedSummaries)\n    }\n\n    suspend fun search(query: String, filter: SearchFilter): Result<SearchResult> = runCatching {\n        val response = innerTube.search(WEB_REMIX, query, filter.value).body<SearchResponse>()\n        val shelves = response.contents?.tabbedSearchResultsRenderer?.tabs?.firstOrNull()\n            ?.tabRenderer?.content?.sectionListRenderer?.contents\n            ?.mapNotNull { it.musicShelfRenderer }\n            .orEmpty()\n        SearchResult(\n            items = shelves.flatMap { shelf ->\n                shelf.contents?.getItems()?.mapNotNull { SearchPage.toYTItem(it) } ?: emptyList()\n            }.distinctBy { it.id },\n            continuation = shelves.firstOrNull { it.continuations != null }\n                ?.continuations?.getContinuation()\n        )\n    }\n\n    suspend fun searchContinuation(continuation: String): Result<SearchResult> = runCatching {\n        val response = innerTube.search(WEB_REMIX, continuation = continuation).body<SearchResponse>()\n        val items = response.continuationContents?.musicShelfContinuation?.contents\n            ?.mapNotNull {\n                SearchPage.toYTItem(it.musicResponsiveListItemRenderer)\n            } ?: emptyList()\n        SearchResult(\n            items = items,\n            continuation = if (items.isEmpty()) null else response.continuationContents?.musicShelfContinuation?.continuations?.getContinuation()\n        )\n    }\n\n    suspend fun album(browseId: String, withSongs: Boolean = true): Result<AlbumPage> = runCatching {\n        val response = innerTube.browse(WEB_REMIX, browseId).body<BrowseResponse>()\n        if (browseId.contains(\"FEmusic_library_privately_owned_release_detail\")) {\n            val playlistId =\n                response.header?.musicDetailHeaderRenderer?.menu?.menuRenderer?.topLevelButtons?.firstOrNull()?.buttonRenderer?.navigationEndpoint?.watchPlaylistEndpoint?.playlistId!!\n            val albumItem = AlbumItem(\n                browseId = browseId,\n                playlistId = playlistId,\n                title = response.header.musicDetailHeaderRenderer.title.runs?.firstOrNull()?.text!!,\n                artists = response.header.musicDetailHeaderRenderer.subtitle.runs?.filter { it.navigationEndpoint != null }?.map {\n                    Artist(\n                        name = it.text,\n                        id = it.navigationEndpoint?.browseEndpoint?.browseId\n                    )\n                },\n                year = response.header.musicDetailHeaderRenderer.subtitle.runs?.lastOrNull()?.text?.toIntOrNull(),\n                thumbnail = response.header.musicDetailHeaderRenderer.thumbnail.croppedSquareThumbnailRenderer?.thumbnail?.thumbnails?.lastOrNull()!!.url,\n                explicit = false, // TODO: Extract explicit badge for albums from YouTube response\n            )\n            return@runCatching AlbumPage(\n                album = albumItem,\n                songs = response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.musicShelfRenderer?.contents?.getItems()?.mapNotNull {\n                    AlbumPage.getSong(it, albumItem)\n                }!!.toMutableList(),\n                otherVersions = emptyList()\n            )\n        } else {\n            val playlistId =\n                response.microformat?.microformatDataRenderer?.urlCanonical?.substringAfterLast('=')!!\n            val albumItem = AlbumItem(\n                browseId = browseId,\n                playlistId = playlistId,\n                title = response.contents?.twoColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.musicResponsiveHeaderRenderer?.title?.runs?.firstOrNull()?.text!!,\n                artists = response.contents.twoColumnBrowseResultsRenderer.tabs.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.musicResponsiveHeaderRenderer?.straplineTextOne?.runs?.oddElements()\n                    ?.map {\n                        Artist(\n                            name = it.text,\n                            id = it.navigationEndpoint?.browseEndpoint?.browseId\n                        )\n                    }!!,\n                year = response.contents.twoColumnBrowseResultsRenderer.tabs.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.musicResponsiveHeaderRenderer?.subtitle?.runs?.lastOrNull()?.text?.toIntOrNull(),\n                thumbnail = response.contents.twoColumnBrowseResultsRenderer.tabs.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.musicResponsiveHeaderRenderer?.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.lastOrNull()?.url!!,\n                explicit = false, // TODO: Extract explicit badge for albums from YouTube response\n            )\n            return@runCatching AlbumPage(\n                album = albumItem,\n                songs = if (withSongs) albumSongs(\n                    playlistId, albumItem\n                ).getOrThrow() else emptyList(),\n                otherVersions = response.contents.twoColumnBrowseResultsRenderer.secondaryContents?.sectionListRenderer?.contents?.getOrNull(\n                    1\n                )?.musicCarouselShelfRenderer?.contents\n                    ?.mapNotNull { it.musicTwoRowItemRenderer }\n                    ?.mapNotNull(NewReleaseAlbumPage::fromMusicTwoRowItemRenderer)\n                    .orEmpty()\n            )\n        }\n    }\n\n    suspend fun albumSongs(playlistId: String, album: AlbumItem? = null): Result<List<SongItem>> = runCatching {\n        var response = innerTube.browse(WEB_REMIX, \"VL$playlistId\").body<BrowseResponse>()\n        val songs = response.contents?.twoColumnBrowseResultsRenderer\n            ?.secondaryContents?.sectionListRenderer\n            ?.contents?.firstOrNull()\n            ?.musicPlaylistShelfRenderer?.contents?.getItems()\n            ?.mapNotNull {\n                AlbumPage.getSong(it, album)\n            }!!\n            .toMutableList()\n        var continuation = response.contents.twoColumnBrowseResultsRenderer.secondaryContents.sectionListRenderer\n            .contents.firstOrNull()?.musicPlaylistShelfRenderer?.contents?.getContinuation()\n        val seenContinuations = mutableSetOf<String>()\n        var requestCount = 0\n        val maxRequests = 50 // Prevent excessive API calls\n        \n        while (continuation != null && requestCount < maxRequests) {\n            // Prevent infinite loops by tracking seen continuations\n            if (continuation in seenContinuations) {\n                break\n            }\n            seenContinuations.add(continuation)\n            requestCount++\n            \n            response = innerTube.browse(\n                client = WEB_REMIX,\n                continuation = continuation,\n            ).body<BrowseResponse>()\n            songs += response.onResponseReceivedActions?.firstOrNull()?.appendContinuationItemsAction?.continuationItems?.getItems()?.mapNotNull {\n                AlbumPage.getSong(it, album)\n            }.orEmpty()\n            continuation = response.continuationContents?.musicPlaylistShelfContinuation?.continuations?.getContinuation()\n        }\n        songs\n    }\n\n    suspend fun artist(browseId: String): Result<ArtistPage> = runCatching {\n        val response = innerTube.browse(WEB_REMIX, browseId).body<BrowseResponse>()\n\n        fun mapRuns(runs: List<Run>?): List<Run>? = runs?.map { run ->\n            Run(\n                text = run.text,\n                navigationEndpoint = run.navigationEndpoint\n            )\n        }\n\n        val descriptionRuns = response.contents?.sectionListRenderer?.contents\n            ?.firstOrNull { it.musicDescriptionShelfRenderer != null }\n            ?.musicDescriptionShelfRenderer?.description?.runs\n            ?.let(::mapRuns)\n            ?: response.header?.musicImmersiveHeaderRenderer?.description?.runs?.let(::mapRuns)\n\n        // Check subscription state from multiple locations:\n        // 1. musicImmersiveHeaderRenderer.subscriptionButton (regular artists)\n        // 2. musicVisualHeaderRenderer.subscriptionButton (podcast channels)\n        val immersiveSubscribed = response.header?.musicImmersiveHeaderRenderer?.subscriptionButton?.subscribeButtonRenderer?.subscribed\n        val visualSubscribed = response.header?.musicVisualHeaderRenderer?.subscriptionButton?.subscribeButtonRenderer?.subscribed\n        val isSubscribed = immersiveSubscribed ?: visualSubscribed ?: false\n\n        // Also extract channelId from visual header if not in immersive header\n        val channelIdFromVisual = response.header?.musicVisualHeaderRenderer?.subscriptionButton?.subscribeButtonRenderer?.channelId\n\n        ArtistPage(\n            artist = ArtistItem(\n                id = browseId,\n                title = response.header?.musicImmersiveHeaderRenderer?.title?.runs?.firstOrNull()?.text\n                    ?: response.header?.musicVisualHeaderRenderer?.title?.runs?.firstOrNull()?.text\n                    ?: response.header?.musicHeaderRenderer?.title?.runs?.firstOrNull()?.text!!,\n                thumbnail = response.header?.musicImmersiveHeaderRenderer?.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl()\n                    ?: response.header?.musicVisualHeaderRenderer?.foregroundThumbnail?.musicThumbnailRenderer?.getThumbnailUrl()\n                    ?: response.header?.musicDetailHeaderRenderer?.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl(),\n                channelId = response.header?.musicImmersiveHeaderRenderer?.subscriptionButton?.subscribeButtonRenderer?.channelId\n                    ?: channelIdFromVisual,\n                playEndpoint = response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()\n                    ?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.musicShelfRenderer\n                    ?.contents?.firstOrNull()?.musicResponsiveListItemRenderer?.overlay?.musicItemThumbnailOverlayRenderer\n                    ?.content?.musicPlayButtonRenderer?.playNavigationEndpoint?.watchEndpoint,\n                shuffleEndpoint = response.header?.musicImmersiveHeaderRenderer?.playButton?.buttonRenderer?.navigationEndpoint?.watchEndpoint\n                    ?: response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer\n                        ?.contents?.firstOrNull()?.musicShelfRenderer?.contents?.firstOrNull()?.musicResponsiveListItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint,\n                radioEndpoint = response.header?.musicImmersiveHeaderRenderer?.startRadioButton?.buttonRenderer?.navigationEndpoint?.watchEndpoint\n            ),\n            sections = response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()\n                ?.tabRenderer?.content?.sectionListRenderer?.contents\n                ?.mapNotNull(ArtistPage::fromSectionListRendererContent)!!,\n            description = descriptionRuns?.joinToString(separator = \"\") { it.text },\n                subscriberCountText = response.header?.musicImmersiveHeaderRenderer?.subscriptionButton2\n                    ?.subscribeButtonRenderer?.subscriberCountWithSubscribeText?.runs?.firstOrNull()?.text\n                    ?: response.header?.musicImmersiveHeaderRenderer?.subscriptionButton?.subscribeButtonRenderer\n                        ?.longSubscriberCountText?.runs?.firstOrNull()?.text\n                    ?: response.header?.musicImmersiveHeaderRenderer?.subscriptionButton?.subscribeButtonRenderer\n                        ?.shortSubscriberCountText?.runs?.firstOrNull()?.text,\n            monthlyListenerCount = response.header?.musicImmersiveHeaderRenderer?.monthlyListenerCount?.runs?.firstOrNull()?.text,\n            descriptionRuns = descriptionRuns,\n            isSubscribed = isSubscribed\n        )\n    }\n\n    suspend fun artistItems(endpoint: BrowseEndpoint): Result<ArtistItemsPage> = runCatching {\n        val response = innerTube.browse(WEB_REMIX, endpoint.browseId, endpoint.params).body<BrowseResponse>()\n        val sectionContent = response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()\n            ?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()\n        \n        val gridRenderer = sectionContent?.gridRenderer\n        val musicCarouselShelfRenderer = sectionContent?.musicCarouselShelfRenderer\n        val musicPlaylistShelfRenderer = sectionContent?.musicPlaylistShelfRenderer\n        val musicShelfRenderer = sectionContent?.musicShelfRenderer\n        \n        when {\n            gridRenderer != null -> {\n                ArtistItemsPage(\n                    title = gridRenderer.header?.gridHeaderRenderer?.title?.runs?.firstOrNull()?.text.orEmpty(),\n                    items = gridRenderer.items.mapNotNull {\n                        it.musicTwoRowItemRenderer?.let { renderer ->\n                            ArtistItemsPage.fromMusicTwoRowItemRenderer(renderer)\n                        }\n                    },\n                    continuation = gridRenderer.continuations?.getContinuation()\n                )\n            }\n            musicCarouselShelfRenderer != null -> {\n                ArtistItemsPage(\n                    title = musicCarouselShelfRenderer.header?.musicCarouselShelfBasicHeaderRenderer?.title?.runs?.firstOrNull()?.text.orEmpty(),\n                    items = musicCarouselShelfRenderer.contents.mapNotNull { content ->\n                        content.musicTwoRowItemRenderer?.let { renderer ->\n                            ArtistItemsPage.fromMusicTwoRowItemRenderer(renderer)\n                        } ?: content.musicResponsiveListItemRenderer?.let { renderer ->\n                            ArtistItemsPage.fromMusicResponsiveListItemRenderer(renderer)\n                        }\n                    },\n                    continuation = null\n                )\n            }\n            musicShelfRenderer != null -> {\n                ArtistItemsPage(\n                    title = musicShelfRenderer.title?.runs?.firstOrNull()?.text \n                        ?: response.header?.musicHeaderRenderer?.title?.runs?.firstOrNull()?.text \n                        ?: \"\",\n                    items = musicShelfRenderer.contents?.getItems()?.mapNotNull {\n                        ArtistItemsPage.fromMusicResponsiveListItemRenderer(it)\n                    } ?: emptyList(),\n                    continuation = musicShelfRenderer.continuations?.getContinuation()\n                )\n            }\n            else -> {\n                ArtistItemsPage(\n                    title = response.header?.musicHeaderRenderer?.title?.runs?.firstOrNull()?.text ?: \"\",\n                    items = musicPlaylistShelfRenderer?.contents?.getItems()?.mapNotNull {\n                        ArtistItemsPage.fromMusicResponsiveListItemRenderer(it)\n                    } ?: emptyList(),\n                    continuation = musicPlaylistShelfRenderer?.contents?.getContinuation()\n                )\n            }\n        }\n    }\n\n    suspend fun artistItemsContinuation(continuation: String): Result<ArtistItemsContinuationPage> = runCatching {\n        val response = innerTube.browse(WEB_REMIX, continuation = continuation).body<BrowseResponse>()\n\n        when {\n            response.continuationContents?.gridContinuation != null -> {\n                val gridContinuation = response.continuationContents.gridContinuation\n                val items = gridContinuation.items.mapNotNull {\n                    it.musicTwoRowItemRenderer?.let { renderer ->\n                        ArtistItemsPage.fromMusicTwoRowItemRenderer(renderer)\n                    }\n                }\n                ArtistItemsContinuationPage(\n                    items = items,\n                    continuation = if (items.isEmpty()) null else gridContinuation.continuations?.getContinuation()\n                )\n            }\n\n            response.continuationContents?.musicPlaylistShelfContinuation != null -> {\n                val musicPlaylistShelfContinuation = response.continuationContents.musicPlaylistShelfContinuation\n                val items = musicPlaylistShelfContinuation.contents.getItems().mapNotNull {\n                    ArtistItemsPage.fromMusicResponsiveListItemRenderer(it)\n                }\n                ArtistItemsContinuationPage(\n                    items = items,\n                    continuation = if (items.isEmpty()) null else musicPlaylistShelfContinuation.continuations?.getContinuation()\n                )\n            }\n\n            else -> {\n                val continuationItems = response.onResponseReceivedActions?.firstOrNull()\n                    ?.appendContinuationItemsAction?.continuationItems\n                val items = continuationItems?.getItems()?.mapNotNull {\n                    ArtistItemsPage.fromMusicResponsiveListItemRenderer(it)\n                } ?: emptyList()\n                ArtistItemsContinuationPage(\n                    items = items,\n                    continuation = if (items.isEmpty()) null else continuationItems?.getContinuation()\n                )\n            }\n        }\n    }\n\n    suspend fun playlist(playlistId: String): Result<PlaylistPage> = runCatching {\n        val response = innerTube.browse(\n            client = WEB_REMIX,\n            browseId = \"VL$playlistId\",\n            setLogin = true\n        ).body<BrowseResponse>()\n        val base = response.contents?.twoColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()\n        val header = base?.musicResponsiveHeaderRenderer ?: base?.musicEditablePlaylistDetailHeaderRenderer?.header?.musicResponsiveHeaderRenderer\n\n        val editable = base?.musicEditablePlaylistDetailHeaderRenderer != null\n\n        PlaylistPage(\n            playlist = PlaylistItem(\n                id = playlistId,\n                title = header?.title?.runs?.firstOrNull()?.text!!,\n                author = header.straplineTextOne?.runs?.firstOrNull()?.let {\n                    Artist(\n                        name = it.text,\n                        id = it.navigationEndpoint?.browseEndpoint?.browseId\n                    )\n                },\n                songCountText = header.secondSubtitle?.runs?.firstOrNull()?.text,\n                thumbnail = header.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.lastOrNull()?.url!!,\n                playEndpoint = null,\n                shuffleEndpoint = header.buttons.lastOrNull()?.menuRenderer?.items?.firstOrNull()?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint!!,\n                radioEndpoint = header.buttons.getOrNull(2)?.menuRenderer?.items?.find {\n                    it.menuNavigationItemRenderer?.icon?.iconType == \"MIX\"\n                }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint,\n                isEditable = editable\n            ),\n            songs = response.contents?.twoColumnBrowseResultsRenderer?.secondaryContents?.sectionListRenderer\n                ?.contents?.firstOrNull()?.musicPlaylistShelfRenderer?.contents?.getItems()?.mapNotNull {\n                    PlaylistPage.fromMusicResponsiveListItemRenderer(it)\n                } ?: emptyList(),\n            songsContinuation = response.contents?.twoColumnBrowseResultsRenderer?.secondaryContents?.sectionListRenderer\n                ?.contents?.firstOrNull()?.musicPlaylistShelfRenderer?.contents?.getContinuation()\n                ?: response.contents?.twoColumnBrowseResultsRenderer?.secondaryContents?.sectionListRenderer\n                    ?.contents?.firstOrNull()?.musicPlaylistShelfRenderer?.continuations?.getContinuation(),\n            continuation = response.contents?.twoColumnBrowseResultsRenderer?.secondaryContents?.sectionListRenderer\n                ?.continuations?.getContinuation()\n        )\n    }\n\n    suspend fun playlistContinuation(continuation: String): Result<PlaylistContinuationPage> = runCatching {\n        val response = innerTube.browse(\n            client = WEB_REMIX,\n            continuation = continuation,\n            setLogin = true\n        ).body<BrowseResponse>()\n\n        val mainContents: List<MusicShelfRenderer.Content> = response.continuationContents?.sectionListContinuation?.contents\n            ?.mapNotNull { content: SectionListRenderer.Content -> content.musicPlaylistShelfRenderer?.contents }\n            ?.flatten()\n            ?: emptyList()\n\n        val shelfContents: List<MusicShelfRenderer.Content> =\n            response.continuationContents?.musicPlaylistShelfContinuation?.contents ?: emptyList()\n\n        val appendedContents: List<MusicShelfRenderer.Content> = response.onResponseReceivedActions\n            ?.firstOrNull()\n            ?.appendContinuationItemsAction\n            ?.continuationItems\n            .orEmpty()\n\n        val allContents = mainContents + shelfContents + appendedContents\n\n        val songs = allContents\n            .mapNotNull { content: MusicShelfRenderer.Content -> content.musicResponsiveListItemRenderer }\n            .mapNotNull { renderer -> PlaylistPage.fromMusicResponsiveListItemRenderer(renderer) }\n\n        val nextContinuation = if (songs.isEmpty()) null else {\n            response.continuationContents\n                ?.sectionListContinuation\n                ?.continuations\n                ?.getContinuation()\n                ?: response.continuationContents\n                    ?.musicPlaylistShelfContinuation\n                    ?.continuations\n                    ?.getContinuation()\n                ?: response.continuationContents\n                    ?.musicShelfContinuation\n                    ?.continuations\n                    ?.getContinuation()\n                ?: response.onResponseReceivedActions\n                    ?.firstOrNull()\n                    ?.appendContinuationItemsAction\n                    ?.continuationItems\n                    ?.getContinuation()\n        }\n\n        PlaylistContinuationPage(\n            songs = songs,\n            continuation = nextContinuation\n        )\n    }\n\n    suspend fun podcast(podcastId: String): Result<PodcastPage> = podcastWithDebug(podcastId) { }\n\n    suspend fun podcastWithDebug(podcastId: String, log: (String) -> Unit): Result<PodcastPage> = runCatching {\n        Timber.d(\"Fetching podcast with ID: $podcastId\")\n        val response = innerTube.browse(\n            client = WEB_REMIX,\n            browseId = podcastId,\n            setLogin = true\n        ).body<BrowseResponse>()\n\n        Timber.d(\"Response received, twoColumnBrowseResultsRenderer: ${response.contents?.twoColumnBrowseResultsRenderer != null}\")\n        Timber.d(\"singleColumnBrowseResultsRenderer: ${response.contents?.singleColumnBrowseResultsRenderer != null}\")\n\n        // Try twoColumn first (standard layout)\n        var header = response.contents?.twoColumnBrowseResultsRenderer?.tabs?.firstOrNull()\n            ?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()\n            ?.musicResponsiveHeaderRenderer\n\n        // Fallback to singleColumn layout\n        if (header == null) {\n            header = response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()\n                ?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()\n                ?.musicResponsiveHeaderRenderer\n            Timber.d(\"Using singleColumn layout, header found: ${header != null}\")\n        }\n\n        Timber.d(\"Header title: ${header?.title?.runs?.firstOrNull()?.text}\")\n\n        // Debug: Log button structure\n        header?.buttons?.forEachIndexed { i, button ->\n            Timber.d(\"[PODCAST] Button[$i]: menuRenderer=${button.menuRenderer != null}, toggleButtonRenderer=${button.toggleButtonRenderer != null}, playButtonRenderer=${button.musicPlayButtonRenderer != null}\")\n            button.menuRenderer?.items?.forEachIndexed { j, item ->\n                Timber.d(\"[PODCAST] Button[$i].menuItems[$j]: toggle=${item.toggleMenuServiceItemRenderer?.defaultIcon?.iconType}, nav=${item.menuNavigationItemRenderer?.icon?.iconType}\")\n                // Check for SUBSCRIBE button (like artists have)\n                if (item.toggleMenuServiceItemRenderer?.defaultIcon?.iconType == \"SUBSCRIBE\") {\n                    val channelIds = item.toggleMenuServiceItemRenderer.defaultServiceEndpoint.subscribeEndpoint?.channelIds\n                    Timber.d(\"[PODCAST] Found SUBSCRIBE button! channelIds=$channelIds\")\n                }\n            }\n            button.toggleButtonRenderer?.let { toggle ->\n                Timber.d(\"[PODCAST] Button[$i].toggleButtonRenderer: defaultIcon=${toggle.defaultIcon?.iconType}, defaultToken=${toggle.defaultServiceEndpoint?.feedbackEndpoint?.feedbackToken?.take(30)}, subscribeChannelIds=${toggle.defaultServiceEndpoint?.subscribeEndpoint?.channelIds}\")\n            }\n        }\n\n        // Extract channelId and subscription state for subscription (like artists)\n        val subscribeToggle = header?.buttons?.flatMap { button ->\n            button.menuRenderer?.items ?: emptyList()\n        }?.find {\n            it.toggleMenuServiceItemRenderer?.defaultIcon?.iconType == \"SUBSCRIBE\"\n        }?.toggleMenuServiceItemRenderer\n        val channelId = subscribeToggle?.defaultServiceEndpoint?.subscribeEndpoint?.channelIds?.firstOrNull()\n        // isSelected indicates user is currently subscribed (toggle is in \"toggled\" state)\n        val isChannelSubscribed = subscribeToggle?.isSelected == true\n        Timber.d(\"[PODCAST] Extracted channelId for subscription: $channelId, isSubscribed: $isChannelSubscribed\")\n\n        // Extract library tokens from the header's menu buttons OR toggle buttons\n        var libraryTokens = header?.buttons?.flatMap { button ->\n            button.menuRenderer?.items ?: emptyList()\n        }?.let { menuItems ->\n            PageHelper.extractLibraryTokensFromMenuItems(menuItems)\n        }\n\n        // Also check for standalone toggle buttons (used by some podcasts)\n        if (libraryTokens?.addToken == null && libraryTokens?.removeToken == null) {\n            header?.buttons?.forEach { button ->\n                button.toggleButtonRenderer?.let { toggle ->\n                    val iconType = toggle.defaultIcon?.iconType\n                    if (iconType != null && PageHelper.isLibraryIcon(iconType)) {\n                        val defaultToken = toggle.defaultServiceEndpoint?.feedbackEndpoint?.feedbackToken\n                        val toggledToken = toggle.toggledServiceEndpoint?.feedbackEndpoint?.feedbackToken\n                        libraryTokens = if (PageHelper.isAddLibraryIcon(iconType)) {\n                            // BOOKMARK_BORDER: default=add, toggled=remove\n                            PageHelper.LibraryFeedbackTokens(defaultToken, toggledToken)\n                        } else {\n                            // BOOKMARK: default=remove, toggled=add\n                            PageHelper.LibraryFeedbackTokens(toggledToken, defaultToken)\n                        }\n                        Timber.d(\"[PODCAST] Found toggle button with library tokens - add: ${libraryTokens.addToken != null}, remove: ${libraryTokens.removeToken != null}\")\n                    }\n                }\n            }\n        }\n        Timber.d(\"[PODCAST] Library tokens - add: ${libraryTokens?.addToken != null}, remove: ${libraryTokens?.removeToken != null}\")\n\n        val podcastItem = PodcastItem(\n            id = podcastId,\n            title = header?.title?.runs?.firstOrNull()?.text ?: \"\",\n            author = header?.straplineTextOne?.runs?.firstOrNull()?.let {\n                Artist(\n                    name = it.text,\n                    id = it.navigationEndpoint?.browseEndpoint?.browseId\n                )\n            },\n            episodeCountText = header?.secondSubtitle?.runs?.firstOrNull()?.text,\n            thumbnail = header?.thumbnail?.musicThumbnailRenderer?.thumbnail?.thumbnails?.lastOrNull()?.url,\n            playEndpoint = header?.buttons?.find {\n                it.menuRenderer?.items?.firstOrNull()?.menuNavigationItemRenderer?.icon?.iconType == \"PLAY_ARROW\"\n            }?.menuRenderer?.items?.firstOrNull()?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint,\n            shuffleEndpoint = header?.buttons?.find {\n                it.menuRenderer?.items?.any { item -> item.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\" } == true\n            }?.menuRenderer?.items?.find { it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\" }\n                ?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint,\n            libraryAddToken = libraryTokens?.addToken,\n            libraryRemoveToken = libraryTokens?.removeToken,\n            channelId = channelId,\n        )\n\n        // Try twoColumn for episodes\n        val secondaryContents = response.contents?.twoColumnBrowseResultsRenderer?.secondaryContents\n        Timber.d(\"secondaryContents null: ${secondaryContents == null}\")\n        Timber.d(\"secondaryContents.sectionListRenderer null: ${secondaryContents?.sectionListRenderer == null}\")\n        Timber.d(\"sectionListRenderer.contents size: ${secondaryContents?.sectionListRenderer?.contents?.size ?: 0}\")\n\n        secondaryContents?.sectionListRenderer?.contents?.forEachIndexed { index, content ->\n            Timber.d(\"Content[$index]: musicShelfRenderer=${content.musicShelfRenderer != null}, musicPlaylistShelfRenderer=${content.musicPlaylistShelfRenderer != null}, gridRenderer=${content.gridRenderer != null}\")\n            content.musicShelfRenderer?.let { shelf ->\n                Timber.d(\"musicShelfRenderer.contents size: ${shelf.contents?.size ?: 0}\")\n            }\n            content.musicPlaylistShelfRenderer?.let { shelf ->\n                Timber.d(\"musicPlaylistShelfRenderer.contents size: ${shelf.contents.size}\")\n            }\n        }\n\n        var episodeContents = secondaryContents?.sectionListRenderer\n            ?.contents?.firstOrNull()?.musicShelfRenderer?.contents\n\n        // Try musicPlaylistShelfRenderer\n        if (episodeContents == null) {\n            episodeContents = secondaryContents?.sectionListRenderer\n                ?.contents?.firstOrNull()?.musicPlaylistShelfRenderer?.contents\n            Timber.d(\"Trying musicPlaylistShelfRenderer: ${episodeContents?.size ?: 0}\")\n        }\n\n        // Fallback to singleColumn\n        if (episodeContents == null) {\n            episodeContents = response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()\n                ?.tabRenderer?.content?.sectionListRenderer?.contents\n                ?.find { it.musicShelfRenderer != null }?.musicShelfRenderer?.contents\n            Timber.d(\"Using singleColumn for episodes, found: ${episodeContents?.size ?: 0}\")\n        }\n\n        Timber.d(\"Episode contents count: ${episodeContents?.size ?: 0}\")\n\n        // Get episodes from musicMultiRowListItemRenderer (used for podcasts)\n        val multiRowItems = episodeContents?.mapNotNull { it.musicMultiRowListItemRenderer } ?: emptyList()\n        Timber.d(\"multiRowItems count: ${multiRowItems.size}\")\n\n        multiRowItems.take(2).forEachIndexed { idx, renderer ->\n            Timber.d(\"Episode[$idx] title: ${renderer.title?.runs?.firstOrNull()?.text}\")\n            Timber.d(\"Episode[$idx] subtitle: ${renderer.subtitle?.runs?.map { it.text }}\")\n            Timber.d(\"Episode[$idx] videoId: ${renderer.onTap?.watchEndpoint?.videoId}\")\n            Timber.d(\"Episode[$idx] thumbnail: ${renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl()}\")\n        }\n\n        val episodes = multiRowItems.mapNotNull { renderer ->\n            PodcastPage.fromMusicMultiRowListItemRenderer(renderer, podcastItem)\n        }\n\n        Timber.d(\"Parsed episodes: ${episodes.size}\")\n\n        PodcastPage(\n            podcast = podcastItem,\n            episodes = episodes,\n            continuation = response.contents?.twoColumnBrowseResultsRenderer?.secondaryContents?.sectionListRenderer\n                ?.contents?.firstOrNull()?.musicShelfRenderer?.continuations?.getContinuation()\n                ?: response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()\n                    ?.tabRenderer?.content?.sectionListRenderer?.contents\n                    ?.find { it.musicShelfRenderer != null }?.musicShelfRenderer?.continuations?.getContinuation(),\n            isChannelSubscribed = isChannelSubscribed,\n        )\n    }\n\n    suspend fun home(continuation: String? = null, params: String? = null): Result<HomePage> = runCatching {\n        Timber.d(\"home() called with continuation=$continuation, params=$params\")\n        if (continuation != null) {\n            return@runCatching homeContinuation(continuation).getOrThrow()\n        }\n\n        val response = innerTube.browse(WEB_REMIX, browseId = \"FEmusic_home\", params = params).body<BrowseResponse>()\n        Timber.d(\"home() response received\")\n        val continuation = response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()\n            ?.tabRenderer?.content?.sectionListRenderer?.continuations?.getContinuation()\n        val sectionListRender = response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()\n            ?.tabRenderer?.content?.sectionListRenderer\n        Timber.d(\"home() sectionListRender contents size: ${sectionListRender?.contents?.size ?: 0}\")\n        val carousels = sectionListRender?.contents?.mapNotNull { it.musicCarouselShelfRenderer } ?: emptyList()\n        Timber.d(\"home() carousels count: ${carousels.size}\")\n        val sections = carousels.mapNotNull {\n            HomePage.Section.fromMusicCarouselShelfRenderer(it)\n        }.toMutableList()\n        Timber.d(\"home() sections parsed: ${sections.size}\")\n        val chips = sectionListRender?.header?.chipCloudRenderer?.chips?.mapNotNull { HomePage.Chip.fromChipCloudChipRenderer(it) }\n        Timber.d(\"home() chips: ${chips?.size ?: 0}\")\n        HomePage(chips, sections, continuation)\n    }\n\n    private suspend fun homeContinuation(continuation: String): Result<HomePage> = runCatching {\n        val response =\n            innerTube.browse(WEB_REMIX, continuation = continuation).body<BrowseResponse>()\n        val continuation =\n            response.continuationContents?.sectionListContinuation?.continuations?.getContinuation()\n        HomePage(\n            null,\n            response.continuationContents?.sectionListContinuation?.contents\n            ?.mapNotNull { it.musicCarouselShelfRenderer }\n            ?.mapNotNull {\n                HomePage.Section.fromMusicCarouselShelfRenderer(it)\n            }.orEmpty(), continuation\n        )\n    }\n\n    suspend fun explore(): Result<ExplorePage> = runCatching {\n        val response = innerTube.browse(WEB_REMIX, browseId = \"FEmusic_explore\").body<BrowseResponse>()\n        ExplorePage(\n            newReleaseAlbums = response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.find {\n                it.musicCarouselShelfRenderer?.header?.musicCarouselShelfBasicHeaderRenderer?.moreContentButton?.buttonRenderer?.navigationEndpoint?.browseEndpoint?.browseId == \"FEmusic_new_releases_albums\"\n            }?.musicCarouselShelfRenderer?.contents\n                ?.mapNotNull { it.musicTwoRowItemRenderer }\n                ?.mapNotNull(NewReleaseAlbumPage::fromMusicTwoRowItemRenderer).orEmpty(),\n            moodAndGenres = response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.find {\n                it.musicCarouselShelfRenderer?.header?.musicCarouselShelfBasicHeaderRenderer?.moreContentButton?.buttonRenderer?.navigationEndpoint?.browseEndpoint?.browseId == \"FEmusic_moods_and_genres\"\n            }?.musicCarouselShelfRenderer?.contents\n                ?.mapNotNull { it.musicNavigationButtonRenderer }\n                ?.mapNotNull(MoodAndGenres.Companion::fromMusicNavigationButtonRenderer)\n                .orEmpty()\n        )\n    }\n\n    suspend fun newReleaseAlbums(): Result<List<AlbumItem>> = runCatching {\n        val response = innerTube.browse(WEB_REMIX, browseId = \"FEmusic_new_releases_albums\").body<BrowseResponse>()\n        response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.gridRenderer?.items\n            ?.mapNotNull { it.musicTwoRowItemRenderer }\n            ?.mapNotNull(NewReleaseAlbumPage::fromMusicTwoRowItemRenderer)\n            .orEmpty()\n    }\n\n    suspend fun moodAndGenres(): Result<List<MoodAndGenres>> = runCatching {\n        val response = innerTube.browse(WEB_REMIX, browseId = \"FEmusic_moods_and_genres\").body<BrowseResponse>()\n        response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents!!\n            .mapNotNull(MoodAndGenres.Companion::fromSectionListRendererContent)\n    }\n\n    suspend fun browse(browseId: String, params: String?): Result<BrowseResult> = runCatching {\n        // Use authentication for library endpoints\n        val needsLogin = browseId.startsWith(\"FEmusic_library\") || browseId == \"VLSE\" || browseId == \"VLRDPN\"\n        val response = innerTube.browse(WEB_REMIX, browseId = browseId, params = params, setLogin = needsLogin).body<BrowseResponse>()\n        val sectionContents = response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents\n        BrowseResult(\n            title = response.header?.musicHeaderRenderer?.title?.runs?.firstOrNull()?.text,\n            items = sectionContents?.mapNotNull { content ->\n                when {\n                    content.gridRenderer != null -> {\n                        BrowseResult.Item(\n                            title = content.gridRenderer.header?.gridHeaderRenderer?.title?.runs?.firstOrNull()?.text,\n                            items = content.gridRenderer.items\n                                .mapNotNull(GridRenderer.Item::musicTwoRowItemRenderer)\n                                .mapNotNull { renderer ->\n                                    // Try LibraryPage first (more lenient for library endpoints), fall back to RelatedPage\n                                    LibraryPage.fromMusicTwoRowItemRenderer(renderer)\n                                        ?: RelatedPage.fromMusicTwoRowItemRenderer(renderer)\n                                }\n                        )\n                    }\n\n                    content.musicCarouselShelfRenderer != null -> {\n                        BrowseResult.Item(\n                            title = content.musicCarouselShelfRenderer.header?.musicCarouselShelfBasicHeaderRenderer?.title?.runs?.firstOrNull()?.text,\n                            items = content.musicCarouselShelfRenderer.contents\n                                .mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer)\n                                .mapNotNull { renderer ->\n                                    LibraryPage.fromMusicTwoRowItemRenderer(renderer)\n                                        ?: RelatedPage.fromMusicTwoRowItemRenderer(renderer)\n                                }\n                        )\n                    }\n\n                    content.musicShelfRenderer != null -> {\n                        BrowseResult.Item(\n                            title = content.musicShelfRenderer.title?.runs?.firstOrNull()?.text,\n                            items = content.musicShelfRenderer.contents\n                                ?.mapNotNull(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)\n                                ?.mapNotNull(LibraryPage.Companion::fromMusicResponsiveListItemRenderer)\n                                ?: emptyList()\n                        )\n                    }\n\n                    content.musicPlaylistShelfRenderer != null -> {\n                        BrowseResult.Item(\n                            title = null, // MusicPlaylistShelfRenderer doesn't have a title\n                            items = content.musicPlaylistShelfRenderer.contents.getItems()\n                                .mapNotNull(LibraryPage.Companion::fromMusicResponsiveListItemRenderer)\n                        )\n                    }\n\n                    else -> null\n                }\n            }.orEmpty()\n        )\n    }\n\n    suspend fun library(browseId: String, tabIndex: Int = 0): Result<LibraryPage> {\n        return runCatching {\n            val response = innerTube.browse(\n                client = WEB_REMIX,\n                browseId = browseId,\n                setLogin = true\n            ).body<BrowseResponse>()\n\n            val tabs = response.contents?.singleColumnBrowseResultsRenderer?.tabs\n            val contents = if (tabs != null && tabs.size > tabIndex) {\n                tabs[tabIndex].tabRenderer.content?.sectionListRenderer?.contents?.firstOrNull()\n            } else {\n                null\n            }\n\n            when {\n                contents?.gridRenderer != null -> {\n                    val gridItems = contents.gridRenderer.items\n                    val parsedItems = gridItems\n                        .mapNotNull(GridRenderer.Item::musicTwoRowItemRenderer)\n                        .mapNotNull { LibraryPage.fromMusicTwoRowItemRenderer(it) }\n                    LibraryPage(\n                        items = parsedItems,\n                        continuation = contents.gridRenderer.continuations?.getContinuation()\n                    )\n                }\n\n                else -> {\n                    val shelfContents = contents?.musicShelfRenderer?.contents\n                    if (shelfContents == null) {\n                        throw IllegalStateException(\"No content found for browseId=$browseId\")\n                    }\n                    val listItemRenderers = shelfContents.mapNotNull(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)\n                    val parsedItems = listItemRenderers.mapNotNull { renderer ->\n                        LibraryPage.fromMusicResponsiveListItemRenderer(renderer)\n                    }\n                    LibraryPage(\n                        items = parsedItems,\n                        continuation = contents.musicShelfRenderer.continuations?.getContinuation()\n                    )\n                }\n            }\n        }\n    }\n\n    suspend fun libraryContinuation(continuation: String) = runCatching {\n        val response = innerTube.browse(\n            client = WEB_REMIX,\n            continuation = continuation,\n            setLogin = true\n        ).body<BrowseResponse>()\n\n        val contents = response.continuationContents\n\n        when {\n            contents?.gridContinuation != null -> {\n                LibraryContinuationPage(\n                    items = contents.gridContinuation.items\n                        .mapNotNull (GridRenderer.Item::musicTwoRowItemRenderer)\n                        .mapNotNull { LibraryPage.fromMusicTwoRowItemRenderer(it) },\n                    continuation = contents.gridContinuation.continuations?.getContinuation()\n                )\n            }\n\n            else -> { // contents?.musicShelfContinuation != null\n                LibraryContinuationPage(\n                    items = contents?.musicShelfContinuation?.contents!!\n                        .mapNotNull (MusicShelfRenderer.Content::musicResponsiveListItemRenderer)\n                        .mapNotNull { LibraryPage.fromMusicResponsiveListItemRenderer(it) },\n                    continuation = contents.musicShelfContinuation.continuations?.getContinuation()\n                )\n            }\n        }\n    }\n\n    suspend fun libraryRecentActivity(): Result<LibraryPage> = runCatching {\n        val continuation = LibraryFilter.FILTER_RECENT_ACTIVITY.value\n\n        val response = innerTube.browse(\n            client = WEB_REMIX,\n            continuation = continuation,\n            setLogin = true\n        ).body<BrowseResponse>()\n\n        val gridItems = response.continuationContents?.sectionListContinuation?.contents?.firstOrNull()\n            ?.gridRenderer?.items\n        \n        if (gridItems == null) {\n            return@runCatching LibraryPage(\n                items = emptyList(),\n                continuation = null\n            )\n        }\n        \n        val items = gridItems.mapNotNull {\n            it.musicTwoRowItemRenderer?.let { renderer ->\n                LibraryPage.fromMusicTwoRowItemRenderer(renderer)\n            }\n        }.toMutableList()\n\n        /*\n         * We need to fetch the artist page when accessing the library because it allows to have\n         * a proper playEndpoint, which is needed to correctly report the playing indicator in\n         * the home page.\n         *\n         * Despite this, we need to use the old thumbnail because it's the proper format for a\n         * square picture, which is what we need.\n         */\n        items.forEachIndexed { index, item ->\n            if (item is ArtistItem) {\n                artist(item.id).getOrNull()?.artist?.let { fetchedArtist ->\n                    items[index] = fetchedArtist.copy(thumbnail = item.thumbnail)\n                }\n            }\n        }\n\n        LibraryPage(\n            items = items,\n            continuation = null\n        )\n    }\n\n    suspend fun getChartsPage(continuation: String? = null): Result<ChartsPage> = runCatching {\n        val response = innerTube.browse(\n            client = WEB_REMIX,\n            browseId = \"FEmusic_charts\",\n            params = \"ggMGCgQIgAQ%3D\",\n            continuation = continuation\n        ).body<BrowseResponse>()\n\n        val sections = mutableListOf<ChartsPage.ChartSection>()\n    \n        response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()\n            ?.tabRenderer?.content?.sectionListRenderer?.contents?.forEach { content ->\n            \n                content.musicCarouselShelfRenderer?.let { renderer ->\n                    val title = renderer.header?.musicCarouselShelfBasicHeaderRenderer?.title?.runs?.firstOrNull()?.text\n                        ?: return@forEach\n                \n                    val items = renderer.contents.mapNotNull { item ->\n                        when {\n                            item.musicResponsiveListItemRenderer != null -> \n                                convertToChartItem(item.musicResponsiveListItemRenderer)\n                            item.musicTwoRowItemRenderer != null -> \n                                convertMusicTwoRowItem(item.musicTwoRowItemRenderer)\n                            else -> null\n                        }\n                    }.filterNotNull()\n                \n                    if (items.isNotEmpty()) {\n                        sections.add(\n                            ChartsPage.ChartSection(\n                                title = title,\n                                items = items,\n                                chartType = determineChartType(title)\n                            )\n                        )\n                    }\n                }\n            \n                content.gridRenderer?.let { renderer ->\n                    val title = renderer.header?.gridHeaderRenderer?.title?.runs?.firstOrNull()?.text\n                        ?: return@let\n                \n                    val items = renderer.items.mapNotNull { item ->\n                        item.musicTwoRowItemRenderer?.let { renderer ->\n                            convertMusicTwoRowItem(renderer)\n                        }\n                    }.filterNotNull()\n                \n                    if (items.isNotEmpty()) {\n                        sections.add(\n                            ChartsPage.ChartSection(\n                                title = title,\n                                items = items,\n                                chartType = ChartsPage.ChartType.NEW_RELEASES\n                            )\n                        )\n                    }\n                }\n            }\n\n        ChartsPage(\n            sections = sections,\n            continuation = response.continuationContents?.sectionListContinuation?.continuations?.getContinuation()\n        )\n    }\n\n    private fun determineChartType(title: String): ChartsPage.ChartType {\n        return when {\n            title.contains(\"Trending\", ignoreCase = true) -> ChartsPage.ChartType.TRENDING\n            title.contains(\"Top\", ignoreCase = true) -> ChartsPage.ChartType.TOP\n            else -> ChartsPage.ChartType.GENRE\n        }\n    }\n\n    private fun convertToChartItem(renderer: MusicResponsiveListItemRenderer): YTItem? {\n        return try {\n            when {\n                renderer.flexColumns.size >= 3 && renderer.playlistItemData?.videoId != null -> {\n                    val firstColumn = renderer.flexColumns.getOrNull(0)\n                        ?.musicResponsiveListItemFlexColumnRenderer\n                        ?.text ?: return null\n                \n                    val secondColumn = renderer.flexColumns.getOrNull(1)\n                        ?.musicResponsiveListItemFlexColumnRenderer\n                        ?.text ?: return null\n\n                    val titleRun = firstColumn.runs?.firstOrNull() ?: return null\n                    val title = titleRun.text.takeIf { it.isNotBlank() } ?: return null\n\n                    val artists = secondColumn.runs?.mapNotNull { run ->\n                        run.text.takeIf { it.isNotBlank() }?.let { name ->\n                            Artist(\n                                name = name,\n                                id = run.navigationEndpoint?.browseEndpoint?.browseId\n                            )\n                        }\n                    } ?: emptyList()\n\n                    val thirdColumn = renderer.flexColumns.getOrNull(2)\n                        ?.musicResponsiveListItemFlexColumnRenderer\n                        ?.text\n\n                    SongItem(\n                        id = renderer.playlistItemData.videoId,\n                        title = title,\n                        artists = artists,\n                        thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        musicVideoType = renderer.musicVideoType,\n                        explicit = renderer.badges?.any { \n                            it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\" \n                        } == true,\n                        chartPosition = thirdColumn?.runs?.firstOrNull()?.text?.toIntOrNull(),\n                        chartChange = thirdColumn?.runs?.getOrNull(1)?.text\n                    )\n                }\n                else -> null\n            }\n        } catch (e: Exception) {\n            println(\"Error converting chart item: ${e.message}\\n${Json.encodeToString(renderer)}\")\n            null\n        }\n    }\n\n    private fun convertMusicTwoRowItem(renderer: MusicTwoRowItemRenderer): YTItem? {\n        return try {\n            when {\n                renderer.isSong -> {\n                    val subtitle = renderer.subtitle?.runs ?: return null\n                    SongItem(\n                        id = renderer.navigationEndpoint.watchEndpoint?.videoId ?: return null,\n                        title = renderer.title.runs?.firstOrNull()?.text ?: return null,\n                        artists = subtitle.mapNotNull {\n                            it.navigationEndpoint?.browseEndpoint?.browseId?.let { id ->\n                                Artist(name = it.text, id = id)\n                            }\n                        },\n                        thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        musicVideoType = renderer.musicVideoType,\n                        explicit = renderer.subtitleBadges?.any {\n                            it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                        } == true\n                    )\n                }\n                renderer.isAlbum -> {\n                    AlbumItem(\n                        browseId = renderer.navigationEndpoint.browseEndpoint?.browseId ?: return null,\n                        playlistId = renderer.thumbnailOverlay?.musicItemThumbnailOverlayRenderer?.content\n                            ?.musicPlayButtonRenderer?.playNavigationEndpoint\n                            ?.watchPlaylistEndpoint?.playlistId ?: return null,\n                        title = renderer.title.runs?.firstOrNull()?.text ?: return null,\n                        artists = renderer.subtitle?.runs?.oddElements()?.drop(1)?.mapNotNull {\n                            it.navigationEndpoint?.browseEndpoint?.browseId?.let { id ->\n                                Artist(name = it.text, id = id)\n                            }\n                        },\n                        year = renderer.subtitle?.runs?.lastOrNull()?.text?.toIntOrNull(),\n                        thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        explicit = renderer.subtitleBadges?.any {\n                            it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                        } == true\n                    )\n                }\n                else -> null\n            }\n        } catch (e: Exception) {\n            println(\"Error converting two row item: ${e.message}\\n${Json.encodeToString(renderer)}\")\n            null\n        }\n    }\n\n    suspend fun musicHistory() = runCatching {\n        val response = innerTube.browse(\n            client = WEB_REMIX,\n            browseId = \"FEmusic_history\",\n            setLogin = true\n        ).body<BrowseResponse>()\n\n        HistoryPage(\n            sections = response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()\n                ?.tabRenderer?.content?.sectionListRenderer?.contents\n                ?.mapNotNull {\n                    it.musicShelfRenderer?.let { musicShelfRenderer ->\n                        HistoryPage.fromMusicShelfRenderer(musicShelfRenderer)\n                    }\n                }\n        )\n    }\n\n    /**\n     * Fetch podcast discovery/recommendations page.\n     * Returns sections like \"Popular shows\", \"Popular episodes\", category sections.\n     */\n    suspend fun podcastDiscover(): Result<HomePage> = runCatching {\n        val response = innerTube.browse(\n            client = WEB_REMIX,\n            browseId = \"FEmusic_non_music_audio\",\n            setLogin = true\n        ).body<BrowseResponse>()\n\n        val sectionListRenderer = response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()\n            ?.tabRenderer?.content?.sectionListRenderer\n        val carousels = sectionListRenderer?.contents?.mapNotNull { it.musicCarouselShelfRenderer } ?: emptyList()\n        val sections = carousels.mapNotNull {\n            HomePage.Section.fromMusicCarouselShelfRenderer(it)\n        }\n        val chips = sectionListRenderer?.header?.chipCloudRenderer?.chips?.mapNotNull {\n            HomePage.Chip.fromChipCloudChipRenderer(it)\n        }\n        val continuation = sectionListRenderer?.continuations?.getContinuation()\n\n        HomePage(chips, sections, continuation)\n    }\n\n    suspend fun likeVideo(videoId: String, like: Boolean) = runCatching {\n        if (like)\n            innerTube.likeVideo(WEB_REMIX, videoId)\n        else\n            innerTube.unlikeVideo(WEB_REMIX, videoId)\n    }\n\n    suspend fun likePlaylist(playlistId: String, like: Boolean) = runCatching {\n        if (like)\n            innerTube.likePlaylist(WEB_REMIX, playlistId)\n        else\n            innerTube.unlikePlaylist(WEB_REMIX, playlistId)\n    }\n\n    suspend fun subscribeChannel(channelId: String, subscribe: Boolean, params: String? = null) = runCatching {\n        // Default params from YouTube Music API - required for subscription to work\n        val subscribeParams = params ?: \"EgIIAhgA\"\n        if (subscribe)\n            innerTube.subscribeChannel(WEB_REMIX, channelId, subscribeParams)\n        else\n            innerTube.unsubscribeChannel(WEB_REMIX, channelId, subscribeParams)\n    }\n\n    /**\n     * Save a podcast show to library.\n     * Uses likePlaylist API. Podcast IDs are \"MPSP<playlistId>\".\n     */\n    suspend fun savePodcast(podcastId: String, save: Boolean) = runCatching {\n        val playlistId = podcastId.removePrefix(\"MPSP\")\n        Timber.d(\"[PODCAST_API] savePodcast: podcastId=$podcastId, playlistId=$playlistId, save=$save\")\n        if (save)\n            innerTube.likePlaylist(WEB_REMIX, playlistId)\n        else\n            innerTube.unlikePlaylist(WEB_REMIX, playlistId)\n    }\n\n    /**\n     * Add episode to \"Episodes for Later\" playlist (SE).\n     */\n    suspend fun addEpisodeToSavedEpisodes(videoId: String) = runCatching {\n        innerTube.addToPlaylist(WEB_REMIX, \"SE\", videoId)\n    }\n\n    /**\n     * Remove episode from \"Episodes for Later\" playlist (SE).\n     * Note: setVideoId is required for removal and must be obtained from the playlist response.\n     */\n    suspend fun removeEpisodeFromSavedEpisodes(videoId: String, setVideoId: String) = runCatching {\n        innerTube.removeFromPlaylist(WEB_REMIX, \"SE\", videoId, setVideoId)\n    }\n\n    suspend fun libraryPodcastChannels(): Result<LibraryPage> {\n        Timber.d(\"[PODCAST_API] libraryPodcastChannels: calling browse with FEmusic_library_non_music_audio_channels_list\")\n        return runCatching {\n            val response = innerTube.browse(\n                client = WEB_REMIX,\n                browseId = \"FEmusic_library_non_music_audio_channels_list\",\n                setLogin = true\n            ).body<BrowseResponse>()\n\n            val contentList = response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()\n                ?.tabRenderer?.content?.sectionListRenderer?.contents ?: emptyList()\n\n            val items = contentList.flatMap { content ->\n                when {\n                    content.gridRenderer != null -> {\n                        content.gridRenderer.items\n                            .mapNotNull(GridRenderer.Item::musicTwoRowItemRenderer)\n                            .mapNotNull { LibraryPage.fromMusicTwoRowItemRenderer(it) }\n                    }\n                    content.musicShelfRenderer != null -> {\n                        content.musicShelfRenderer.contents\n                            ?.mapNotNull(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)\n                            ?.mapNotNull { LibraryPage.fromMusicResponsiveListItemRenderer(it) }\n                            ?: emptyList()\n                    }\n                    content.musicCarouselShelfRenderer != null -> {\n                        content.musicCarouselShelfRenderer.contents\n                            .mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer)\n                            .mapNotNull { LibraryPage.fromMusicTwoRowItemRenderer(it) }\n                    }\n                    else -> emptyList()\n                }\n            }\n\n            LibraryPage(\n                items = items,\n                continuation = null\n            )\n        }.also { result ->\n            result.onFailure { e -> Timber.e(e, \"[PODCAST_API] libraryPodcastChannels FAILED\") }\n            result.onSuccess { Timber.d(\"[PODCAST_API] libraryPodcastChannels SUCCESS: ${it.items.size} items\") }\n        }\n    }\n\n    suspend fun libraryPodcastEpisodes(): Result<LibraryPage> {\n        Timber.d(\"[PODCAST_API] libraryPodcastEpisodes: calling browse with FEmusic_library_non_music_audio_list\")\n        return runCatching {\n            val response = innerTube.browse(\n                client = WEB_REMIX,\n                browseId = \"FEmusic_library_non_music_audio_list\",\n                setLogin = true\n            ).body<BrowseResponse>()\n\n            val contents = response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()\n                ?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()\n\n            val items = when {\n                contents?.gridRenderer != null -> {\n                    contents.gridRenderer.items\n                        .mapNotNull(GridRenderer.Item::musicTwoRowItemRenderer)\n                        .mapNotNull { LibraryPage.fromMusicTwoRowItemRenderer(it) }\n                }\n                contents?.musicShelfRenderer != null -> {\n                    contents.musicShelfRenderer.contents\n                        ?.mapNotNull(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)\n                        ?.mapNotNull { LibraryPage.fromMusicResponsiveListItemRenderer(it) }\n                        ?: emptyList()\n                }\n                else -> emptyList()\n            }\n\n            LibraryPage(\n                items = items,\n                continuation = null\n            )\n        }.also { result ->\n            result.onFailure { e -> Timber.e(e, \"[PODCAST_API] libraryPodcastEpisodes FAILED\") }\n            result.onSuccess { Timber.d(\"[PODCAST_API] libraryPodcastEpisodes SUCCESS: ${it.items.size} items\") }\n        }\n    }\n\n    /**\n     * Fetch saved podcast shows from library.\n     * Uses FEmusic_library_non_music_audio_list and filters to only PodcastItem.\n     */\n    suspend fun savedPodcastShows(): Result<List<PodcastItem>> = runCatching {\n        val libraryPage = libraryPodcastEpisodes().getOrThrow()\n        libraryPage.items.filterIsInstance<PodcastItem>()\n    }\n\n    /**\n     * Fetch \"New Episodes\" auto-playlist (VLRDPN).\n     * Returns new episodes from saved/subscribed podcasts.\n     */\n    suspend fun newEpisodes(): Result<List<SongItem>> {\n        Timber.d(\"[PODCAST_API] newEpisodes: calling browse with VLRDPN\")\n        return runCatching {\n            val response = innerTube.browse(\n                client = WEB_REMIX,\n                browseId = \"VLRDPN\",\n                setLogin = true\n            ).body<BrowseResponse>()\n\n            response.contents?.twoColumnBrowseResultsRenderer?.secondaryContents?.sectionListRenderer\n                ?.contents?.firstOrNull()?.musicShelfRenderer?.contents\n                ?.mapNotNull { it.musicMultiRowListItemRenderer }\n                ?.map { renderer ->\n                    SongItem(\n                        id = renderer.onTap?.watchEndpoint?.videoId ?: \"\",\n                        title = renderer.title?.runs?.firstOrNull()?.text ?: \"\",\n                        artists = renderer.subtitle?.runs?.mapNotNull { run ->\n                            run.navigationEndpoint?.browseEndpoint?.let { endpoint ->\n                                Artist(name = run.text, id = endpoint.browseId)\n                            }\n                        } ?: emptyList(),\n                        album = null,\n                        duration = null,\n                        thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: \"\",\n                        isEpisode = true,\n                    )\n                } ?: emptyList()\n        }.also { result ->\n            result.onFailure { e -> Timber.e(e, \"[PODCAST_API] newEpisodes FAILED\") }\n            result.onSuccess { Timber.d(\"[PODCAST_API] newEpisodes SUCCESS: ${it.size} items\") }\n        }\n    }\n\n    /**\n     * Fetch the RDPN \"New Episodes\" playlist info (title + thumbnail).\n     * Uses the same VLRDPN browse call as [newEpisodes] but parses the header instead.\n     * Falls back to the first episode thumbnail if no header thumbnail is found.\n     */\n    suspend fun newEpisodesPlaylistInfo(): Result<PlaylistItem> = runCatching {\n        val response = innerTube.browse(\n            client = WEB_REMIX,\n            browseId = \"VLRDPN\",\n            setLogin = true\n        ).body<BrowseResponse>()\n\n        // Try all known header renderers in priority order\n        val thumbnail: String? =\n            response.header?.musicImmersiveHeaderRenderer?.thumbnail\n                ?.musicThumbnailRenderer?.getThumbnailUrl()\n                ?: response.header?.musicVisualHeaderRenderer?.thumbnail\n                    ?.musicThumbnailRenderer?.getThumbnailUrl()\n                ?: response.header?.musicDetailHeaderRenderer?.thumbnail\n                    ?.croppedSquareThumbnailRenderer?.thumbnail?.thumbnails?.lastOrNull()?.url\n                // Fall back: thumbnail of the first episode in the list\n                ?: response.contents?.twoColumnBrowseResultsRenderer?.secondaryContents\n                    ?.sectionListRenderer?.contents?.firstOrNull()\n                    ?.musicShelfRenderer?.contents?.firstOrNull()\n                    ?.musicMultiRowListItemRenderer?.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl()\n\n        val title = response.header?.musicImmersiveHeaderRenderer?.title?.runs\n            ?.joinToString(\"\") { it.text }\n            ?: response.header?.musicVisualHeaderRenderer?.title?.runs\n                ?.joinToString(\"\") { it.text }\n            ?: \"New Episodes\"\n\n        PlaylistItem(\n            id = \"RDPN\",\n            title = title,\n            author = null,\n            songCountText = null,\n            thumbnail = thumbnail,\n            playEndpoint = null,\n            shuffleEndpoint = null,\n            radioEndpoint = null,\n        )\n    }\n\n    /**\n     * Fetch \"Episodes for Later\" playlist (VLSE).\n     * Returns manually saved episodes.\n     */\n    suspend fun episodesForLater(): Result<List<SongItem>> = runCatching {\n        Timber.d(\"[PODCAST_API] episodesForLater: calling browse with VLSE\")\n        val response = innerTube.browse(\n            client = WEB_REMIX,\n            browseId = \"VLSE\",\n            setLogin = true\n        ).body<BrowseResponse>()\n\n        // VLSE uses musicPlaylistShelfRenderer, not musicShelfRenderer\n        val contents = response.contents?.twoColumnBrowseResultsRenderer?.secondaryContents?.sectionListRenderer\n            ?.contents?.firstOrNull()\n\n        val shelfContents = contents?.musicPlaylistShelfRenderer?.contents\n            ?: contents?.musicShelfRenderer?.contents\n\n        // Parse musicResponsiveListItemRenderer (standard playlist format)\n        shelfContents?.mapNotNull { it.musicResponsiveListItemRenderer }\n            ?.mapNotNull { renderer ->\n                val videoId = renderer.playlistItemData?.videoId ?: return@mapNotNull null\n                val setVideoId = renderer.playlistItemData.playlistSetVideoId\n                val title = renderer.flexColumns.firstOrNull()\n                    ?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()?.text\n                    ?: return@mapNotNull null\n                val artistRun = renderer.flexColumns.getOrNull(1)\n                    ?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()\n                SongItem(\n                    id = videoId,\n                    title = title,\n                    artists = artistRun?.let { listOf(Artist(name = it.text, id = it.navigationEndpoint?.browseEndpoint?.browseId)) } ?: emptyList(),\n                    album = null,\n                    duration = null,\n                    thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: \"\",\n                    setVideoId = setVideoId,\n                    isEpisode = true,\n                )\n            } ?: emptyList()\n    }\n\n    /**\n     * Fetch \"Continue Listening\" / Resume Playback.\n     * Returns partially played episodes for resumption.\n     */\n    suspend fun continueListening(): Result<List<SongItem>> = runCatching {\n        val response = innerTube.browse(\n            client = WEB_REMIX,\n            browseId = \"FEmusic_listening_review\",\n            setLogin = true\n        ).body<BrowseResponse>()\n\n        response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()\n            ?.tabRenderer?.content?.sectionListRenderer?.contents\n            ?.flatMap { section ->\n                section.musicShelfRenderer?.contents?.mapNotNull { content ->\n                    content.musicResponsiveListItemRenderer?.let { renderer ->\n                        val videoId = renderer.playlistItemData?.videoId ?: return@mapNotNull null\n                        val title = renderer.flexColumns.firstOrNull()\n                            ?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()?.text\n                            ?: return@mapNotNull null\n                        val artistRun = renderer.flexColumns.getOrNull(1)\n                            ?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()\n                        SongItem(\n                            id = videoId,\n                            title = title,\n                            artists = artistRun?.let { listOf(Artist(name = it.text, id = it.navigationEndpoint?.browseEndpoint?.browseId)) } ?: emptyList(),\n                            album = null,\n                            duration = null,\n                            thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: \"\",\n                            isEpisode = true,\n                        )\n                    }\n                } ?: emptyList()\n            } ?: emptyList()\n    }\n\n    suspend fun getChannelId(browseId: String): String {\n        artist(browseId).onSuccess {\n            return it.artist.channelId ?: \"\"\n        }\n        return \"\"\n    }\n\n    suspend fun addToPlaylist(playlistId: String, videoId: String) = runCatching {\n        innerTube.addToPlaylist(WEB_REMIX, playlistId, videoId)\n    }\n\n    suspend fun addPlaylistToPlaylist(playlistId: String, addPlaylistId: String) = runCatching {\n        innerTube.addPlaylistToPlaylist(WEB_REMIX, playlistId, addPlaylistId)\n    }\n\n    suspend fun removeFromPlaylist(playlistId: String, videoId: String, setVideoId: String) = runCatching {\n        innerTube.removeFromPlaylist(WEB_REMIX, playlistId, videoId, setVideoId)\n    }\n\n    suspend fun moveSongPlaylist(playlistId: String, setVideoId: String, successorSetVideoId: String?) = runCatching {\n        innerTube.moveSongPlaylist(WEB_REMIX, playlistId, setVideoId, successorSetVideoId)\n    }\n\n    fun createPlaylist(title: String) = runBlocking {\n        innerTube.createPlaylist(WEB_REMIX, title).body<CreatePlaylistResponse>().playlistId\n    }\n\n    suspend fun renamePlaylist(playlistId: String, name: String) = runCatching {\n        innerTube.renamePlaylist(WEB_REMIX, playlistId, name)\n    }\n\n    suspend fun uploadCustomThumbnailLink(playlistId: String, image: ByteArray) = runCatching {\n        val uploadUrl = innerTube.getUploadCustomThumbnailLink(WEB_REMIX, image.size).headers[\"x-guploader-uploadid\"]\n        val blobReq = innerTube.uploadCustomThumbnail(\n            WEB_REMIX,\n            uploadUrl!!,\n            image\n        )\n        val blobId = Json.decodeFromString<ImageUploadResponse>(blobReq.bodyAsText()).encryptedBlobId\n        innerTube.setThumbnailPlaylist(WEB_REMIX, playlistId, blobId).body<EditPlaylistResponse>().newHeader?.musicEditablePlaylistDetailHeaderRenderer?.header?.musicResponsiveHeaderRenderer?.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl()\n    }\n\n    suspend fun removeThumbnailPlaylist(playlistId: String) = runCatching {\n        innerTube.removeThumbnailPlaylist(WEB_REMIX, playlistId).body<EditPlaylistResponse>().newHeader?.musicEditablePlaylistDetailHeaderRenderer?.header?.musicResponsiveHeaderRenderer?.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl()\n    }\n\n    suspend fun deletePlaylist(playlistId: String) = runCatching {\n        innerTube.deletePlaylist(WEB_REMIX, playlistId)\n    }\n\n    suspend fun player(videoId: String, playlistId: String? = null, client: YouTubeClient, signatureTimestamp: Int? = null, poToken: String? = null): Result<PlayerResponse> = runCatching {\n        innerTube.player(client, videoId, playlistId, signatureTimestamp, poToken).body<PlayerResponse>()\n    }\n\n    suspend fun registerPlayback(playlistId: String? = null, playbackTracking: String) = runCatching {\n        val cpn = (1..16).map {\n            \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_\"[Random.Default.nextInt(\n                0,\n                64\n            )]\n        }.joinToString(\"\")\n\n        val playbackUrl = playbackTracking.replace(\n            \"https://s.youtube.com\",\n            \"https://music.youtube.com\",\n        )\n\n        innerTube.registerPlayback(\n            url = playbackUrl,\n            playlistId = playlistId,\n            cpn = cpn\n        )\n    }\n\n    suspend fun next(endpoint: WatchEndpoint, continuation: String? = null): Result<NextResult> = runCatching {\n        val response = innerTube.next(\n            WEB_REMIX,\n            endpoint.videoId,\n            endpoint.playlistId,\n            endpoint.playlistSetVideoId,\n            endpoint.index,\n            endpoint.params,\n            continuation).body<NextResponse>()\n        val playlistPanelRenderer = response.continuationContents?.playlistPanelContinuation\n            ?: response.contents.singleColumnMusicWatchNextResultsRenderer?.tabbedRenderer\n                ?.watchNextTabbedResultsRenderer?.tabs?.get(0)?.tabRenderer?.content?.musicQueueRenderer\n                ?.content?.playlistPanelRenderer!!\n        val title = response.contents.singleColumnMusicWatchNextResultsRenderer?.tabbedRenderer\n            ?.watchNextTabbedResultsRenderer?.tabs?.get(0)?.tabRenderer?.content?.musicQueueRenderer\n            ?.header?.musicQueueHeaderRenderer?.subtitle?.runs?.firstOrNull()?.text\n        val items = playlistPanelRenderer.contents.mapNotNull { content ->\n            content.playlistPanelVideoRenderer\n                ?.let(NextPage::fromPlaylistPanelVideoRenderer)\n                ?.let { it to content.playlistPanelVideoRenderer.selected }\n        }\n        val songs = items.map { it.first }\n        val currentIndex = items.indexOfFirst { it.second }.takeIf { it != -1 }\n\n        // load automix items\n        playlistPanelRenderer.contents.lastOrNull()?.automixPreviewVideoRenderer?.content?.automixPlaylistVideoRenderer?.navigationEndpoint?.watchPlaylistEndpoint?.let { watchPlaylistEndpoint ->\n            return@runCatching next(watchPlaylistEndpoint).getOrThrow().let { result ->\n                result.copy(\n                    title = title,\n                    items = songs + result.items,\n                    lyricsEndpoint = response.contents.singleColumnMusicWatchNextResultsRenderer?.tabbedRenderer?.watchNextTabbedResultsRenderer?.tabs?.getOrNull(1)?.tabRenderer?.endpoint?.browseEndpoint,\n                    relatedEndpoint = response.contents.singleColumnMusicWatchNextResultsRenderer?.tabbedRenderer?.watchNextTabbedResultsRenderer?.tabs?.getOrNull(2)?.tabRenderer?.endpoint?.browseEndpoint,\n                    currentIndex = currentIndex,\n                    endpoint = watchPlaylistEndpoint\n                )\n            }\n        }\n        NextResult(\n            title = title,\n            items = songs,\n            currentIndex = currentIndex,\n            lyricsEndpoint = response.contents.singleColumnMusicWatchNextResultsRenderer?.tabbedRenderer?.watchNextTabbedResultsRenderer?.tabs?.getOrNull(1)?.tabRenderer?.endpoint?.browseEndpoint,\n            relatedEndpoint = response.contents.singleColumnMusicWatchNextResultsRenderer?.tabbedRenderer?.watchNextTabbedResultsRenderer?.tabs?.getOrNull(2)?.tabRenderer?.endpoint?.browseEndpoint,\n            continuation = playlistPanelRenderer.continuations?.getContinuation(),\n            endpoint = endpoint\n        )\n    }\n\n    suspend fun lyrics(endpoint: BrowseEndpoint): Result<String?> = runCatching {\n        val response = innerTube.browse(WEB_REMIX, endpoint.browseId, endpoint.params).body<BrowseResponse>()\n        response.contents?.sectionListRenderer?.contents\n            ?.firstOrNull { it.musicDescriptionShelfRenderer != null }\n            ?.musicDescriptionShelfRenderer?.description?.runs\n            ?.joinToString(separator = \"\") { it.text }\n    }\n\n    suspend fun related(endpoint: BrowseEndpoint): Result<RelatedPage> = runCatching {\n        val response = innerTube.browse(WEB_REMIX, endpoint.browseId).body<BrowseResponse>()\n        val songs = mutableListOf<SongItem>()\n        val albums = mutableListOf<AlbumItem>()\n        val artists = mutableListOf<ArtistItem>()\n        val playlists = mutableListOf<PlaylistItem>()\n        response.contents?.sectionListRenderer?.contents?.forEach { sectionContent ->\n            sectionContent.musicCarouselShelfRenderer?.contents?.forEach { content ->\n                when (val item = content.musicResponsiveListItemRenderer?.let(RelatedPage.Companion::fromMusicResponsiveListItemRenderer)\n                    ?: content.musicTwoRowItemRenderer?.let(RelatedPage.Companion::fromMusicTwoRowItemRenderer)) {\n                    is SongItem -> if (content.musicResponsiveListItemRenderer?.overlay\n                            ?.musicItemThumbnailOverlayRenderer?.content\n                            ?.musicPlayButtonRenderer?.playNavigationEndpoint\n                            ?.watchEndpoint?.watchEndpointMusicSupportedConfigs\n                            ?.watchEndpointMusicConfig?.musicVideoType == MUSIC_VIDEO_TYPE_ATV\n                    ) songs.add(item)\n\n                    is AlbumItem -> albums.add(item)\n                    is ArtistItem -> artists.add(item)\n                    is PlaylistItem -> playlists.add(item)\n                    is PodcastItem, is EpisodeItem -> {}\n                    null -> {}\n                }\n            }\n        }\n        RelatedPage(songs, albums, artists, playlists)\n    }\n\n    suspend fun queue(videoIds: List<String>? = null, playlistId: String? = null): Result<List<SongItem>> = runCatching {\n        if (videoIds != null) {\n            assert(videoIds.size <= MAX_GET_QUEUE_SIZE) // Max video limit\n        }\n        innerTube.getQueue(WEB_REMIX, videoIds, playlistId).body<GetQueueResponse>().queueDatas\n            .mapNotNull {\n                it.content.playlistPanelVideoRenderer?.let { renderer ->\n                    NextPage.fromPlaylistPanelVideoRenderer(renderer)\n                }\n            }\n    }\n\n    suspend fun transcript(videoId: String): Result<String> = runCatching {\n        val response = innerTube.getTranscript(WEB, videoId).body<GetTranscriptResponse>()\n        response.actions?.firstOrNull()?.updateEngagementPanelAction?.content?.transcriptRenderer?.body?.transcriptBodyRenderer?.cueGroups?.joinToString(separator = \"\\n\") { group ->\n            val time = group.transcriptCueGroupRenderer.cues[0].transcriptCueRenderer.startOffsetMs\n            val text = group.transcriptCueGroupRenderer.cues[0].transcriptCueRenderer.cue.simpleText\n                .trim('♪')\n                .trim(' ')\n            \"[%02d:%02d.%03d]$text\".format(time / 60000, (time / 1000) % 60, time % 1000)\n        }!!\n    }\n\n    suspend fun visitorData(): Result<String> = runCatching {\n        Json.parseToJsonElement(innerTube.getSwJsData().bodyAsText().substring(5))\n            .jsonArray[0]\n            .jsonArray[2]\n            .jsonArray.first {\n                (it as? JsonPrimitive)?.contentOrNull?.let { candidate ->\n                    VISITOR_DATA_REGEX.containsMatchIn(candidate)\n                } ?: false\n            }\n            .jsonPrimitive.content\n    }\n\n    suspend fun accountInfo(): Result<AccountInfo> = runCatching {\n        innerTube.accountMenu(WEB_REMIX).body<AccountMenuResponse>()\n            .actions[0].openPopupAction.popup.multiPageMenuRenderer\n            .header?.activeAccountHeaderRenderer\n            ?.toAccountInfo()!!\n    }\n\n    suspend fun feedback(tokens: List<String>): Result<Boolean> = runCatching {\n        innerTube.feedback(WEB_REMIX, tokens).body<FeedbackResponse>().feedbackResponses.all { it.isProcessed }\n    }\n\n    /**\n     * Add a song to library by fetching fresh feedback tokens from the next endpoint\n     * This is more reliable than using cached tokens which might be stale\n     */\n    suspend fun addSongToLibrary(videoId: String): Result<Boolean> = runCatching {\n        // Get fresh song data with menu tokens using next endpoint\n        val nextResult = next(WatchEndpoint(videoId = videoId)).getOrThrow()\n        val song = nextResult.items.find { it.id == videoId }\n            ?: throw Exception(\"Song not found in next response\")\n        \n        val addToken = song.libraryAddToken\n            ?: throw Exception(\"Add to library token not available\")\n        \n        feedback(listOf(addToken)).getOrThrow()\n    }\n\n    /**\n     * Remove a song from library by fetching fresh feedback tokens from the next endpoint\n     */\n    suspend fun removeSongFromLibrary(videoId: String): Result<Boolean> = runCatching {\n        // Get fresh song data with menu tokens using next endpoint\n        val nextResult = next(WatchEndpoint(videoId = videoId)).getOrThrow()\n        val song = nextResult.items.find { it.id == videoId }\n            ?: throw Exception(\"Song not found in next response\")\n        \n        val removeToken = song.libraryRemoveToken\n            ?: throw Exception(\"Remove from library token not available\")\n        \n        feedback(listOf(removeToken)).getOrThrow()\n    }\n\n    /**\n     * Toggle song library status - adds if not in library, removes if in library\n     * Uses fresh tokens fetched from the API for reliability\n     */\n    suspend fun toggleSongLibrary(videoId: String, addToLibrary: Boolean): Result<Boolean> = runCatching {\n        if (addToLibrary) {\n            addSongToLibrary(videoId).getOrThrow()\n        } else {\n            removeSongFromLibrary(videoId).getOrThrow()\n        }\n    }\n\n    suspend fun getMediaInfo(videoId: String): Result<MediaInfo> = runCatching {\n        return innerTube.getMediaInfo(videoId)\n    }\n\n    suspend fun getTasteProfile(): Result<TasteProfile> = runCatching {\n        // Browse the taste builder page\n        // Note: Full parsing requires additional model support for musicTastebuilderShelfRenderer\n        // This returns an empty profile for now - can be enhanced when models are added\n        innerTube.browse(\n            client = WEB_REMIX,\n            browseId = \"FEmusic_tastebuilder\",\n            setLogin = true\n        ).body<BrowseResponse>()\n\n        TasteProfile(artists = emptyMap())\n    }\n\n    suspend fun setTasteProfile(selectedArtists: List<String>, allArtists: Map<String, TasteArtist>): Result<Unit> = runCatching {\n        val selectedValues = selectedArtists.mapNotNull { allArtists[it]?.selectionValue }\n        val impressionValues = allArtists.values.map { it.impressionValue }\n\n        if (selectedValues.isNotEmpty()) {\n            feedback(selectedValues + impressionValues).getOrThrow()\n        }\n    }\n\n    suspend fun removeHistoryItems(feedbackTokens: List<String>): Result<Boolean> = runCatching {\n        feedback(feedbackTokens).getOrThrow()\n    }\n\n    @JvmInline\n    value class SearchFilter(val value: String) {\n        companion object {\n            val FILTER_SONG = SearchFilter(\"EgWKAQIIAWoKEAkQBRAKEAMQBA%3D%3D\")\n            val FILTER_VIDEO = SearchFilter(\"EgWKAQIQAWoKEAkQChAFEAMQBA%3D%3D\")\n            val FILTER_ALBUM = SearchFilter(\"EgWKAQIYAWoKEAkQChAFEAMQBA%3D%3D\")\n            val FILTER_ARTIST = SearchFilter(\"EgWKAQIgAWoKEAkQChAFEAMQBA%3D%3D\")\n            val FILTER_FEATURED_PLAYLIST = SearchFilter(\"EgeKAQQoADgBagwQDhAKEAMQBRAJEAQ%3D\")\n            val FILTER_COMMUNITY_PLAYLIST = SearchFilter(\"EgeKAQQoAEABagoQAxAEEAoQCRAF\")\n            val FILTER_PODCAST = SearchFilter(\"EgWKAQJQAWoKEAkQChAFEAMQBA%3D%3D\")\n            val FILTER_EPISODE = SearchFilter(\"EgWKAQJYAWoKEAkQChAFEAMQBA%3D%3D\")\n            val FILTER_PROFILE = SearchFilter(\"EgWKAQJYAWoSEAUQCRADEAQQEBAVEAoQDhAR\")\n        }\n    }\n\n    @JvmInline\n    value class LibraryFilter(val value: String) {\n        companion object {\n            val FILTER_RECENT_ACTIVITY = LibraryFilter(\"4qmFsgIrEhdGRW11c2ljX2xpYnJhcnlfbGFuZGluZxoQZ2dNR0tnUUlCaEFCb0FZQg%3D%3D\")\n            val FILTER_RECENTLY_PLAYED = LibraryFilter(\"4qmFsgIrEhdGRW11c2ljX2xpYnJhcnlfbGFuZGluZxoQZ2dNR0tnUUlCUkFCb0FZQg%3D%3D\")\n            val FILTER_PLAYLISTS_ALPHABETICAL = LibraryFilter(\"4qmFsgIrEhdGRW11c2ljX2xpa2VkX3BsYXlsaXN0cxoQZ2dNR0tnUUlBUkFBb0FZQg%3D%3D\")\n            val FILTER_PLAYLISTS_RECENTLY_SAVED = LibraryFilter(\"4qmFsgIrEhdGRW11c2ljX2xpa2VkX3BsYXlsaXN0cxoQZ2dNR0tnUUlBQkFCb0FZQg%3D%3D\")\n        }\n    }\n\n    const val MAX_GET_QUEUE_SIZE = 1000\n\n    private val VISITOR_DATA_REGEX = Regex(\"^Cg[t|s]\")\n\n    fun getNewPipeStreamUrls(videoId: String): List<Pair<Int, String>> {\n        return NewPipeExtractor.newPipePlayer(videoId)\n    }\n\n    suspend fun newPipePlayer(\n        videoId: String,\n        tempRes: PlayerResponse,\n    ): PlayerResponse? {\n        if (tempRes.playabilityStatus.status != \"OK\") {\n            return null\n        }\n\n        val streamsList = getNewPipeStreamUrls(videoId)\n        if (streamsList.isEmpty()) return null\n\n        val decodedSigResponse = tempRes.copy(\n            streamingData = tempRes.streamingData?.copy(\n                formats = tempRes.streamingData.formats?.map { format ->\n                    format.copy(\n                        url = streamsList.find { it.first == format.itag }?.second ?: format.url,\n                    )\n                },\n                adaptiveFormats = tempRes.streamingData.adaptiveFormats.map { adaptiveFormat ->\n                    adaptiveFormat.copy(\n                        url = streamsList.find { it.first == adaptiveFormat.itag }?.second ?: adaptiveFormat.url,\n                    )\n                },\n            ),\n        )\n\n        val urlList = (\n            decodedSigResponse.streamingData?.adaptiveFormats?.mapNotNull { it.url }?.toMutableList() ?: mutableListOf()\n        ).apply {\n            decodedSigResponse.streamingData?.formats?.mapNotNull { it.url }?.let { addAll(it) }\n        }\n\n        return if (urlList.isNotEmpty()) {\n            decodedSigResponse\n        } else {\n            null\n        }\n    }\n\n    /**\n     * Upload a song to YouTube Music.\n     * @param filename The name of the file\n     * @param data The file data as ByteArray\n     * @param onProgress Callback for upload progress (0.0 to 1.0)\n     * @return true if upload succeeded\n     */\n    suspend fun uploadSong(\n        filename: String,\n        data: ByteArray,\n        onProgress: ((Float) -> Unit)? = null\n    ): Result<Boolean> = runCatching {\n        onProgress?.invoke(0f)\n\n        // Step 1: Initialize upload (5% of progress)\n        val initResponse = innerTube.initSongUpload(filename, data.size.toLong())\n        val uploadUrl = initResponse.headers[\"X-Goog-Upload-URL\"]\n            ?: throw Exception(\"Failed to get upload URL\")\n\n        onProgress?.invoke(0.05f)\n\n        // Step 2: Upload file data (5% to 100% of progress)\n        val uploadResponse = innerTube.uploadSongData(\n            uploadUrl = uploadUrl,\n            data = data,\n            onProgress = { uploadProgress ->\n                // Map upload progress (0-1) to overall progress (0.05-1.0)\n                onProgress?.invoke(0.05f + uploadProgress * 0.95f)\n            }\n        )\n\n        val status = uploadResponse.headers[\"X-Goog-Upload-Status\"]\n        status == \"final\"\n    }\n\n    /**\n     * Delete an uploaded song from YouTube Music library.\n     * @param entityId The entity ID of the uploaded song (typically the video ID)\n     * @return true if deletion succeeded\n     */\n    suspend fun deleteUploadedSong(entityId: String): Result<Boolean> = runCatching {\n        innerTube.deletePrivatelyOwnedEntity(entityId)\n        true\n    }\n\n    /**\n     * Supported file types for upload\n     */\n    val SUPPORTED_UPLOAD_TYPES = listOf(\"mp3\", \"m4a\", \"wma\", \"flac\", \"ogg\")\n\n    /**\n     * Maximum file size for upload (300MB)\n     */\n    const val MAX_UPLOAD_SIZE = 314572800L\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/YouTubeConstants.kt",
    "content": "package com.metrolist.innertube\n\nobject YouTubeConstants {\n    const val DEFAULT_TOP_RESULT = \"Top result\"\n    const val DEFAULT_OTHER_RESULTS = \"Other\"\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/AccountInfo.kt",
    "content": "package com.metrolist.innertube.models\n\ndata class AccountInfo(\n    val name: String,\n    val email: String?,\n    val channelHandle: String?,\n    val thumbnailUrl: String?,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/AutomixPreviewVideoRenderer.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class AutomixPreviewVideoRenderer(\n    val content: Content,\n) {\n    @Serializable\n    data class Content(\n        val automixPlaylistVideoRenderer: AutomixPlaylistVideoRenderer,\n    ) {\n        @Serializable\n        data class AutomixPlaylistVideoRenderer(\n            val navigationEndpoint: NavigationEndpoint,\n        )\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/Badges.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Badges(\n    val musicInlineBadgeRenderer: MusicInlineBadgeRenderer?,\n) {\n    @Serializable\n    data class MusicInlineBadgeRenderer(\n        val icon: Icon,\n    )\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/Button.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Button(\n    val buttonRenderer: ButtonRenderer,\n) {\n    @Serializable\n    data class ButtonRenderer(\n        val text: Runs,\n        val navigationEndpoint: NavigationEndpoint?,\n        val command: NavigationEndpoint?,\n        val icon: Icon?,\n    )\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/Context.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Context(\n    val client: Client,\n    val thirdParty: ThirdParty? = null,\n    val request: Request = Request(),\n    val user: User = User()\n) {\n    @Serializable\n    data class Client(\n        val clientName: String,\n        val clientVersion: String,\n        val osName: String? = null,\n        val osVersion: String? = null,\n        val deviceMake: String? = null,\n        val deviceModel: String? = null,\n        val androidSdkVersion: String? = null,\n        val gl: String,\n        val hl: String,\n        val visitorData: String?,\n    )\n\n    @Serializable\n    data class ThirdParty(\n        val embedUrl: String,\n    )\n\n    @Serializable\n    data class Request(\n        val internalExperimentFlags: Array<String> = emptyArray(),\n        val useSsl: Boolean = true,\n    )\n\n    @Serializable\n    data class User(\n        val lockedSafetyMode: Boolean = false,\n        val onBehalfOfUser: String? = null,\n    )\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/Continuation.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.ExperimentalSerializationApi\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonNames\n\n@OptIn(ExperimentalSerializationApi::class)\n@Serializable\ndata class Continuation(\n    @JsonNames(\"nextContinuationData\", \"nextRadioContinuationData\")\n    val nextContinuationData: NextContinuationData?,\n) {\n    @Serializable\n    data class NextContinuationData(\n        val continuation: String,\n    )\n}\n\nfun List<Continuation>.getContinuation() =\n    firstOrNull()?.nextContinuationData?.continuation"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/ContinuationItemRenderer.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class ContinuationItemRenderer(\n    val continuationEndpoint: ContinuationEndpoint?,\n) {\n    @Serializable\n    data class ContinuationEndpoint(\n        val continuationCommand: ContinuationCommand?,\n    ) {\n        @Serializable\n        data class ContinuationCommand(\n            val token: String?,\n        )\n    }\n}"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/Endpoint.kt",
    "content": "package com.metrolist.innertube.models\n\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_ALBUM\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_ARTIST\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_PLAYLIST\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_AUDIOBOOK\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_PODCAST_SHOW_DETAIL_PAGE\nimport kotlinx.serialization.Serializable\n\n@Serializable\nsealed class Endpoint\n\n@Serializable\ndata class WatchEndpoint(\n    val videoId: String? = null,\n    val playlistId: String? = null,\n    val playlistSetVideoId: String? = null,\n    val params: String? = null,\n    val index: Int? = null,\n    val watchEndpointMusicSupportedConfigs: WatchEndpointMusicSupportedConfigs? = null,\n) : Endpoint() {\n\n    @Serializable\n    data class WatchEndpointMusicSupportedConfigs(\n        val watchEndpointMusicConfig: WatchEndpointMusicConfig,\n    ) {\n        @Serializable\n        data class WatchEndpointMusicConfig(\n            val musicVideoType: String,\n        ) {\n            companion object {\n                const val MUSIC_VIDEO_TYPE_OMV = \"MUSIC_VIDEO_TYPE_OMV\"\n                const val MUSIC_VIDEO_TYPE_UGC = \"MUSIC_VIDEO_TYPE_UGC\"\n                const val MUSIC_VIDEO_TYPE_ATV = \"MUSIC_VIDEO_TYPE_ATV\"\n            }\n        }\n    }\n}\n\n@Serializable\ndata class BrowseEndpoint(\n    val browseId: String,\n    val params: String? = null,\n    val browseEndpointContextSupportedConfigs: BrowseEndpointContextSupportedConfigs? = null,\n) : Endpoint() {\n    val isArtistEndpoint: Boolean\n        get() = browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType == MUSIC_PAGE_TYPE_ARTIST\n    val isAlbumEndpoint: Boolean\n        get() = browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType == MUSIC_PAGE_TYPE_ALBUM ||\n                browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType == MUSIC_PAGE_TYPE_AUDIOBOOK\n    val isPlaylistEndpoint: Boolean\n        get() = browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType == MUSIC_PAGE_TYPE_PLAYLIST\n    val isPodcastEndpoint: Boolean\n        get() = browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType == MUSIC_PAGE_TYPE_PODCAST_SHOW_DETAIL_PAGE\n\n    @Serializable\n    data class BrowseEndpointContextSupportedConfigs(\n        val browseEndpointContextMusicConfig: BrowseEndpointContextMusicConfig,\n    ) {\n        @Serializable\n        data class BrowseEndpointContextMusicConfig(\n            val pageType: String,\n        ) {\n            companion object {\n                const val MUSIC_PAGE_TYPE_ALBUM = \"MUSIC_PAGE_TYPE_ALBUM\"\n                const val MUSIC_PAGE_TYPE_AUDIOBOOK = \"MUSIC_PAGE_TYPE_AUDIOBOOK\"\n                const val MUSIC_PAGE_TYPE_PLAYLIST = \"MUSIC_PAGE_TYPE_PLAYLIST\"\n                const val MUSIC_PAGE_TYPE_ARTIST = \"MUSIC_PAGE_TYPE_ARTIST\"\n                const val MUSIC_PAGE_TYPE_LIBRARY_ARTIST = \"MUSIC_PAGE_TYPE_LIBRARY_ARTIST\"\n                const val MUSIC_PAGE_TYPE_USER_CHANNEL = \"MUSIC_PAGE_TYPE_USER_CHANNEL\"\n                const val MUSIC_PAGE_TYPE_TRACK_LYRICS = \"MUSIC_PAGE_TYPE_TRACK_LYRICS\"\n                const val MUSIC_PAGE_TYPE_TRACK_RELATED = \"MUSIC_PAGE_TYPE_TRACK_RELATED\"\n                const val MUSIC_PAGE_TYPE_PODCAST_SHOW_DETAIL_PAGE = \"MUSIC_PAGE_TYPE_PODCAST_SHOW_DETAIL_PAGE\"\n                const val MUSIC_PAGE_TYPE_NON_MUSIC_AUDIO_TRACK_PAGE = \"MUSIC_PAGE_TYPE_NON_MUSIC_AUDIO_TRACK_PAGE\"\n            }\n        }\n    }\n}\n\n@Serializable\ndata class SearchEndpoint(\n    val params: String?,\n    val query: String,\n) : Endpoint()\n\n@Serializable\ndata class FeedbackEndpoint(\n    val feedbackToken: String\n) : Endpoint()\n\n@Serializable\ndata class QueueAddEndpoint(\n    val queueInsertPosition: String,\n    val queueTarget: QueueTarget,\n) : Endpoint() {\n    @Serializable\n    data class QueueTarget(\n        val videoId: String? = null,\n        val playlistId: String? = null,\n    )\n}\n\n@Serializable\ndata class ShareEntityEndpoint(\n    val serializedShareEntity: String,\n) : Endpoint()\n\n@Serializable\ndata class DefaultServiceEndpoint(\n    var subscribeEndpoint: SubscribeEndpoint?,\n    var feedbackEndpoint: FeedbackEndpoint?\n) : Endpoint() {\n    @Serializable\n    data class SubscribeEndpoint(\n        val channelIds: List<String>,\n        val params: String? = null,\n    ) : Endpoint()\n}\n\n@Serializable\ndata class ToggledServiceEndpoint(\n    var feedbackEndpoint: FeedbackEndpoint?\n) : Endpoint()"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/GridRenderer.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GridRenderer(\n    val header: Header?,\n    val items: List<Item>,\n    val continuations: List<Continuation>?,\n) {\n    @Serializable\n    data class Header(\n        val gridHeaderRenderer: GridHeaderRenderer,\n    ) {\n        @Serializable\n        data class GridHeaderRenderer(\n            val title: Runs,\n        )\n    }\n\n    @Serializable\n    data class Item(\n        val musicNavigationButtonRenderer: MusicNavigationButtonRenderer?,\n        val musicTwoRowItemRenderer: MusicTwoRowItemRenderer?,\n    )\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/Icon.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Icon(\n    val iconType: String,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/MediaInfo.kt",
    "content": "package com.metrolist.innertube.models\n\ndata class MediaInfo(\n    val videoId: String,\n    val title: String? = null,\n    val author: String? = null,\n    val authorId: String? = null,\n    val authorThumbnail: String? = null,\n    val description: String? = null,\n    val uploadDate: String? = null,\n    val subscribers: String? = null,\n    val viewCount: Int? = null,\n    val like: Int? = null,\n    val dislike: Int? = null,\n)"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/Menu.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Menu(\n    val menuRenderer: MenuRenderer,\n) {\n    @Serializable\n    data class MenuRenderer(\n        val items: List<Item>?,\n        val topLevelButtons: List<TopLevelButton>?,\n    ) {\n        @Serializable\n        data class Item(\n            val menuNavigationItemRenderer: MenuNavigationItemRenderer?,\n            val menuServiceItemRenderer: MenuServiceItemRenderer?,\n            val toggleMenuServiceItemRenderer: ToggleMenuServiceRenderer?,\n        ) {\n            @Serializable\n            data class MenuNavigationItemRenderer(\n                val text: Runs,\n                val icon: Icon,\n                val navigationEndpoint: NavigationEndpoint,\n            )\n\n            @Serializable\n            data class MenuServiceItemRenderer(\n                val text: Runs,\n                val icon: Icon,\n                val serviceEndpoint: NavigationEndpoint,\n            )\n            @Serializable\n            data class ToggleMenuServiceRenderer(\n                val defaultIcon: Icon,\n                val defaultServiceEndpoint: DefaultServiceEndpoint,\n                val toggledServiceEndpoint: ToggledServiceEndpoint?,\n                val isSelected: Boolean = false,\n            )\n        }\n\n        @Serializable\n        data class TopLevelButton(\n            val buttonRenderer: ButtonRenderer?,\n        ) {\n            @Serializable\n            data class ButtonRenderer(\n                val icon: Icon,\n                val navigationEndpoint: NavigationEndpoint,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/MusicCardShelfRenderer.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class MusicCardShelfRenderer(\n    val title: Runs,\n    val subtitle: Runs,\n    val thumbnail: ThumbnailRenderer,\n    val header: Header?,\n    val contents: List<Content>?,\n    val buttons: List<Button>,\n    val onTap: NavigationEndpoint,\n    val subtitleBadges: List<Badges>?,\n) {\n    @Serializable\n    data class Header(\n        val musicCardShelfHeaderBasicRenderer: MusicCardShelfHeaderBasicRenderer,\n    ) {\n        @Serializable\n        data class MusicCardShelfHeaderBasicRenderer(\n            val title: Runs,\n        )\n    }\n\n    @Serializable\n    data class Content(\n        val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer?,\n    )\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/MusicCarouselShelfRenderer.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class MusicCarouselShelfRenderer(\n    val header: Header?,\n    val contents: List<Content>,\n    val itemSize: String,\n    val numItemsPerColumn: Int?,\n) {\n    @Serializable\n    data class Header(\n        val musicCarouselShelfBasicHeaderRenderer: MusicCarouselShelfBasicHeaderRenderer,\n    ) {\n        @Serializable\n        data class MusicCarouselShelfBasicHeaderRenderer(\n            val strapline: Runs?,\n            val title: Runs,\n            val thumbnail: ThumbnailRenderer?,\n            val moreContentButton: Button?,\n        )\n    }\n\n    @Serializable\n    data class Content(\n        val musicTwoRowItemRenderer: MusicTwoRowItemRenderer?,\n        val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer?,\n        val musicMultiRowListItemRenderer: MusicMultiRowListItemRenderer?,\n        val musicNavigationButtonRenderer: MusicNavigationButtonRenderer?, // navigation button in explore tab\n    )\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/MusicDescriptionShelfRenderer.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class MusicDescriptionShelfRenderer(\n    val header: Runs?,\n    val subheader: Runs?,\n    val description: Runs,\n    val footer: Runs?,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/MusicEditablePlaylistDetailHeaderRenderer.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class MusicEditablePlaylistDetailHeaderRenderer(\n    val header: Header,\n    val editHeader: EditHeader\n) {\n    @Serializable\n    data class Header(\n        val musicDetailHeaderRenderer: MusicDetailHeaderRenderer?,\n        val musicResponsiveHeaderRenderer: MusicResponsiveHeaderRenderer?\n    )\n\n    @Serializable\n    data class EditHeader(\n        val musicPlaylistEditHeaderRenderer: MusicPlaylistEditHeaderRenderer?\n    )\n}\n\n@Serializable\ndata class MusicDetailHeaderRenderer(\n    val title: Runs,\n    val subtitle: Runs,\n    val secondSubtitle: Runs,\n    val description: Runs?,\n    val thumbnail: ThumbnailRenderer,\n    val menu: Menu,\n)\n\n@Serializable\ndata class MusicPlaylistEditHeaderRenderer(\n    val editTitle: Runs?\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/MusicMultiRowImageItemRenderer.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class MusicMultiRowImageItemRenderer(\n    val title: Runs,\n    val subtitle: Runs,\n    val thumbnail: ThumbnailRenderer,\n    val onTap: NavigationEndpoint,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/MusicMultiRowListItemRenderer.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class MusicMultiRowListItemRenderer(\n    val title: Runs?,\n    val subtitle: Runs?,\n    val thumbnail: ThumbnailRenderer?,\n    val onTap: NavigationEndpoint?,\n    val playbackProgress: PlaybackProgress?,\n    val displayStyle: String?,\n    val menu: Menu?,\n) {\n    @Serializable\n    data class PlaybackProgress(\n        val value: Float? = null,\n    )\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/MusicNavigationButtonRenderer.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class MusicNavigationButtonRenderer(\n    val buttonText: Runs,\n    val solid: Solid?,\n    val iconStyle: IconStyle?,\n    val clickCommand: NavigationEndpoint,\n) {\n    @Serializable\n    data class Solid(\n        val leftStripeColor: Long,\n    )\n\n    @Serializable\n    data class IconStyle(\n        val icon: Icon,\n    )\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/MusicPlaylistShelfRenderer.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class MusicPlaylistShelfRenderer(\n    val playlistId: String?,\n    val contents: List<MusicShelfRenderer.Content> = emptyList(),\n    val collapsedItemCount: Int? = null,\n    val continuations: List<Continuation>? = null,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/MusicQueueRenderer.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class MusicQueueRenderer(\n    val content: Content?,\n    val header: Header?,\n) {\n    @Serializable\n    data class Content(\n        val playlistPanelRenderer: PlaylistPanelRenderer,\n    )\n\n    @Serializable\n    data class Header(\n        val musicQueueHeaderRenderer: MusicQueueHeaderRenderer?,\n    ) {\n        @Serializable\n        data class MusicQueueHeaderRenderer(\n            val title: Runs?,\n            val subtitle: Runs?,\n        )\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/MusicResponsiveHeaderRenderer.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class MusicResponsiveHeaderRenderer(\n    val thumbnail: ThumbnailRenderer?,\n    val buttons: List<Button>,\n    val title: Runs,\n    val subtitle: Runs,\n    val secondSubtitle: Runs?,\n    val straplineTextOne: Runs?\n) {\n    @Serializable\n    data class Button(\n        val musicPlayButtonRenderer: MusicPlayButtonRenderer?,\n        val menuRenderer: Menu.MenuRenderer?,\n        val toggleButtonRenderer: ToggleButtonRenderer?,\n    ) {\n        @Serializable\n        data class MusicPlayButtonRenderer(\n            val playNavigationEndpoint: NavigationEndpoint?,\n        )\n\n        @Serializable\n        data class ToggleButtonRenderer(\n            val defaultIcon: Icon?,\n            val defaultServiceEndpoint: DefaultServiceEndpoint?,\n            val toggledServiceEndpoint: ToggledServiceEndpoint?,\n        )\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/MusicResponsiveListItemRenderer.kt",
    "content": "@file:OptIn(ExperimentalSerializationApi::class)\n\npackage com.metrolist.innertube.models\n\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_ALBUM\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_ARTIST\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_AUDIOBOOK\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_LIBRARY_ARTIST\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_PLAYLIST\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_PODCAST_SHOW_DETAIL_PAGE\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_NON_MUSIC_AUDIO_TRACK_PAGE\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_USER_CHANNEL\nimport kotlinx.serialization.ExperimentalSerializationApi\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonNames\n\n/**\n * Typical list item\n * Used in [MusicCarouselShelfRenderer], [MusicShelfRenderer]\n * Appears in quick picks, search results, table items, etc.\n */\n@Serializable\ndata class MusicResponsiveListItemRenderer(\n    val badges: List<Badges>?,\n    val fixedColumns: List<FlexColumn>?,\n    val flexColumns: List<FlexColumn>,\n    val thumbnail: ThumbnailRenderer?,\n    val menu: Menu?,\n    val playlistItemData: PlaylistItemData?,\n    val overlay: Overlay?,\n    val navigationEndpoint: NavigationEndpoint?,\n) {\n    val isSong: Boolean\n        get() = navigationEndpoint == null || navigationEndpoint.watchEndpoint != null || navigationEndpoint.watchPlaylistEndpoint != null\n    val isPlaylist: Boolean\n        get() = navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType == MUSIC_PAGE_TYPE_PLAYLIST\n    val isAlbum: Boolean\n        get() = navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType == MUSIC_PAGE_TYPE_ALBUM ||\n                navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType == MUSIC_PAGE_TYPE_AUDIOBOOK\n    val isArtist: Boolean\n        get() = navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType == MUSIC_PAGE_TYPE_ARTIST\n                || navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType == MUSIC_PAGE_TYPE_LIBRARY_ARTIST\n    val isPodcast: Boolean\n        get() = navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType == MUSIC_PAGE_TYPE_PODCAST_SHOW_DETAIL_PAGE\n    val isUserChannel: Boolean\n        get() = navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType == MUSIC_PAGE_TYPE_USER_CHANNEL\n    val isEpisode: Boolean\n        get() {\n            // Method 1: Check browse endpoint (for episode detail pages)\n            if (navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType == MUSIC_PAGE_TYPE_NON_MUSIC_AUDIO_TRACK_PAGE) {\n                return true\n            }\n            // Method 2: Check if first subtitle text is \"Episode\" (for search results)\n            val firstSubtitleText = flexColumns.getOrNull(1)\n                ?.musicResponsiveListItemFlexColumnRenderer\n                ?.text?.runs?.firstOrNull()?.text\n            if (firstSubtitleText == \"Episode\") {\n                return true\n            }\n            // Method 3: Check for podcast link in subtitle (backup detection).\n            //\n            // Episode items that appear in filtered search results may have:\n            //   - navigationEndpoint.watchEndpoint  (playable) → isSong=true would wrongly match\n            //   - playlistItemData = null            (not in a playlist context)\n            //   - subtitle: [date · podcast-name]   (podcast-name links to PODCAST_SHOW_DETAIL_PAGE)\n            //\n            // The presence of a PODCAST_SHOW_DETAIL_PAGE browse link in the subtitle is unique to\n            // episodes — regular songs never carry such a link.  We accept either a playlistItemData\n            // videoId OR a direct watchEndpoint videoId to handle both playlist and standalone contexts.\n            val hasPodcastLink = flexColumns.getOrNull(1)\n                ?.musicResponsiveListItemFlexColumnRenderer\n                ?.text?.runs?.any { run ->\n                    run.navigationEndpoint?.browseEndpoint\n                        ?.browseEndpointContextSupportedConfigs\n                        ?.browseEndpointContextMusicConfig\n                        ?.pageType == MUSIC_PAGE_TYPE_PODCAST_SHOW_DETAIL_PAGE\n                } == true\n            val hasVideoId = playlistItemData?.videoId != null ||\n                navigationEndpoint?.watchEndpoint?.videoId != null\n            return hasPodcastLink && hasVideoId\n        }\n\n    val musicVideoType: String?\n        get() =\n            overlay\n                ?.musicItemThumbnailOverlayRenderer\n                ?.content\n                ?.musicPlayButtonRenderer\n                ?.playNavigationEndpoint\n                ?.musicVideoType\n                ?: navigationEndpoint?.musicVideoType\n\n    @Serializable\n    data class FlexColumn(\n        @JsonNames(\"musicResponsiveListItemFixedColumnRenderer\")\n        val musicResponsiveListItemFlexColumnRenderer: MusicResponsiveListItemFlexColumnRenderer,\n    ) {\n        @Serializable\n        data class MusicResponsiveListItemFlexColumnRenderer(\n            val text: Runs?,\n        )\n    }\n\n    @Serializable\n    data class PlaylistItemData(\n        val playlistSetVideoId: String?,\n        val videoId: String,\n    )\n\n    @Serializable\n    data class Overlay(\n        val musicItemThumbnailOverlayRenderer: MusicItemThumbnailOverlayRenderer,\n    ) {\n        @Serializable\n        data class MusicItemThumbnailOverlayRenderer(\n            val content: Content,\n        ) {\n            @Serializable\n            data class Content(\n                val musicPlayButtonRenderer: MusicPlayButtonRenderer,\n            ) {\n                @Serializable\n                data class MusicPlayButtonRenderer(\n                    val playNavigationEndpoint: NavigationEndpoint?,\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/MusicShelfRenderer.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class MusicShelfRenderer(\n    val title: Runs?,\n    val contents: List<Content>?,\n    val continuations: List<Continuation>?,\n    val bottomEndpoint: NavigationEndpoint?,\n    val moreContentButton: Button?,\n) {\n    @Serializable\n    data class Content(\n        val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer?,\n        val musicMultiRowListItemRenderer: MusicMultiRowListItemRenderer?,\n        val continuationItemRenderer: ContinuationItemRenderer?,\n    )\n}\n\nfun List<MusicShelfRenderer.Content>.getItems(): List<MusicResponsiveListItemRenderer> =\n    mapNotNull { it.musicResponsiveListItemRenderer }\n\nfun List<MusicShelfRenderer.Content>.getContinuation(): String? =\n    firstOrNull { it.continuationItemRenderer != null }\n        ?.continuationItemRenderer\n        ?.continuationEndpoint\n        ?.continuationCommand\n        ?.token"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/MusicTwoRowItemRenderer.kt",
    "content": "package com.metrolist.innertube.models\n\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_ALBUM\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_ARTIST\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_AUDIOBOOK\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_NON_MUSIC_AUDIO_TRACK_PAGE\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_PLAYLIST\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_PODCAST_SHOW_DETAIL_PAGE\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_USER_CHANNEL\nimport kotlinx.serialization.Serializable\n\n/**\n * Two row: a big thumbnail, a title, and a subtitle\n * Used in [GridRenderer] and [MusicCarouselShelfRenderer]\n * Item type: song, video, album, playlist, artist\n */\n@Serializable\ndata class MusicTwoRowItemRenderer(\n    val title: Runs,\n    val subtitle: Runs?,\n    val subtitleBadges: List<Badges>?,\n    val menu: Menu?,\n    val thumbnailRenderer: ThumbnailRenderer,\n    val navigationEndpoint: NavigationEndpoint,\n    val thumbnailOverlay: MusicResponsiveListItemRenderer.Overlay?,\n) {\n    val isSong: Boolean\n        get() = navigationEndpoint.endpoint is WatchEndpoint\n    val isPlaylist: Boolean\n        get() =\n            navigationEndpoint.browseEndpoint\n                ?.browseEndpointContextSupportedConfigs\n                ?.browseEndpointContextMusicConfig\n                ?.pageType ==\n                MUSIC_PAGE_TYPE_PLAYLIST\n    val isAlbum: Boolean\n        get() =\n            navigationEndpoint.browseEndpoint\n                ?.browseEndpointContextSupportedConfigs\n                ?.browseEndpointContextMusicConfig\n                ?.pageType ==\n                MUSIC_PAGE_TYPE_ALBUM ||\n                navigationEndpoint.browseEndpoint\n                    ?.browseEndpointContextSupportedConfigs\n                    ?.browseEndpointContextMusicConfig\n                    ?.pageType ==\n                MUSIC_PAGE_TYPE_AUDIOBOOK\n    val isArtist: Boolean\n        get() =\n            navigationEndpoint.browseEndpoint\n                ?.browseEndpointContextSupportedConfigs\n                ?.browseEndpointContextMusicConfig\n                ?.pageType ==\n                MUSIC_PAGE_TYPE_ARTIST\n    val isPodcast: Boolean\n        get() =\n            navigationEndpoint.browseEndpoint\n                ?.browseEndpointContextSupportedConfigs\n                ?.browseEndpointContextMusicConfig\n                ?.pageType ==\n                MUSIC_PAGE_TYPE_PODCAST_SHOW_DETAIL_PAGE\n    val isUserChannel: Boolean\n        get() =\n            navigationEndpoint.browseEndpoint\n                ?.browseEndpointContextSupportedConfigs\n                ?.browseEndpointContextMusicConfig\n                ?.pageType ==\n                MUSIC_PAGE_TYPE_USER_CHANNEL\n    val isEpisode: Boolean\n        get() =\n            navigationEndpoint.browseEndpoint\n                ?.browseEndpointContextSupportedConfigs\n                ?.browseEndpointContextMusicConfig\n                ?.pageType ==\n                MUSIC_PAGE_TYPE_NON_MUSIC_AUDIO_TRACK_PAGE\n\n    val musicVideoType: String?\n        get() =\n            thumbnailOverlay\n                ?.musicItemThumbnailOverlayRenderer\n                ?.content\n                ?.musicPlayButtonRenderer\n                ?.playNavigationEndpoint\n                ?.musicVideoType\n                ?: navigationEndpoint.musicVideoType\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/NavigationEndpoint.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class NavigationEndpoint(\n    val watchEndpoint: WatchEndpoint? = null,\n    val watchPlaylistEndpoint: WatchEndpoint? = null,\n    val browseEndpoint: BrowseEndpoint? = null,\n    val searchEndpoint: SearchEndpoint? = null,\n    val queueAddEndpoint: QueueAddEndpoint? = null,\n    val shareEntityEndpoint: ShareEntityEndpoint? = null,\n    val feedbackEndpoint: FeedbackEndpoint? = null,\n    val urlEndpoint: UrlEndpoint? = null,\n    val deletePrivatelyOwnedEntityCommand: DeletePrivatelyOwnedEntityCommand? = null,\n    val confirmDialogEndpoint: ConfirmDialogEndpoint? = null,\n) {\n    @Serializable\n    data class DeletePrivatelyOwnedEntityCommand(\n        val entityId: String,\n    )\n\n    @Serializable\n    data class ConfirmDialogEndpoint(\n        val content: ConfirmDialogContent? = null,\n    ) {\n        @Serializable\n        data class ConfirmDialogContent(\n            val confirmDialogRenderer: ConfirmDialogRenderer? = null,\n        )\n\n        @Serializable\n        data class ConfirmDialogRenderer(\n            val confirmButton: ConfirmButton? = null,\n        )\n\n        @Serializable\n        data class ConfirmButton(\n            val buttonRenderer: ConfirmButtonRenderer? = null,\n        )\n\n        @Serializable\n        data class ConfirmButtonRenderer(\n            val command: ConfirmCommand? = null,\n        )\n\n        @Serializable\n        data class ConfirmCommand(\n            val musicDeletePrivatelyOwnedEntityCommand: DeletePrivatelyOwnedEntityCommand? = null,\n        )\n    }\n    val endpoint: Endpoint?\n        get() =\n            watchEndpoint\n                ?: watchPlaylistEndpoint\n                ?: browseEndpoint\n                ?: searchEndpoint\n                ?: queueAddEndpoint\n                ?: shareEntityEndpoint\n\n    val anyWatchEndpoint: WatchEndpoint?\n        get() = watchEndpoint\n            ?: watchPlaylistEndpoint\n\n    val musicVideoType: String?\n        get() = anyWatchEndpoint\n            ?.watchEndpointMusicSupportedConfigs\n            ?.watchEndpointMusicConfig\n            ?.musicVideoType\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/PlaylistDeleteBody.kt",
    "content": "package com.metrolist.innertube.models.body\n\nimport com.metrolist.innertube.models.Context\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class PlaylistDeleteBody(\n    val context: Context,\n    val playlistId: String\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/PlaylistPanelRenderer.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class PlaylistPanelRenderer(\n    val title: String?,\n    val titleText: Runs?,\n    val shortBylineText: Runs?,\n    val contents: List<Content>,\n    val isInfinite: Boolean?,\n    val numItemsToShow: Int?,\n    val playlistId: String?,\n    val continuations: List<Continuation>?,\n) {\n    @Serializable\n    data class Content(\n        val playlistPanelVideoRenderer: PlaylistPanelVideoRenderer?,\n        val automixPreviewVideoRenderer: AutomixPreviewVideoRenderer?,\n    )\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/PlaylistPanelVideoRenderer.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class PlaylistPanelVideoRenderer(\n    val title: Runs?,\n    val lengthText: Runs?,\n    val longBylineText: Runs?,\n    val shortBylineText: Runs?,\n    val badges: List<Badges>?,\n    val videoId: String?,\n    val playlistSetVideoId: String?,\n    val selected: Boolean,\n    val thumbnail: Thumbnails,\n    val unplayableText: Runs?,\n    val menu: Menu?,\n    val navigationEndpoint: NavigationEndpoint,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/ResponseContext.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class ResponseContext(\n    val visitorData: String?,\n    val serviceTrackingParams: List<ServiceTrackingParam>?,\n) {\n    @Serializable\n    data class ServiceTrackingParam(\n        val params: List<Param>,\n        val service: String,\n    ) {\n        @Serializable\n        data class Param(\n            val key: String,\n            val value: String,\n        )\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/ReturnYouTubeDislikeResponse.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class ReturnYouTubeDislikeResponse(\n    val id: String? = null,\n    val dateCreated: String? = null,\n    val likes: Int? = null,\n    val dislikes: Int? = null,\n    val rating: Double? = null,\n    val viewCount: Int? = null,\n    val deleted: Boolean? = null,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/Runs.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Runs(\n    val runs: List<Run>?,\n)\n\n@Serializable\ndata class Run(\n    val text: String,\n    val navigationEndpoint: NavigationEndpoint?,\n)\n\nfun List<Run>.splitBySeparator(): List<List<Run>> {\n    val res = mutableListOf<List<Run>>()\n    var tmp = mutableListOf<Run>()\n    forEach { run ->\n        if (run.text == \" • \") {\n            res.add(tmp)\n            tmp = mutableListOf()\n        } else {\n            tmp.add(run)\n        }\n    }\n    res.add(tmp)\n    return res\n}\n\nfun List<List<Run>>.clean(): List<List<Run>> =\n    if (getOrNull(0)?.getOrNull(0)?.navigationEndpoint != null ||\n        (getOrNull(0)?.getOrNull(0)?.text?.contains(regex = Regex(\"[&,]\"))) != false\n    ) {\n        this\n    } else {\n        this.drop(1)\n    }\n\nfun List<Run>.oddElements() =\n    filterIndexed { index, _ ->\n        index % 2 == 0\n    }\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/SearchSuggestions.kt",
    "content": "package com.metrolist.innertube.models\n\ndata class SearchSuggestions(\n    val queries: List<String>,\n    val recommendedItems: List<YTItem>,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/SearchSuggestionsSectionRenderer.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class SearchSuggestionsSectionRenderer(\n    val contents: List<Content>,\n) {\n    @Serializable\n    data class Content(\n        val searchSuggestionRenderer: SearchSuggestionRenderer?,\n        val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer?,\n    ) {\n        @Serializable\n        data class SearchSuggestionRenderer(\n            val suggestion: Runs,\n            val navigationEndpoint: NavigationEndpoint,\n        )\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/SectionListRenderer.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.ExperimentalSerializationApi\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonNames\n\n@Serializable\ndata class SectionListRenderer(\n    val header: Header?,\n    val contents: List<Content>?,\n    val continuations: List<Continuation>?,\n) {\n    @Serializable\n    data class Header(\n        val chipCloudRenderer: ChipCloudRenderer?,\n    ) {\n        @Serializable\n        data class ChipCloudRenderer(\n            val chips: List<Chip>,\n        ) {\n            @Serializable\n            data class Chip(\n                val chipCloudChipRenderer: ChipCloudChipRenderer,\n            ) {\n                @Serializable\n                data class ChipCloudChipRenderer(\n                    val isSelected: Boolean = false,\n                    val navigationEndpoint: NavigationEndpoint? = null,\n                    val onDeselectedCommand: NavigationEndpoint? = null,\n                    // The close button doesn't have the following two fields\n                    val text: Runs?,\n                    val uniqueId: String?,\n                )\n            }\n        }\n    }\n\n    @OptIn(ExperimentalSerializationApi::class)\n    @Serializable\n    data class Content(\n        @JsonNames(\"musicImmersiveCarouselShelfRenderer\")\n        val musicCarouselShelfRenderer: MusicCarouselShelfRenderer?,\n        val musicShelfRenderer: MusicShelfRenderer?,\n        val musicCardShelfRenderer: MusicCardShelfRenderer?,\n        val musicPlaylistShelfRenderer: MusicPlaylistShelfRenderer?,\n        val musicDescriptionShelfRenderer: MusicDescriptionShelfRenderer?,\n        val musicResponsiveHeaderRenderer: MusicResponsiveHeaderRenderer?,\n        val musicEditablePlaylistDetailHeaderRenderer: MusicEditablePlaylistDetailHeaderRenderer?,\n        val gridRenderer: GridRenderer?,\n    )\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/SubscriptionButton.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class SubscriptionButton(\n    val subscribeButtonRenderer: SubscribeButtonRenderer,\n) {\n    @Serializable\n    data class SubscribeButtonRenderer(\n        val subscribed: Boolean,\n        val channelId: String,\n        val longSubscriberCountText: Runs? = null,\n        val shortSubscriberCountText: Runs? = null,\n    )\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/Tabs.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Tabs(\n    val tabs: List<Tab>,\n) {\n    @Serializable\n    data class Tab(\n        val tabRenderer: TabRenderer,\n    ) {\n        @Serializable\n        data class TabRenderer(\n            val title: String?,\n            val content: Content?,\n            val endpoint: NavigationEndpoint?,\n        ) {\n            @Serializable\n            data class Content(\n                val sectionListRenderer: SectionListRenderer?,\n                val musicQueueRenderer: MusicQueueRenderer?,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/TasteProfile.kt",
    "content": "package com.metrolist.innertube.models\n\ndata class TasteArtist(\n    val name: String,\n    val selectionValue: String,\n    val impressionValue: String,\n)\n\ndata class TasteProfile(\n    val artists: Map<String, TasteArtist>,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/ThumbnailRenderer.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.ExperimentalSerializationApi\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonNames\n\n@OptIn(ExperimentalSerializationApi::class)\n@Serializable\ndata class ThumbnailRenderer(\n    @JsonNames(\"croppedSquareThumbnailRenderer\")\n    val musicThumbnailRenderer: MusicThumbnailRenderer?,\n    val musicAnimatedThumbnailRenderer: MusicAnimatedThumbnailRenderer?,\n    val croppedSquareThumbnailRenderer: MusicThumbnailRenderer?,\n) {\n    @Serializable\n    data class MusicThumbnailRenderer(\n        val thumbnail: Thumbnails,\n        val thumbnailCrop: String?,\n        val thumbnailScale: String?,\n    ) {\n        fun getThumbnailUrl() = thumbnail.thumbnails.lastOrNull()?.url\n    }\n\n    @Serializable\n    data class MusicAnimatedThumbnailRenderer(\n        val animatedThumbnail: Thumbnails,\n        val backupRenderer: MusicThumbnailRenderer,\n    )\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/Thumbnails.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Thumbnails(\n    val thumbnails: List<Thumbnail>,\n)\n\n@Serializable\ndata class Thumbnail(\n    val url: String,\n    val width: Int?,\n    val height: Int?,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/TwoColumnBrowseResultsRenderer.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class TwoColumnBrowseResultsRenderer(\n    val secondaryContents: SecondaryContents?,\n    val tabs: List<Tabs.Tab>?\n) {\n    @Serializable\n    data class SecondaryContents(\n        val sectionListRenderer: SectionListRenderer?\n    )\n\n    @Serializable\n    data class SectionListRenderer(\n        val contents: List<Content>?,\n        val continuations: List<Continuation>?,\n    ) {\n        @Serializable\n        data class Content(\n            val musicPlaylistShelfRenderer: MusicPlaylistShelfRenderer?,\n            val musicShelfRenderer: MusicShelfRenderer?\n        )\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/UrlEndpoint.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class UrlEndpoint(\n    val url: String? = null,\n    val target: String? = null,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/YTItem.kt",
    "content": "package com.metrolist.innertube.models\n\nimport com.metrolist.innertube.models.WatchEndpoint.WatchEndpointMusicSupportedConfigs.WatchEndpointMusicConfig.Companion.MUSIC_VIDEO_TYPE_ATV\n\nsealed class YTItem {\n    abstract val id: String\n    abstract val title: String\n    abstract val thumbnail: String?\n    abstract val explicit: Boolean\n    abstract val shareLink: String\n}\n\ndata class Artist(\n    val name: String,\n    val id: String?,\n)\n\ndata class Album(\n    val name: String,\n    val id: String,\n)\n\ndata class SongItem(\n    override val id: String,\n    override val title: String,\n    val artists: List<Artist>,\n    val album: Album? = null,\n    val duration: Int? = null,\n    val musicVideoType: String? = null,\n    val chartPosition: Int? = null,\n    val chartChange: String? = null,\n    override val thumbnail: String,\n    override val explicit: Boolean = false,\n    val endpoint: WatchEndpoint? = null,\n    val setVideoId: String? = null,\n    val libraryAddToken: String? = null,\n    val libraryRemoveToken: String? = null,\n    val historyRemoveToken: String? = null,\n    val isEpisode: Boolean = false,\n    val uploadEntityId: String? = null\n) : YTItem() {\n    val isVideoSong: Boolean\n        get() = musicVideoType != null && musicVideoType != MUSIC_VIDEO_TYPE_ATV\n\n    override val shareLink: String\n        get() = \"https://music.youtube.com/watch?v=$id\"\n}\n\ndata class AlbumItem(\n    val browseId: String,\n    val playlistId: String,\n    override val id: String = browseId,\n    override val title: String,\n    val artists: List<Artist>?,\n    val year: Int? = null,\n    override val thumbnail: String,\n    override val explicit: Boolean = false,\n) : YTItem() {\n    override val shareLink: String\n        get() = \"https://music.youtube.com/playlist?list=$playlistId\"\n}\n\ndata class PlaylistItem(\n    override val id: String,\n    override val title: String,\n    val author: Artist?,\n    val songCountText: String?,\n    override val thumbnail: String?,\n    val playEndpoint: WatchEndpoint?,\n    val shuffleEndpoint: WatchEndpoint?,\n    val radioEndpoint: WatchEndpoint?,\n    val isEditable: Boolean = false,\n    val isPodcast: Boolean = false,\n) : YTItem() {\n    override val explicit: Boolean\n        get() = false\n    override val shareLink: String\n        get() = \"https://music.youtube.com/playlist?list=$id\"\n}\n\ndata class ArtistItem(\n    override val id: String,\n    override val title: String,\n    override val thumbnail: String?,\n    val channelId: String? = null,\n    val playEndpoint: WatchEndpoint? = null,\n    val shuffleEndpoint: WatchEndpoint?,\n    val radioEndpoint: WatchEndpoint?,\n    val isProfile: Boolean = false,\n) : YTItem() {\n    override val explicit: Boolean\n        get() = false\n    override val shareLink: String\n        get() = \"https://music.youtube.com/channel/$id\"\n}\n\ndata class PodcastItem(\n    override val id: String,\n    override val title: String,\n    val author: Artist?,\n    val episodeCountText: String?,\n    override val thumbnail: String?,\n    val playEndpoint: WatchEndpoint?,\n    val shuffleEndpoint: WatchEndpoint?,\n    val libraryAddToken: String? = null,\n    val libraryRemoveToken: String? = null,\n    val channelId: String? = null,\n) : YTItem() {\n    override val explicit: Boolean\n        get() = false\n    override val shareLink: String\n        get() = \"https://music.youtube.com/playlist?list=$id\"\n\n    fun asPlaylistItem() = PlaylistItem(\n        id = id,\n        title = title,\n        author = author,\n        songCountText = episodeCountText,\n        thumbnail = thumbnail,\n        playEndpoint = playEndpoint,\n        shuffleEndpoint = shuffleEndpoint,\n        radioEndpoint = null,\n        isEditable = false,\n        isPodcast = true\n    )\n}\n\ndata class EpisodeItem(\n    override val id: String,\n    override val title: String,\n    val author: Artist?,\n    val podcast: Album? = null,\n    val duration: Int? = null,\n    val publishDateText: String? = null,\n    override val thumbnail: String,\n    override val explicit: Boolean = false,\n    val endpoint: WatchEndpoint? = null,\n    val libraryAddToken: String? = null,\n    val libraryRemoveToken: String? = null,\n    val markAsPlayedToken: String? = null,\n    val markAsUnplayedToken: String? = null,\n) : YTItem() {\n    override val shareLink: String\n        get() = \"https://music.youtube.com/watch?v=$id\"\n\n    fun asSongItem() = SongItem(\n        id = id,\n        title = title,\n        artists = listOfNotNull(author),\n        album = podcast,\n        duration = duration,\n        thumbnail = thumbnail,\n        explicit = explicit,\n        endpoint = endpoint,\n        isEpisode = true,\n        libraryAddToken = libraryAddToken,\n        libraryRemoveToken = libraryRemoveToken,\n    )\n}\n\nfun <T : YTItem> List<T>.filterExplicit(enabled: Boolean = true) =\n    if (enabled) {\n        filter { !it.explicit }\n    } else {\n        this\n    }\n\nfun <T : YTItem> List<T>.filterVideoSongs(disableVideos: Boolean = false) =\n    if (disableVideos) {\n        filterNot { it is SongItem && it.isVideoSong }\n    } else {\n        this\n    }\n\nfun <T : YTItem> List<T>.filterYoutubeShorts(enabled: Boolean = false) =\n    if (enabled) {\n        filterNot { it is PlaylistItem && it.id.startsWith(\"SS\") }\n    } else {\n        this\n    }\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/YouTubeClient.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class YouTubeClient(\n    val clientName: String,\n    val clientVersion: String,\n    val clientId: String,\n    val userAgent: String,\n    val osName: String? = null,\n    val osVersion: String? = null,\n    val deviceMake: String? = null,\n    val deviceModel: String? = null,\n    val androidSdkVersion: String? = null,\n    val buildId: String? = null,\n    val cronetVersion: String? = null,\n    val packageName: String? = null,\n    val friendlyName: String? = null,\n    val loginSupported: Boolean = false,\n    val loginRequired: Boolean = false,\n    val useSignatureTimestamp: Boolean = false,\n    val isEmbedded: Boolean = false,\n    val useWebPoTokens: Boolean = false,\n) {\n    fun toContext(locale: YouTubeLocale, visitorData: String?, dataSyncId: String?) = Context(\n        client = Context.Client(\n            clientName = clientName,\n            clientVersion = clientVersion,\n            osName = osName,\n            osVersion = osVersion,\n            deviceMake = deviceMake,\n            deviceModel = deviceModel,\n            androidSdkVersion = androidSdkVersion,\n            gl = locale.gl,\n            hl = locale.hl,\n            visitorData = visitorData\n        ),\n        user = Context.User(\n            onBehalfOfUser = if (loginSupported) dataSyncId else null\n        ),\n    )\n\n    companion object {\n        const val USER_AGENT_WEB = \"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0\"\n\n        const val ORIGIN_YOUTUBE_MUSIC = \"https://music.youtube.com\"\n        const val REFERER_YOUTUBE_MUSIC = \"$ORIGIN_YOUTUBE_MUSIC/\"\n        const val API_URL_YOUTUBE_MUSIC = \"$ORIGIN_YOUTUBE_MUSIC/youtubei/v1/\"\n\n        val WEB = YouTubeClient(\n            clientName = \"WEB\",\n            clientVersion = \"2.20260213.00.00\",\n            clientId = \"1\",\n            userAgent = USER_AGENT_WEB,\n        )\n\n        val WEB_REMIX = YouTubeClient(\n            clientName = \"WEB_REMIX\",\n            clientVersion = \"1.20260213.01.00\",\n            clientId = \"67\",\n            userAgent = USER_AGENT_WEB,\n            loginSupported = true,\n            useSignatureTimestamp = true,\n            useWebPoTokens = true,\n        )\n\n        val WEB_CREATOR = YouTubeClient(\n            clientName = \"WEB_CREATOR\",\n            clientVersion = \"1.20260213.00.00\",\n            clientId = \"62\",\n            userAgent = USER_AGENT_WEB,\n            loginSupported = true,\n            loginRequired = true,\n            useSignatureTimestamp = true,\n        )\n\n        val TVHTML5 = YouTubeClient(\n            clientName = \"TVHTML5\",\n            clientVersion = \"7.20260213.00.00\",\n            clientId = \"7\",\n            userAgent = \"Mozilla/5.0(SMART-TV; Linux; Tizen 4.0.0.2) AppleWebkit/605.1.15 (KHTML, like Gecko) SamsungBrowser/9.2 TV Safari/605.1.15\",\n            loginSupported = true,\n            loginRequired = true,\n            useSignatureTimestamp = true,\n            useWebPoTokens = true,\n        )\n\n        /**\n         * Embedded player that can bypass age-restriction.\n         * Does not require login for age-restricted content.\n         */\n        val TVHTML5_SIMPLY_EMBEDDED_PLAYER = YouTubeClient(\n            clientName = \"TVHTML5_SIMPLY_EMBEDDED_PLAYER\",\n            clientVersion = \"2.0\",\n            clientId = \"85\",\n            userAgent = \"Mozilla/5.0 (PlayStation; PlayStation 4/12.02) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.4 Safari/605.1.15\",\n            loginSupported = true,\n            loginRequired = false,\n            useSignatureTimestamp = true,\n            isEmbedded = true,\n        )\n\n        val IOS = YouTubeClient(\n            clientName = \"IOS\",\n            clientVersion = \"21.03.1\",\n            clientId = \"5\",\n            userAgent = \"com.google.ios.youtube/21.03.1 (iPhone16,2; U; CPU iOS 18_2 like Mac OS X;)\",\n            osVersion = \"18.2.22C152\",\n        )\n\n        val MOBILE = YouTubeClient(\n            clientName = \"ANDROID\",\n            clientVersion = \"21.03.38\",\n            clientId = \"3\",\n            userAgent = \"com.google.android.youtube/21.03.38 (Linux; U; Android 14) gzip\",\n            loginSupported = true,\n            useSignatureTimestamp = true\n        )\n\n        /**\n         * Video not playable: Paid / Movie / Private / Age-restricted.\n         * Note: The 'Authorization' key must be excluded from the header.\n         * For some reason, PoToken is not required.\n         */\n        val ANDROID_NO_SDK = YouTubeClient(\n            clientName = \"ANDROID\",\n            clientVersion = \"21.03.38\",\n            clientId = \"3\",\n            userAgent = \"com.google.android.youtube/21.03.38 (Linux; U; Android 14) gzip\",\n            friendlyName = \"Android No SDK\",\n            loginSupported = false,\n            useSignatureTimestamp = false\n        )\n\n        val ANDROID_VR_NO_AUTH = YouTubeClient(\n            clientName = \"ANDROID_VR\",\n            clientVersion = \"1.61.48\",\n            clientId = \"28\",\n            userAgent = \"com.google.android.apps.youtube.vr.oculus/1.61.48 (Linux; U; Android 12; en_US; Oculus Quest 3; Build/SQ3A.220605.009.A1; Cronet/132.0.6808.3)\",\n            loginSupported = false,\n            useSignatureTimestamp = false\n        )\n\n        /**\n         * Video not playable: Kids / Paid / Movie / Private / Age-restricted.\n         * This client can only be used when logged out.\n         */\n        val ANDROID_VR_1_61_48 = YouTubeClient(\n            clientName = \"ANDROID_VR\",\n            clientVersion = \"1.61.48\",\n            clientId = \"28\",\n            userAgent = \"com.google.android.apps.youtube.vr.oculus/1.61.48 (Linux; U; Android 12; en_US; Quest 3; Build/SQ3A.220605.009.A1; Cronet/132.0.6808.3)\",\n            osName = \"Android\",\n            osVersion = \"12\",\n            deviceMake = \"Oculus\",\n            deviceModel = \"Quest 3\",\n            androidSdkVersion = \"32\",\n            buildId = \"SQ3A.220605.009.A1\",\n            cronetVersion = \"132.0.6808.3\",\n            packageName = \"com.google.android.apps.youtube.vr.oculus\",\n            friendlyName = \"Android VR 1.61\",\n            loginSupported = false,\n            useSignatureTimestamp = false\n        )\n\n        /**\n         * Uses non adaptive bitrate, which fixes audio stuttering with YT Music.\n         * Does not use AV1.\n         */\n        val ANDROID_VR_1_43_32 = YouTubeClient(\n            clientName = \"ANDROID_VR\",\n            clientVersion = \"1.43.32\",\n            clientId = \"28\",\n            userAgent = \"com.google.android.apps.youtube.vr.oculus/1.43.32 (Linux; U; Android 12; en_US; Quest 3; Build/SQ3A.220605.009.A1; Cronet/107.0.5284.2)\",\n            osName = \"Android\",\n            osVersion = \"12\",\n            deviceMake = \"Oculus\",\n            deviceModel = \"Quest 3\",\n            androidSdkVersion = \"32\",\n            buildId = \"SQ3A.220605.009.A1\",\n            cronetVersion = \"107.0.5284.2\",\n            packageName = \"com.google.android.apps.youtube.vr.oculus\",\n            friendlyName = \"Android VR 1.43\",\n            loginSupported = false,\n            useSignatureTimestamp = false\n        )\n\n        /**\n         * Cannot play livestreams and lacks HDR, but can play videos with music and labeled \"for children\".\n         */\n        val ANDROID_CREATOR = YouTubeClient(\n            clientName = \"ANDROID_CREATOR\",\n            clientVersion = \"25.03.101\",\n            clientId = \"14\",\n            userAgent = \"com.google.android.apps.youtube.creator/25.03.101 (Linux; U; Android 15; en_US; Pixel 9 Pro Fold; Build/AP3A.241005.015.A2; Cronet/132.0.6779.0)\",\n            osName = \"Android\",\n            osVersion = \"15\",\n            deviceMake = \"Google\",\n            deviceModel = \"Pixel 9 Pro Fold\",\n            androidSdkVersion = \"35\",\n            buildId = \"AP3A.241005.015.A2\",\n            cronetVersion = \"132.0.6779.0\",\n            packageName = \"com.google.android.apps.youtube.creator\",\n            friendlyName = \"Android Studio\",\n            loginSupported = true,\n            useSignatureTimestamp = true\n        )\n\n        /**\n         * Internal YT client for an unreleased YT client. May stop working at any time.\n         */\n        val VISIONOS = YouTubeClient(\n            clientName = \"VISIONOS\",\n            clientVersion = \"0.1\",\n            clientId = \"101\",\n            userAgent = \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15\",\n            osName = \"visionOS\",\n            osVersion = \"1.3.21O771\",\n            deviceMake = \"Apple\",\n            deviceModel = \"RealityDevice14,1\",\n            friendlyName = \"visionOS\",\n            loginSupported = false,\n            useSignatureTimestamp = false\n        )\n\n        /**\n         * The device machine id for the iPad 6th Gen (iPad7,6).\n         * AV1 hardware decoding is not supported.\n         */\n        val IPADOS = YouTubeClient(\n            clientName = \"IOS\",\n            clientVersion = \"21.03.3\",\n            clientId = \"5\",\n            userAgent = \"com.google.ios.youtube/21.03.3 (iPad7,6; U; CPU iPadOS 17_7_10 like Mac OS X; en-US)\",\n            osName = \"iPadOS\",\n            osVersion = \"17.7.10.21H450\",\n            deviceMake = \"Apple\",\n            deviceModel = \"iPad7,6\",\n            friendlyName = \"iPadOS\",\n            loginSupported = false,\n            useSignatureTimestamp = false,\n            packageName = \"com.google.ios.youtube\"\n        )\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/YouTubeDataPage.kt",
    "content": "package com.metrolist.innertube.models\n\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class YouTubeDataPage(\n    @SerialName(\"contents\")\n    val contents: Contents? = null,\n) {\n    @Serializable\n    data class Contents(\n        @SerialName(\"twoColumnWatchNextResults\")\n        val twoColumnWatchNextResults: TwoColumnWatchNextResults? = null,\n    ) {\n        @Serializable\n        data class TwoColumnWatchNextResults(\n            @SerialName(\"results\")\n            val results: Results? = null,\n        ) {\n            @Serializable\n            data class Results(\n                @SerialName(\"results\")\n                val results: Results? = null,\n            ) {\n                @Serializable\n                data class Results(\n                    @SerialName(\"contents\")\n                    val content: List<Content?>? = null,\n                ) {\n                    @Serializable\n                    data class Content(\n                        @SerialName(\"videoPrimaryInfoRenderer\")\n                        val videoPrimaryInfoRenderer: VideoPrimaryInfoRenderer? = null,\n                        @SerialName(\"videoSecondaryInfoRenderer\")\n                        val videoSecondaryInfoRenderer: VideoSecondaryInfoRenderer? = null,\n                        @SerialName(\"itemSectionRenderer\")\n                        val itemSectionRenderer: ItemSectionRenderer? = null,\n                    ) {\n                        @Serializable\n                        data class ItemSectionRenderer(\n                            @SerialName(\"contents\")\n                            val contents: List<Content?>? = null,\n                        ) {\n                            @Serializable\n                            data class Content(\n                                @SerialName(\"continuationItemRenderer\")\n                                val continuationItemRenderer: ContinuationItemRenderer? = null,\n                            ) {\n                                @Serializable\n                                data class ContinuationItemRenderer(\n                                    @SerialName(\"trigger\")\n                                    val trigger: String? = null,\n                                    @SerialName(\"continuationEndpoint\")\n                                    val continuationEndpoint: ContinuationEndpoint? = null,\n                                ) {\n                                    @Serializable\n                                    data class ContinuationEndpoint(\n                                        @SerialName(\"clickTrackingParams\")\n                                        val clickTrackingParams: String? = null,\n                                        @SerialName(\"continuationCommand\")\n                                        val continuationCommand: ContinuationCommand? = null,\n                                    ) {\n                                        @Serializable\n                                        data class ContinuationCommand(\n                                            @SerialName(\"token\")\n                                            val token: String? = null,\n                                            @SerialName(\"request\")\n                                            val request: String? = null,\n                                        )\n                                    }\n                                }\n                            }\n                        }\n\n                        @Serializable\n                        data class VideoPrimaryInfoRenderer(\n                            @SerialName(\"title\")\n                            val title: Title? = null,\n                            @SerialName(\"viewCount\")\n                            val viewCount: ViewCount? = null,\n                            @SerialName(\"dateText\")\n                            val dateText: DateText? = null,\n                        ) {\n                            @Serializable\n                            data class Title(\n                                @SerialName(\"runs\")\n                                val runs: List<Run>? = null,\n                            ) {\n                                @Serializable\n                                data class Run(\n                                    @SerialName(\"text\")\n                                    val text: String? = null,\n                                )\n                            }\n\n                            @Serializable\n                            data class ViewCount(\n                                @SerialName(\"videoViewCountRenderer\")\n                                val videoViewCountRenderer: VideoViewCountRenderer? = null,\n                            ) {\n                                @Serializable\n                                data class VideoViewCountRenderer(\n                                    @SerialName(\"viewCount\")\n                                    val viewCount: VideoViewCountRenderer.ViewCount? = null,\n                                ) {\n                                    @Serializable\n                                    data class ViewCount(\n                                        @SerialName(\"simpleText\")\n                                        val simpleText: String? = null,\n                                    )\n                                }\n                            }\n\n                            @Serializable\n                            data class DateText(\n                                @SerialName(\"simpleText\")\n                                val simpleText: String? = null,\n                            )\n                        }\n\n                        @Serializable\n                        data class VideoSecondaryInfoRenderer(\n                            @SerialName(\"owner\")\n                            val owner: Owner? = null,\n                            @SerialName(\"attributedDescription\")\n                            val attributedDescription: AttributedDescription? = null,\n                        ) {\n                            @Serializable\n                            data class AttributedDescription(\n                                @SerialName(\"content\")\n                                val content: String? = null,\n                            )\n\n                            @Serializable\n                            data class Owner(\n                                @SerialName(\"videoOwnerRenderer\")\n                                val videoOwnerRenderer: VideoOwnerRenderer? = null,\n                            ) {\n                                @Serializable\n                                data class VideoOwnerRenderer(\n                                    @SerialName(\"thumbnail\")\n                                    val thumbnail: Thumbnail? = null,\n                                    @SerialName(\"subscriberCountText\")\n                                    val subscriberCountText: SubscriberCountText? = null,\n                                    @SerialName(\"title\")\n                                    val title: Title? = null,\n                                    @SerialName(\"navigationEndpoint\")\n                                    val navigationEndpoint: NavigationEndpoint? = null,\n                                ) {\n                                    @Serializable\n                                    data class Thumbnail(\n                                        @SerialName(\"thumbnails\")\n                                        val thumbnails: List<com.metrolist.innertube.models.Thumbnail>? = null,\n                                    )\n\n                                    @Serializable\n                                    data class SubscriberCountText(\n                                        @SerialName(\"simpleText\")\n                                        val simpleText: String? = null,\n                                    )\n\n                                    @Serializable\n                                    data class Title(\n                                        @SerialName(\"runs\")\n                                        val runs: List<Run>? = null,\n                                    ) {\n                                        @Serializable\n                                        data class Run(\n                                            @SerialName(\"text\")\n                                            val text: String? = null,\n                                        )\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/YouTubeLocale.kt",
    "content": "package com.metrolist.innertube.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class YouTubeLocale(\n    val gl: String, // geolocation\n    val hl: String, // host language\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/body/AccountMenuBody.kt",
    "content": "package com.metrolist.innertube.models.body\n\nimport com.metrolist.innertube.models.Context\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class AccountMenuBody(\n    val context: Context,\n    val deviceTheme: String = \"DEVICE_THEME_SELECTED\",\n    val userInterfaceTheme: String = \"USER_INTERFACE_THEME_DARK\",\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/body/BrowseBody.kt",
    "content": "package com.metrolist.innertube.models.body\n\nimport com.metrolist.innertube.models.Context\nimport com.metrolist.innertube.models.Continuation\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class BrowseBody(\n    val context: Context,\n    val browseId: String?,\n    val params: String?,\n    val continuation: String?\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/body/CreatePlaylistBody.kt",
    "content": "package com.metrolist.innertube.models.body\n\nimport com.metrolist.innertube.models.Context\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class CreatePlaylistBody(\n    val context: Context,\n    val title: String,\n    val privacyStatus: String = PrivacyStatus.PRIVATE,\n    val videoIds: List<String>? = null\n) {\n    object PrivacyStatus {\n        const val PRIVATE = \"PRIVATE\"\n        const val PUBLIC = \"PUBLIC\"\n        const val UNLISTED = \"UNLISTED\"\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/body/EditPlaylistBody.kt",
    "content": "package com.metrolist.innertube.models.body\n\nimport com.metrolist.innertube.models.Context\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class EditPlaylistBody(\n    val context: Context,\n    val playlistId: String,\n    val actions: List<Action>\n)\n\n@Serializable\nsealed class Action {\n    @Serializable\n    data class AddVideoAction(\n        val action: String = \"ACTION_ADD_VIDEO\",\n        val addedVideoId: String\n    ) : Action()\n\n    @Serializable\n    data class AddPlaylistAction(\n        val action: String = \"ACTION_ADD_PLAYLIST\",\n        val addedFullListId: String\n    ) : Action()\n\n    @Serializable\n    data class MoveVideoAction(\n        val action: String = \"ACTION_MOVE_VIDEO_BEFORE\",\n        val setVideoId: String,\n        val movedSetVideoIdSuccessor: String?\n    ) : Action()\n\n    @Serializable\n    data class RemoveVideoAction(\n        val action: String = \"ACTION_REMOVE_VIDEO\",\n        val setVideoId: String,\n        val removedVideoId: String\n    ) : Action()\n\n    @Serializable\n    data class RenamePlaylistAction(\n        val action: String = \"ACTION_SET_PLAYLIST_NAME\",\n        val playlistName: String\n    ) : Action()\n\n    @Serializable\n    data class SetCustomThumbnailAction(\n        val action: String = \"ACTION_SET_CUSTOM_THUMBNAIL\",\n        val addedCustomThumbnail: AddedCustomThumbnail\n    ) : Action() {\n        @Serializable\n        data class AddedCustomThumbnail(\n            val imageKey: ImageKey = ImageKey(\n                name = \"studio_square_thumbnail\",\n                type = \"PLAYLIST_IMAGE_TYPE_CUSTOM_THUMBNAIL\"\n            ),\n            val playlistScottyEncryptedBlobId: String\n        ) {\n            @Serializable\n            data class ImageKey(\n                val name: String,\n                val type: String\n            )\n        }\n    }\n\n    @Serializable\n    data class RemoveCustomThumbnailAction(\n        val action: String = \"ACTION_REMOVE_CUSTOM_THUMBNAIL\",\n        val deletedCustomThumbnail: DeletedCustomThumbnail = DeletedCustomThumbnail()\n    ) : Action() {\n        @Serializable\n        data class DeletedCustomThumbnail(\n            val name: String = \"studio_square_thumbnail\",\n            val type: String = \"PLAYLIST_IMAGE_TYPE_CUSTOM_THUMBNAIL\"\n        )\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/body/FeedbackBody.kt",
    "content": "package com.metrolist.innertube.models.body\n\nimport com.metrolist.innertube.models.Context\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class FeedbackBody(\n    val context: Context,\n    val feedbackTokens: List<String>,\n    val isFeedbackTokenUnencrypted: Boolean = false,\n    val shouldMerge: Boolean = false,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/body/GetQueueBody.kt",
    "content": "package com.metrolist.innertube.models.body\n\nimport com.metrolist.innertube.models.Context\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GetQueueBody(\n    val context: Context,\n    val videoIds: List<String>?,\n    val playlistId: String?,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/body/GetSearchSuggestionsBody.kt",
    "content": "package com.metrolist.innertube.models.body\n\nimport com.metrolist.innertube.models.Context\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GetSearchSuggestionsBody(\n    val context: Context,\n    val input: String,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/body/GetTranscriptBody.kt",
    "content": "package com.metrolist.innertube.models.body\n\nimport com.metrolist.innertube.models.Context\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GetTranscriptBody(\n    val context: Context,\n    val params: String,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/body/LikeBody.kt",
    "content": "package com.metrolist.innertube.models.body\n\nimport com.metrolist.innertube.models.Context\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class LikeBody(\n    val context: Context,\n    val target: Target,\n) {\n    /**\n     * Target for like/unlike operations.\n     * Note: Only one of videoId or playlistId should be set.\n     * Using a flat structure instead of sealed class to avoid type discriminator in serialization.\n     */\n    @Serializable\n    data class Target(\n        val videoId: String? = null,\n        val playlistId: String? = null,\n    ) {\n        companion object {\n            fun video(id: String) = Target(videoId = id)\n            fun playlist(id: String) = Target(playlistId = id)\n        }\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/body/NextBody.kt",
    "content": "package com.metrolist.innertube.models.body\n\nimport com.metrolist.innertube.models.Context\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class NextBody(\n    val context: Context,\n    val videoId: String?,\n    val playlistId: String?,\n    val playlistSetVideoId: String?,\n    val index: Int?,\n    val params: String?,\n    val continuation: String?,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/body/PlayerBody.kt",
    "content": "package com.metrolist.innertube.models.body\n\nimport com.metrolist.innertube.models.Context\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class PlayerBody(\n    val context: Context,\n    val videoId: String,\n    val playlistId: String?,\n    val playbackContext: PlaybackContext? = null,\n    val serviceIntegrityDimensions: ServiceIntegrityDimensions? = null,\n    val contentCheckOk: Boolean = true,\n    val racyCheckOk: Boolean = true,\n) {\n    @Serializable\n    data class PlaybackContext(\n        val contentPlaybackContext: ContentPlaybackContext\n    ) {\n        @Serializable\n        data class ContentPlaybackContext(\n            val signatureTimestamp: Int\n        )\n    }\n\n    @Serializable\n    data class ServiceIntegrityDimensions(\n        val poToken: String\n    )\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/body/SearchBody.kt",
    "content": "package com.metrolist.innertube.models.body\n\nimport com.metrolist.innertube.models.Context\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class SearchBody(\n    val context: Context,\n    val query: String?,\n    val params: String?,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/body/SubscribeBody.kt",
    "content": "package com.metrolist.innertube.models.body\n\nimport com.metrolist.innertube.models.Context\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class SubscribeBody(\n    val channelIds: List<String>,\n    val context: Context,\n    val params: String? = null,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/response/AccountMenuResponse.kt",
    "content": "package com.metrolist.innertube.models.response\n\nimport com.metrolist.innertube.models.AccountInfo\nimport com.metrolist.innertube.models.Runs\nimport com.metrolist.innertube.models.Thumbnails\nimport com.metrolist.innertube.models.Thumbnail\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class AccountMenuResponse(\n    val actions: List<Action>,\n) {\n    @Serializable\n    data class Action(\n        val openPopupAction: OpenPopupAction,\n    ) {\n        @Serializable\n        data class OpenPopupAction(\n            val popup: Popup,\n        ) {\n            @Serializable\n            data class Popup(\n                val multiPageMenuRenderer: MultiPageMenuRenderer,\n            ) {\n                @Serializable\n                data class MultiPageMenuRenderer(\n                    val header: Header?,\n                ) {\n                    @Serializable\n                    data class Header(\n                        val activeAccountHeaderRenderer: ActiveAccountHeaderRenderer,\n                    ) {\n                        @Serializable\n                        data class ActiveAccountHeaderRenderer(\n                            val accountName: Runs,\n                            val email: Runs?,\n                            val channelHandle: Runs?,\n                            val accountPhoto: Thumbnails,\n                        ) {\n                            fun toAccountInfo() =\n                                AccountInfo(\n                                    name = accountName.runs!!.first().text,\n                                    email = email?.runs?.first()?.text,\n                                    channelHandle = channelHandle?.runs?.first()?.text,\n                                    thumbnailUrl = accountPhoto.thumbnails.lastOrNull()?.url,\n                                )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/response/AddItemYouTubePlaylistResponse.kt",
    "content": "package com.metrolist.innertube.models.response\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class AddItemYouTubePlaylistResponse(\n    val status: String,\n    val playlistEditResults: List<PlaylistEditResult>\n) {\n    @Serializable\n    data class PlaylistEditResult(\n        val playlistEditVideoAddedResultData: PlaylistEditVideoAddedResultData,\n    ) {\n        @Serializable\n        data class PlaylistEditVideoAddedResultData(\n            val setVideoId: String,\n            val videoId: String\n        )\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/response/BrowseResponse.kt",
    "content": "package com.metrolist.innertube.models.response\n\nimport com.metrolist.innertube.models.Button\nimport com.metrolist.innertube.models.Continuation\nimport com.metrolist.innertube.models.GridRenderer\nimport com.metrolist.innertube.models.Menu\nimport com.metrolist.innertube.models.MusicDetailHeaderRenderer\nimport com.metrolist.innertube.models.MusicEditablePlaylistDetailHeaderRenderer\nimport com.metrolist.innertube.models.MusicShelfRenderer\nimport com.metrolist.innertube.models.ResponseContext\nimport com.metrolist.innertube.models.Runs\nimport com.metrolist.innertube.models.SectionListRenderer\nimport com.metrolist.innertube.models.SubscriptionButton\nimport com.metrolist.innertube.models.Tabs\nimport com.metrolist.innertube.models.ThumbnailRenderer\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class BrowseResponse(\n    val contents: Contents?,\n    val continuationContents: ContinuationContents?,\n    val onResponseReceivedActions: List<ResponseAction>?,\n    val header: Header?,\n    val microformat: Microformat?,\n    val responseContext: ResponseContext,\n    val background: ThumbnailRenderer?\n) {\n    @Serializable\n    data class Contents(\n        val singleColumnBrowseResultsRenderer: Tabs?,\n        val sectionListRenderer: SectionListRenderer?,\n        val twoColumnBrowseResultsRenderer: TwoColumnBrowseResultsRenderer?,\n    )\n\n    @Serializable\n    data class TwoColumnBrowseResultsRenderer(\n        val tabs: List<Tabs.Tab?>?,\n        val secondaryContents: SecondaryContents?,\n    )\n    @Serializable\n    data class SecondaryContents(\n        val sectionListRenderer: SectionListRenderer?,\n    )\n\n    @Serializable\n    data class ContinuationContents(\n        val sectionListContinuation: SectionListContinuation?,\n        val musicPlaylistShelfContinuation: MusicPlaylistShelfContinuation?,\n        val gridContinuation: GridContinuation?,\n        val musicShelfContinuation: MusicShelfRenderer?\n    ) {\n        @Serializable\n        data class SectionListContinuation(\n            val contents: List<SectionListRenderer.Content> = emptyList(),\n            val continuations: List<Continuation>?,\n        )\n\n        @Serializable\n        data class MusicPlaylistShelfContinuation(\n            val contents: List<MusicShelfRenderer.Content> = emptyList(),\n            val continuations: List<Continuation>?,\n        )\n\n        @Serializable\n        data class GridContinuation(\n            val items: List<GridRenderer.Item> = emptyList(),\n            val continuations: List<Continuation>?,\n        )\n    }\n\n    @Serializable\n    data class ResponseAction(\n        val appendContinuationItemsAction: ContinuationItems?,\n    ) {\n        @Serializable\n        data class ContinuationItems(\n            val continuationItems: List<MusicShelfRenderer.Content>?,\n        )\n    }\n\n    @Serializable\n    data class Header(\n        val musicImmersiveHeaderRenderer: MusicImmersiveHeaderRenderer?,\n        val musicDetailHeaderRenderer: MusicDetailHeaderRenderer?,\n        val musicEditablePlaylistDetailHeaderRenderer: MusicEditablePlaylistDetailHeaderRenderer?,\n        val musicVisualHeaderRenderer: MusicVisualHeaderRenderer?,\n        val musicHeaderRenderer: MusicHeaderRenderer?,\n    ) {\n        @Serializable\n        data class MusicImmersiveHeaderRenderer(\n            val title: Runs,\n            val description: Runs?,\n            val thumbnail: ThumbnailRenderer?,\n            val playButton: Button?,\n            val startRadioButton: Button?,\n            val subscriptionButton: SubscriptionButton?,\n            val menu: Menu,\n            val subscriptionButton2: SubscriptionButton2?,\n            val monthlyListenerCount: Runs? = null,\n        ) {\n            @Serializable\n            data class SubscriptionButton2(\n                val subscribeButtonRenderer: SubscribeButtonRenderer?,\n            ) {\n                @Serializable\n                data class SubscribeButtonRenderer(\n                    val subscriberCountWithSubscribeText: Runs?,\n                )\n            }\n        }\n\n        @Serializable\n        data class MusicVisualHeaderRenderer(\n            val title: Runs,\n            val foregroundThumbnail: ThumbnailRenderer,\n            val thumbnail: ThumbnailRenderer?,\n            val subscriptionButton: SubscriptionButton?,\n        )\n\n        @Serializable\n        data class Buttons(\n            val menuRenderer: Menu.MenuRenderer?,\n        )\n\n        @Serializable\n        data class MusicHeaderRenderer(\n            val buttons: List<Buttons>?,\n            val title: Runs?,\n            val thumbnail: MusicThumbnailRenderer?,\n            val subtitle: Runs?,\n            val secondSubtitle: Runs?,\n            val straplineTextOne: Runs?,\n            val straplineThumbnail: MusicThumbnailRenderer?,\n        )\n        @Serializable\n        data class MusicThumbnail(\n            val url: String?,\n        )\n        @Serializable\n        data class MusicThumbnailRenderer(\n            val musicThumbnailRenderer: MusicThumbnailRenderer,\n            val thumbnails: List<MusicThumbnail>?,\n        )\n    }\n\n    @Serializable\n    data class Microformat(\n        val microformatDataRenderer: MicroformatDataRenderer?,\n    ) {\n        @Serializable\n        data class MicroformatDataRenderer(\n            val urlCanonical: String?,\n        )\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/response/ContinuationResponse.kt",
    "content": "package com.metrolist.innertube.models.response\n \n import com.metrolist.innertube.models.MusicShelfRenderer\n import kotlinx.serialization.Serializable\n \n @Serializable\n data class ContinuationResponse(\n     val onResponseReceivedActions: List<ResponseAction>?,\n ) {\n \n     @Serializable\n     data class ResponseAction(\n         val appendContinuationItemsAction: ContinuationItems?,\n     )\n \n     @Serializable\n     data class ContinuationItems(\n         val continuationItems: List<MusicShelfRenderer.Content>?,\n     )\n }\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/response/CreatePlaylistResponse.kt",
    "content": "package com.metrolist.innertube.models.response\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class CreatePlaylistResponse(\n    val playlistId: String\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/response/EditPlaylistResponse.kt",
    "content": "package com.metrolist.innertube.models.response\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class EditPlaylistResponse(\n    val newHeader: BrowseResponse.Header?,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/response/FeedbackResponse.kt",
    "content": "package com.metrolist.innertube.models.response\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class FeedbackResponse(\n    val feedbackResponses: List<Status>,\n) {\n    @Serializable\n    data class Status(\n        val isProcessed: Boolean,\n    )\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/response/GetQueueResponse.kt",
    "content": "package com.metrolist.innertube.models.response\n\nimport com.metrolist.innertube.models.PlaylistPanelRenderer\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GetQueueResponse(\n    val queueDatas: List<QueueData>,\n) {\n    @Serializable\n    data class QueueData(\n        val content: PlaylistPanelRenderer.Content,\n    )\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/response/GetSearchSuggestionsResponse.kt",
    "content": "package com.metrolist.innertube.models.response\n\nimport com.metrolist.innertube.models.SearchSuggestionsSectionRenderer\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GetSearchSuggestionsResponse(\n    val contents: List<Content>?,\n) {\n    @Serializable\n    data class Content(\n        val searchSuggestionsSectionRenderer: SearchSuggestionsSectionRenderer,\n    )\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/response/GetTranscriptResponse.kt",
    "content": "package com.metrolist.innertube.models.response\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class GetTranscriptResponse(\n    val actions: List<Action>?,\n) {\n    @Serializable\n    data class Action(\n        val updateEngagementPanelAction: UpdateEngagementPanelAction,\n    ) {\n        @Serializable\n        data class UpdateEngagementPanelAction(\n            val content: Content,\n        ) {\n            @Serializable\n            data class Content(\n                val transcriptRenderer: TranscriptRenderer,\n            ) {\n                @Serializable\n                data class TranscriptRenderer(\n                    val body: Body,\n                ) {\n                    @Serializable\n                    data class Body(\n                        val transcriptBodyRenderer: TranscriptBodyRenderer,\n                    ) {\n                        @Serializable\n                        data class TranscriptBodyRenderer(\n                            val cueGroups: List<CueGroup>,\n                        ) {\n                            @Serializable\n                            data class CueGroup(\n                                val transcriptCueGroupRenderer: TranscriptCueGroupRenderer,\n                            ) {\n                                @Serializable\n                                data class TranscriptCueGroupRenderer(\n                                    val cues: List<Cue>,\n                                ) {\n                                    @Serializable\n                                    data class Cue(\n                                        val transcriptCueRenderer: TranscriptCueRenderer,\n                                    ) {\n                                        @Serializable\n                                        data class TranscriptCueRenderer(\n                                            val cue: SimpleText,\n                                            val startOffsetMs: Long,\n                                            val durationMs: Long,\n                                        ) {\n                                            @Serializable\n                                            data class SimpleText(\n                                                val simpleText: String,\n                                            )\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/response/ImageUploadResponse.kt",
    "content": "package com.metrolist.innertube.models.response\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class ImageUploadResponse(\n    val encryptedBlobId: String\n)"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/response/NextResponse.kt",
    "content": "package com.metrolist.innertube.models.response\n\nimport com.metrolist.innertube.models.NavigationEndpoint\nimport com.metrolist.innertube.models.PlaylistPanelRenderer\nimport com.metrolist.innertube.models.Tabs\nimport com.metrolist.innertube.models.YouTubeDataPage\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class NextResponse(\n    val contents: Contents,\n    val continuationContents: ContinuationContents?,\n    val currentVideoEndpoint: NavigationEndpoint?,\n) {\n    @Serializable\n    data class Contents(\n        val singleColumnMusicWatchNextResultsRenderer: SingleColumnMusicWatchNextResultsRenderer?,\n        val twoColumnWatchNextResults: YouTubeDataPage.Contents.TwoColumnWatchNextResults?,\n    ) {\n        @Serializable\n        data class SingleColumnMusicWatchNextResultsRenderer(\n            val tabbedRenderer: TabbedRenderer?,\n        ) {\n            @Serializable\n            data class TabbedRenderer(\n                val watchNextTabbedResultsRenderer: WatchNextTabbedResultsRenderer?,\n            ) {\n                @Serializable\n                data class WatchNextTabbedResultsRenderer(\n                    val tabs: List<Tabs.Tab>,\n                )\n            }\n        }\n    }\n\n    @Serializable\n    data class ContinuationContents(\n        val playlistPanelContinuation: PlaylistPanelRenderer,\n    )\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/response/PlayerResponse.kt",
    "content": "package com.metrolist.innertube.models.response\n\nimport com.metrolist.innertube.models.ResponseContext\nimport com.metrolist.innertube.models.Thumbnails\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n/**\n * PlayerResponse with [com.metrolist.innertube.models.YouTubeClient.WEB_REMIX] client\n */\n@Serializable\ndata class PlayerResponse(\n    val responseContext: ResponseContext,\n    val playabilityStatus: PlayabilityStatus,\n    val playerConfig: PlayerConfig?,\n    val streamingData: StreamingData?,\n    val videoDetails: VideoDetails?,\n    @SerialName(\"playbackTracking\")\n    val playbackTracking: PlaybackTracking?,\n) {\n    @Serializable\n    data class PlayabilityStatus(\n        val status: String,\n        val reason: String?,\n    )\n\n    @Serializable\n    data class PlayerConfig(\n        val audioConfig: AudioConfig,\n    ) {\n        @Serializable\n        data class AudioConfig(\n            val loudnessDb: Double?,\n            val perceptualLoudnessDb: Double?,\n        )\n    }\n\n    @Serializable\n    data class StreamingData(\n        val formats: List<Format>?,\n        val adaptiveFormats: List<Format>,\n        val expiresInSeconds: Int,\n    ) {\n        @Serializable\n        data class Format(\n            val itag: Int,\n            val url: String?,\n            val mimeType: String,\n            val bitrate: Int,\n            val width: Int?,\n            val height: Int?,\n            val contentLength: Long?,\n            val quality: String,\n            val fps: Int?,\n            val qualityLabel: String?,\n            val averageBitrate: Int?,\n            val audioQuality: String?,\n            val approxDurationMs: String?,\n            val audioSampleRate: Int?,\n            val audioChannels: Int?,\n            val loudnessDb: Double?,\n            val lastModified: Long?,\n            val signatureCipher: String?,\n            val cipher: String?,\n            val audioTrack: AudioTrack?\n        ) {\n            val isAudio: Boolean\n                get() = width == null\n            val isOriginal: Boolean\n                get() = audioTrack?.isAutoDubbed == null\n\n            @Serializable\n            data class AudioTrack(\n                val displayName: String?,\n                val id: String?,\n                val isAutoDubbed: Boolean?,\n            )\n        }\n    }\n\n    @Serializable\n    data class VideoDetails(\n        val videoId: String,\n        val title: String?,\n        val author: String?,\n        val channelId: String,\n        val lengthSeconds: String,\n        val musicVideoType: String?,\n        val viewCount: String?,\n        val thumbnail: Thumbnails,\n    )\n\n    @Serializable\n    data class PlaybackTracking(\n        @SerialName(\"videostatsPlaybackUrl\")\n        val videostatsPlaybackUrl: VideostatsPlaybackUrl?,\n        @SerialName(\"videostatsWatchtimeUrl\")\n        val videostatsWatchtimeUrl: VideostatsWatchtimeUrl?,\n        @SerialName(\"atrUrl\")\n        val atrUrl: AtrUrl?,\n    ) {\n        @Serializable\n        data class VideostatsPlaybackUrl(\n            @SerialName(\"baseUrl\")\n            val baseUrl: String?,\n        )\n        @Serializable\n        data class VideostatsWatchtimeUrl(\n            @SerialName(\"baseUrl\")\n            val baseUrl: String?,\n        )\n        @Serializable\n        data class AtrUrl(\n            @SerialName(\"baseUrl\")\n            val baseUrl: String?,\n        )\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/models/response/SearchResponse.kt",
    "content": "package com.metrolist.innertube.models.response\n\nimport com.metrolist.innertube.models.Continuation\nimport com.metrolist.innertube.models.MusicResponsiveListItemRenderer\nimport com.metrolist.innertube.models.Tabs\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class SearchResponse(\n    val contents: Contents?,\n    val continuationContents: ContinuationContents?,\n) {\n    @Serializable\n    data class Contents(\n        val tabbedSearchResultsRenderer: Tabs?,\n    )\n\n    @Serializable\n    data class ContinuationContents(\n        val musicShelfContinuation: MusicShelfContinuation,\n    ) {\n        @Serializable\n        data class MusicShelfContinuation(\n            val contents: List<Content>,\n            val continuations: List<Continuation>?,\n        ) {\n            @Serializable\n            data class Content(\n                val musicResponsiveListItemRenderer: MusicResponsiveListItemRenderer,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/AlbumPage.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.Album\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.Artist\nimport com.metrolist.innertube.models.MusicResponsiveHeaderRenderer\nimport com.metrolist.innertube.models.MusicResponsiveListItemRenderer\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.getItems\nimport com.metrolist.innertube.models.oddElements\nimport com.metrolist.innertube.models.response.BrowseResponse\nimport com.metrolist.innertube.models.splitBySeparator\nimport com.metrolist.innertube.utils.parseTime\n\ndata class AlbumPage(\n    val album: AlbumItem,\n    val songs: List<SongItem>,\n    val otherVersions: List<AlbumItem>,\n) {\n    companion object {\n        fun getPlaylistId(response: BrowseResponse): String? {\n            var playlistId = response.microformat?.microformatDataRenderer?.urlCanonical?.substringAfterLast('=')\n            if (playlistId == null)\n            {\n                playlistId = response.header?.musicDetailHeaderRenderer?.menu?.menuRenderer?.topLevelButtons?.firstOrNull()\n                    ?.buttonRenderer?.navigationEndpoint?.watchPlaylistEndpoint?.playlistId\n            }\n            return playlistId\n        }\n\n        fun getTitle(response: BrowseResponse): String? {\n            val title = getHeader(response)?.title ?: response.header?.musicDetailHeaderRenderer?.title\n            return title?.runs?.firstOrNull()?.text\n        }\n\n        fun getYear(response: BrowseResponse): Int? {\n            val title = getHeader(response)?.subtitle ?: response.header?.musicDetailHeaderRenderer?.subtitle\n            return title?.runs?.lastOrNull()?.text?.toIntOrNull()\n        }\n\n        fun getThumbnail(response: BrowseResponse): String? {\n            return response.background?.musicThumbnailRenderer?.getThumbnailUrl() ?: response.header?.musicDetailHeaderRenderer?.thumbnail\n                ?.croppedSquareThumbnailRenderer?.getThumbnailUrl()\n        }\n\n        fun getArtists(response: BrowseResponse): List<Artist> {\n            val artists = getHeader(response)?.straplineTextOne?.runs?.oddElements()?.map {\n                Artist(\n                    name = it.text,\n                    id = it.navigationEndpoint?.browseEndpoint?.browseId\n                )\n            } ?: response.header?.musicDetailHeaderRenderer?.subtitle?.runs?.splitBySeparator()?.getOrNull(1)?.oddElements()?.map {\n                Artist(\n                    name = it.text,\n                    id = it.navigationEndpoint?.browseEndpoint?.browseId\n                )\n            } ?: emptyList()\n\n            return artists\n        }\n\n        private fun getHeader(response: BrowseResponse): MusicResponsiveHeaderRenderer? {\n            val tabs = response.contents?.singleColumnBrowseResultsRenderer?.tabs\n                ?: response.contents?.twoColumnBrowseResultsRenderer?.tabs\n            val section =\n                tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()\n            val header = section?.musicResponsiveHeaderRenderer\n            return header\n        }\n\n        fun getSongs(response: BrowseResponse, album: AlbumItem): List<SongItem> {\n            val tabs = response.contents?.singleColumnBrowseResultsRenderer?.tabs ?: response.contents?.twoColumnBrowseResultsRenderer?.tabs\n            val shelfRenderer = tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.firstOrNull()?.musicShelfRenderer ?:\n                response.contents?.twoColumnBrowseResultsRenderer?.secondaryContents?.sectionListRenderer?.contents?.firstOrNull()?.musicShelfRenderer\n\n            val songs = shelfRenderer?.contents?.getItems()?.mapNotNull {\n                getSong(it, album)\n            }\n            return songs ?: emptyList()\n        }\n\n        fun getSong(renderer: MusicResponsiveListItemRenderer, album: AlbumItem? = null): SongItem? {\n            // Extract library tokens using the new method that properly handles multiple toggle items\n            val libraryTokens = PageHelper.extractLibraryTokensFromMenuItems(renderer.menu?.menuRenderer?.items)\n\n            return SongItem(\n                id = renderer.playlistItemData?.videoId ?: return null,\n                title = PageHelper.extractRuns(renderer.flexColumns, \"MUSIC_VIDEO\").firstOrNull()?.text ?: return null,\n                artists = PageHelper.extractRuns(renderer.flexColumns, \"MUSIC_PAGE_TYPE_ARTIST\").map{\n                    Artist(\n                        name = it.text,\n                        id = it.navigationEndpoint?.browseEndpoint?.browseId\n                    )\n                }.ifEmpty {\n                    // Fallback to album artists if no artists found in song data\n                    album?.artists ?: emptyList()\n                },\n                album = album?.let {\n                    Album(it.title, it.browseId)\n                } ?: renderer.flexColumns.getOrNull(2)?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()?.let {\n                    Album(\n                        name = it.text,\n                        id = it.navigationEndpoint?.browseEndpoint?.browseId!!\n                    )\n                }!!,\n                duration = renderer.fixedColumns?.firstOrNull()\n                    ?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()\n                    ?.text?.parseTime() ?: return null,\n                musicVideoType = renderer.musicVideoType,\n                thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: album?.thumbnail!!,\n                explicit = renderer.badges?.find {\n                    it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                } != null,\n                libraryAddToken = libraryTokens.addToken,\n                libraryRemoveToken = libraryTokens.removeToken\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/ArtistItemsContinuationPage.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.YTItem\n\ndata class ArtistItemsContinuationPage(\n    val items: List<YTItem>,\n    val continuation: String?,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/ArtistItemsPage.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.Album\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.Artist\nimport com.metrolist.innertube.models.MusicResponsiveListItemRenderer\nimport com.metrolist.innertube.models.MusicTwoRowItemRenderer\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.YTItem\nimport com.metrolist.innertube.models.oddElements\nimport com.metrolist.innertube.models.splitBySeparator\nimport com.metrolist.innertube.utils.parseTime\n\ndata class ArtistItemsPage(\n    val title: String,\n    val items: List<YTItem>,\n    val continuation: String?,\n) {\n    companion object {\n        fun fromMusicResponsiveListItemRenderer(renderer: MusicResponsiveListItemRenderer): SongItem? {\n            // Split the secondary line by bullet separator to separate artists from other metadata (like views)\n            val secondaryLineRuns = renderer.flexColumns\n                .getOrNull(1)\n                ?.musicResponsiveListItemFlexColumnRenderer\n                ?.text\n                ?.runs\n                ?.splitBySeparator()\n\n            // Extract artists from the first segment after splitting\n            val artists = secondaryLineRuns?.firstOrNull()?.oddElements()?.map {\n                Artist(\n                    name = it.text,\n                    id = it.navigationEndpoint?.browseEndpoint?.browseId\n                )\n            }\n\n            // Extract album from last flexColumn (like SimpMusic does)\n            val album = renderer.flexColumns.lastOrNull()\n                ?.musicResponsiveListItemFlexColumnRenderer?.text?.runs\n                ?.firstOrNull()?.let {\n                    if (it.navigationEndpoint?.browseEndpoint?.browseId != null) {\n                        Album(\n                            name = it.text,\n                            id = it.navigationEndpoint.browseEndpoint.browseId\n                        )\n                    } else null\n                }\n\n            // Extract library tokens using the new method that properly handles multiple toggle items\n            val libraryTokens = PageHelper.extractLibraryTokensFromMenuItems(renderer.menu?.menuRenderer?.items)\n\n            return SongItem(\n                id = renderer.playlistItemData?.videoId ?: return null,\n                title = renderer.flexColumns.firstOrNull()\n                    ?.musicResponsiveListItemFlexColumnRenderer?.text\n                    ?.runs?.firstOrNull()?.text ?: return null,\n                artists = artists ?: return null,\n                album = album,\n                duration = renderer.fixedColumns?.firstOrNull()\n                    ?.musicResponsiveListItemFlexColumnRenderer?.text\n                    ?.runs?.firstOrNull()\n                    ?.text?.parseTime(),\n                musicVideoType = renderer.musicVideoType,\n                thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                explicit = renderer.badges?.find {\n                    it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                } != null,\n                endpoint = renderer.overlay?.musicItemThumbnailOverlayRenderer?.content?.musicPlayButtonRenderer?.playNavigationEndpoint?.watchEndpoint,\n                libraryAddToken = libraryTokens.addToken,\n                libraryRemoveToken = libraryTokens.removeToken,\n                isEpisode = renderer.isEpisode\n            )\n        }\n\n        fun fromMusicTwoRowItemRenderer(renderer: MusicTwoRowItemRenderer): YTItem? {\n            return when {\n                renderer.isAlbum -> AlbumItem(\n                    browseId = renderer.navigationEndpoint.browseEndpoint?.browseId ?: return null,\n                    playlistId = renderer.thumbnailOverlay?.musicItemThumbnailOverlayRenderer\n                        ?.content?.musicPlayButtonRenderer?.playNavigationEndpoint\n                        ?.anyWatchEndpoint?.playlistId ?: return null,\n                    title = renderer.title.runs?.firstOrNull()?.text ?: return null,\n                    artists = null,\n                    year = renderer.subtitle?.runs?.lastOrNull()?.text?.toIntOrNull(),\n                    thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                    explicit = renderer.subtitleBadges?.find {\n                        it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                    } != null\n                )\n                // Video\n                renderer.isSong -> SongItem(\n                    id = renderer.navigationEndpoint.watchEndpoint?.videoId ?: return null,\n                    title = renderer.title.runs?.firstOrNull()?.text ?: return null,\n                    artists = renderer.subtitle?.runs?.splitBySeparator()?.firstOrNull()?.oddElements()?.map {\n                        Artist(\n                            name = it.text,\n                            id = it.navigationEndpoint?.browseEndpoint?.browseId\n                        )\n                    } ?: return null,\n                    album = null,\n                    duration = null,\n                    musicVideoType = renderer.musicVideoType,\n                    thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                    endpoint = renderer.navigationEndpoint.watchEndpoint\n                )\n                renderer.isPlaylist -> PlaylistItem(\n                    id = renderer.navigationEndpoint.browseEndpoint?.browseId?.removePrefix(\"VL\") ?: return null,\n                    title = renderer.title.runs?.firstOrNull()?.text ?: return null,\n                    author = renderer.subtitle?.runs?.firstOrNull()?.let {\n                        Artist(\n                            name = it.text,\n                            id = it.navigationEndpoint?.browseEndpoint?.browseId\n                        )\n                    },\n                    songCountText = renderer.subtitle?.runs?.getOrNull(4)?.text,\n                    thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                    playEndpoint = renderer.thumbnailOverlay\n                        ?.musicItemThumbnailOverlayRenderer?.content\n                        ?.musicPlayButtonRenderer?.playNavigationEndpoint\n                        ?.watchPlaylistEndpoint ?: return null,\n                    shuffleEndpoint = renderer.menu?.menuRenderer?.items?.find {\n                        it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\"\n                    }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint ?: return null,\n                    radioEndpoint = renderer.menu.menuRenderer.items.find {\n                        it.menuNavigationItemRenderer?.icon?.iconType == \"MIX\"\n                    }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint ?: return null\n                )\n                else -> null\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/ArtistPage.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.Album\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.Artist\nimport com.metrolist.innertube.models.ArtistItem\nimport com.metrolist.innertube.models.BrowseEndpoint\nimport com.metrolist.innertube.models.EpisodeItem\nimport com.metrolist.innertube.models.MusicCarouselShelfRenderer\nimport com.metrolist.innertube.models.MusicResponsiveListItemRenderer\nimport com.metrolist.innertube.models.MusicShelfRenderer\nimport com.metrolist.innertube.models.MusicTwoRowItemRenderer\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.PodcastItem\nimport com.metrolist.innertube.models.Run\nimport com.metrolist.innertube.models.SectionListRenderer\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.WatchEndpoint\nimport com.metrolist.innertube.models.YTItem\nimport com.metrolist.innertube.models.filterExplicit\nimport com.metrolist.innertube.models.getItems\nimport com.metrolist.innertube.models.oddElements\nimport com.metrolist.innertube.models.splitBySeparator\n\ndata class ArtistSection(\n    val title: String,\n    val items: List<YTItem>,\n    val moreEndpoint: BrowseEndpoint?,\n)\n\ndata class ArtistPage(\n    val artist: ArtistItem,\n    val sections: List<ArtistSection>,\n    val description: String?,\n    val subscriberCountText: String?,\n    val monthlyListenerCount: String? = null,\n    val descriptionRuns: List<Run>? = null,\n    val isSubscribed: Boolean = false,\n) {\n    companion object {\n        fun fromSectionListRendererContent(content: SectionListRenderer.Content): ArtistSection? {\n            return when {\n                content.musicShelfRenderer != null -> fromMusicShelfRenderer(content.musicShelfRenderer)\n                content.musicCarouselShelfRenderer != null -> fromMusicCarouselShelfRenderer(content.musicCarouselShelfRenderer)\n                else -> null\n            }\n        }\n\n        private fun fromMusicShelfRenderer(renderer: MusicShelfRenderer): ArtistSection? {\n            return ArtistSection(\n                title = renderer.title?.runs?.firstOrNull()?.text ?: \"\",\n                items = renderer.contents?.getItems()?.mapNotNull {\n                    fromMusicResponsiveListItemRenderer(it)\n                }?.ifEmpty { null } ?: return null,\n                moreEndpoint = renderer.title?.runs?.firstOrNull()?.navigationEndpoint?.browseEndpoint\n            )\n        }\n\n        private fun fromMusicCarouselShelfRenderer(renderer: MusicCarouselShelfRenderer): ArtistSection? {\n            return ArtistSection(\n                title = renderer.header?.musicCarouselShelfBasicHeaderRenderer?.title?.runs?.firstOrNull()?.text ?: return null,\n                items = renderer.contents.mapNotNull { content ->\n                    content.musicTwoRowItemRenderer?.let { twoRowRenderer ->\n                        fromMusicTwoRowItemRenderer(twoRowRenderer)\n                    } ?: content.musicResponsiveListItemRenderer?.let { listItemRenderer ->\n                        fromMusicResponsiveListItemRenderer(listItemRenderer)\n                    }\n                }.ifEmpty { null } ?: return null,\n                moreEndpoint = renderer.header.musicCarouselShelfBasicHeaderRenderer.moreContentButton?.buttonRenderer?.navigationEndpoint?.browseEndpoint\n            )\n        }\n\n        private fun fromMusicResponsiveListItemRenderer(renderer: MusicResponsiveListItemRenderer): SongItem? {\n            // Split the secondary line by bullet separator to separate artists from other metadata (like views)\n            val secondaryLineRuns = renderer.flexColumns\n                .getOrNull(1)\n                ?.musicResponsiveListItemFlexColumnRenderer\n                ?.text\n                ?.runs\n                ?.splitBySeparator()\n\n            // Extract artists from the first segment after splitting\n            val artists = secondaryLineRuns?.firstOrNull()?.oddElements()?.map {\n                Artist(\n                    name = it.text,\n                    id = it.navigationEndpoint?.browseEndpoint?.browseId\n                )\n            }\n\n            // Extract album from last flexColumn (like SimpMusic)\n            val album = renderer.flexColumns.lastOrNull()\n                ?.musicResponsiveListItemFlexColumnRenderer?.text?.runs\n                ?.firstOrNull()?.let {\n                    if (it.navigationEndpoint?.browseEndpoint?.browseId != null) {\n                        Album(\n                            name = it.text,\n                            id = it.navigationEndpoint.browseEndpoint.browseId\n                        )\n                    } else null\n                }\n\n            // Extract library tokens using the new method that properly handles multiple toggle items\n            val libraryTokens = PageHelper.extractLibraryTokensFromMenuItems(renderer.menu?.menuRenderer?.items)\n\n            return SongItem(\n                id = renderer.playlistItemData?.videoId ?: return null,\n                title = renderer.flexColumns.firstOrNull()\n                    ?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()\n                    ?.text ?: return null,\n                artists = artists ?: return null,\n                album = album,\n                duration = null,\n                musicVideoType = renderer.musicVideoType,\n                thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                explicit = renderer.badges?.find {\n                    it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                } != null,\n                endpoint = renderer.overlay?.musicItemThumbnailOverlayRenderer?.content\n                    ?.musicPlayButtonRenderer?.playNavigationEndpoint?.watchEndpoint,\n                libraryAddToken = libraryTokens.addToken,\n                libraryRemoveToken = libraryTokens.removeToken\n            )\n        }\n\n        private fun fromMusicTwoRowItemRenderer(renderer: MusicTwoRowItemRenderer): YTItem? {\n            return when {\n                renderer.isSong -> {\n                    val subtitleRuns = renderer.subtitle?.runs?.oddElements() ?: return null\n                    SongItem(\n                        id = renderer.navigationEndpoint.watchEndpoint?.videoId ?: return null,\n                        title = renderer.title.runs?.firstOrNull()?.text ?: return null,\n                        artists = subtitleRuns.filter { \n                            it.navigationEndpoint?.browseEndpoint?.browseId?.startsWith(\"UC\") == true ||\n                            it.navigationEndpoint?.browseEndpoint != null\n                        }.map {\n                            Artist(\n                                name = it.text,\n                                id = it.navigationEndpoint?.browseEndpoint?.browseId\n                            )\n                        }.ifEmpty {\n                            subtitleRuns.firstOrNull()?.let { \n                                listOf(Artist(name = it.text, id = null)) \n                            } ?: emptyList()\n                        },\n                        album = null,\n                        duration = null,\n                        musicVideoType = renderer.musicVideoType,\n                        thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        explicit = renderer.subtitleBadges?.find {\n                            it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                        } != null\n                    )\n                }\n\n                renderer.isAlbum -> {\n                    AlbumItem(\n                        browseId = renderer.navigationEndpoint.browseEndpoint?.browseId ?: return null,\n                        playlistId = renderer.thumbnailOverlay?.musicItemThumbnailOverlayRenderer?.content\n                            ?.musicPlayButtonRenderer?.playNavigationEndpoint\n                            ?.anyWatchEndpoint?.playlistId ?: return null,\n                        title = renderer.title.runs?.firstOrNull()?.text ?: return null,\n                        artists = null,\n                        year = renderer.subtitle?.runs?.lastOrNull()?.text?.toIntOrNull(),\n                        thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        explicit = renderer.subtitleBadges?.find {\n                            it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                        } != null\n                    )\n                }\n\n                renderer.isPlaylist -> {\n                    // Playlist from YouTube Music\n                    PlaylistItem(\n                        id = renderer.navigationEndpoint.browseEndpoint?.browseId?.removePrefix(\"VL\") ?: return null,\n                        title = renderer.title.runs?.firstOrNull()?.text ?: return null,\n                        author = Artist(\n                            name = renderer.subtitle?.runs?.firstOrNull()?.text ?: return null,\n                            id = null\n                        ),\n                        songCountText = null,\n                        thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        playEndpoint = renderer.thumbnailOverlay\n                            ?.musicItemThumbnailOverlayRenderer?.content\n                            ?.musicPlayButtonRenderer?.playNavigationEndpoint\n                            ?.watchPlaylistEndpoint ?: return null,\n                        shuffleEndpoint = renderer.menu?.menuRenderer?.items?.find {\n                            it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\"\n                        }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint ?: return null,\n                        radioEndpoint = renderer.menu.menuRenderer.items.find {\n                            it.menuNavigationItemRenderer?.icon?.iconType == \"MIX\"\n                        }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint ?: return null\n                    )\n                }\n\n                renderer.isArtist -> {\n                    ArtistItem(\n                        id = renderer.navigationEndpoint.browseEndpoint?.browseId ?: return null,\n                        title = renderer.title.runs?.lastOrNull()?.text ?: return null,\n                        thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        channelId = renderer.menu?.menuRenderer?.items?.find {\n                            it.toggleMenuServiceItemRenderer?.defaultIcon?.iconType == \"SUBSCRIBE\"\n                        }?.toggleMenuServiceItemRenderer?.defaultServiceEndpoint?.subscribeEndpoint?.channelIds?.firstOrNull(),\n                        shuffleEndpoint = renderer.menu?.menuRenderer?.items?.find {\n                            it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\"\n                        }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint ?: return null,\n                        radioEndpoint = renderer.menu.menuRenderer.items.find {\n                            it.menuNavigationItemRenderer?.icon?.iconType == \"MIX\"\n                        }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint ?: return null,\n                    )\n                }\n\n                renderer.isEpisode -> {\n                    val videoId = renderer.thumbnailOverlay\n                        ?.musicItemThumbnailOverlayRenderer?.content\n                        ?.musicPlayButtonRenderer?.playNavigationEndpoint\n                        ?.watchEndpoint?.videoId ?: return null\n                    EpisodeItem(\n                        id = videoId,\n                        title = renderer.title.runs?.firstOrNull()?.text ?: return null,\n                        author = renderer.subtitle?.runs?.firstOrNull()?.let {\n                            Artist(name = it.text, id = it.navigationEndpoint?.browseEndpoint?.browseId)\n                        },\n                        thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        endpoint = WatchEndpoint(videoId = videoId),\n                        publishDateText = renderer.subtitle?.runs?.lastOrNull()?.text,\n                    )\n                }\n\n                renderer.isPodcast -> {\n                    PodcastItem(\n                        id = renderer.navigationEndpoint.browseEndpoint?.browseId ?: return null,\n                        title = renderer.title.runs?.firstOrNull()?.text ?: return null,\n                        author = renderer.subtitle?.runs?.firstOrNull()?.let {\n                            Artist(name = it.text, id = it.navigationEndpoint?.browseEndpoint?.browseId)\n                        },\n                        episodeCountText = renderer.subtitle?.runs?.lastOrNull()?.text,\n                        thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl(),\n                        playEndpoint = renderer.thumbnailOverlay\n                            ?.musicItemThumbnailOverlayRenderer?.content\n                            ?.musicPlayButtonRenderer?.playNavigationEndpoint\n                            ?.watchPlaylistEndpoint,\n                        shuffleEndpoint = renderer.menu?.menuRenderer?.items?.find {\n                            it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\"\n                        }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint,\n                    )\n                }\n\n                else -> null\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/BrowseResult.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.YTItem\nimport com.metrolist.innertube.models.filterExplicit\nimport com.metrolist.innertube.models.filterVideoSongs\nimport com.metrolist.innertube.models.filterYoutubeShorts\n\ndata class BrowseResult(\n    val title: String?,\n    val items: List<Item>,\n) {\n    data class Item(\n        val title: String?,\n        val items: List<YTItem>,\n    )\n\n    fun filterExplicit(enabled: Boolean = true) =\n        if (enabled) {\n            copy(\n                items =\n                    items.mapNotNull {\n                        it.copy(\n                            items =\n                                it.items\n                                    .filterExplicit()\n                                    .ifEmpty { return@mapNotNull null },\n                        )\n                    },\n            )\n        } else {\n            this\n        }\n\n    fun filterVideoSongs(disableVideos: Boolean = false) =\n        if (disableVideos) {\n            copy(\n                items =\n                    items.mapNotNull {\n                        it.copy(\n                            items =\n                                it.items\n                                    .filterVideoSongs(true)\n                                    .ifEmpty { return@mapNotNull null },\n                        )\n                    },\n            )\n        } else {\n            this\n        }\n\n    fun filterYoutubeShorts(enabled: Boolean = false) =\n        if (enabled) {\n            copy(\n                items =\n                    items.mapNotNull {\n                        it.copy(\n                            items =\n                                it.items\n                                    .filterYoutubeShorts(true)\n                                    .ifEmpty { return@mapNotNull null },\n                        )\n                    },\n            )\n        } else {\n            this\n        }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/ChartsPage.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.*\n\ndata class ChartsPage(\n    val sections: List<ChartSection>,\n    val continuation: String?\n) {\n    data class ChartSection(\n        val title: String,\n        val items: List<YTItem>,\n        val chartType: ChartType\n    )\n\n    enum class ChartType {\n        TRENDING, TOP, GENRE, NEW_RELEASES\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/ExplorePage.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.AlbumItem\n\ndata class ExplorePage(\n    val newReleaseAlbums: List<AlbumItem>,\n    val moodAndGenres: List<MoodAndGenres.Item>,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/HistoryPage.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.Album\nimport com.metrolist.innertube.models.Artist\nimport com.metrolist.innertube.models.MusicResponsiveListItemRenderer\nimport com.metrolist.innertube.models.MusicShelfRenderer\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.getItems\nimport com.metrolist.innertube.models.oddElements\nimport com.metrolist.innertube.models.splitBySeparator\nimport com.metrolist.innertube.utils.parseTime\n\ndata class HistoryPage(\n    val sections: List<HistorySection>?,\n) {\n    data class HistorySection(\n        val title: String,\n        val songs: List<SongItem>\n    )\n\n    companion object {\n        fun fromMusicShelfRenderer(renderer: MusicShelfRenderer): HistorySection {\n            return HistorySection(\n                title = renderer.title?.runs?.firstOrNull()?.text!!,\n                songs = renderer.contents?.getItems()?.mapNotNull {\n                    fromMusicResponsiveListItemRenderer(it)\n                }!!\n            )\n        }\n\n        private fun fromMusicResponsiveListItemRenderer(renderer: MusicResponsiveListItemRenderer): SongItem? {\n            // Extract library tokens using the new method that properly handles multiple toggle items\n            val libraryTokens = PageHelper.extractLibraryTokensFromMenuItems(renderer.menu?.menuRenderer?.items)\n\n            // Split the secondary line by bullet separator to separate artists from other metadata (like views)\n            val secondaryLineRuns = renderer.flexColumns\n                .getOrNull(1)\n                ?.musicResponsiveListItemFlexColumnRenderer\n                ?.text\n                ?.runs\n                ?.splitBySeparator()\n\n            return SongItem(\n                id = renderer.playlistItemData?.videoId ?: return null,\n                title = renderer.flexColumns.firstOrNull()\n                    ?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()\n                    ?.text ?: return null,\n                artists = secondaryLineRuns?.firstOrNull()?.oddElements()?.map {\n                    Artist(\n                        name = it.text,\n                        id = it.navigationEndpoint?.browseEndpoint?.browseId\n                    )\n                } ?: emptyList(),\n                album = renderer.flexColumns.getOrNull(3)?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()?.let {\n                    Album(\n                        name = it.text,\n                        id = it.navigationEndpoint?.browseEndpoint?.browseId ?: return@let null\n                    )\n                },\n                duration = renderer.fixedColumns?.firstOrNull()?.musicResponsiveListItemFlexColumnRenderer\n                    ?.text?.runs?.firstOrNull()?.text?.parseTime(),\n                musicVideoType = renderer.musicVideoType,\n                thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                explicit = renderer.badges?.find {\n                    it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                } != null,\n                endpoint = renderer.overlay?.musicItemThumbnailOverlayRenderer?.content\n                    ?.musicPlayButtonRenderer?.playNavigationEndpoint?.watchEndpoint,\n                libraryAddToken = libraryTokens.addToken,\n                libraryRemoveToken = libraryTokens.removeToken,\n                historyRemoveToken = renderer.menu?.menuRenderer?.items?.find {\n                    it.menuServiceItemRenderer?.icon?.iconType == \"REMOVE_FROM_HISTORY\"\n                }?.menuServiceItemRenderer?.serviceEndpoint?.feedbackEndpoint?.feedbackToken,\n                isEpisode = renderer.isEpisode\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/HomePage.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.Album\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.Artist\nimport com.metrolist.innertube.models.ArtistItem\nimport com.metrolist.innertube.models.BrowseEndpoint\nimport com.metrolist.innertube.models.EpisodeItem\nimport com.metrolist.innertube.models.MusicCarouselShelfRenderer\nimport com.metrolist.innertube.models.MusicMultiRowListItemRenderer\nimport com.metrolist.innertube.models.MusicResponsiveListItemRenderer\nimport com.metrolist.innertube.models.MusicTwoRowItemRenderer\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.PodcastItem\nimport com.metrolist.innertube.models.SectionListRenderer\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.YTItem\nimport com.metrolist.innertube.models.oddElements\nimport com.metrolist.innertube.models.splitBySeparator\nimport com.metrolist.innertube.models.filterExplicit\nimport com.metrolist.innertube.models.filterVideoSongs\nimport com.metrolist.innertube.utils.parseTime\nimport timber.log.Timber\n\ndata class HomePage(\n    val chips: List<Chip>?,\n    val sections: List<Section>,\n    val continuation: String? = null,\n) {\n    data class Chip(\n        val title: String,\n        val endpoint: BrowseEndpoint?,\n        val deselectEndPoint: BrowseEndpoint?,\n    ) {\n        companion object {\n            fun fromChipCloudChipRenderer(renderer: SectionListRenderer.Header.ChipCloudRenderer.Chip): Chip? {\n                return Chip(\n                    title = renderer.chipCloudChipRenderer.text?.runs?.firstOrNull()?.text ?: return null,\n                    endpoint = renderer.chipCloudChipRenderer.navigationEndpoint?.browseEndpoint,\n                    deselectEndPoint = renderer.chipCloudChipRenderer.onDeselectedCommand?.browseEndpoint,\n                )\n            }\n        }\n    }\n\n    data class Section(\n        val title: String,\n        val label: String?,\n        val thumbnail: String?,\n        val endpoint: BrowseEndpoint?,\n        val items: List<YTItem>,\n    ) {\n        companion object {\n            fun fromMusicCarouselShelfRenderer(renderer: MusicCarouselShelfRenderer): Section? {\n                val title = renderer.header?.musicCarouselShelfBasicHeaderRenderer?.title?.runs?.firstOrNull()?.text\n                Timber.d(\"HomePage section title: $title, contents: ${renderer.contents.size}\")\n\n                if (title == null) {\n                    Timber.d(\"HomePage section skipped: no title\")\n                    return null\n                }\n\n                val twoRowCount = renderer.contents.count { it.musicTwoRowItemRenderer != null }\n                val multiRowCount = renderer.contents.count { it.musicMultiRowListItemRenderer != null }\n                val responsiveCount = renderer.contents.count { it.musicResponsiveListItemRenderer != null }\n                Timber.d(\"HomePage section '$title': twoRow=$twoRowCount, multiRow=$multiRowCount, responsive=$responsiveCount\")\n\n                val items = mutableListOf<YTItem>()\n\n                // Parse musicTwoRowItemRenderer items (songs, albums, playlists, artists, podcasts)\n                renderer.contents.mapNotNull { it.musicTwoRowItemRenderer }\n                    .mapNotNull { fromMusicTwoRowItemRenderer(it) }\n                    .let { items.addAll(it) }\n\n                // Parse musicMultiRowListItemRenderer items (podcast episodes)\n                renderer.contents.mapNotNull { it.musicMultiRowListItemRenderer }\n                    .mapNotNull { fromMusicMultiRowListItemRenderer(it) }\n                    .let { items.addAll(it) }\n\n                // Parse musicResponsiveListItemRenderer items (quick picks songs)\n                renderer.contents.mapNotNull { it.musicResponsiveListItemRenderer }\n                    .mapNotNull { fromMusicResponsiveListItemRenderer(it) }\n                    .let { items.addAll(it) }\n\n                val podcastCount = items.count { it is PodcastItem }\n                val episodeCount = items.count { it is EpisodeItem }\n                val songCount = items.count { it is SongItem }\n                Timber.d(\"HomePage section '$title': parsed ${items.size} items (podcasts=$podcastCount, episodes=$episodeCount, songs=$songCount)\")\n\n                if (items.isEmpty()) {\n                    Timber.d(\"HomePage section '$title' skipped: no items\")\n                    return null\n                }\n\n                return Section(\n                    title = title,\n                    label = renderer.header.musicCarouselShelfBasicHeaderRenderer.strapline?.runs?.firstOrNull()?.text,\n                    thumbnail = renderer.header.musicCarouselShelfBasicHeaderRenderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl(),\n                    endpoint = renderer.header.musicCarouselShelfBasicHeaderRenderer.moreContentButton?.buttonRenderer?.navigationEndpoint?.browseEndpoint,\n                    items = items\n                )\n            }\n\n            private fun fromMusicMultiRowListItemRenderer(renderer: MusicMultiRowListItemRenderer): EpisodeItem? {\n                val subtitleRuns = renderer.subtitle?.runs?.splitBySeparator()\n                val libraryTokens = PageHelper.extractLibraryTokensFromMenuItems(renderer.menu?.menuRenderer?.items)\n\n                return EpisodeItem(\n                    id = renderer.onTap?.watchEndpoint?.videoId ?: return null,\n                    title = renderer.title?.runs?.firstOrNull()?.text ?: return null,\n                    author = null,\n                    podcast = null,\n                    duration = subtitleRuns?.lastOrNull()?.firstOrNull()?.text?.parseTime(),\n                    publishDateText = subtitleRuns?.firstOrNull()?.firstOrNull()?.text,\n                    thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                    explicit = false,\n                    endpoint = renderer.onTap.watchEndpoint,\n                    libraryAddToken = libraryTokens.addToken,\n                    libraryRemoveToken = libraryTokens.removeToken,\n                )\n            }\n\n            private fun fromMusicResponsiveListItemRenderer(renderer: MusicResponsiveListItemRenderer): SongItem? {\n                // Quick picks uses musicResponsiveListItemRenderer for songs\n                if (!renderer.isSong) return null\n\n                val secondaryLine = renderer.flexColumns\n                    .getOrNull(1)\n                    ?.musicResponsiveListItemFlexColumnRenderer\n                    ?.text\n                    ?.runs\n                    ?.splitBySeparator()\n                    ?: return null\n\n                return SongItem(\n                    id = renderer.playlistItemData?.videoId ?: return null,\n                    title = renderer.flexColumns\n                        .firstOrNull()\n                        ?.musicResponsiveListItemFlexColumnRenderer\n                        ?.text\n                        ?.runs\n                        ?.firstOrNull()\n                        ?.text ?: return null,\n                    artists = secondaryLine.getOrNull(0)?.oddElements()?.map {\n                        Artist(\n                            name = it.text,\n                            id = it.navigationEndpoint?.browseEndpoint?.browseId\n                        )\n                    } ?: return null,\n                    album = secondaryLine.getOrNull(1)?.firstOrNull()\n                        ?.takeIf { it.navigationEndpoint?.browseEndpoint != null }\n                        ?.let {\n                            Album(\n                                name = it.text,\n                                id = it.navigationEndpoint?.browseEndpoint?.browseId!!\n                            )\n                        },\n                    duration = secondaryLine.lastOrNull()?.firstOrNull()?.text?.parseTime(),\n                    thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                    explicit = renderer.badges?.find {\n                        it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                    } != null,\n                    isEpisode = renderer.isEpisode\n                )\n            }\n\n            private fun fromMusicTwoRowItemRenderer(renderer: MusicTwoRowItemRenderer): YTItem? {\n                // Debug logging for type detection\n                val title = renderer.title.runs?.firstOrNull()?.text ?: \"unknown\"\n                val pageType = renderer.navigationEndpoint.browseEndpoint\n                    ?.browseEndpointContextSupportedConfigs\n                    ?.browseEndpointContextMusicConfig\n                    ?.pageType\n                val hasWatchEndpoint = renderer.navigationEndpoint.watchEndpoint != null\n\n                if (!renderer.isSong && !renderer.isAlbum && !renderer.isPlaylist && !renderer.isArtist && !renderer.isPodcast && !renderer.isEpisode) {\n                    Timber.d(\"HomePage twoRow '$title': no type matched - pageType=$pageType, hasWatchEndpoint=$hasWatchEndpoint\")\n                }\n\n                // Debug for episodes\n                if (renderer.isEpisode) {\n                    val overlayVideoId = renderer.thumbnailOverlay\n                        ?.musicItemThumbnailOverlayRenderer?.content\n                        ?.musicPlayButtonRenderer?.playNavigationEndpoint\n                        ?.watchEndpoint?.videoId\n                    val browseId = renderer.navigationEndpoint.browseEndpoint?.browseId\n                    Timber.d(\"HomePage episode '$title': overlayVideoId=$overlayVideoId, browseId=$browseId\")\n                }\n\n                return when {\n                    renderer.isSong -> {\n                        val subtitleRuns = renderer.subtitle?.runs?.oddElements() ?: return null\n                        SongItem(\n                            id = renderer.navigationEndpoint.watchEndpoint?.videoId ?: return null,\n                            title = renderer.title.runs?.firstOrNull()?.text ?: return null,\n                            artists = subtitleRuns.filter { run ->\n                                run.navigationEndpoint?.browseEndpoint?.browseId?.startsWith(\"UC\") == true ||\n                                (run.navigationEndpoint?.browseEndpoint != null && \n                                 run.navigationEndpoint.browseEndpoint.browseId.startsWith(\"MPREb_\") != true)\n                            }.map { run ->\n                                Artist(\n                                    name = run.text,\n                                    id = run.navigationEndpoint?.browseEndpoint?.browseId\n                                )\n                            }.ifEmpty {\n                                subtitleRuns.firstOrNull()?.let { run -> \n                                    listOf(Artist(name = run.text, id = null)) \n                                } ?: emptyList()\n                            },\n                            album = subtitleRuns.firstOrNull { \n                                it.navigationEndpoint?.browseEndpoint?.browseId?.startsWith(\"MPREb_\") == true \n                            }?.let {\n                                Album(\n                                    name = it.text,\n                                    id = it.navigationEndpoint?.browseEndpoint?.browseId ?: return@let null\n                                )\n                            },\n                            duration = null,\n                            thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl()\n                                ?: return null,\n                            explicit = renderer.subtitleBadges?.any {\n                                it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                            } == true\n                        )\n                    }\n                    renderer.isAlbum -> {\n                        AlbumItem(\n                            browseId = renderer.navigationEndpoint.browseEndpoint?.browseId ?: return null,\n                            playlistId = renderer.thumbnailOverlay?.musicItemThumbnailOverlayRenderer?.content\n                                ?.musicPlayButtonRenderer?.playNavigationEndpoint\n                                ?.watchPlaylistEndpoint?.playlistId ?: return null,\n                            title = renderer.title.runs?.firstOrNull()?.text ?: return null,\n                            artists = renderer.subtitle?.runs?.oddElements()?.drop(1)?.map {\n                                Artist(\n                                    name = it.text,\n                                    id = it.navigationEndpoint?.browseEndpoint?.browseId\n                                )\n                            },\n                            year = null,\n                            thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                            explicit = renderer.subtitleBadges?.find {\n                                it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                            } != null\n                        )\n                    }\n\n                    renderer.isPlaylist -> {\n                        PlaylistItem(\n                            id = renderer.navigationEndpoint.browseEndpoint?.browseId?.removePrefix(\"VL\") ?: return null,\n                            title = renderer.title.runs?.firstOrNull()?.text ?: return null,\n                            author = Artist(\n                                name = renderer.subtitle?.runs?.firstOrNull()?.text ?: return null,\n                                id = null\n                            ),\n                            songCountText = null,\n                            thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                            playEndpoint = renderer.thumbnailOverlay\n                                ?.musicItemThumbnailOverlayRenderer?.content\n                                ?.musicPlayButtonRenderer?.playNavigationEndpoint\n                                ?.watchPlaylistEndpoint ?: return null,\n                            shuffleEndpoint = renderer.menu?.menuRenderer?.items?.find {\n                                it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\"\n                            }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint ?: return null,\n                            radioEndpoint = renderer.menu.menuRenderer.items.find {\n                                it.menuNavigationItemRenderer?.icon?.iconType == \"MIX\"\n                            }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint\n                        )\n                    }\n\n                    renderer.isArtist -> {\n                        ArtistItem(\n                            id = renderer.navigationEndpoint.browseEndpoint?.browseId ?: return null,\n                            title = renderer.title.runs?.lastOrNull()?.text ?: return null,\n                            thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                            shuffleEndpoint = renderer.menu?.menuRenderer?.items?.find {\n                                it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\"\n                            }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint ?: return null,\n                            radioEndpoint = renderer.menu.menuRenderer.items.find {\n                                it.menuNavigationItemRenderer?.icon?.iconType == \"MIX\"\n                            }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint ?: return null,\n                        )\n                    }\n\n                    renderer.isPodcast -> {\n                        PodcastItem(\n                            id = renderer.navigationEndpoint.browseEndpoint?.browseId ?: return null,\n                            title = renderer.title.runs?.firstOrNull()?.text ?: return null,\n                            author = renderer.subtitle?.runs?.firstOrNull()?.let {\n                                Artist(\n                                    name = it.text,\n                                    id = it.navigationEndpoint?.browseEndpoint?.browseId\n                                )\n                            },\n                            episodeCountText = null,\n                            thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl(),\n                            playEndpoint = renderer.thumbnailOverlay\n                                ?.musicItemThumbnailOverlayRenderer?.content\n                                ?.musicPlayButtonRenderer?.playNavigationEndpoint\n                                ?.watchPlaylistEndpoint,\n                            shuffleEndpoint = renderer.menu?.menuRenderer?.items?.find {\n                                it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\"\n                            }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint,\n                        )\n                    }\n\n                    renderer.isEpisode -> {\n                        val videoId = renderer.thumbnailOverlay\n                            ?.musicItemThumbnailOverlayRenderer?.content\n                            ?.musicPlayButtonRenderer?.playNavigationEndpoint\n                            ?.watchEndpoint?.videoId\n                        val titleText = renderer.title.runs?.firstOrNull()?.text\n                        val thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl()\n\n                        if (videoId == null || titleText == null || thumbnail == null) {\n                            Timber.d(\"HomePage episode FAILED: videoId=$videoId, title=$titleText, thumbnail=$thumbnail\")\n                            return null\n                        }\n\n                        val subtitleRuns = renderer.subtitle?.runs?.splitBySeparator()\n                        val libraryTokens = PageHelper.extractLibraryTokensFromMenuItems(renderer.menu?.menuRenderer?.items)\n\n                        // Find podcast link in subtitle (has isPodcastEndpoint)\n                        val podcastRun = renderer.subtitle?.runs?.find {\n                            it.navigationEndpoint?.browseEndpoint?.isPodcastEndpoint == true\n                        }\n                        val podcastAlbum = podcastRun?.let {\n                            Album(\n                                name = it.text,\n                                id = it.navigationEndpoint?.browseEndpoint?.browseId ?: return@let null\n                            )\n                        }\n\n                        Timber.d(\"HomePage episode SUCCESS: '$titleText', podcast: ${podcastAlbum?.name}\")\n                        EpisodeItem(\n                            id = videoId,\n                            title = titleText,\n                            author = subtitleRuns?.firstOrNull()?.firstOrNull()?.let {\n                                Artist(\n                                    name = it.text,\n                                    id = it.navigationEndpoint?.browseEndpoint?.browseId\n                                )\n                            },\n                            podcast = podcastAlbum,\n                            duration = subtitleRuns?.lastOrNull()?.firstOrNull()?.text?.parseTime(),\n                            publishDateText = subtitleRuns?.getOrNull(1)?.firstOrNull()?.text,\n                            thumbnail = thumbnail,\n                            explicit = renderer.subtitleBadges?.any {\n                                it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                            } == true,\n                            endpoint = renderer.thumbnailOverlay\n                                .musicItemThumbnailOverlayRenderer.content\n                                .musicPlayButtonRenderer.playNavigationEndpoint\n                                .watchEndpoint,\n                            libraryAddToken = libraryTokens.addToken,\n                            libraryRemoveToken = libraryTokens.removeToken,\n                        )\n                    }\n\n                    else -> null\n                }\n            }\n        }\n    }\n\n    fun filterExplicit(enabled: Boolean = true) =\n        if (enabled) {\n            copy(sections = sections.map {\n                it.copy(items = it.items.filterExplicit())\n            })\n        } else this\n\n    fun filterVideoSongs(disableVideos: Boolean = false) =\n        if (disableVideos) {\n            copy(sections = sections.map { section ->\n                section.copy(items = section.items.filterVideoSongs(true))\n            })\n        } else this\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/LibraryAlbumsPage.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.Album\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.Artist\nimport com.metrolist.innertube.models.ArtistItem\nimport com.metrolist.innertube.models.MusicResponsiveListItemRenderer\nimport com.metrolist.innertube.models.MusicTwoRowItemRenderer\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.YTItem\nimport com.metrolist.innertube.models.oddElements\nimport com.metrolist.innertube.utils.parseTime\n\ndata class LibraryAlbumsPage(\n    val albums: List<AlbumItem>,\n    val continuation: String?,\n) {\n    companion object {\n        fun fromMusicTwoRowItemRenderer(renderer: MusicTwoRowItemRenderer): AlbumItem? {\n            return AlbumItem(\n                        browseId = renderer.navigationEndpoint.browseEndpoint?.browseId ?: return null,\n                        playlistId = renderer.thumbnailOverlay?.musicItemThumbnailOverlayRenderer?.content\n                            ?.musicPlayButtonRenderer?.playNavigationEndpoint\n                            ?.watchPlaylistEndpoint?.playlistId ?: return null,\n                        title = renderer.title.runs?.firstOrNull()?.text ?: return null,\n                        artists = null,\n                        year = renderer.subtitle?.runs?.lastOrNull()?.text?.toIntOrNull(),\n                        thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        explicit = renderer.subtitleBadges?.find {\n                            it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                        } != null\n                    )\n        }\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/LibraryContinuationPage.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.YTItem\n\ndata class LibraryContinuationPage(\n    val items: List<YTItem>,\n    val continuation: String?,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/LibraryPage.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.Album\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.Artist\nimport com.metrolist.innertube.models.ArtistItem\nimport com.metrolist.innertube.models.MusicResponsiveListItemRenderer\nimport com.metrolist.innertube.models.MusicTwoRowItemRenderer\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.PodcastItem\nimport com.metrolist.innertube.models.Run\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.YTItem\nimport com.metrolist.innertube.models.oddElements\nimport com.metrolist.innertube.models.splitBySeparator\nimport com.metrolist.innertube.utils.parseTime\n\ndata class LibraryPage(\n    val items: List<YTItem>,\n    val continuation: String?,\n) {\n    companion object {\n        fun fromMusicTwoRowItemRenderer(renderer: MusicTwoRowItemRenderer): YTItem? {\n            return when {\n                renderer.isAlbum -> AlbumItem(\n                    browseId = renderer.navigationEndpoint.browseEndpoint?.browseId ?: return null,\n                    playlistId = renderer.thumbnailOverlay?.musicItemThumbnailOverlayRenderer?.content\n                        ?.musicPlayButtonRenderer?.playNavigationEndpoint\n                        ?.watchPlaylistEndpoint?.playlistId ?: return null,\n                    title = renderer.title.runs?.firstOrNull()?.text ?: return null,\n                    artists = parseArtists(renderer.subtitle?.runs),\n                    year = renderer.subtitle?.runs?.lastOrNull()?.text?.toIntOrNull(),\n                    thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl()\n                        ?: return null,\n                    explicit = renderer.subtitleBadges?.find {\n                        it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                    } != null\n                )\n\n                renderer.isPlaylist -> PlaylistItem(\n                    id = renderer.navigationEndpoint.browseEndpoint?.browseId?.removePrefix(\"VL\") ?: return null,\n                    title = renderer.title.runs?.firstOrNull()?.text ?: return null,\n                    author = renderer.subtitle?.runs?.firstOrNull()?.let {\n                        Artist(\n                            name = it.text,\n                            id = it.navigationEndpoint?.browseEndpoint?.browseId\n                        )\n                    },\n                    songCountText = renderer.subtitle?.runs?.lastOrNull()?.text,\n                    thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                    playEndpoint = renderer.thumbnailOverlay\n                        ?.musicItemThumbnailOverlayRenderer?.content\n                        ?.musicPlayButtonRenderer?.playNavigationEndpoint\n                        ?.watchPlaylistEndpoint,\n                    shuffleEndpoint = renderer.menu?.menuRenderer?.items?.find {\n                        it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\"\n                    }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint,\n                    radioEndpoint = renderer.menu?.menuRenderer?.items?.find {\n                        it.menuNavigationItemRenderer?.icon?.iconType == \"MIX\"\n                    }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint,\n                    isEditable = renderer.menu?.menuRenderer?.items?.find {\n                        it.menuNavigationItemRenderer?.icon?.iconType == \"EDIT\"\n                    } != null\n                )\n\n                renderer.isArtist -> ArtistItem(\n                    id = renderer.navigationEndpoint.browseEndpoint?.browseId ?: return null,\n                    title = renderer.title.runs?.lastOrNull()?.text ?: return null,\n                    thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                    shuffleEndpoint = renderer.menu?.menuRenderer?.items?.find {\n                        it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\"\n                    }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint ?: return null,\n                    radioEndpoint = renderer.menu.menuRenderer.items.find {\n                        it.menuNavigationItemRenderer?.icon?.iconType == \"MIX\"\n                    }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint ?: return null,\n                )\n\n                // Podcast host channels use MUSIC_PAGE_TYPE_USER_CHANNEL (not ARTIST)\n                renderer.isUserChannel -> ArtistItem(\n                    id = renderer.navigationEndpoint.browseEndpoint?.browseId ?: return null,\n                    title = renderer.title.runs?.firstOrNull()?.text ?: return null,\n                    thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl(),\n                    shuffleEndpoint = renderer.menu?.menuRenderer?.items?.find {\n                        it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\"\n                    }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint,\n                    radioEndpoint = renderer.menu?.menuRenderer?.items?.find {\n                        it.menuNavigationItemRenderer?.icon?.iconType == \"MIX\"\n                    }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint,\n                )\n\n                renderer.isPodcast -> {\n                    val libraryTokens = PageHelper.extractLibraryTokensFromMenuItems(renderer.menu?.menuRenderer?.items)\n                    PodcastItem(\n                        id = renderer.navigationEndpoint.browseEndpoint?.browseId ?: return null,\n                        title = renderer.title.runs?.firstOrNull()?.text ?: return null,\n                        author = renderer.subtitle?.runs?.firstOrNull()?.let {\n                            Artist(\n                                name = it.text,\n                                id = it.navigationEndpoint?.browseEndpoint?.browseId\n                            )\n                        },\n                        episodeCountText = renderer.subtitle?.runs?.lastOrNull()?.text,\n                        thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl(),\n                        playEndpoint = renderer.thumbnailOverlay\n                            ?.musicItemThumbnailOverlayRenderer?.content\n                            ?.musicPlayButtonRenderer?.playNavigationEndpoint\n                            ?.watchPlaylistEndpoint,\n                        shuffleEndpoint = renderer.menu?.menuRenderer?.items?.find {\n                            it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\"\n                        }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint,\n                        libraryAddToken = libraryTokens.addToken,\n                        libraryRemoveToken = libraryTokens.removeToken,\n                    )\n                }\n\n                renderer.isEpisode || renderer.isSong -> {\n                    val libraryTokens = PageHelper.extractLibraryTokensFromMenuItems(renderer.menu?.menuRenderer?.items)\n                    val videoId = renderer.thumbnailOverlay\n                        ?.musicItemThumbnailOverlayRenderer?.content\n                        ?.musicPlayButtonRenderer?.playNavigationEndpoint\n                        ?.watchEndpoint?.videoId ?: return null\n                    val subtitleRuns = renderer.subtitle?.runs?.splitBySeparator()\n                    SongItem(\n                        id = videoId,\n                        title = renderer.title.runs?.firstOrNull()?.text ?: return null,\n                        artists = subtitleRuns?.firstOrNull()?.mapNotNull {\n                            Artist(\n                                name = it.text,\n                                id = it.navigationEndpoint?.browseEndpoint?.browseId\n                            )\n                        } ?: emptyList(),\n                        album = subtitleRuns?.getOrNull(1)?.firstOrNull()?.let {\n                            Album(\n                                name = it.text,\n                                id = it.navigationEndpoint?.browseEndpoint?.browseId ?: \"\"\n                            )\n                        },\n                        duration = subtitleRuns?.lastOrNull()?.firstOrNull()?.text?.parseTime(),\n                        thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        explicit = renderer.subtitleBadges?.any {\n                            it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                        } == true,\n                        endpoint = renderer.thumbnailOverlay\n                            .musicItemThumbnailOverlayRenderer.content\n                            .musicPlayButtonRenderer.playNavigationEndpoint\n                            .watchEndpoint,\n                        libraryAddToken = libraryTokens.addToken,\n                        libraryRemoveToken = libraryTokens.removeToken,\n                        isEpisode = renderer.isEpisode,\n                    )\n                }\n\n                else -> null\n            }\n        }\n\n        fun fromMusicResponsiveListItemRenderer(renderer: MusicResponsiveListItemRenderer): YTItem? {\n            // Extract library tokens using the new method that properly handles multiple toggle items\n            val libraryTokens = PageHelper.extractLibraryTokensFromMenuItems(renderer.menu?.menuRenderer?.items)\n\n            return when {\n                renderer.isSong -> {\n                    val videoId = renderer.playlistItemData?.videoId ?: return null\n                    val title = renderer.flexColumns.firstOrNull()\n                        ?.musicResponsiveListItemFlexColumnRenderer?.text\n                        ?.runs?.firstOrNull()?.text ?: return null\n\n                    val artistRuns = renderer.flexColumns.getOrNull(1)?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.oddElements()\n\n                    // For uploaded songs, artists may not have browseEndpoint - make it optional\n                    val artists = artistRuns?.mapNotNull {\n                        val browseId = it.navigationEndpoint?.browseEndpoint?.browseId\n                        // For uploaded songs, use empty string for artist ID if not available\n                        Artist(name = it.text, id = browseId ?: \"\")\n                    } ?: emptyList()\n\n                    val albumRun = renderer.flexColumns.getOrNull(2)?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()\n\n                    // For uploaded songs, album may not have browseEndpoint - make it optional\n                    val album = albumRun?.let {\n                        val albumBrowseId = it.navigationEndpoint?.browseEndpoint?.browseId\n                        Album(name = it.text, id = albumBrowseId ?: \"\")\n                    }\n\n                    val thumbnailUrl = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null\n\n                    // Extract uploadEntityId from delete menu item (for uploaded songs)\n                    // The entityId is nested in confirmDialogEndpoint -> content -> confirmDialogRenderer ->\n                    // confirmButton -> buttonRenderer -> command -> musicDeletePrivatelyOwnedEntityCommand -> entityId\n                    val uploadEntityId = renderer.menu?.menuRenderer?.items?.firstNotNullOfOrNull { item ->\n                        item.menuNavigationItemRenderer?.navigationEndpoint?.confirmDialogEndpoint\n                            ?.content?.confirmDialogRenderer?.confirmButton?.buttonRenderer\n                            ?.command?.musicDeletePrivatelyOwnedEntityCommand?.entityId\n                    }\n                    timber.log.Timber.d(\"Parsed uploaded song: id=$videoId, entityId=$uploadEntityId\")\n\n                    SongItem(\n                        id = videoId,\n                        title = title,\n                        artists = artists,\n                        album = album,\n                        duration = renderer.fixedColumns?.firstOrNull()?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()?.text?.parseTime(),\n                        musicVideoType = renderer.musicVideoType,\n                        thumbnail = thumbnailUrl,\n                        explicit = renderer.badges?.find {\n                            it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                        } != null,\n                        endpoint = renderer.overlay?.musicItemThumbnailOverlayRenderer?.content?.musicPlayButtonRenderer?.playNavigationEndpoint?.watchEndpoint,\n                        libraryAddToken = libraryTokens.addToken,\n                        libraryRemoveToken = libraryTokens.removeToken,\n                        isEpisode = renderer.isEpisode,\n                        uploadEntityId = uploadEntityId\n                    )\n                }\n\n                renderer.isArtist -> ArtistItem(\n                    id = renderer.navigationEndpoint?.browseEndpoint?.browseId ?: return null,\n                    title = renderer.flexColumns.firstOrNull()?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()?.text\n                        ?: return null,\n                    thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl()\n                        ?: return null,\n                    shuffleEndpoint = renderer.menu?.menuRenderer?.items\n                        ?.find { it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\" }\n                        ?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint,\n                    radioEndpoint = renderer.menu?.menuRenderer?.items\n                        ?.find { it.menuNavigationItemRenderer?.icon?.iconType == \"MIX\" }\n                        ?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint\n                )\n\n                // Podcast host channels use MUSIC_PAGE_TYPE_USER_CHANNEL (not ARTIST)\n                renderer.isUserChannel -> ArtistItem(\n                    id = renderer.navigationEndpoint?.browseEndpoint?.browseId ?: return null,\n                    title = renderer.flexColumns.firstOrNull()?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()?.text\n                        ?: return null,\n                    thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl(),\n                    shuffleEndpoint = renderer.menu?.menuRenderer?.items\n                        ?.find { it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\" }\n                        ?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint,\n                    radioEndpoint = renderer.menu?.menuRenderer?.items\n                        ?.find { it.menuNavigationItemRenderer?.icon?.iconType == \"MIX\" }\n                        ?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint\n                )\n\n                renderer.isPodcast -> {\n                    val podcastLibraryTokens = PageHelper.extractLibraryTokensFromMenuItems(renderer.menu?.menuRenderer?.items)\n                    PodcastItem(\n                        id = renderer.navigationEndpoint?.browseEndpoint?.browseId ?: return null,\n                        title = renderer.flexColumns.firstOrNull()?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()?.text\n                            ?: return null,\n                        author = renderer.flexColumns.getOrNull(1)?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()?.let {\n                            Artist(\n                                name = it.text,\n                                id = it.navigationEndpoint?.browseEndpoint?.browseId\n                            )\n                        },\n                        episodeCountText = renderer.flexColumns.getOrNull(1)?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.lastOrNull()?.text,\n                        thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl(),\n                        playEndpoint = renderer.overlay?.musicItemThumbnailOverlayRenderer?.content?.musicPlayButtonRenderer?.playNavigationEndpoint?.watchPlaylistEndpoint,\n                        shuffleEndpoint = renderer.menu?.menuRenderer?.items\n                            ?.find { it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\" }\n                            ?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint,\n                        libraryAddToken = podcastLibraryTokens.addToken,\n                        libraryRemoveToken = podcastLibraryTokens.removeToken,\n                    )\n                }\n\n                else -> null\n            }\n        }\n\n        private fun parseArtists(runs: List<Run>?): List<Artist> {\n            val artists = mutableListOf<Artist>()\n\n            if (runs != null) {\n                for (run in runs) {\n                    if (run.navigationEndpoint != null) {\n                        artists.add(\n                            Artist(\n                                id = run.navigationEndpoint.browseEndpoint?.browseId!!,\n                                name = run.text\n                            )\n                        )\n                    }\n                }\n            }\n            return artists\n        }\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/MoodAndGenres.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.BrowseEndpoint\nimport com.metrolist.innertube.models.GridRenderer\nimport com.metrolist.innertube.models.MusicNavigationButtonRenderer\nimport com.metrolist.innertube.models.SectionListRenderer\n\ndata class MoodAndGenres(\n    val title: String,\n    val items: List<Item>,\n) {\n    data class Item(\n        val title: String,\n        val stripeColor: Long,\n        val endpoint: BrowseEndpoint,\n    )\n\n    companion object {\n        fun fromSectionListRendererContent(content: SectionListRenderer.Content): MoodAndGenres? {\n            return MoodAndGenres(\n                title =\n                    content.gridRenderer\n                        ?.header\n                        ?.gridHeaderRenderer\n                        ?.title\n                        ?.runs\n                        ?.firstOrNull()\n                        ?.text ?: return null,\n                items =\n                    content.gridRenderer.items\n                        .mapNotNull(GridRenderer.Item::musicNavigationButtonRenderer)\n                        .mapNotNull(::fromMusicNavigationButtonRenderer),\n            )\n        }\n\n        fun fromMusicNavigationButtonRenderer(renderer: MusicNavigationButtonRenderer): Item? {\n            return Item(\n                title =\n                    renderer.buttonText.runs\n                        ?.firstOrNull()\n                        ?.text ?: return null,\n                stripeColor = renderer.solid?.leftStripeColor ?: return null,\n                endpoint = renderer.clickCommand.browseEndpoint ?: return null,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/NewPipe.kt",
    "content": "package com.metrolist.innertube\n\nimport com.metrolist.innertube.models.YouTubeClient\nimport com.metrolist.innertube.models.response.PlayerResponse\nimport io.ktor.http.URLBuilder\nimport io.ktor.http.parseQueryString\nimport okhttp3.OkHttpClient\nimport okhttp3.RequestBody.Companion.toRequestBody\nimport org.schabi.newpipe.extractor.NewPipe\nimport org.schabi.newpipe.extractor.downloader.Downloader\nimport org.schabi.newpipe.extractor.downloader.Request\nimport org.schabi.newpipe.extractor.downloader.Response\nimport org.schabi.newpipe.extractor.exceptions.ParsingException\nimport org.schabi.newpipe.extractor.exceptions.ReCaptchaException\nimport org.schabi.newpipe.extractor.services.youtube.YoutubeJavaScriptPlayerManager\nimport org.schabi.newpipe.extractor.stream.StreamInfo\nimport java.io.IOException\nimport java.net.Proxy\n\nclass NewPipeDownloaderImpl(\n    proxy: Proxy?,\n    proxyAuth: String? = null,\n) : Downloader() {\n    private val client =\n        OkHttpClient\n            .Builder()\n            .proxy(proxy)\n            .proxyAuthenticator { _, response ->\n                proxyAuth?.let { auth ->\n                    response.request.newBuilder()\n                        .header(\"Proxy-Authorization\", auth)\n                        .build()\n                } ?: response.request\n            }\n            .build()\n\n    @Throws(IOException::class, ReCaptchaException::class)\n    override fun execute(request: Request): Response {\n        val httpMethod = request.httpMethod()\n        val url = request.url()\n        val headers = request.headers()\n        val dataToSend = request.dataToSend()\n\n        val requestBuilder =\n            okhttp3.Request\n                .Builder()\n                .method(httpMethod, dataToSend?.toRequestBody())\n                .url(url)\n                .addHeader(\"User-Agent\", YouTubeClient.USER_AGENT_WEB)\n\n        headers.forEach { (headerName, headerValueList) ->\n            if (headerValueList.size > 1) {\n                requestBuilder.removeHeader(headerName)\n                headerValueList.forEach { headerValue ->\n                    requestBuilder.addHeader(headerName, headerValue)\n                }\n            } else if (headerValueList.size == 1) {\n                requestBuilder.header(headerName, headerValueList[0])\n            }\n        }\n\n        val response = client.newCall(requestBuilder.build()).execute()\n\n        if (response.code == 429) {\n            response.close()\n            throw ReCaptchaException(\"reCaptcha Challenge requested\", url)\n        }\n\n        val responseBodyToReturn = response.body.string()\n        val latestUrl = response.request.url.toString()\n        return Response(response.code, response.message, response.headers.toMultimap(), responseBodyToReturn, latestUrl)\n    }\n}\n\nclass NewPipeUtils(\n    downloader: Downloader,\n) {\n    init {\n        NewPipe.init(downloader)\n    }\n\n    fun getSignatureTimestamp(videoId: String): Result<Int> =\n        runCatching {\n            YoutubeJavaScriptPlayerManager.getSignatureTimestamp(videoId)\n        }\n\n    fun getStreamUrl(\n        format: PlayerResponse.StreamingData.Format,\n        videoId: String,\n    ): String? =\n        try {\n            val url =\n                format.url ?: format.signatureCipher?.let { signatureCipher ->\n                    val params = parseQueryString(signatureCipher)\n                    val obfuscatedSignature =\n                        params[\"s\"]\n                            ?: throw ParsingException(\"Could not parse cipher signature\")\n                    val signatureParam =\n                        params[\"sp\"]\n                            ?: throw ParsingException(\"Could not parse cipher signature parameter\")\n                    val url =\n                        params[\"url\"]?.let { URLBuilder(it) }\n                            ?: throw ParsingException(\"Could not parse cipher url\")\n                    url.parameters[signatureParam] =\n                        YoutubeJavaScriptPlayerManager.deobfuscateSignature(\n                            videoId,\n                            obfuscatedSignature,\n                        )\n                    url.toString()\n                } ?: throw ParsingException(\"Could not find format url\")\n\n            YoutubeJavaScriptPlayerManager.getUrlWithThrottlingParameterDeobfuscated(\n                videoId,\n                url,\n            )\n        } catch (e: Exception) {\n            // Don't print stack trace - caller handles errors\n            null\n        }\n}\n\nobject NewPipeExtractor {\n    private var newPipeDownloader: NewPipeDownloaderImpl? = null\n    private var newPipeUtils: NewPipeUtils? = null\n    private var isInitialized = false\n\n    fun init() {\n        if (!isInitialized) {\n            newPipeDownloader = NewPipeDownloaderImpl(\n                proxy = YouTube.proxy,\n                proxyAuth = YouTube.proxyAuth\n            )\n            newPipeUtils = NewPipeUtils(newPipeDownloader!!)\n            isInitialized = true\n        }\n    }\n\n    fun getSignatureTimestamp(videoId: String): Result<Int> {\n        init()\n        return newPipeUtils?.getSignatureTimestamp(videoId)\n            ?: Result.failure(Exception(\"NewPipeUtils not initialized\"))\n    }\n\n    fun getStreamUrl(\n        format: PlayerResponse.StreamingData.Format,\n        videoId: String\n    ): String? {\n        init()\n        return newPipeUtils?.getStreamUrl(format, videoId)\n    }\n\n    fun newPipePlayer(videoId: String): List<Pair<Int, String>> {\n        init()\n        return try {\n            val streamInfo = StreamInfo.getInfo(\n                NewPipe.getService(0),\n                \"https://www.youtube.com/watch?v=$videoId\"\n            )\n            val streamsList = streamInfo.audioStreams + streamInfo.videoStreams + streamInfo.videoOnlyStreams\n            streamsList.mapNotNull {\n                (it.itagItem?.id ?: return@mapNotNull null) to it.content\n            }\n        } catch (e: Exception) {\n            // Don't print stack trace - caller handles errors\n            emptyList()\n        }\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/NewReleaseAlbumPage.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.Artist\nimport com.metrolist.innertube.models.MusicTwoRowItemRenderer\nimport com.metrolist.innertube.models.oddElements\nimport com.metrolist.innertube.models.splitBySeparator\n\nobject NewReleaseAlbumPage {\n    fun fromMusicTwoRowItemRenderer(renderer: MusicTwoRowItemRenderer): AlbumItem? {\n        return AlbumItem(\n            browseId = renderer.navigationEndpoint.browseEndpoint?.browseId ?: return null,\n            playlistId =\n                renderer.thumbnailOverlay\n                    ?.musicItemThumbnailOverlayRenderer\n                    ?.content\n                    ?.musicPlayButtonRenderer\n                    ?.playNavigationEndpoint\n                    ?.watchPlaylistEndpoint\n                    ?.playlistId ?: return null,\n            title =\n                renderer.title.runs\n                    ?.firstOrNull()\n                    ?.text ?: return null,\n            artists =\n                renderer.subtitle?.runs?.splitBySeparator()?.getOrNull(1)?.oddElements()?.map {\n                    Artist(\n                        name = it.text,\n                        id = it.navigationEndpoint?.browseEndpoint?.browseId,\n                    )\n                } ?: return null,\n            year =\n                renderer.subtitle.runs\n                    .lastOrNull()\n                    ?.text\n                    ?.toIntOrNull(),\n            thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n            explicit =\n                renderer.subtitleBadges?.find {\n                    it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                } != null,\n        )\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/NextPage.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.Album\nimport com.metrolist.innertube.models.Artist\nimport com.metrolist.innertube.models.BrowseEndpoint\nimport com.metrolist.innertube.models.PlaylistPanelVideoRenderer\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.WatchEndpoint\nimport com.metrolist.innertube.models.oddElements\nimport com.metrolist.innertube.models.splitBySeparator\nimport com.metrolist.innertube.utils.parseTime\n\ndata class NextResult(\n    val title: String? = null,\n    val items: List<SongItem>,\n    val currentIndex: Int? = null,\n    val lyricsEndpoint: BrowseEndpoint? = null,\n    val relatedEndpoint: BrowseEndpoint? = null,\n    val continuation: String?,\n    val endpoint: WatchEndpoint, // current or continuation next endpoint\n)\n\nobject NextPage {\n    fun fromPlaylistPanelVideoRenderer(renderer: PlaylistPanelVideoRenderer): SongItem? {\n        val longByLineRuns = renderer.longBylineText?.runs?.splitBySeparator() ?: return null\n\n        // Extract library tokens using the new method that properly handles multiple toggle items\n        val libraryTokens = PageHelper.extractLibraryTokensFromMenuItems(renderer.menu?.menuRenderer?.items)\n\n        return SongItem(\n            id = renderer.videoId ?: return null,\n            title =\n                renderer.title\n                    ?.runs\n                    ?.firstOrNull()\n                    ?.text ?: return null,\n            artists =\n                longByLineRuns.firstOrNull()?.oddElements()?.map {\n                    Artist(\n                        name = it.text,\n                        id = it.navigationEndpoint?.browseEndpoint?.browseId,\n                    )\n                } ?: return null,\n            album =\n                longByLineRuns\n                    .getOrNull(1)\n                    ?.firstOrNull()\n                    ?.takeIf {\n                        it.navigationEndpoint?.browseEndpoint != null\n                    }?.let {\n                        Album(\n                            name = it.text,\n                            id = it.navigationEndpoint?.browseEndpoint?.browseId!!,\n                        )\n                    },\n            duration =\n                renderer.lengthText\n                    ?.runs\n                    ?.firstOrNull()\n                    ?.text\n                    ?.parseTime() ?: return null,\n            musicVideoType = renderer.navigationEndpoint.musicVideoType,\n            thumbnail =\n                renderer.thumbnail.thumbnails\n                    .lastOrNull()\n                    ?.url ?: return null,\n            explicit =\n                renderer.badges?.find {\n                    it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                } != null,\n            libraryAddToken = libraryTokens.addToken,\n            libraryRemoveToken = libraryTokens.removeToken\n        )\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/PageHelper.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.Menu\nimport com.metrolist.innertube.models.MusicResponsiveListItemRenderer.FlexColumn\nimport com.metrolist.innertube.models.Run\n\nobject PageHelper {\n    // Icon types for library management (YouTube changed these in Feb 2026)\n    // Old icons: LIBRARY_ADD (not in library), LIBRARY_SAVED/LIBRARY_REMOVE (in library)\n    // New icons: BOOKMARK_BORDER (not in library), BOOKMARK (in library)\n    // Note: KEEP/KEEP_OFF are for \"Pin to Listen Again\" - different from library!\n    private val LIBRARY_ADD_ICONS = setOf(\"LIBRARY_ADD\", \"BOOKMARK_BORDER\")\n    private val LIBRARY_SAVED_ICONS = setOf(\"LIBRARY_SAVED\", \"BOOKMARK\", \"LIBRARY_REMOVE\")\n    private val ALL_LIBRARY_ICONS = LIBRARY_ADD_ICONS + LIBRARY_SAVED_ICONS\n\n    /**\n     * Data class to hold both library feedback tokens extracted from a menu\n     */\n    data class LibraryFeedbackTokens(\n        val addToken: String?,      // Token to add song to library (from BOOKMARK_BORDER)\n        val removeToken: String?    // Token to remove song from library (from BOOKMARK)\n    )\n\n    /**\n     * Check if an icon type is a library-related icon (for filtering menu items)\n     * Excludes KEEP/KEEP_OFF which are for \"Pin to Listen Again\"\n     */\n    fun isLibraryIcon(iconType: String?): Boolean {\n        if (iconType == null) return false\n        // Exclude KEEP/KEEP_OFF (Listen Again pins)\n        if (iconType == \"KEEP\" || iconType == \"KEEP_OFF\") return false\n        return iconType in ALL_LIBRARY_ICONS || iconType.startsWith(\"LIBRARY_\")\n    }\n\n    /**\n     * Check if an icon type indicates the song is NOT in library (add state)\n     */\n    fun isAddLibraryIcon(iconType: String?): Boolean {\n        return iconType in LIBRARY_ADD_ICONS\n    }\n\n    /**\n     * Check if an icon type indicates the song IS in library (saved/remove state)\n     */\n    fun isSavedLibraryIcon(iconType: String?): Boolean {\n        return iconType in LIBRARY_SAVED_ICONS\n    }\n\n    fun extractRuns(columns: List<FlexColumn>, typeLike: String): List<Run> {\n        val filteredRuns = mutableListOf<Run>()\n        for (column in columns) {\n            val runs = column.musicResponsiveListItemFlexColumnRenderer.text?.runs\n                ?: continue\n\n            for (run in runs) {\n                val typeStr = run.navigationEndpoint?.watchEndpoint?.watchEndpointMusicSupportedConfigs?.watchEndpointMusicConfig?.musicVideoType\n                    ?: run.navigationEndpoint?.browseEndpoint?.browseEndpointContextSupportedConfigs?.browseEndpointContextMusicConfig?.pageType\n                    ?: continue\n\n                if (typeLike in typeStr) {\n                    filteredRuns.add(run)\n                }\n            }\n        }\n        return filteredRuns\n    }\n\n    /**\n     * Extract library feedback tokens from a list of menu items.\n     *\n     * This function iterates through ALL toggle menu items and extracts tokens\n     * based on their icon types, ensuring we don't confuse library tokens with\n     * \"Pin to Listen Again\" tokens (KEEP/KEEP_OFF).\n     *\n     * YouTube's icon system (Feb 2026):\n     * - BOOKMARK_BORDER: Song NOT in library -> defaultToken = ADD, toggledToken = REMOVE\n     * - BOOKMARK: Song IS in library -> defaultToken = REMOVE, toggledToken = ADD\n     * - KEEP/KEEP_OFF: \"Pin to Listen Again\" - COMPLETELY DIFFERENT, must be ignored!\n     *\n     * @param menuItems The list of menu items to search through\n     * @return LibraryFeedbackTokens containing both add and remove tokens\n     */\n    fun extractLibraryTokensFromMenuItems(\n        menuItems: List<Menu.MenuRenderer.Item>?\n    ): LibraryFeedbackTokens {\n        if (menuItems == null) return LibraryFeedbackTokens(null, null)\n\n        var addToken: String? = null\n        var removeToken: String? = null\n\n        for (item in menuItems) {\n            val toggleRenderer = item.toggleMenuServiceItemRenderer ?: continue\n            val iconType = toggleRenderer.defaultIcon.iconType\n\n            // Skip KEEP/KEEP_OFF icons (Pin to Listen Again) - these are NOT library actions\n            if (iconType == \"KEEP\" || iconType == \"KEEP_OFF\") continue\n\n            // Only process library-related icons\n            if (!isLibraryIcon(iconType)) continue\n\n            val defaultToken = toggleRenderer.defaultServiceEndpoint.feedbackEndpoint?.feedbackToken\n            val toggledToken = toggleRenderer.toggledServiceEndpoint?.feedbackEndpoint?.feedbackToken\n\n            // Determine which token is which based on icon type\n            when {\n                isAddLibraryIcon(iconType) -> {\n                    // BOOKMARK_BORDER or LIBRARY_ADD: default=add, toggled=remove\n                    if (addToken == null) addToken = defaultToken\n                    if (removeToken == null) removeToken = toggledToken\n                }\n                isSavedLibraryIcon(iconType) -> {\n                    // BOOKMARK or LIBRARY_SAVED/REMOVE: default=remove, toggled=add\n                    if (removeToken == null) removeToken = defaultToken\n                    if (addToken == null) addToken = toggledToken\n                }\n            }\n        }\n\n        return LibraryFeedbackTokens(addToken, removeToken)\n    }\n\n    /**\n     * Extract feedback token for library operations.\n     *\n     * YouTube's new icon system (Feb 2026):\n     * - BOOKMARK_BORDER: Song NOT in library -> defaultToken = ADD, toggledToken = REMOVE\n     * - BOOKMARK: Song IS in library -> defaultToken = REMOVE, toggledToken = ADD\n     *\n     * @param menu The toggle menu renderer containing the feedback tokens\n     * @param type \"LIBRARY_ADD\" to get the add token, \"LIBRARY_REMOVE\" to get the remove token\n     * @return The appropriate feedback token, or null if not found\n     */\n    fun extractFeedbackToken(menu: Menu.MenuRenderer.Item.ToggleMenuServiceRenderer?, type: String): String? {\n        if (menu == null) return null\n        val defaultToken = menu.defaultServiceEndpoint.feedbackEndpoint?.feedbackToken\n        val toggledToken = menu.toggledServiceEndpoint?.feedbackEndpoint?.feedbackToken\n        val iconType = menu.defaultIcon.iconType\n\n        // Determine if the current icon indicates song is NOT in library\n        // BOOKMARK_BORDER or LIBRARY_ADD = song is NOT in library (default action is ADD)\n        val songNotInLibrary = iconType in LIBRARY_ADD_ICONS\n\n        return when (type) {\n            \"LIBRARY_ADD\" -> {\n                // We want the ADD token\n                if (songNotInLibrary) {\n                    // Icon shows \"add\" state, default action adds to library\n                    defaultToken\n                } else {\n                    // Icon shows \"saved\" state, toggled action would add back\n                    toggledToken\n                }\n            }\n            \"LIBRARY_REMOVE\", \"LIBRARY_SAVED\" -> {\n                // We want the REMOVE token\n                if (songNotInLibrary) {\n                    // Icon shows \"add\" state, toggled action would remove\n                    toggledToken\n                } else {\n                    // Icon shows \"saved\" state, default action removes from library\n                    defaultToken\n                }\n            }\n            else -> if (iconType == type) defaultToken else toggledToken\n        }\n    }\n}"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/PlaylistContinuationPage.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.SongItem\n\ndata class PlaylistContinuationPage(\n    val songs: List<SongItem>,\n    val continuation: String?,\n)\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/PlaylistPage.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.Album\nimport com.metrolist.innertube.models.Artist\nimport com.metrolist.innertube.models.MusicResponsiveListItemRenderer\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.oddElements\nimport com.metrolist.innertube.models.splitBySeparator\nimport com.metrolist.innertube.utils.parseTime\n\ndata class PlaylistPage(\n    val playlist: PlaylistItem,\n    val songs: List<SongItem>,\n    val songsContinuation: String?,\n    val continuation: String?,\n) {\n    companion object {\n        fun fromMusicResponsiveListItemRenderer(renderer: MusicResponsiveListItemRenderer): SongItem? {\n            // Extract library tokens using the new method that properly handles multiple toggle items\n            val libraryTokens = PageHelper.extractLibraryTokensFromMenuItems(renderer.menu?.menuRenderer?.items)\n\n            // Split the secondary line by bullet separator to separate artists from other metadata (like views)\n            val secondaryLineRuns = renderer.flexColumns\n                .getOrNull(1)\n                ?.musicResponsiveListItemFlexColumnRenderer\n                ?.text\n                ?.runs\n                ?.splitBySeparator()\n\n            return SongItem(\n                id = renderer.playlistItemData?.videoId ?: return null,\n                title = renderer.flexColumns.firstOrNull()\n                    ?.musicResponsiveListItemFlexColumnRenderer?.text\n                    ?.runs?.firstOrNull()?.text ?: return null,\n                artists = secondaryLineRuns?.firstOrNull()?.oddElements()?.map {\n                    Artist(\n                        name = it.text,\n                        id = it.navigationEndpoint?.browseEndpoint?.browseId,\n                    )\n                }.orEmpty(),\n                album = renderer.flexColumns.getOrNull(2)?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()?.let {\n                    Album(\n                        name = it.text,\n                        id = it.navigationEndpoint?.browseEndpoint?.browseId ?: return@let null\n                    )\n                },\n                duration = renderer.fixedColumns?.firstOrNull()?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()?.text?.parseTime(),\n                musicVideoType = renderer.musicVideoType,\n                thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                explicit = renderer.badges?.find {\n                    it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                } != null,\n                endpoint = renderer.overlay?.musicItemThumbnailOverlayRenderer?.content?.musicPlayButtonRenderer?.playNavigationEndpoint?.watchEndpoint,\n                setVideoId = renderer.playlistItemData.playlistSetVideoId ?: return null,\n                libraryAddToken = libraryTokens.addToken,\n                libraryRemoveToken = libraryTokens.removeToken,\n                isEpisode = renderer.isEpisode\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/PodcastPage.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.Album\nimport com.metrolist.innertube.models.Artist\nimport com.metrolist.innertube.models.EpisodeItem\nimport com.metrolist.innertube.models.MusicMultiRowListItemRenderer\nimport com.metrolist.innertube.models.MusicResponsiveListItemRenderer\nimport com.metrolist.innertube.models.PodcastItem\nimport com.metrolist.innertube.models.splitBySeparator\nimport com.metrolist.innertube.utils.parseTime\n\ndata class PodcastPage(\n    val podcast: PodcastItem,\n    val episodes: List<EpisodeItem>,\n    val continuation: String?,\n    val isChannelSubscribed: Boolean = false,\n) {\n    companion object {\n        fun fromMusicMultiRowListItemRenderer(\n            renderer: MusicMultiRowListItemRenderer,\n            podcast: PodcastItem? = null\n        ): EpisodeItem? {\n            val subtitleRuns = renderer.subtitle?.runs?.splitBySeparator()\n            val libraryTokens = PageHelper.extractLibraryTokensFromMenuItems(renderer.menu?.menuRenderer?.items)\n\n            return EpisodeItem(\n                id = renderer.onTap?.watchEndpoint?.videoId ?: return null,\n                title = renderer.title?.runs?.firstOrNull()?.text ?: return null,\n                author = podcast?.author,\n                podcast = podcast?.let {\n                    Album(name = it.title, id = it.id)\n                },\n                duration = subtitleRuns?.lastOrNull()?.firstOrNull()?.text?.parseTime(),\n                publishDateText = subtitleRuns?.firstOrNull()?.firstOrNull()?.text,\n                thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                explicit = false,\n                endpoint = renderer.onTap.watchEndpoint,\n                libraryAddToken = libraryTokens.addToken,\n                libraryRemoveToken = libraryTokens.removeToken,\n            )\n        }\n\n        fun fromMusicResponsiveListItemRenderer(\n            renderer: MusicResponsiveListItemRenderer,\n            podcast: PodcastItem? = null\n        ): EpisodeItem? {\n            val secondaryLineRuns = renderer.flexColumns\n                .getOrNull(1)\n                ?.musicResponsiveListItemFlexColumnRenderer\n                ?.text\n                ?.runs\n                ?.splitBySeparator()\n            val libraryTokens = PageHelper.extractLibraryTokensFromMenuItems(renderer.menu?.menuRenderer?.items)\n\n            return EpisodeItem(\n                id = renderer.playlistItemData?.videoId ?: return null,\n                title = renderer.flexColumns.firstOrNull()\n                    ?.musicResponsiveListItemFlexColumnRenderer?.text\n                    ?.runs?.firstOrNull()?.text ?: return null,\n                author = podcast?.author ?: secondaryLineRuns?.firstOrNull()?.firstOrNull()?.let {\n                    Artist(\n                        name = it.text,\n                        id = it.navigationEndpoint?.browseEndpoint?.browseId,\n                    )\n                },\n                podcast = podcast?.let {\n                    Album(name = it.title, id = it.id)\n                },\n                duration = secondaryLineRuns?.lastOrNull()?.firstOrNull()?.text?.parseTime(),\n                publishDateText = secondaryLineRuns?.getOrNull(1)?.firstOrNull()?.text,\n                thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                explicit = renderer.badges?.find {\n                    it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                } != null,\n                endpoint = renderer.overlay?.musicItemThumbnailOverlayRenderer?.content?.musicPlayButtonRenderer?.playNavigationEndpoint?.watchEndpoint,\n                libraryAddToken = libraryTokens.addToken,\n                libraryRemoveToken = libraryTokens.removeToken,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/RelatedPage.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.Album\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.Artist\nimport com.metrolist.innertube.models.ArtistItem\nimport com.metrolist.innertube.models.MusicResponsiveListItemRenderer\nimport com.metrolist.innertube.models.MusicTwoRowItemRenderer\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.PodcastItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.YTItem\nimport com.metrolist.innertube.models.oddElements\nimport com.metrolist.innertube.models.splitBySeparator\n\ndata class RelatedPage(\n    val songs: List<SongItem>,\n    val albums: List<AlbumItem>,\n    val artists: List<ArtistItem>,\n    val playlists: List<PlaylistItem>,\n) {\n    companion object {\n        fun fromMusicResponsiveListItemRenderer(renderer: MusicResponsiveListItemRenderer): SongItem? {\n            // Extract library tokens using the new method that properly handles multiple toggle items\n            val libraryTokens = PageHelper.extractLibraryTokensFromMenuItems(renderer.menu?.menuRenderer?.items)\n\n            // Split the secondary line by bullet separator to separate artists from other metadata (like views)\n            val secondaryLineRuns = renderer.flexColumns\n                .getOrNull(1)\n                ?.musicResponsiveListItemFlexColumnRenderer\n                ?.text\n                ?.runs\n                ?.splitBySeparator()\n\n            return SongItem(\n                id = renderer.playlistItemData?.videoId ?: return null,\n                title =\n                    renderer.flexColumns\n                        .firstOrNull()\n                        ?.musicResponsiveListItemFlexColumnRenderer\n                        ?.text\n                        ?.runs\n                        ?.firstOrNull()\n                        ?.text ?: return null,\n                artists =\n                    secondaryLineRuns?.firstOrNull()?.oddElements()?.map {\n                        Artist(\n                            name = it.text,\n                            id = it.navigationEndpoint?.browseEndpoint?.browseId,\n                        )\n                    } ?: return null,\n                album =\n                    renderer.flexColumns.getOrNull(2)?.musicResponsiveListItemFlexColumnRenderer?.text?.runs?.firstOrNull()?.let {\n                        Album(\n                            name = it.text,\n                            id = it.navigationEndpoint?.browseEndpoint?.browseId ?: return null,\n                        )\n                    },\n                duration = null,\n                musicVideoType = renderer.musicVideoType,\n                thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                explicit =\n                    renderer.badges?.find {\n                        it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                    } != null,\n                libraryAddToken = libraryTokens.addToken,\n                libraryRemoveToken = libraryTokens.removeToken,\n                isEpisode = renderer.isEpisode\n            )\n        }\n\n        fun fromMusicTwoRowItemRenderer(renderer: MusicTwoRowItemRenderer): YTItem? {\n            return when {\n                renderer.isAlbum ->\n                    AlbumItem(\n                        browseId = renderer.navigationEndpoint.browseEndpoint?.browseId ?: return null,\n                        playlistId =\n                            renderer.thumbnailOverlay\n                                ?.musicItemThumbnailOverlayRenderer\n                                ?.content\n                                ?.musicPlayButtonRenderer\n                                ?.playNavigationEndpoint\n                                ?.watchPlaylistEndpoint\n                                ?.playlistId ?: return null,\n                        title =\n                            renderer.title.runs\n                                ?.firstOrNull()\n                                ?.text ?: return null,\n                        artists = listOfNotNull(Artist(\n                                name = \"\",\n                                id = renderer.menu?.menuRenderer?.items?.find {\n                                    it.menuNavigationItemRenderer?.icon?.iconType == \"ARTIST\"\n                                }?.menuNavigationItemRenderer?.navigationEndpoint?.browseEndpoint?.browseId,\n                            )),\n                        year =\n                            renderer.subtitle\n                                ?.runs\n                                ?.lastOrNull()\n                                ?.text\n                                ?.toIntOrNull(),\n                        thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        explicit =\n                            renderer.subtitleBadges?.find {\n                                it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                            } != null,\n                    )\n                renderer.isPlaylist ->\n                    PlaylistItem(\n                        id =\n                            renderer.navigationEndpoint.browseEndpoint\n                                ?.browseId\n                                ?.removePrefix(\"VL\") ?: return null,\n                        title =\n                            renderer.title.runs\n                                ?.firstOrNull()\n                                ?.text ?: return null,\n                        author = Artist(\n                            name = renderer.subtitle?.runs?.lastOrNull()?.text ?: return null,\n                            id = null\n                        ),\n                        songCountText = renderer.subtitle.runs.findLast {\n                            it.text.any { c -> c.isDigit() } && !it.text.contains(\"view\", ignoreCase = true)\n                        }?.text,\n                        thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        playEndpoint =\n                            renderer.thumbnailOverlay\n                                ?.musicItemThumbnailOverlayRenderer\n                                ?.content\n                                ?.musicPlayButtonRenderer\n                                ?.playNavigationEndpoint\n                                ?.watchPlaylistEndpoint ?: return null,\n                        shuffleEndpoint =\n                            renderer.menu\n                                ?.menuRenderer\n                                ?.items\n                                ?.find {\n                                    it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\"\n                                }?.menuNavigationItemRenderer\n                                ?.navigationEndpoint\n                                ?.watchPlaylistEndpoint ?: return null,\n                        radioEndpoint =\n                            renderer.menu.menuRenderer.items\n                                .find {\n                                    it.menuNavigationItemRenderer?.icon?.iconType == \"MIX\"\n                                }?.menuNavigationItemRenderer\n                                ?.navigationEndpoint\n                                ?.watchPlaylistEndpoint\n                    )\n                renderer.isArtist -> {\n                    ArtistItem(\n                        id = renderer.navigationEndpoint.browseEndpoint?.browseId ?: return null,\n                        title =\n                            renderer.title.runs\n                                ?.firstOrNull()\n                                ?.text ?: return null,\n                        thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        shuffleEndpoint =\n                            renderer.menu\n                                ?.menuRenderer\n                                ?.items\n                                ?.find {\n                                    it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\"\n                                }?.menuNavigationItemRenderer\n                                ?.navigationEndpoint\n                                ?.watchPlaylistEndpoint ?: return null,\n                        radioEndpoint =\n                            renderer.menu.menuRenderer.items\n                                .find {\n                                    it.menuNavigationItemRenderer?.icon?.iconType == \"MIX\"\n                                }?.menuNavigationItemRenderer\n                                ?.navigationEndpoint\n                                ?.watchPlaylistEndpoint ?: return null,\n                    )\n                }\n                renderer.isPodcast -> {\n                    PodcastItem(\n                        id = renderer.navigationEndpoint.browseEndpoint?.browseId ?: return null,\n                        title = renderer.title.runs?.firstOrNull()?.text ?: return null,\n                        author = renderer.subtitle?.runs?.firstOrNull()?.let {\n                            Artist(\n                                name = it.text,\n                                id = it.navigationEndpoint?.browseEndpoint?.browseId\n                            )\n                        },\n                        episodeCountText = renderer.subtitle?.runs?.lastOrNull()?.text,\n                        thumbnail = renderer.thumbnailRenderer.musicThumbnailRenderer?.getThumbnailUrl(),\n                        playEndpoint = renderer.thumbnailOverlay\n                            ?.musicItemThumbnailOverlayRenderer?.content\n                            ?.musicPlayButtonRenderer?.playNavigationEndpoint\n                            ?.watchPlaylistEndpoint,\n                        shuffleEndpoint = renderer.menu?.menuRenderer?.items?.find {\n                            it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\"\n                        }?.menuNavigationItemRenderer?.navigationEndpoint?.watchPlaylistEndpoint,\n                    )\n                }\n                else -> null\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/SearchPage.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.Album\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.Artist\nimport com.metrolist.innertube.models.ArtistItem\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_PODCAST_SHOW_DETAIL_PAGE\nimport com.metrolist.innertube.models.EpisodeItem\nimport com.metrolist.innertube.models.MusicResponsiveListItemRenderer\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.PodcastItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.YTItem\nimport com.metrolist.innertube.models.oddElements\nimport com.metrolist.innertube.models.splitBySeparator\nimport com.metrolist.innertube.utils.parseTime\n\ndata class SearchResult(\n    val items: List<YTItem>,\n    val continuation: String? = null,\n)\n\nobject SearchPage {\n    fun toYTItem(renderer: MusicResponsiveListItemRenderer): YTItem? {\n        val secondaryLine =\n            renderer.flexColumns\n                .getOrNull(1)\n                ?.musicResponsiveListItemFlexColumnRenderer\n                ?.text\n                ?.runs\n                ?.splitBySeparator()\n                ?: return null\n        return when {\n            // CRITICAL: Check isEpisode BEFORE isSong — both can match isSong (watchEndpoint or\n            // null navigationEndpoint), so episodes must be identified first.\n            renderer.isEpisode -> {\n                val libraryTokens = PageHelper.extractLibraryTokensFromMenuItems(renderer.menu?.menuRenderer?.items)\n\n                // The subtitle line structure differs between filtered and unfiltered search:\n                //   Unfiltered: [\"Episode\", \"·\", \"Jan 2025\", \"·\", \"Podcast Name\", \"·\", \"1:00:00\"]\n                //     → secondaryLine = [[\"Episode\"], [\"Jan 2025\"], [\"Podcast Name\"], [\"1:00:00\"]]\n                //   Filtered:   [\"Jan 2025\", \"·\", \"Podcast Name\"]\n                //     → secondaryLine = [[\"Jan 2025\"], [\"Podcast Name\"]]\n                //\n                // Strategy: locate the podcast section by its PODCAST_SHOW_DETAIL_PAGE link;\n                // the date is in the section immediately before it.\n                val podcastSectionIndex = secondaryLine.indexOfFirst { section ->\n                    section.any { run ->\n                        run.navigationEndpoint?.browseEndpoint\n                            ?.browseEndpointContextSupportedConfigs\n                            ?.browseEndpointContextMusicConfig\n                            ?.pageType == MUSIC_PAGE_TYPE_PODCAST_SHOW_DETAIL_PAGE\n                    }\n                }\n\n                val podcast = if (podcastSectionIndex >= 0) {\n                    secondaryLine[podcastSectionIndex].firstOrNull()?.let { run ->\n                        Album(\n                            name = run.text,\n                            id = run.navigationEndpoint?.browseEndpoint?.browseId ?: return null,\n                        )\n                    }\n                } else null\n\n                val publishDateText = if (podcastSectionIndex > 0)\n                    secondaryLine.getOrNull(podcastSectionIndex - 1)?.firstOrNull()?.text\n                else null\n\n                EpisodeItem(\n                    // In filtered search, playlistItemData is absent; fall back to watchEndpoint.\n                    id = renderer.playlistItemData?.videoId\n                        ?: renderer.navigationEndpoint?.watchEndpoint?.videoId\n                        ?: return null,\n                    title =\n                        renderer.flexColumns\n                            .firstOrNull()\n                            ?.musicResponsiveListItemFlexColumnRenderer\n                            ?.text\n                            ?.runs\n                            ?.firstOrNull()\n                            ?.text ?: return null,\n                    author = null,\n                    podcast = podcast,\n                    duration =\n                        secondaryLine\n                            .lastOrNull()\n                            ?.firstOrNull()\n                            ?.text\n                            ?.parseTime(),\n                    publishDateText = publishDateText,\n                    thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                    explicit =\n                        renderer.badges?.find {\n                            it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                        } != null,\n                    // In filtered search the overlay play button may be absent; fall back to the\n                    // item's own watchEndpoint so the episode is always playable.\n                    endpoint = renderer.overlay\n                        ?.musicItemThumbnailOverlayRenderer\n                        ?.content\n                        ?.musicPlayButtonRenderer\n                        ?.playNavigationEndpoint\n                        ?.watchEndpoint\n                        ?: renderer.navigationEndpoint?.watchEndpoint,\n                    libraryAddToken = libraryTokens.addToken,\n                    libraryRemoveToken = libraryTokens.removeToken,\n                )\n            }\n            renderer.isSong -> {\n                val libraryTokens = PageHelper.extractLibraryTokensFromMenuItems(renderer.menu?.menuRenderer?.items)\n\n                SongItem(\n                    id = renderer.playlistItemData?.videoId ?: return null,\n                    title =\n                        renderer.flexColumns\n                            .firstOrNull()\n                            ?.musicResponsiveListItemFlexColumnRenderer\n                            ?.text\n                            ?.runs\n                            ?.firstOrNull()\n                            ?.text ?: return null,\n                    artists =\n                        secondaryLine.firstOrNull()?.oddElements()?.map {\n                            Artist(\n                                name = it.text,\n                                id = it.navigationEndpoint?.browseEndpoint?.browseId,\n                            )\n                        } ?: return null,\n                    album =\n                        secondaryLine.getOrNull(1)?.firstOrNull()?.takeIf { it.navigationEndpoint?.browseEndpoint != null }?.let {\n                            Album(\n                                name = it.text,\n                                id = it.navigationEndpoint?.browseEndpoint?.browseId!!,\n                            )\n                        },\n                    duration =\n                        secondaryLine\n                            .lastOrNull()\n                            ?.firstOrNull()\n                            ?.text\n                            ?.parseTime(),\n                    musicVideoType = renderer.musicVideoType,\n                    thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                    explicit =\n                        renderer.badges?.find {\n                            it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                        } != null,\n                    libraryAddToken = libraryTokens.addToken,\n                    libraryRemoveToken = libraryTokens.removeToken,\n                    isEpisode = renderer.isEpisode\n                )\n            }\n            renderer.isArtist -> {\n                ArtistItem(\n                    id = renderer.navigationEndpoint?.browseEndpoint?.browseId ?: return null,\n                    title =\n                        renderer.flexColumns\n                            .firstOrNull()\n                            ?.musicResponsiveListItemFlexColumnRenderer\n                            ?.text\n                            ?.runs\n                            ?.firstOrNull()\n                            ?.text\n                            ?: return null,\n                    thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                    shuffleEndpoint =\n                        renderer.menu\n                            ?.menuRenderer\n                            ?.items\n                            ?.find { it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\" }\n                            ?.menuNavigationItemRenderer\n                            ?.navigationEndpoint\n                            ?.watchPlaylistEndpoint ?: return null,\n                    radioEndpoint =\n                        renderer.menu.menuRenderer.items\n                            .find { it.menuNavigationItemRenderer?.icon?.iconType == \"MIX\" }\n                            ?.menuNavigationItemRenderer\n                            ?.navigationEndpoint\n                            ?.watchPlaylistEndpoint ?: return null,\n                )\n            }\n            renderer.isUserChannel -> {\n                ArtistItem(\n                    id = renderer.navigationEndpoint?.browseEndpoint?.browseId ?: return null,\n                    title =\n                        renderer.flexColumns\n                            .firstOrNull()\n                            ?.musicResponsiveListItemFlexColumnRenderer\n                            ?.text\n                            ?.runs\n                            ?.firstOrNull()\n                            ?.text\n                            ?: return null,\n                    thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                    shuffleEndpoint = renderer.menu\n                        ?.menuRenderer\n                        ?.items\n                        ?.find { it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\" }\n                        ?.menuNavigationItemRenderer\n                        ?.navigationEndpoint\n                        ?.watchPlaylistEndpoint,\n                    radioEndpoint = renderer.menu\n                        ?.menuRenderer\n                        ?.items\n                        ?.find { it.menuNavigationItemRenderer?.icon?.iconType == \"MIX\" }\n                        ?.menuNavigationItemRenderer\n                        ?.navigationEndpoint\n                        ?.watchPlaylistEndpoint,\n                    isProfile = true,\n                )\n            }\n            renderer.isAlbum -> {\n                AlbumItem(\n                    browseId = renderer.navigationEndpoint?.browseEndpoint?.browseId ?: return null,\n                    playlistId =\n                        renderer.overlay\n                            ?.musicItemThumbnailOverlayRenderer\n                            ?.content\n                            ?.musicPlayButtonRenderer\n                            ?.playNavigationEndpoint\n                            ?.anyWatchEndpoint\n                            ?.playlistId\n                            ?: return null,\n                    title =\n                        renderer.flexColumns\n                            .firstOrNull()\n                            ?.musicResponsiveListItemFlexColumnRenderer\n                            ?.text\n                            ?.runs\n                            ?.firstOrNull()\n                            ?.text ?: return null,\n                    artists =\n                        secondaryLine.getOrNull(1)?.oddElements()?.map {\n                            Artist(\n                                name = it.text,\n                                id = it.navigationEndpoint?.browseEndpoint?.browseId,\n                            )\n                        } ?: return null,\n                    year =\n                        secondaryLine\n                            .getOrNull(2)\n                            ?.firstOrNull()\n                            ?.text\n                            ?.toIntOrNull(),\n                    thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                    explicit =\n                        renderer.badges?.find {\n                            it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                        } != null,\n                )\n            }\n            renderer.isPlaylist -> {\n                PlaylistItem(\n                    id =\n                        renderer.navigationEndpoint\n                            ?.browseEndpoint\n                            ?.browseId\n                            ?.removePrefix(\"VL\") ?: return null,\n                    title =\n                        renderer.flexColumns\n                            .firstOrNull()\n                            ?.musicResponsiveListItemFlexColumnRenderer\n                            ?.text\n                            ?.runs\n                            ?.firstOrNull()\n                            ?.text ?: return null,\n                    author =\n                        secondaryLine.firstOrNull()?.firstOrNull()?.let {\n                            Artist(\n                                name = it.text,\n                                id = it.navigationEndpoint?.browseEndpoint?.browseId,\n                            )\n                        } ?: return null,\n                    songCountText =\n                        renderer.flexColumns\n                            .getOrNull(1)\n                            ?.musicResponsiveListItemFlexColumnRenderer\n                            ?.text\n                            ?.runs\n                            ?.lastOrNull()\n                            ?.text ?: return null,\n                    thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                    playEndpoint =\n                        renderer.overlay\n                            ?.musicItemThumbnailOverlayRenderer\n                            ?.content\n                            ?.musicPlayButtonRenderer\n                            ?.playNavigationEndpoint\n                            ?.watchPlaylistEndpoint ?: return null,\n                    shuffleEndpoint =\n                        renderer.menu\n                            ?.menuRenderer\n                            ?.items\n                            ?.find { it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\" }\n                            ?.menuNavigationItemRenderer\n                            ?.navigationEndpoint\n                            ?.watchPlaylistEndpoint ?: return null,\n                    radioEndpoint =\n                        renderer.menu.menuRenderer.items\n                            .find { it.menuNavigationItemRenderer?.icon?.iconType == \"MIX\" }\n                            ?.menuNavigationItemRenderer\n                            ?.navigationEndpoint\n                            ?.watchPlaylistEndpoint ?: return null,\n                )\n            }\n            renderer.isPodcast -> {\n                PodcastItem(\n                    id =\n                        renderer.navigationEndpoint\n                            ?.browseEndpoint\n                            ?.browseId\n                            ?: return null,\n                    title =\n                        renderer.flexColumns\n                            .firstOrNull()\n                            ?.musicResponsiveListItemFlexColumnRenderer\n                            ?.text\n                            ?.runs\n                            ?.firstOrNull()\n                            ?.text ?: return null,\n                    author =\n                        secondaryLine.firstOrNull()?.firstOrNull()?.let {\n                            Artist(\n                                name = it.text,\n                                id = it.navigationEndpoint?.browseEndpoint?.browseId,\n                            )\n                        },\n                    episodeCountText =\n                        renderer.flexColumns\n                            .getOrNull(1)\n                            ?.musicResponsiveListItemFlexColumnRenderer\n                            ?.text\n                            ?.runs\n                            ?.lastOrNull()\n                            ?.text,\n                    thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                    playEndpoint =\n                        renderer.overlay\n                            ?.musicItemThumbnailOverlayRenderer\n                            ?.content\n                            ?.musicPlayButtonRenderer\n                            ?.playNavigationEndpoint\n                            ?.watchPlaylistEndpoint,\n                    shuffleEndpoint =\n                        renderer.menu\n                            ?.menuRenderer\n                            ?.items\n                            ?.find { it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\" }\n                            ?.menuNavigationItemRenderer\n                            ?.navigationEndpoint\n                            ?.watchPlaylistEndpoint,\n                )\n            }\n            else -> null\n        }\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/SearchSuggestionPage.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.Album\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.Artist\nimport com.metrolist.innertube.models.ArtistItem\nimport com.metrolist.innertube.models.EpisodeItem\nimport com.metrolist.innertube.models.MusicResponsiveListItemRenderer\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.PodcastItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.YTItem\nimport com.metrolist.innertube.models.clean\nimport com.metrolist.innertube.models.oddElements\nimport com.metrolist.innertube.models.splitBySeparator\nimport com.metrolist.innertube.utils.parseTime\n\nobject SearchSuggestionPage {\n    fun fromMusicResponsiveListItemRenderer(renderer: MusicResponsiveListItemRenderer): YTItem? {\n        return when {\n            renderer.isPodcast -> {\n                val secondaryLine =\n                    renderer.flexColumns\n                        .getOrNull(1)\n                        ?.musicResponsiveListItemFlexColumnRenderer\n                        ?.text\n                        ?.runs\n                        ?.splitBySeparator()\n                PodcastItem(\n                    id = renderer.navigationEndpoint?.browseEndpoint?.browseId ?: return null,\n                    title =\n                        renderer.flexColumns\n                            .firstOrNull()\n                            ?.musicResponsiveListItemFlexColumnRenderer\n                            ?.text\n                            ?.runs\n                            ?.firstOrNull()\n                            ?.text ?: return null,\n                    author = secondaryLine?.getOrNull(0)?.firstOrNull()?.let {\n                        Artist(\n                            name = it.text,\n                            id = it.navigationEndpoint?.browseEndpoint?.browseId,\n                        )\n                    },\n                    episodeCountText = secondaryLine?.lastOrNull()?.firstOrNull()?.text,\n                    thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                    playEndpoint = renderer.overlay\n                        ?.musicItemThumbnailOverlayRenderer\n                        ?.content\n                        ?.musicPlayButtonRenderer\n                        ?.playNavigationEndpoint\n                        ?.watchPlaylistEndpoint,\n                    shuffleEndpoint = renderer.menu\n                        ?.menuRenderer\n                        ?.items\n                        ?.find { it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\" }\n                        ?.menuNavigationItemRenderer\n                        ?.navigationEndpoint\n                        ?.watchPlaylistEndpoint,\n                )\n            }\n            renderer.isEpisode -> {\n                val secondaryLine =\n                    renderer.flexColumns\n                        .getOrNull(1)\n                        ?.musicResponsiveListItemFlexColumnRenderer\n                        ?.text\n                        ?.runs\n                        ?.splitBySeparator()\n                val firstSubtitle = secondaryLine?.getOrNull(0)?.firstOrNull()?.text\n                val isUnfilteredSearch = firstSubtitle == \"Episode\"\n                val dateIndex = if (isUnfilteredSearch) 1 else 0\n                val podcastIndex = if (isUnfilteredSearch) 2 else 1\n                EpisodeItem(\n                    id = renderer.playlistItemData?.videoId ?: return null,\n                    title =\n                        renderer.flexColumns\n                            .firstOrNull()\n                            ?.musicResponsiveListItemFlexColumnRenderer\n                            ?.text\n                            ?.runs\n                            ?.firstOrNull()\n                            ?.text ?: return null,\n                    author = null,\n                    podcast = secondaryLine?.getOrNull(podcastIndex)?.firstOrNull()?.takeIf {\n                        it.navigationEndpoint?.browseEndpoint != null\n                    }?.let {\n                        Album(\n                            name = it.text,\n                            id = it.navigationEndpoint?.browseEndpoint?.browseId!!,\n                        )\n                    },\n                    duration = secondaryLine?.lastOrNull()?.firstOrNull()?.text?.parseTime(),\n                    publishDateText = secondaryLine?.getOrNull(dateIndex)?.firstOrNull()?.text,\n                    thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                    endpoint = renderer.overlay\n                        ?.musicItemThumbnailOverlayRenderer\n                        ?.content\n                        ?.musicPlayButtonRenderer\n                        ?.playNavigationEndpoint\n                        ?.watchEndpoint,\n                    libraryAddToken = null,\n                    libraryRemoveToken = null,\n                )\n            }\n            renderer.isPlaylist -> {\n                val secondaryLine =\n                    renderer.flexColumns\n                        .getOrNull(1)\n                        ?.musicResponsiveListItemFlexColumnRenderer\n                        ?.text\n                        ?.runs\n                        ?.splitBySeparator()\n                PlaylistItem(\n                    id = renderer.navigationEndpoint?.browseEndpoint?.browseId\n                        ?.removePrefix(\"VL\") ?: return null,\n                    title =\n                        renderer.flexColumns\n                            .firstOrNull()\n                            ?.musicResponsiveListItemFlexColumnRenderer\n                            ?.text\n                            ?.runs\n                            ?.firstOrNull()\n                            ?.text ?: return null,\n                    author = secondaryLine?.firstOrNull()?.firstOrNull()?.let {\n                        Artist(\n                            name = it.text,\n                            id = it.navigationEndpoint?.browseEndpoint?.browseId,\n                        )\n                    } ?: return null,\n                    songCountText = secondaryLine.lastOrNull()?.firstOrNull()?.text,\n                    thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                    playEndpoint = renderer.overlay\n                        ?.musicItemThumbnailOverlayRenderer\n                        ?.content\n                        ?.musicPlayButtonRenderer\n                        ?.playNavigationEndpoint\n                        ?.watchPlaylistEndpoint ?: return null,\n                    shuffleEndpoint = renderer.menu\n                        ?.menuRenderer\n                        ?.items\n                        ?.find { it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\" }\n                        ?.menuNavigationItemRenderer\n                        ?.navigationEndpoint\n                        ?.watchPlaylistEndpoint ?: return null,\n                    radioEndpoint = renderer.menu.menuRenderer.items\n                        .find { it.menuNavigationItemRenderer?.icon?.iconType == \"MIX\" }\n                        ?.menuNavigationItemRenderer\n                        ?.navigationEndpoint\n                        ?.watchPlaylistEndpoint ?: return null,\n                )\n            }\n            renderer.isSong -> {\n                val secondaryLine =\n                    renderer.flexColumns\n                        .getOrNull(1)\n                        ?.musicResponsiveListItemFlexColumnRenderer\n                        ?.text\n                        ?.runs\n                        ?.splitBySeparator()\n                        ?.clean()\n                SongItem(\n                    id = renderer.playlistItemData?.videoId ?: return null,\n                    title =\n                        renderer.flexColumns\n                            .firstOrNull()\n                            ?.musicResponsiveListItemFlexColumnRenderer\n                            ?.text\n                            ?.runs\n                            ?.firstOrNull()\n                            ?.text ?: return null,\n                    artists =\n                        secondaryLine\n                            ?.firstOrNull()\n                            ?.oddElements()\n                            ?.map {\n                                Artist(\n                                    name = it.text,\n                                    id = it.navigationEndpoint?.browseEndpoint?.browseId,\n                                )\n                            } ?: return null,\n                    album =\n                        renderer.flexColumns\n                            .getOrNull(\n                                2,\n                            )?.musicResponsiveListItemFlexColumnRenderer\n                            ?.text\n                            ?.runs\n                            ?.firstOrNull()\n                            ?.let {\n                                Album(\n                                    name = it.text,\n                                    id = it.navigationEndpoint?.browseEndpoint?.browseId ?: return null,\n                                )\n                            },\n                    duration = null,\n                    musicVideoType = renderer.musicVideoType,\n                    thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                    explicit =\n                        renderer.badges?.find {\n                            it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                        } != null,\n                )\n            }\n            renderer.isArtist -> {\n                ArtistItem(\n                    id = renderer.navigationEndpoint?.browseEndpoint?.browseId ?: return null,\n                    title =\n                        renderer.flexColumns\n                            .firstOrNull()\n                            ?.musicResponsiveListItemFlexColumnRenderer\n                            ?.text\n                            ?.runs\n                            ?.firstOrNull()\n                            ?.text\n                            ?: return null,\n                    thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                    shuffleEndpoint =\n                        renderer.menu\n                            ?.menuRenderer\n                            ?.items\n                            ?.find { it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\" }\n                            ?.menuNavigationItemRenderer\n                            ?.navigationEndpoint\n                            ?.watchPlaylistEndpoint,\n                    radioEndpoint =\n                        renderer.menu\n                            ?.menuRenderer\n                            ?.items\n                            ?.find { it.menuNavigationItemRenderer?.icon?.iconType == \"MIX\" }\n                            ?.menuNavigationItemRenderer\n                            ?.navigationEndpoint\n                            ?.watchPlaylistEndpoint,\n                )\n            }\n            renderer.isUserChannel -> {\n                ArtistItem(\n                    id = renderer.navigationEndpoint?.browseEndpoint?.browseId ?: return null,\n                    title =\n                        renderer.flexColumns\n                            .firstOrNull()\n                            ?.musicResponsiveListItemFlexColumnRenderer\n                            ?.text\n                            ?.runs\n                            ?.firstOrNull()\n                            ?.text\n                            ?: return null,\n                    thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                    shuffleEndpoint = renderer.menu\n                        ?.menuRenderer\n                        ?.items\n                        ?.find { it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\" }\n                        ?.menuNavigationItemRenderer\n                        ?.navigationEndpoint\n                        ?.watchPlaylistEndpoint,\n                    radioEndpoint = renderer.menu\n                        ?.menuRenderer\n                        ?.items\n                        ?.find { it.menuNavigationItemRenderer?.icon?.iconType == \"MIX\" }\n                        ?.menuNavigationItemRenderer\n                        ?.navigationEndpoint\n                        ?.watchPlaylistEndpoint,\n                )\n            }\n            renderer.isAlbum -> {\n                val secondaryLine =\n                    renderer.flexColumns\n                        .getOrNull(1)\n                        ?.musicResponsiveListItemFlexColumnRenderer\n                        ?.text\n                        ?.runs\n                        ?.splitBySeparator() ?: return null\n                AlbumItem(\n                    browseId = renderer.navigationEndpoint?.browseEndpoint?.browseId ?: return null,\n                    playlistId =\n                        renderer.menu\n                            ?.menuRenderer\n                            ?.items\n                            ?.find {\n                                it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\"\n                            }?.menuNavigationItemRenderer\n                            ?.navigationEndpoint\n                            ?.watchPlaylistEndpoint\n                            ?.playlistId ?: return null,\n                    title =\n                        renderer.flexColumns\n                            .firstOrNull()\n                            ?.musicResponsiveListItemFlexColumnRenderer\n                            ?.text\n                            ?.runs\n                            ?.firstOrNull()\n                            ?.text ?: return null,\n                    artists =\n                        secondaryLine.getOrNull(1)?.oddElements()?.map {\n                            Artist(\n                                name = it.text,\n                                id = it.navigationEndpoint?.browseEndpoint?.browseId,\n                            )\n                        } ?: return null,\n                    year =\n                        secondaryLine\n                            .lastOrNull()\n                            ?.firstOrNull()\n                            ?.text\n                            ?.toIntOrNull(),\n                    thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                    explicit =\n                        renderer.badges?.find {\n                            it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                        } != null,\n                )\n            }\n            else -> null\n        }\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/pages/SearchSummaryPage.kt",
    "content": "package com.metrolist.innertube.pages\n\nimport com.metrolist.innertube.models.Album\nimport com.metrolist.innertube.models.AlbumItem\nimport com.metrolist.innertube.models.Artist\nimport com.metrolist.innertube.models.ArtistItem\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_ALBUM\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_ARTIST\nimport com.metrolist.innertube.models.BrowseEndpoint.BrowseEndpointContextSupportedConfigs.BrowseEndpointContextMusicConfig.Companion.MUSIC_PAGE_TYPE_USER_CHANNEL\nimport com.metrolist.innertube.models.EpisodeItem\nimport com.metrolist.innertube.models.MusicCardShelfRenderer\nimport com.metrolist.innertube.models.MusicResponsiveListItemRenderer\nimport com.metrolist.innertube.models.PlaylistItem\nimport com.metrolist.innertube.models.PodcastItem\nimport com.metrolist.innertube.models.SongItem\nimport com.metrolist.innertube.models.YTItem\nimport com.metrolist.innertube.models.clean\nimport com.metrolist.innertube.models.filterExplicit\nimport com.metrolist.innertube.models.filterVideoSongs\nimport com.metrolist.innertube.models.filterYoutubeShorts\nimport com.metrolist.innertube.models.oddElements\nimport com.metrolist.innertube.models.splitBySeparator\nimport com.metrolist.innertube.utils.parseTime\n\ndata class SearchSummary(\n    val title: String,\n    val items: List<YTItem>,\n)\n\ndata class SearchSummaryPage(\n    val summaries: List<SearchSummary>,\n) {\n    fun filterExplicit(enabled: Boolean) =\n        if (enabled) {\n            SearchSummaryPage(\n                summaries.mapNotNull { s ->\n                    SearchSummary(\n                        title = s.title,\n                        items =\n                            s.items.filterExplicit().ifEmpty {\n                                return@mapNotNull null\n                            },\n                    )\n                },\n            )\n        } else {\n            this\n        }\n\n    fun filterVideoSongs(disableVideos: Boolean) =\n        if (disableVideos) {\n            SearchSummaryPage(\n                summaries.mapNotNull { s ->\n                    SearchSummary(\n                        title = s.title,\n                        items =\n                            s.items.filterVideoSongs(true).ifEmpty {\n                                return@mapNotNull null\n                            },\n                    )\n                },\n            )\n        } else {\n            this\n        }\n\n    fun filterYoutubeShorts(enabled: Boolean = false) =\n        if (enabled) {\n            SearchSummaryPage(\n                summaries.mapNotNull { s ->\n                    SearchSummary(\n                        title = s.title,\n                        items =\n                            s.items.filterYoutubeShorts(true).ifEmpty {\n                                return@mapNotNull null\n                            },\n                    )\n                },\n            )\n        } else {\n            this\n        }\n\n    companion object {\n        fun fromMusicCardShelfRenderer(renderer: MusicCardShelfRenderer): YTItem? {\n            val subtitle = renderer.subtitle.runs?.splitBySeparator()\n            return when {\n                renderer.onTap.watchEndpoint != null -> {\n                    SongItem(\n                        id = renderer.onTap.watchEndpoint.videoId ?: return null,\n                        title =\n                            renderer.title.runs\n                                ?.firstOrNull()\n                                ?.text ?: return null,\n                        artists =\n                            subtitle?.getOrNull(1)?.oddElements()?.map {\n                                Artist(\n                                    name = it.text,\n                                    id = it.navigationEndpoint?.browseEndpoint?.browseId,\n                                )\n                            } ?: return null,\n                        album =\n                            subtitle.getOrNull(2)?.firstOrNull()?.takeIf { it.navigationEndpoint?.browseEndpoint != null }?.let {\n                                Album(\n                                    name = it.text,\n                                    id = it.navigationEndpoint?.browseEndpoint?.browseId!!,\n                                )\n                            },\n                        duration =\n                            subtitle\n                                .lastOrNull()\n                                ?.firstOrNull()\n                                ?.text\n                                ?.parseTime(),\n                        musicVideoType = renderer.onTap.musicVideoType,\n                        thumbnail = renderer.thumbnail.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        explicit =\n                            renderer.subtitleBadges?.find {\n                                it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                            } != null,\n                    )\n                }\n\n                renderer.onTap.browseEndpoint?.isArtistEndpoint == true -> {\n                    ArtistItem(\n                        id = renderer.onTap.browseEndpoint.browseId,\n                        title =\n                            renderer.title.runs\n                                ?.firstOrNull()\n                                ?.text ?: return null,\n                        thumbnail = renderer.thumbnail.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        shuffleEndpoint =\n                            renderer.buttons\n                                .find { it.buttonRenderer.icon?.iconType == \"MUSIC_SHUFFLE\" }\n                                ?.buttonRenderer\n                                ?.command\n                                ?.watchPlaylistEndpoint ?: return null,\n                        radioEndpoint =\n                            renderer.buttons\n                                .find { it.buttonRenderer.icon?.iconType == \"MIX\" }\n                                ?.buttonRenderer\n                                ?.command\n                                ?.watchPlaylistEndpoint ?: return null,\n                    )\n                }\n\n                renderer.onTap.browseEndpoint?.isAlbumEndpoint == true -> {\n                    AlbumItem(\n                        browseId = renderer.onTap.browseEndpoint.browseId,\n                        playlistId =\n                            renderer.buttons\n                                .firstOrNull()\n                                ?.buttonRenderer\n                                ?.command\n                                ?.anyWatchEndpoint\n                                ?.playlistId ?: return null,\n                        title =\n                            renderer.title.runs\n                                ?.firstOrNull()\n                                ?.text ?: return null,\n                        artists =\n                            subtitle?.getOrNull(1)?.oddElements()?.map {\n                                Artist(\n                                    name = it.text,\n                                    id = it.navigationEndpoint?.browseEndpoint?.browseId,\n                                )\n                            } ?: return null,\n                        year = null,\n                        thumbnail = renderer.thumbnail.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        explicit =\n                            renderer.subtitleBadges?.find {\n                                it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                            } != null,\n                    )\n                }\n\n                renderer.onTap.browseEndpoint?.isPlaylistEndpoint == true -> {\n                    PlaylistItem(\n                        id =\n                            renderer.onTap.browseEndpoint.browseId\n                                .removePrefix(\"VL\"),\n                        title =\n                            renderer.header?.musicCardShelfHeaderBasicRenderer?.title?.runs\n                                ?.joinToString(separator = \"\") { it.text }\n                                ?: return null,\n                        author =\n                            Artist(\n                                id = null,\n                                name = renderer.subtitle.runs?.joinToString { it.text } ?: return null,\n                            ),\n                        songCountText = null,\n                        thumbnail = renderer.thumbnail.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        playEndpoint =\n                            renderer.buttons\n                                .find { it.buttonRenderer.icon?.iconType == \"PLAY_ARROW\" }\n                                ?.buttonRenderer\n                                ?.command\n                                ?.watchPlaylistEndpoint\n                                ?: return null,\n                        shuffleEndpoint =\n                            renderer.buttons\n                                .find { it.buttonRenderer.icon?.iconType == \"MUSIC_SHUFFLE\" }\n                                ?.buttonRenderer\n                                ?.command\n                                ?.watchPlaylistEndpoint\n                                ?: return null,\n                        radioEndpoint = null,\n                    )\n                }\n\n                renderer.onTap.browseEndpoint?.isPodcastEndpoint == true -> {\n                    PodcastItem(\n                        id = renderer.onTap.browseEndpoint.browseId,\n                        title =\n                            renderer.header?.musicCardShelfHeaderBasicRenderer?.title?.runs\n                                ?.joinToString(separator = \"\") { it.text }\n                                ?: return null,\n                        author =\n                            Artist(\n                                id = null,\n                                name = renderer.subtitle.runs?.joinToString { it.text } ?: return null,\n                            ),\n                        episodeCountText = null,\n                        thumbnail = renderer.thumbnail.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        playEndpoint =\n                            renderer.buttons\n                                .find { it.buttonRenderer.icon?.iconType == \"PLAY_ARROW\" }\n                                ?.buttonRenderer\n                                ?.command\n                                ?.watchPlaylistEndpoint,\n                        shuffleEndpoint =\n                            renderer.buttons\n                                .find { it.buttonRenderer.icon?.iconType == \"MUSIC_SHUFFLE\" }\n                                ?.buttonRenderer\n                                ?.command\n                                ?.watchPlaylistEndpoint,\n                    )\n                }\n\n                else -> null\n            }\n        }\n\n        fun fromMusicResponsiveListItemRenderer(renderer: MusicResponsiveListItemRenderer): YTItem? {\n            val secondaryLine =\n                renderer.flexColumns\n                    .getOrNull(1)\n                    ?.musicResponsiveListItemFlexColumnRenderer\n                    ?.text\n                    ?.runs\n                    ?.splitBySeparator()\n                    ?: return null\n            return when {\n                // CRITICAL: Check isEpisode BEFORE isSong because both have videoId and no browseEndpoint\n                // Episodes are identified by firstSubtitle == \"Episode\" in unfiltered search\n                renderer.isEpisode -> {\n                    val libraryTokens = PageHelper.extractLibraryTokensFromMenuItems(renderer.menu?.menuRenderer?.items)\n\n                    // Check if firstSubtitle is \"Episode\" (unfiltered) or something else (filtered)\n                    // Unfiltered: [Episode][date][podcast]  -> date at index 1, podcast at index 2\n                    // Filtered:   [date][podcast]           -> date at index 0, podcast at index 1\n                    val firstSubtitle = secondaryLine.getOrNull(0)?.firstOrNull()?.text\n                    val isUnfilteredSearch = firstSubtitle == \"Episode\"\n                    val dateIndex = if (isUnfilteredSearch) 1 else 0\n                    val podcastIndex = if (isUnfilteredSearch) 2 else 1\n\n                    EpisodeItem(\n                        id = renderer.playlistItemData?.videoId ?: return null,\n                        title =\n                            renderer.flexColumns\n                                .firstOrNull()\n                                ?.musicResponsiveListItemFlexColumnRenderer\n                                ?.text\n                                ?.runs\n                                ?.firstOrNull()\n                                ?.text ?: return null,\n                        author = null,  // Episodes don't have a separate author - the podcast is the source\n                        podcast =\n                            secondaryLine.getOrNull(podcastIndex)?.firstOrNull()?.takeIf {\n                                it.navigationEndpoint?.browseEndpoint != null\n                            }?.let {\n                                Album(\n                                    name = it.text,\n                                    id = it.navigationEndpoint?.browseEndpoint?.browseId!!,\n                                )\n                            },\n                        duration =\n                            secondaryLine\n                                .lastOrNull()\n                                ?.firstOrNull()\n                                ?.text\n                                ?.parseTime(),\n                        publishDateText =\n                            secondaryLine\n                                .getOrNull(dateIndex)\n                                ?.firstOrNull()\n                                ?.text,\n                        thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        explicit =\n                            renderer.badges?.find {\n                                it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                            } != null,\n                        endpoint = renderer.overlay\n                            ?.musicItemThumbnailOverlayRenderer\n                            ?.content\n                            ?.musicPlayButtonRenderer\n                            ?.playNavigationEndpoint\n                            ?.watchEndpoint,\n                        libraryAddToken = libraryTokens.addToken,\n                        libraryRemoveToken = libraryTokens.removeToken,\n                    )\n                }\n\n                renderer.isSong -> {\n                    // Extract library tokens using the new method that properly handles multiple toggle items\n                    val libraryTokens = PageHelper.extractLibraryTokensFromMenuItems(renderer.menu?.menuRenderer?.items)\n                    val thirdLine =\n                        renderer.flexColumns\n                            .getOrNull(2)\n                            ?.musicResponsiveListItemFlexColumnRenderer\n                            ?.text\n                            ?.runs\n                            ?.splitBySeparator()\n                            ?: emptyList()\n                    val listRun = (secondaryLine + thirdLine).clean()\n\n                    SongItem(\n                        id = renderer.playlistItemData?.videoId ?: return null,\n                        title =\n                            renderer.flexColumns\n                                .firstOrNull()\n                                ?.musicResponsiveListItemFlexColumnRenderer\n                                ?.text\n                                ?.runs\n                                ?.firstOrNull()\n                                ?.text ?: return null,\n                        artists = listRun.getOrNull(0)?.oddElements()?.map {\n                            Artist(\n                                name = it.text,\n                                id = it.navigationEndpoint?.browseEndpoint?.browseId\n                            )\n                        } ?: return null,\n                        album = listRun.getOrNull(1)?.firstOrNull()?.takeIf { it.navigationEndpoint?.browseEndpoint != null }?.let {\n                            Album(\n                                name = it.text,\n                                id = it.navigationEndpoint?.browseEndpoint?.browseId!!\n                            )\n                        },\n                        duration =\n                            secondaryLine\n                                .lastOrNull()\n                                ?.firstOrNull()\n                                ?.text\n                                ?.parseTime(),\n                        musicVideoType = renderer.musicVideoType,\n                        thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        explicit =\n                            renderer.badges?.find {\n                                it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                            } != null,\n                        libraryAddToken = libraryTokens.addToken,\n                        libraryRemoveToken = libraryTokens.removeToken,\n                        isEpisode = renderer.isEpisode\n                    )\n                }\n\n                renderer.isArtist -> {\n                    ArtistItem(\n                        id = renderer.navigationEndpoint?.browseEndpoint?.browseId ?: return null,\n                        title =\n                            renderer.flexColumns\n                                .firstOrNull()\n                                ?.musicResponsiveListItemFlexColumnRenderer\n                                ?.text\n                                ?.runs\n                                ?.firstOrNull()\n                                ?.text\n                                ?: return null,\n                        thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        shuffleEndpoint =\n                            renderer.menu\n                                ?.menuRenderer\n                                ?.items\n                                ?.find { it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\" }\n                                ?.menuNavigationItemRenderer\n                                ?.navigationEndpoint\n                                ?.watchPlaylistEndpoint ?: return null,\n                        radioEndpoint =\n                            renderer.menu.menuRenderer.items\n                                .find { it.menuNavigationItemRenderer?.icon?.iconType == \"MIX\" }\n                                ?.menuNavigationItemRenderer\n                                ?.navigationEndpoint\n                                ?.watchPlaylistEndpoint ?: return null,\n                    )\n                }\n\n                renderer.isUserChannel -> {\n                    ArtistItem(\n                        id = renderer.navigationEndpoint?.browseEndpoint?.browseId ?: return null,\n                        title =\n                            renderer.flexColumns\n                                .firstOrNull()\n                                ?.musicResponsiveListItemFlexColumnRenderer\n                                ?.text\n                                ?.runs\n                                ?.firstOrNull()\n                                ?.text\n                                ?: return null,\n                        thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        shuffleEndpoint = renderer.menu\n                            ?.menuRenderer\n                            ?.items\n                            ?.find { it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\" }\n                            ?.menuNavigationItemRenderer\n                            ?.navigationEndpoint\n                            ?.watchPlaylistEndpoint,\n                        radioEndpoint = renderer.menu\n                            ?.menuRenderer\n                            ?.items\n                            ?.find { it.menuNavigationItemRenderer?.icon?.iconType == \"MIX\" }\n                            ?.menuNavigationItemRenderer\n                            ?.navigationEndpoint\n                            ?.watchPlaylistEndpoint,\n                        isProfile = true,\n                    )\n                }\n\n                renderer.isAlbum -> {\n                    AlbumItem(\n                        browseId = renderer.navigationEndpoint?.browseEndpoint?.browseId ?: return null,\n                        playlistId =\n                            renderer.overlay\n                                ?.musicItemThumbnailOverlayRenderer\n                                ?.content\n                                ?.musicPlayButtonRenderer\n                                ?.playNavigationEndpoint\n                                ?.watchPlaylistEndpoint\n                                ?.playlistId\n                                ?: return null,\n                        title =\n                            renderer.flexColumns\n                                .firstOrNull()\n                                ?.musicResponsiveListItemFlexColumnRenderer\n                                ?.text\n                                ?.runs\n                                ?.firstOrNull()\n                                ?.text ?: return null,\n                        artists =\n                            secondaryLine.getOrNull(1)?.oddElements()?.map {\n                                Artist(\n                                    name = it.text,\n                                    id = it.navigationEndpoint?.browseEndpoint?.browseId,\n                                )\n                            } ?: return null,\n                        year =\n                            secondaryLine\n                                .getOrNull(2)\n                                ?.firstOrNull()\n                                ?.text\n                                ?.toIntOrNull(),\n                        thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        explicit =\n                            renderer.badges?.find {\n                                it.musicInlineBadgeRenderer?.icon?.iconType == \"MUSIC_EXPLICIT_BADGE\"\n                            } != null,\n                    )\n                }\n\n                renderer.isPlaylist -> {\n                    PlaylistItem(\n                        id =\n                            renderer.navigationEndpoint\n                                ?.browseEndpoint\n                                ?.browseId\n                                ?.removePrefix(\"VL\") ?: return null,\n                        title =\n                            renderer.flexColumns\n                                .firstOrNull()\n                                ?.musicResponsiveListItemFlexColumnRenderer\n                                ?.text\n                                ?.runs\n                                ?.firstOrNull()\n                                ?.text ?: return null,\n                        author =\n                            secondaryLine.getOrNull(1)?.firstOrNull()?.let {\n                                Artist(\n                                    name = it.text,\n                                    id = it.navigationEndpoint?.browseEndpoint?.browseId,\n                                )\n                            } ?: return null,\n                        songCountText =\n                            renderer.flexColumns\n                                .getOrNull(1)\n                                ?.musicResponsiveListItemFlexColumnRenderer\n                                ?.text\n                                ?.runs\n                                ?.lastOrNull()\n                                ?.text ?: return null,\n                        thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        playEndpoint =\n                            renderer.overlay\n                                ?.musicItemThumbnailOverlayRenderer\n                                ?.content\n                                ?.musicPlayButtonRenderer\n                                ?.playNavigationEndpoint\n                                ?.watchPlaylistEndpoint ?: return null,\n                        shuffleEndpoint =\n                            renderer.menu\n                                ?.menuRenderer\n                                ?.items\n                                ?.find { it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\" }\n                                ?.menuNavigationItemRenderer\n                                ?.navigationEndpoint\n                                ?.watchPlaylistEndpoint ?: return null,\n                        radioEndpoint =\n                            renderer.menu.menuRenderer.items\n                                .find { it.menuNavigationItemRenderer?.icon?.iconType == \"MIX\" }\n                                ?.menuNavigationItemRenderer\n                                ?.navigationEndpoint\n                                ?.watchPlaylistEndpoint ?: return null,\n                    )\n                }\n\n                renderer.isPodcast -> {\n                    PodcastItem(\n                        id =\n                            renderer.navigationEndpoint\n                                ?.browseEndpoint\n                                ?.browseId\n                                ?: return null,\n                        title =\n                            renderer.flexColumns\n                                .firstOrNull()\n                                ?.musicResponsiveListItemFlexColumnRenderer\n                                ?.text\n                                ?.runs\n                                ?.firstOrNull()\n                                ?.text ?: return null,\n                        author =\n                            secondaryLine.getOrNull(0)?.firstOrNull()?.let {\n                                Artist(\n                                    name = it.text,\n                                    id = it.navigationEndpoint?.browseEndpoint?.browseId,\n                                )\n                            },\n                        episodeCountText =\n                            renderer.flexColumns\n                                .getOrNull(1)\n                                ?.musicResponsiveListItemFlexColumnRenderer\n                                ?.text\n                                ?.runs\n                                ?.lastOrNull()\n                                ?.text,\n                        thumbnail = renderer.thumbnail?.musicThumbnailRenderer?.getThumbnailUrl() ?: return null,\n                        playEndpoint =\n                            renderer.overlay\n                                ?.musicItemThumbnailOverlayRenderer\n                                ?.content\n                                ?.musicPlayButtonRenderer\n                                ?.playNavigationEndpoint\n                                ?.watchPlaylistEndpoint,\n                        shuffleEndpoint =\n                            renderer.menu\n                                ?.menuRenderer\n                                ?.items\n                                ?.find { it.menuNavigationItemRenderer?.icon?.iconType == \"MUSIC_SHUFFLE\" }\n                                ?.menuNavigationItemRenderer\n                                ?.navigationEndpoint\n                                ?.watchPlaylistEndpoint,\n                    )\n                }\n\n                else -> null\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "innertube/src/main/kotlin/com/metrolist/innertube/utils/Utils.kt",
    "content": "package com.metrolist.innertube.utils\n\nimport com.metrolist.innertube.YouTube\nimport com.metrolist.innertube.pages.LibraryPage\nimport com.metrolist.innertube.pages.PlaylistPage\nimport java.security.MessageDigest\n\n@JvmName(\"completedLibrary\")\nsuspend fun Result<PlaylistPage>.completed(): Result<PlaylistPage> = runCatching {\n    val page = getOrThrow()\n    val songs = page.songs.toMutableList()\n    var continuation = page.songsContinuation\n    val seenContinuations = mutableSetOf<String>()\n    var requestCount = 0\n    val maxRequests = 50\n    var consecutiveEmptyResponses = 0\n    \n    while (continuation != null && requestCount < maxRequests) {\n        if (continuation in seenContinuations) {\n            break\n        }\n        seenContinuations.add(continuation)\n        requestCount++\n        \n        val continuationPage = YouTube.playlistContinuation(continuation).getOrNull() ?: break\n        \n        if (continuationPage.songs.isEmpty()) {\n            consecutiveEmptyResponses++\n            if (consecutiveEmptyResponses >= 2) break\n        } else {\n            consecutiveEmptyResponses = 0\n            songs += continuationPage.songs\n        }\n        \n        continuation = continuationPage.continuation\n    }\n    PlaylistPage(\n        playlist = page.playlist,\n        songs = songs,\n        songsContinuation = null,\n        continuation = page.continuation\n    )\n}\n\n@JvmName(\"completedPlaylist\")\nsuspend fun Result<LibraryPage>.completed(): Result<LibraryPage> = runCatching {\n    val page = getOrThrow()\n    val items = page.items.toMutableList()\n    var continuation = page.continuation\n    val seenContinuations = mutableSetOf<String>()\n    var requestCount = 0\n    val maxRequests = 50\n    var consecutiveEmptyResponses = 0\n    \n    while (continuation != null && requestCount < maxRequests) {\n        if (continuation in seenContinuations) {\n            break\n        }\n        seenContinuations.add(continuation)\n        requestCount++\n        \n        val continuationPage = YouTube.libraryContinuation(continuation).getOrNull() ?: break\n        \n        if (continuationPage.items.isEmpty()) {\n            consecutiveEmptyResponses++\n            if (consecutiveEmptyResponses >= 2) break\n        } else {\n            consecutiveEmptyResponses = 0\n            items += continuationPage.items\n        }\n        \n        continuation = continuationPage.continuation\n    }\n    LibraryPage(\n        items = items,\n        continuation = null\n    )\n}\n\nfun ByteArray.toHex(): String = joinToString(separator = \"\") { eachByte -> \"%02x\".format(eachByte) }\n\nfun sha1(str: String): String = MessageDigest.getInstance(\"SHA-1\").digest(str.toByteArray()).toHex()\n\nfun parseCookieString(cookie: String): Map<String, String> =\n    cookie.split(\"; \")\n        .filter { it.isNotEmpty() }\n        .mapNotNull { part ->\n            val splitIndex = part.indexOf('=')\n            if (splitIndex == -1) null\n            else part.substring(0, splitIndex) to part.substring(splitIndex + 1)\n        }\n        .toMap()\n\nfun String.parseTime(): Int? {\n    try {\n        val parts = split(\":\").map { it.toInt() }\n        if (parts.size == 2) {\n            return parts[0] * 60 + parts[1]\n        }\n        if (parts.size == 3) {\n            return parts[0] * 3600 + parts[1] * 60 + parts[2]\n        }\n    } catch (e: Exception) {\n        return null\n    }\n    return null\n}\n\nfun isPrivateId(browseId: String): Boolean {\n    return browseId.contains(\"privately\")\n}\n"
  },
  {
    "path": "kizzy/.gitignore",
    "content": "/build"
  },
  {
    "path": "kizzy/build.gradle.kts",
    "content": "plugins {\n    id(\"com.android.library\")\n    alias(libs.plugins.kotlin.serialization)\n}\n\nandroid {\n    namespace = \"com.my.kizzy\"\n    compileSdk = 36\n\n    defaultConfig {\n        minSdk = 26\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_21\n        targetCompatibility = JavaVersion.VERSION_21\n    }\n}\n\nkotlin {\n    jvmToolchain(21)\n}\n\ndependencies {\n    implementation(libs.ktor.client.core)\n    implementation(libs.ktor.client.okhttp)\n    implementation(libs.ktor.client.content.negotiation)\n    implementation(libs.ktor.serialization.json)\n    implementation(libs.ktor.client.encoding)\n    implementation(libs.json)\n    testImplementation(libs.junit)\n\n    coreLibraryDesugaring(libs.desugaring)\n}\n"
  },
  {
    "path": "kizzy/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n</manifest>\n"
  },
  {
    "path": "kizzy/src/main/kotlin/com/my/kizzy/gateway/DiscordWebSocket.kt",
    "content": "package com.my.kizzy.gateway\n\nimport com.my.kizzy.gateway.entities.Heartbeat\nimport com.my.kizzy.gateway.entities.Identify.Companion.toIdentifyPayload\nimport com.my.kizzy.gateway.entities.Payload\nimport com.my.kizzy.gateway.entities.Ready\nimport com.my.kizzy.gateway.entities.Resume\nimport com.my.kizzy.gateway.entities.op.OpCode\nimport com.my.kizzy.gateway.entities.op.OpCode.DISPATCH\nimport com.my.kizzy.gateway.entities.op.OpCode.HEARTBEAT\nimport com.my.kizzy.gateway.entities.op.OpCode.HELLO\nimport com.my.kizzy.gateway.entities.op.OpCode.IDENTIFY\nimport com.my.kizzy.gateway.entities.op.OpCode.INVALID_SESSION\nimport com.my.kizzy.gateway.entities.op.OpCode.PRESENCE_UPDATE\nimport com.my.kizzy.gateway.entities.op.OpCode.RECONNECT\nimport com.my.kizzy.gateway.entities.op.OpCode.RESUME\nimport com.my.kizzy.gateway.entities.presence.Presence\nimport io.ktor.client.HttpClient\nimport io.ktor.client.request.header\nimport io.ktor.client.plugins.websocket.DefaultClientWebSocketSession\nimport io.ktor.client.plugins.websocket.WebSockets\nimport io.ktor.client.plugins.websocket.webSocketSession\nimport io.ktor.websocket.CloseReason\nimport io.ktor.websocket.Frame\nimport io.ktor.websocket.close\nimport io.ktor.websocket.readText\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.DelicateCoroutinesApi\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.cancel\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.flow.receiveAsFlow\nimport kotlinx.coroutines.isActive\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.runBlocking\nimport kotlinx.serialization.encodeToString\nimport kotlinx.serialization.json.Json\nimport kotlinx.serialization.json.decodeFromJsonElement\nimport kotlinx.serialization.json.encodeToJsonElement\nimport java.util.logging.Level\nimport java.util.logging.Level.INFO\nimport java.util.logging.Logger\nimport kotlin.coroutines.CoroutineContext\nimport kotlin.time.Duration.Companion.milliseconds\nimport kotlin.time.Duration.Companion.seconds\n\n/**\n * Modified by Zion Huang\n */\nopen class DiscordWebSocket(\n    private val token: String,\n    private val os: String = \"Android\",\n    private val browser: String = \"Discord Android\",\n    private val device: String = \"Generic Android Device\",\n) : CoroutineScope {\n    private val logger = Logger.getLogger(DiscordWebSocket::class.java.name)\n    private val gatewayUrl = \"wss://gateway.discord.gg/?v=9&encoding=json\"\n    private var websocket: DefaultClientWebSocketSession? = null\n    private var sequence = 0\n    private var sessionId: String? = null\n    private var heartbeatInterval = 0L\n    private var resumeGatewayUrl: String? = null\n    private var heartbeatJob: Job? = null\n    private var connected = false\n    private var client: HttpClient = HttpClient {\n        install(WebSockets)\n    }\n    private val json = Json {\n        ignoreUnknownKeys = true\n        encodeDefaults = true\n    }\n\n    private var reconnectionJob: Job? = null\n    private var currentReconnectDelay = INITIAL_RECONNECT_DELAY\n\n    override val coroutineContext: CoroutineContext\n        get() = SupervisorJob() + Dispatchers.Default\n\n    fun connect() {\n        if (connected) {\n            logger.info(\"Gateway already connected.\")\n            return\n        }\n        reconnectionJob?.cancel()\n        reconnectionJob = launch {\n            try {\n                val url = resumeGatewayUrl ?: gatewayUrl\n                logger.info(\"Connecting to Discord Gateway at $url\")\n                websocket = client.webSocketSession(url) {\n                    header(\"User-Agent\", \"Discord-Android/314013;RNA\")\n                    header(\"Accept-Language\", \"en-US\")\n                    header(\"Cache-Control\", \"no-cache\")\n                    header(\"Pragma\", \"no-cache\")\n                }\n                connected = true\n                logger.info(\"Successfully connected to Discord Gateway.\")\n                currentReconnectDelay = INITIAL_RECONNECT_DELAY\n                // start receiving messages\n                websocket!!.incoming.receiveAsFlow()\n                    .collect {\n                        when (it) {\n                            is Frame.Text -> {\n                                val jsonString = it.readText()\n                                onMessage(json.decodeFromString(jsonString))\n                            }\n\n                            else -> {}\n                        }\n                    }\n                handleClose()\n            } catch (e: Exception) {\n                logger.severe(\"Gateway connection error: ${e.stackTraceToString()}\")\n                scheduleReconnection()\n            }\n        }\n    }\n\n    private fun scheduleReconnection() {\n        if (reconnectionJob?.isActive == true) {\n            return\n        }\n        heartbeatJob?.cancel()\n        connected = false\n        reconnectionJob = launch {\n            delay(currentReconnectDelay)\n            logger.info(\"Attempting to reconnect...\")\n            connect()\n            currentReconnectDelay = (currentReconnectDelay * 2).coerceAtMost(MAX_RECONNECT_DELAY)\n        }\n    }\n\n\n    private suspend fun handleClose() {\n        heartbeatJob?.cancel()\n        connected = false\n        val close = websocket?.closeReason?.await()\n        logger.warning(\"Gateway closed with code: ${close?.code}, reason: ${close?.message}, can_reconnect: ${close?.code?.toInt() == 4000}\")\n        if (close?.code?.toInt() == 4000) {\n            delay(200.milliseconds)\n            connect()\n        } else\n            scheduleReconnection()\n    }\n\n    private suspend fun onMessage(payload: Payload) {\n        logger.info(\"Gateway received: op=${payload.op}, seq=${payload.s}, event=${payload.t}\")\n        payload.s?.let {\n            sequence = it\n        }\n        when (payload.op) {\n            DISPATCH -> payload.handleDispatch()\n            HEARTBEAT -> sendHeartBeat()\n            RECONNECT -> reconnectWebSocket()\n            INVALID_SESSION -> handleInvalidSession()\n            HELLO -> payload.handleHello()\n            else -> {}\n        }\n    }\n\n    open fun Payload.handleDispatch() {\n        when (this.t.toString()) {\n            \"READY\" -> {\n                val ready = json.decodeFromJsonElement<Ready>(this.d!!)\n                sessionId = ready.sessionId\n                resumeGatewayUrl = ready.resumeGatewayUrl + \"/?v=9&encoding=json\"\n                logger.info(\"Gateway READY: resume_gateway_url updated to $resumeGatewayUrl, session_id updated to $sessionId\")\n                connected = true\n                return\n            }\n\n            \"RESUMED\" -> {\n                logger.info(\"Gateway: Session Resumed\")\n            }\n\n            else -> {}\n        }\n    }\n\n    private suspend inline fun handleInvalidSession() {\n        logger.warning(\"Gateway: Handling Invalid Session. Sending Identify after 150ms\")\n        delay(150)\n        sendIdentify()\n    }\n\n    private suspend inline fun Payload.handleHello() {\n        if (sequence > 0 && !sessionId.isNullOrBlank()) {\n            sendResume()\n        } else {\n            sendIdentify()\n        }\n        heartbeatInterval = json.decodeFromJsonElement<Heartbeat>(this.d!!).heartbeatInterval\n        logger.info(\"Gateway: Setting heartbeatInterval=$heartbeatInterval\")\n        startHeartbeatJob(heartbeatInterval)\n    }\n\n    private suspend fun sendHeartBeat() {\n        logger.info(\"Gateway: Sending $HEARTBEAT with seq: $sequence\")\n        send(\n            op = HEARTBEAT,\n            d = if (sequence == 0) \"null\" else sequence.toString(),\n        )\n    }\n\n    private suspend inline fun reconnectWebSocket() {\n        websocket?.close(\n            CloseReason(\n                code = 4000,\n                message = \"Attempting to reconnect\"\n            )\n        )\n    }\n\n    private suspend fun sendIdentify() {\n        logger.info(\"Gateway: Sending $IDENTIFY\")\n        send(\n            op = IDENTIFY,\n            d = token.toIdentifyPayload(\n                os = os,\n                browser = browser,\n                device = device\n            )\n        )\n    }\n\n    private suspend fun sendResume() {\n        logger.info(\"Gateway: Sending $RESUME\")\n        send(\n            op = RESUME,\n            d = Resume(\n                seq = sequence,\n                sessionId = sessionId,\n                token = token\n            )\n        )\n    }\n\n    private fun startHeartbeatJob(interval: Long) {\n        heartbeatJob?.cancel()\n        heartbeatJob = launch {\n            while (isActive) {\n                sendHeartBeat()\n                delay(interval)\n            }\n        }\n    }\n\n    private fun isSocketConnectedToAccount(): Boolean {\n        return connected && websocket?.isActive == true\n    }\n\n    @OptIn(DelicateCoroutinesApi::class)\n    fun isWebSocketConnected(): Boolean {\n        return websocket?.incoming != null && websocket?.outgoing?.isClosedForSend == false\n    }\n\n    private suspend inline fun <reified T> send(op: OpCode, d: T?) {\n        if (websocket?.isActive == true) {\n            val payload = json.encodeToString(\n                Payload(\n                    op = op,\n                    d = json.encodeToJsonElement(d),\n                )\n            )\n            if (op == IDENTIFY) {\n                logger.info(\"Gateway sending payload: [REDACTED IDENTIFY PAYLOAD]\")\n            } else {\n                logger.info(\"Gateway sending payload: $payload\")\n            }\n            websocket?.send(Frame.Text(payload))\n        }\n    }\n\n    fun close() {\n        reconnectionJob?.cancel()\n        heartbeatJob?.cancel()\n        heartbeatJob = null\n        this.cancel()\n        resumeGatewayUrl = null\n        sessionId = null\n        connected = false\n        runBlocking {\n            websocket?.close()\n            logger.severe(\"Gateway: Connection to gateway closed\")\n        }\n    }\n\n    suspend fun sendActivity(presence: Presence) {\n        // TODO : Figure out a better way to wait for socket to be connected to account\n        while (!isSocketConnectedToAccount()) {\n            delay(10.milliseconds)\n        }\n        logger.info(\"Gateway: Sending $PRESENCE_UPDATE\")\n        send(\n            op = PRESENCE_UPDATE,\n            d = presence\n        )\n    }\n    companion object {\n        private val INITIAL_RECONNECT_DELAY = 1.seconds\n        private val MAX_RECONNECT_DELAY = 60.seconds\n    }\n}\n"
  },
  {
    "path": "kizzy/src/main/kotlin/com/my/kizzy/gateway/entities/HeartBeat.kt",
    "content": "/*\n *\n *  ******************************************************************\n *  *  * Copyright (C) 2022\n *  *  * HeartBeat.kt is part of Kizzy\n *  *  *  and can not be copied and/or distributed without the express\n *  *  * permission of yzziK(Vaibhav)\n *  *  *****************************************************************\n *\n *\n */\n\npackage com.my.kizzy.gateway.entities\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Heartbeat(\n    @SerialName(\"heartbeat_interval\")\n    val heartbeatInterval: Long,\n)"
  },
  {
    "path": "kizzy/src/main/kotlin/com/my/kizzy/gateway/entities/Identify.kt",
    "content": "package com.my.kizzy.gateway.entities\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\nimport com.my.kizzy.gateway.entities.presence.Presence\n\n@Serializable\ndata class Identify(\n    @SerialName(\"capabilities\")\n    val capabilities: Int,\n    @SerialName(\"compress\")\n    val compress: Boolean,\n    @SerialName(\"largeThreshold\")\n    val largeThreshold: Int,\n    @SerialName(\"properties\")\n    val properties: Properties,\n    @SerialName(\"client_state\")\n    val clientState: ClientState = ClientState(),\n    @SerialName(\"presence\")\n    val presence: Presence? = null,\n    @SerialName(\"shard\")\n    val shard: List<Int> = listOf(0, 1),\n    @SerialName(\"token\")\n    val token: String,\n) {\n    companion object {\n        fun String.toIdentifyPayload(\n            os: String = \"Android\",\n            browser: String = \"Discord Android\",\n            device: String = \"Generic Android Device\"\n        ) = Identify(\n            capabilities = 16381,\n            compress = false,\n            largeThreshold = 100,\n            properties = Properties(\n                os = os,\n                browser = browser,\n                device = device\n            ),\n            presence = Presence(\n                status = \"online\",\n                since = 0,\n                activities = emptyList(),\n                afk = false\n            ),\n            token = this\n        )\n    }\n}\n\n@Serializable\ndata class ClientState(\n    @SerialName(\"guild_versions\")\n    val guildVersions: Map<String, String> = emptyMap(),\n    @SerialName(\"highest_last_message_id\")\n    val highestLastMessageId: String = \"0\",\n    @SerialName(\"read_state_version\")\n    val readStateVersion: Int = 0,\n    @SerialName(\"user_guild_settings_version\")\n    val userGuildSettingsVersion: Int = -1,\n    @SerialName(\"user_settings_version\")\n    val userSettingsVersion: Int = -1,\n    @SerialName(\"private_channels_version\")\n    val privateChannelsVersion: String = \"0\",\n    @SerialName(\"api_code_version\")\n    val apiCodeVersion: Int = 0,\n)\n\n@Serializable\ndata class Properties(\n    @SerialName(\"browser\")\n    val browser: String,\n    @SerialName(\"device\")\n    val device: String,\n    @SerialName(\"os\")\n    val os: String,\n)"
  },
  {
    "path": "kizzy/src/main/kotlin/com/my/kizzy/gateway/entities/Payload.kt",
    "content": "package com.my.kizzy.gateway.entities\n\nimport com.my.kizzy.gateway.entities.op.OpCode\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.JsonElement\n\n@Serializable\ndata class Payload(\n    @SerialName(\"t\")\n    val t: String? = null,\n    @SerialName(\"s\")\n    val s: Int? = null,\n    @SerialName(\"op\")\n    val op: OpCode? = null,\n    @SerialName(\"d\")\n    val d: JsonElement? = null,\n)"
  },
  {
    "path": "kizzy/src/main/kotlin/com/my/kizzy/gateway/entities/Ready.kt",
    "content": "/*\n *\n *  ******************************************************************\n *  *  * Copyright (C) 2022\n *  *  * Ready.kt is part of Kizzy\n *  *  *  and can not be copied and/or distributed without the express\n *  *  * permission of yzziK(Vaibhav)\n *  *  *****************************************************************\n *\n *\n */\n\npackage com.my.kizzy.gateway.entities\n\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Ready(\n    @SerialName(\"resume_gateway_url\")\n    val resumeGatewayUrl: String? = null,\n    @SerialName(\"session_id\")\n    val sessionId: String? = null,\n)"
  },
  {
    "path": "kizzy/src/main/kotlin/com/my/kizzy/gateway/entities/Resume.kt",
    "content": "package com.my.kizzy.gateway.entities\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Resume(\n    @SerialName(\"seq\")\n    val seq: Int,\n    @SerialName(\"session_id\")\n    val sessionId: String?,\n    @SerialName(\"token\")\n    val token: String,\n)"
  },
  {
    "path": "kizzy/src/main/kotlin/com/my/kizzy/gateway/entities/op/OpCode.kt",
    "content": "package com.my.kizzy.gateway.entities.op\n\nimport kotlinx.serialization.Serializable\n\n@Serializable(OpCodeSerializer::class)\nenum class OpCode(val value: Int) {\n    /** An event was dispatched. */\n    DISPATCH(0),\n\n    /** Fired periodically by the client to keep the connection alive. */\n    HEARTBEAT(1),\n\n    /** Starts a new session during the initial handshake. */\n    IDENTIFY(2),\n\n    /** Update the client's presence. */\n    PRESENCE_UPDATE(3),\n\n    /** Joins/leaves or moves between voice channels. */\n    VOICE_STATE(4),\n\n    /** Resume a previous session that was disconnected. */\n    RESUME(6),\n\n    /** You should attempt to reconnect and resume immediately. */\n    RECONNECT(7),\n\n    /** Request information about offline guild members in a large guild. */\n    REQUEST_GUILD_MEMBERS(8),\n\n    /** The session has been invalidated. You should reconnect and identify/resume accordingly */\n    INVALID_SESSION(9),\n\n    /** Sent immediately after connecting, contains the heartbeat_interval to use. */\n    HELLO(10),\n\n    /** Sent in response to receiving a heartbeat to acknowledge that it has been received. */\n    HEARTBEAT_ACK(11),\n\n    /** For future use or unknown opcodes. */\n    UNKNOWN(-1);\n}"
  },
  {
    "path": "kizzy/src/main/kotlin/com/my/kizzy/gateway/entities/op/OpCodesSerializer.kt",
    "content": "package com.my.kizzy.gateway.entities.op\n\nimport kotlinx.serialization.KSerializer\nimport kotlinx.serialization.descriptors.PrimitiveKind\nimport kotlinx.serialization.descriptors.PrimitiveSerialDescriptor\nimport kotlinx.serialization.descriptors.SerialDescriptor\nimport kotlinx.serialization.encoding.Decoder\nimport kotlinx.serialization.encoding.Encoder\n\nclass OpCodeSerializer : KSerializer<OpCode> {\n    override val descriptor: SerialDescriptor\n        get() = PrimitiveSerialDescriptor(\"OpCode\", PrimitiveKind.INT)\n\n    override fun deserialize(decoder: Decoder): OpCode {\n        val opCode = decoder.decodeInt()\n        return OpCode.values().firstOrNull { it.value == opCode } ?: throw IllegalArgumentException(\"Unknown OpCode $opCode\")\n    }\n\n    override fun serialize(encoder: Encoder, value: OpCode) {\n        encoder.encodeInt(value.value)\n    }\n}"
  },
  {
    "path": "kizzy/src/main/kotlin/com/my/kizzy/gateway/entities/presence/Activity.kt",
    "content": "package com.my.kizzy.gateway.entities.presence\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Activity(\n    @SerialName(\"name\")\n    val name: String?,\n    @SerialName(\"state\")\n    val state: String? = null,\n    @SerialName(\"state_url\")\n    val stateUrl: String? = null,\n    @SerialName(\"details\")\n    val details: String? = null,\n    @SerialName(\"details_url\")\n    val detailsUrl: String? = null,\n    @SerialName(\"type\")\n    val type: Int? = 0,\n    @SerialName(\"status_display_type\")\n    val statusDisplayType: Int? = 0,\n    @SerialName(\"timestamps\")\n    val timestamps: Timestamps? = null,\n    @SerialName(\"assets\")\n    val assets: Assets? = null,\n    @SerialName(\"buttons\")\n    val buttons: List<String?>? = null,\n    @SerialName(\"metadata\")\n    val metadata: Metadata? = null,\n    @SerialName(\"application_id\")\n    val applicationId: String? = null,\n    @SerialName(\"url\")\n    val url: String? = null,\n)\n"
  },
  {
    "path": "kizzy/src/main/kotlin/com/my/kizzy/gateway/entities/presence/Assets.kt",
    "content": "package com.my.kizzy.gateway.entities.presence\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Assets(\n    @SerialName(\"large_image\")\n    val largeImage: String?,\n    @SerialName(\"small_image\")\n    val smallImage: String?,\n    @SerialName(\"large_text\")\n    val largeText: String? = null,\n    @SerialName(\"small_text\")\n    val smallText: String? = null,\n)"
  },
  {
    "path": "kizzy/src/main/kotlin/com/my/kizzy/gateway/entities/presence/Metadata.kt",
    "content": "package com.my.kizzy.gateway.entities.presence\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Metadata(\n    @SerialName(\"button_urls\")\n    val buttonUrls: List<String?>?,\n)"
  },
  {
    "path": "kizzy/src/main/kotlin/com/my/kizzy/gateway/entities/presence/Presence.kt",
    "content": "package com.my.kizzy.gateway.entities.presence\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Presence(\n    @SerialName(\"activities\")\n    val activities: List<Activity?>?,\n    @SerialName(\"afk\")\n    val afk: Boolean? = true,\n    @SerialName(\"since\")\n    val since: Long? = 0L,\n    @SerialName(\"status\")\n    val status: String? = \"online\",\n)"
  },
  {
    "path": "kizzy/src/main/kotlin/com/my/kizzy/gateway/entities/presence/Timestamps.kt",
    "content": "package com.my.kizzy.gateway.entities.presence\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Timestamps(\n    @SerialName(\"start\")\n    val start: Long? = null,\n    @SerialName(\"end\")\n    val end: Long? = null,\n)"
  },
  {
    "path": "kizzy/src/main/kotlin/com/my/kizzy/rpc/ArtworkCache.kt",
    "content": "package com.my.kizzy.rpc\n\nimport java.util.concurrent.ConcurrentHashMap\n\ninternal object ArtworkCache {\n    private val cache = ConcurrentHashMap<String, String>()\n\n    suspend fun getOrFetch(key: String, fetch: suspend () -> String?): String? {\n        return cache[key] ?: fetch()?.also { cache[key] = it }\n    }\n}\n"
  },
  {
    "path": "kizzy/src/main/kotlin/com/my/kizzy/rpc/ExternalAssets.kt",
    "content": "package com.my.kizzy.rpc\n\nimport io.ktor.client.HttpClient\nimport io.ktor.client.call.body\nimport io.ktor.client.request.post\nimport io.ktor.client.request.header\nimport io.ktor.client.request.setBody\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\nimport kotlinx.serialization.json.Json\n\n@Serializable\nprivate data class ExternalAssetResponse(\n    val url: String? = null,\n    @SerialName(\"external_asset_path\")\n    val externalAssetPath: String? = null,\n)\n\nsuspend fun fetchExternalAsset(\n    client: HttpClient,\n    applicationId: String,\n    token: String,\n    imageUrl: String,\n    userAgent: String? = null,\n    superPropertiesBase64: String? = null,\n): String? {\n    if (imageUrl.startsWith(\"mp:\")) return imageUrl\n    val api = \"https://discord.com/api/v9/applications/$applicationId/external-assets\"\n    return runCatching {\n        val response = client.post(api) {\n            header(\"Authorization\", token)\n            header(\"User-Agent\", userAgent ?: \"Discord-Android/314013;RNA\")\n            if (superPropertiesBase64 != null) header(\"X-Super-Properties\", superPropertiesBase64)\n            header(\"Content-Type\", \"application/json\")\n            setBody(\"{\\\"urls\\\":[\\\"$imageUrl\\\"]}\")\n        }\n        val text = response.body<String>()\n        val json = Json { ignoreUnknownKeys = true }\n        val list = json.decodeFromString<List<ExternalAssetResponse>>(text)\n        list.firstOrNull()?.externalAssetPath?.let { \"mp:$it\" }\n    }.getOrNull()\n}\n"
  },
  {
    "path": "kizzy/src/main/kotlin/com/my/kizzy/rpc/KizzyRPC.kt",
    "content": "/*\n *\n *  ******************************************************************\n *  *  * Copyright (C) 2022\n *  *  * KizzyRPC.kt is part of Kizzy\n *  *  *  and can not be copied and/or distributed without the express\n *  *  * permission of yzziK(Vaibhav)\n *  *  *****************************************************************\n *\n *\n */\n\npackage com.my.kizzy.rpc\n\nimport com.my.kizzy.gateway.DiscordWebSocket\nimport com.my.kizzy.gateway.entities.presence.Activity\nimport com.my.kizzy.gateway.entities.presence.Assets\nimport com.my.kizzy.gateway.entities.presence.Metadata\nimport com.my.kizzy.gateway.entities.presence.Presence\nimport com.my.kizzy.gateway.entities.presence.Timestamps\nimport io.ktor.client.HttpClient\nimport io.ktor.client.request.get\nimport io.ktor.client.request.header\nimport io.ktor.client.statement.bodyAsText\nimport org.json.JSONObject\n\n/**\n * Modified by Zion Huang\n */\nopen class KizzyRPC(\n    private val token: String,\n    os: String = \"Android\",\n    browser: String = \"Discord Android\",\n    device: String = \"Generic Android Device\",\n    private val userAgent: String = \"Discord-Android/314013;RNA\",\n    private val superPropertiesBase64: String? = null\n) {\n    private val discordWebSocket = DiscordWebSocket(token, os, browser, device)\n    private val discordApiClient = HttpClient()\n\n    fun closeRPC() {\n        discordWebSocket.close()\n    }\n\n    fun isRpcRunning(): Boolean {\n        return discordWebSocket.isWebSocketConnected()\n    }\n\n    open suspend fun close() {\n        if (!isRpcRunning()) {\n            discordWebSocket.connect()\n        }\n        val presence = Presence(\n            activities = emptyList()\n        )\n        discordWebSocket.sendActivity(presence)\n    }\n\n    suspend fun setActivity(\n        name: String,\n        state: String?,\n        stateUrl: String? = null,\n        details: String?,\n        detailsUrl: String? = null,\n        largeImage: RpcImage?,\n        smallImage: RpcImage?,\n        largeText: String? = null,\n        smallText: String? = null,\n        buttons: List<Pair<String, String>>? = null,\n        startTime: Long? = null,\n        endTime: Long? = null,\n        type: Type = Type.LISTENING,\n        statusDisplayType: StatusDisplayType = StatusDisplayType.NAME,\n        streamUrl: String? = null,\n        applicationId: String? = null,\n        status: String? = \"online\",\n        since: Long? = null,\n    ) {\n        if (!isRpcRunning()) {\n            discordWebSocket.connect()\n        }\n\n        val resolveExternal: suspend (String) -> String? = { image ->\n            if (applicationId.isNullOrBlank()) null\n            else fetchExternalAsset(\n                client = discordApiClient,\n                applicationId = applicationId,\n                token = token,\n                imageUrl = image,\n                userAgent = userAgent,\n                superPropertiesBase64 = superPropertiesBase64,\n            )\n        }\n\n        val presence = Presence(\n            activities = listOf(\n                Activity(\n                    name = name,\n                    state = state,\n                    stateUrl = stateUrl,\n                    details = details,\n                    detailsUrl = detailsUrl,\n                    type = type.value,\n                    statusDisplayType = statusDisplayType.value,\n                    timestamps = Timestamps(startTime, endTime),\n                    assets = Assets(\n                        largeImage = largeImage?.resolveImage(resolveExternal),\n                        smallImage = smallImage?.resolveImage(resolveExternal),\n                        largeText = largeText,\n                        smallText = smallText\n                    ),\n                    buttons = buttons?.map { it.first },\n                    metadata = Metadata(buttonUrls = buttons?.map { it.second }),\n                    applicationId = applicationId.takeIf { !buttons.isNullOrEmpty() },\n                    url = streamUrl\n                )\n            ),\n            afk = true,\n            since = since,\n            status = status ?: \"online\"\n        )\n        discordWebSocket.sendActivity(presence)\n    }\n\n    enum class Type(val value: Int) {\n        PLAYING(0),\n        STREAMING(1),\n        LISTENING(2),\n        WATCHING(3),\n        COMPETING(5)\n    }\n\n    enum class StatusDisplayType(val value: Int) {\n        NAME(0),\n        STATE(1),\n        DETAILS(2)\n    }\n\n    companion object {\n        suspend fun getUserInfo(\n            token: String,\n            userAgent: String = \"Discord-Android/314013;RNA\",\n            superPropertiesBase64: String? = null\n        ): Result<UserInfo> = runCatching {\n            val client = HttpClient()\n            val response = client.get(\"https://discord.com/api/v9/users/@me\") {\n                header(\"Authorization\", token)\n                header(\"User-Agent\", userAgent)\n                if (superPropertiesBase64 != null) {\n                    header(\"X-Super-Properties\", superPropertiesBase64)\n                }\n            }.bodyAsText()\n            val json = JSONObject(response)\n            val id = json.getString(\"id\")\n            val username = json.getString(\"username\")\n            val name = json.optString(\"global_name\", username)\n            val avatarHash = json.optString(\"avatar\")\n            val avatar = if (avatarHash.isNotEmpty() && avatarHash != \"null\") {\n                \"https://cdn.discordapp.com/avatars/$id/$avatarHash.png\"\n            } else {\n                null\n            }\n            client.close()\n\n            UserInfo(id, username, name, avatar)\n        }\n    }\n}\n"
  },
  {
    "path": "kizzy/src/main/kotlin/com/my/kizzy/rpc/RpcImage.kt",
    "content": "/*\n *\n *  ******************************************************************\n *  *  * Copyright (C) 2022\n *  *  * RpcImage.kt is part of Kizzy\n *  *  *  and can not be copied and/or distributed without the express\n *  *  * permission of yzziK(Vaibhav)\n *  *  *****************************************************************\n *\n *\n */\n\npackage com.my.kizzy.rpc\n\n/**\n * Modified by Zion Huang\n */\nsealed class RpcImage {\n    abstract suspend fun resolveImage(resolveExternalImage: suspend (String) -> String?): String?\n\n    class DiscordImage(val image: String) : RpcImage() {\n        override suspend fun resolveImage(resolveExternalImage: suspend (String) -> String?): String {\n            return if (image.startsWith(\"http\")) image else \"mp:${image}\"\n        }\n    }\n\n    class ExternalImage(\n        val image: String,\n        private val fallbackDiscordAsset: String? = null,\n    ) : RpcImage() {\n        override suspend fun resolveImage(resolveExternalImage: suspend (String) -> String?): String? {\n            val asset = ArtworkCache.getOrFetch(image) { resolveExternalImage(image) }\n            return when {\n                asset != null -> if (asset.startsWith(\"http\") || asset.startsWith(\"mp:\")) asset else \"mp:$asset\"\n                image.startsWith(\"http\") -> image // Raw URL\n                else -> fallbackDiscordAsset?.let { if (it.startsWith(\"http\")) it else \"mp:${it}\" }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "kizzy/src/main/kotlin/com/my/kizzy/rpc/UserInfo.kt",
    "content": "package com.my.kizzy.rpc\n\n/**\n * Created by Zion Huang\n * Modified by Metrolist contributors\n */\ndata class UserInfo(\n    val id: String,\n    val username: String,\n    val name: String,\n    val avatar: String?,\n)\n"
  },
  {
    "path": "kizzy/src/main/kotlin/com/my/kizzy/utils/Ext.kt",
    "content": "/*\n *\n *  ******************************************************************\n *  *  * Copyright (C) 2022\n *  *  * Ext.kt is part of Kizzy\n *  *  *  and can not be copied and/or distributed without the express\n *  *  * permission of yzziK(Vaibhav)\n *  *  *****************************************************************\n *\n *\n */\n\npackage com.my.kizzy.utils\n\nimport com.my.kizzy.rpc.RpcImage\n\nfun String.toRpcImage(): RpcImage? {\n    return if (this.isBlank())\n        null\n    else if (this.startsWith(\"attachments\"))\n        RpcImage.DiscordImage(this)\n    else\n        RpcImage.ExternalImage(this)\n}\n\n"
  },
  {
    "path": "kugou/.gitignore",
    "content": "/build"
  },
  {
    "path": "kugou/build.gradle.kts",
    "content": "plugins {\n    id(\"com.android.library\")\n    alias(libs.plugins.kotlin.serialization)\n}\n\nandroid {\n    namespace = \"com.metrolist.kugou\"\n    compileSdk = 36\n\n    defaultConfig {\n        minSdk = 26\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_21\n        targetCompatibility = JavaVersion.VERSION_21\n    }\n}\n\nkotlin {\n    jvmToolchain(21)\n}\n\ndependencies {\n    implementation(libs.ktor.client.core)\n    implementation(libs.ktor.client.okhttp)\n    implementation(libs.ktor.client.content.negotiation)\n    implementation(libs.ktor.serialization.json)\n    implementation(libs.ktor.client.encoding)\n    testImplementation(libs.junit)\n\n    coreLibraryDesugaring(libs.desugaring)\n}\n"
  },
  {
    "path": "kugou/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n</manifest>\n"
  },
  {
    "path": "kugou/src/main/kotlin/com/metrolist/kugou/KuGou.kt",
    "content": "package com.metrolist.kugou\n\nimport com.metrolist.kugou.models.DownloadLyricsResponse\nimport com.metrolist.kugou.models.Keyword\nimport com.metrolist.kugou.models.SearchLyricsResponse\nimport com.metrolist.kugou.models.SearchSongResponse\nimport io.ktor.client.HttpClient\nimport io.ktor.client.call.body\nimport io.ktor.client.plugins.compression.ContentEncoding\nimport io.ktor.client.plugins.contentnegotiation.ContentNegotiation\nimport io.ktor.client.request.get\nimport io.ktor.client.request.parameter\nimport io.ktor.http.ContentType\nimport io.ktor.http.encodeURLParameter\nimport io.ktor.serialization.kotlinx.json.json\nimport kotlinx.serialization.ExperimentalSerializationApi\nimport kotlin.io.encoding.Base64\nimport kotlin.io.encoding.ExperimentalEncodingApi\nimport kotlinx.serialization.json.Json\nimport java.lang.Integer.min\nimport kotlin.math.abs\n\n@OptIn(ExperimentalSerializationApi::class, ExperimentalEncodingApi::class)\nprivate val client = HttpClient {\n    expectSuccess = true\n\n    install(ContentNegotiation) {\n        val json = Json {\n            ignoreUnknownKeys = true\n            explicitNulls = false\n            encodeDefaults = true\n        }\n        json(json)\n        json(json, ContentType.Text.Html)\n        json(json, ContentType.Text.Plain)\n    }\n\n    install(ContentEncoding) {\n        gzip()\n        deflate()\n    }\n}\n\nprivate const val PAGE_SIZE = 8\nprivate const val HEAD_CUT_LIMIT = 30\n\n/**\n * KuGou Lyrics Library\n * Modified from [ViMusic](https://github.com/vfsfitvnm/ViMusic)\n */\nobject KuGou {\n    var useTraditionalChinese: Boolean = false\n\n    suspend fun getLyrics(title: String, artist: String, duration: Int, album: String? = null): Result<String> =\n        runCatching {\n            val keyword = generateKeyword(title, artist, album)\n            getLyricsCandidate(keyword, duration)?.let { candidate ->\n                Base64.Default.decode(downloadLyrics(candidate.id, candidate.accesskey).content).decodeToString()\n                    .normalize()\n            } ?: throw IllegalStateException(\"No lyrics candidate\")\n        }\n\n    suspend fun getAllPossibleLyricsOptions(\n        title: String, artist: String, duration: Int, album: String? = null, callback: (String) -> Unit\n    ) {\n        val keyword = generateKeyword(title, artist, album)\n        searchSongs(keyword).data.info.forEach {\n            if (duration == -1 || abs(it.duration - duration) <= DURATION_TOLERANCE) {\n                searchLyricsByHash(it.hash).candidates.firstOrNull()?.let { candidate ->\n                    Base64.Default.decode(downloadLyrics(candidate.id, candidate.accesskey).content).decodeToString()\n                        .normalize().let(callback)\n                }\n            }\n        }\n        searchLyricsByKeyword(keyword, duration).candidates.forEach { candidate ->\n            Base64.Default.decode(downloadLyrics(candidate.id, candidate.accesskey).content).decodeToString()\n                .normalize().let(callback)\n        }\n    }\n\n    suspend fun getLyricsCandidate(\n        keyword: Keyword, duration: Int\n    ): SearchLyricsResponse.Candidate? {\n        searchSongs(keyword).data.info.forEach { song ->\n            if (duration == -1 || abs(song.duration - duration) <= DURATION_TOLERANCE) { // if duration == -1, we don't care duration\n                val candidate = searchLyricsByHash(song.hash).candidates.firstOrNull()\n                if (candidate != null) return candidate\n            }\n        }\n        return searchLyricsByKeyword(keyword, duration).candidates.firstOrNull()\n    }\n\n    suspend fun searchSongs(keyword: Keyword) =\n        client.get(\"https://mobileservice.kugou.com/api/v3/search/song\") {\n            parameter(\"version\", 9108)\n            parameter(\"plat\", 0)\n            parameter(\"pagesize\", PAGE_SIZE)\n            parameter(\"showtype\", 0)\n            val searchQuery = buildString {\n                append(keyword.title)\n                append(\" - \")\n                append(keyword.artist)\n                if (!keyword.album.isNullOrBlank()) {\n                    append(\" \")\n                    append(keyword.album)\n                }\n            }\n            url.encodedParameters.append(\n                \"keyword\",\n                searchQuery.encodeURLParameter(spaceToPlus = false)\n            )\n        }.body<SearchSongResponse>()\n\n    private suspend fun searchLyricsByKeyword(keyword: Keyword, duration: Int) =\n        client.get(\"https://lyrics.kugou.com/search\") {\n            parameter(\"ver\", 1)\n            parameter(\"man\", \"yes\")\n            parameter(\"client\", \"pc\")\n            parameter(\n                \"duration\", duration.takeIf { it != -1 }?.times(1000)\n            ) // if duration == -1, we don't care duration\n            val searchQuery = buildString {\n                append(keyword.title)\n                append(\" - \")\n                append(keyword.artist)\n                if (!keyword.album.isNullOrBlank()) {\n                    append(\" \")\n                    append(keyword.album)\n                }\n            }\n            url.encodedParameters.append(\n                \"keyword\",\n                searchQuery.encodeURLParameter(spaceToPlus = false)\n            )\n        }.body<SearchLyricsResponse>()\n\n    private suspend fun searchLyricsByHash(hash: String) =\n        client.get(\"https://lyrics.kugou.com/search\") {\n            parameter(\"ver\", 1)\n            parameter(\"man\", \"yes\")\n            parameter(\"client\", \"pc\")\n            parameter(\"hash\", hash)\n        }.body<SearchLyricsResponse>()\n\n    private suspend fun downloadLyrics(id: Long, accessKey: String) =\n        client.get(\"https://lyrics.kugou.com/download\") {\n            parameter(\"fmt\", \"lrc\")\n            parameter(\"charset\", \"utf8\")\n            parameter(\"client\", \"pc\")\n            parameter(\"ver\", 1)\n            parameter(\"id\", id)\n            parameter(\"accesskey\", accessKey)\n        }.body<DownloadLyricsResponse>()\n\n    private fun normalizeTitle(title: String) =\n        title.replace(\"\\\\(.*\\\\)\".toRegex(), \"\").replace(\"（.*）\".toRegex(), \"\")\n            .replace(\"「.*」\".toRegex(), \"\").replace(\"『.*』\".toRegex(), \"\")\n            .replace(\"<.*>\".toRegex(), \"\").replace(\"《.*》\".toRegex(), \"\")\n            .replace(\"〈.*〉\".toRegex(), \"\").replace(\"＜.*＞\".toRegex(), \"\")\n\n    private fun normalizeArtist(artist: String) =\n        artist.replace(\", \", \"、\").replace(\" & \", \"、\").replace(\".\", \"\").replace(\"和\", \"、\")\n            .replace(\"\\\\(.*\\\\)\".toRegex(), \"\").replace(\"（.*）\".toRegex(), \"\")\n\n    fun generateKeyword(title: String, artist: String, album: String? = null) =\n        Keyword(normalizeTitle(title), normalizeArtist(artist), album)\n\n    private fun String.normalize(): String =\n        lines().filter { line -> line.matches(ACCEPTED_REGEX) }\n            .let { lines ->\n                // Remove useless information such as singer, writer, composer, guitar, etc.\n                var headCutLine = 0\n                for (i in min(HEAD_CUT_LIMIT, lines.lastIndex) downTo 0) {\n                    if (lines[i].matches(BANNED_REGEX)) {\n                        headCutLine = i + 1\n                        break\n                    }\n                }\n                val filteredLines = lines.drop(headCutLine)\n\n                var tailCutLine = 0\n                for (i in min(lines.size - HEAD_CUT_LIMIT, lines.lastIndex) downTo 0) {\n                    if (lines[lines.lastIndex - i].matches(BANNED_REGEX)) {\n                        tailCutLine = i + 1\n                        break\n                    }\n                }\n                val finalLines = filteredLines.dropLast(tailCutLine)\n\n                return@let finalLines.joinToString(\"\\n\")\n            }\n\n    @Suppress(\"RegExpRedundantEscape\")\n    private val ACCEPTED_REGEX = \"\\\\[(\\\\d\\\\d):(\\\\d\\\\d)\\\\.(\\\\d{2,3})\\\\].*\".toRegex()\n    private val BANNED_REGEX = \".+].+[:：].+\".toRegex()\n\n    private const val DURATION_TOLERANCE = 8\n}\n"
  },
  {
    "path": "kugou/src/main/kotlin/com/metrolist/kugou/models/DownloadLyricsResponse.kt",
    "content": "package com.metrolist.kugou.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class DownloadLyricsResponse(\n    val content: String,\n)\n"
  },
  {
    "path": "kugou/src/main/kotlin/com/metrolist/kugou/models/Keyword.kt",
    "content": "package com.metrolist.kugou.models\n\ndata class Keyword(val title: String, val artist: String, val album: String? = null)\n"
  },
  {
    "path": "kugou/src/main/kotlin/com/metrolist/kugou/models/SearchLyricsResponse.kt",
    "content": "package com.metrolist.kugou.models\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class SearchLyricsResponse(\n    val status: Int,\n    val info: String,\n    val errcode: Int,\n    val errmsg: String,\n    val expire: Int,\n    val candidates: List<Candidate>,\n) {\n    @Serializable\n    data class Candidate(\n        val id: Long,\n        @SerialName(\"product_from\")\n        val productFrom: String, // Consider choosing '官方推荐歌词'\n        val duration: Long,\n        val accesskey: String,\n    )\n}\n"
  },
  {
    "path": "kugou/src/main/kotlin/com/metrolist/kugou/models/SearchSongResponse.kt",
    "content": "package com.metrolist.kugou.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class SearchSongResponse(\n    val status: Int,\n    val errcode: Int,\n    val error: String,\n    val data: Data,\n) {\n    @Serializable\n    data class Data(\n        val info: List<Info>,\n    ) {\n        @Serializable\n        data class Info(\n            val duration: Int,\n            val hash: String,\n        )\n    }\n}\n"
  },
  {
    "path": "lastfm/.gitignore",
    "content": "/build"
  },
  {
    "path": "lastfm/build.gradle.kts",
    "content": "plugins {\n    id(\"com.android.library\")\n    alias(libs.plugins.kotlin.serialization)\n}\n\nandroid {\n    namespace = \"com.metrolist.lastfm\"\n    compileSdk = 36\n\n    defaultConfig {\n        minSdk = 26\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_21\n        targetCompatibility = JavaVersion.VERSION_21\n    }\n}\n\nkotlin {\n    jvmToolchain(21)\n}\n\ndependencies {\n    implementation(libs.ktor.client.core)\n    implementation(libs.ktor.client.okhttp)\n    implementation(libs.ktor.client.content.negotiation)\n    implementation(libs.ktor.serialization.json)\n    implementation(libs.ktor.client.encoding)\n    testImplementation(libs.junit)\n\n    coreLibraryDesugaring(libs.desugaring)\n}\n"
  },
  {
    "path": "lastfm/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n</manifest>\n"
  },
  {
    "path": "lastfm/src/main/kotlin/com/metrolist/lastfm/LastFM.kt",
    "content": "package com.metrolist.lastfm\n\nimport com.metrolist.lastfm.models.Authentication\nimport com.metrolist.lastfm.models.LastFmError\nimport com.metrolist.lastfm.models.TokenResponse\nimport io.ktor.client.HttpClient\nimport io.ktor.client.call.body\nimport io.ktor.client.engine.okhttp.OkHttp\nimport io.ktor.client.plugins.contentnegotiation.ContentNegotiation\nimport io.ktor.client.plugins.defaultRequest\nimport io.ktor.client.request.*\nimport io.ktor.client.request.forms.FormDataContent\nimport io.ktor.client.statement.bodyAsText\nimport io.ktor.http.*\nimport io.ktor.serialization.kotlinx.json.json\nimport kotlinx.serialization.json.Json\nimport java.security.MessageDigest\n\nobject LastFM {\n    var sessionKey: String? = null\n\n    private val json = Json {\n        isLenient = true\n        ignoreUnknownKeys = true\n    }\n\n    private val client by lazy {\n        HttpClient(OkHttp) {\n            install(ContentNegotiation) {\n                json(json)\n            }\n            defaultRequest { url(\"https://ws.audioscrobbler.com/2.0/\") }\n            expectSuccess = false\n        }\n    }\n\n    private fun Map<String, String>.apiSig(secret: String): String {\n        val sorted = toSortedMap()\n        val toHash = sorted.entries.joinToString(\"\") { it.key + it.value } + secret\n        val digest = MessageDigest.getInstance(\"MD5\").digest(toHash.toByteArray())\n        return digest.joinToString(\"\") { \"%02x\".format(it) }\n    }\n\n    private fun HttpRequestBuilder.lastfmParams(\n        method: String,\n        apiKey: String,\n        secret: String,\n        sessionKey: String? = null,\n        extra: Map<String, String> = emptyMap(),\n        format: String = \"json\"\n    ) {\n        contentType(ContentType.Application.FormUrlEncoded)\n        userAgent(\"Metrolist (https://github.com/MetrolistGroup/Metrolist)\")\n        val paramsForSig = mutableMapOf(\n            \"method\" to method,\n            \"api_key\" to apiKey\n        ).apply {\n            sessionKey?.let { put(\"sk\", it) }\n            putAll(extra)\n        }\n        val apiSig = paramsForSig.apiSig(secret)\n        setBody(FormDataContent(Parameters.build {\n            paramsForSig.forEach { (k, v) -> append(k, v) }\n            append(\"api_sig\", apiSig)\n            append(\"format\", format)\n        }))\n    }\n\n    // OAuth methods (kept for backward compatibility)\n    suspend fun getToken() = runCatching {\n        client.post {\n            lastfmParams(\n                method = \"auth.getToken\",\n                apiKey = API_KEY,\n                secret = SECRET\n            )\n        }.body<TokenResponse>()\n    }\n\n    suspend fun getSession(token: String) = runCatching {\n        client.post {\n            lastfmParams(\n                method = \"auth.getSession\",\n                apiKey = API_KEY,\n                secret = SECRET,\n                extra = mapOf(\"token\" to token)\n            )\n        }.body<Authentication>()\n    }\n\n    fun getAuthUrl(token: String): String {\n        return \"https://www.last.fm/api/auth/?api_key=$API_KEY&token=$token\"\n    }\n\n    // Mobile session authentication\n    suspend fun getMobileSession(username: String, password: String) = runCatching {\n        val response = client.post {\n            lastfmParams(\n                method = \"auth.getMobileSession\",\n                apiKey = API_KEY,\n                secret = SECRET,\n                extra = mapOf(\"username\" to username, \"password\" to password)\n            )\n            parameter(\"format\", \"json\")\n        }\n\n        val responseText = response.bodyAsText()\n        if (responseText.contains(\"\\\"error\\\"\")) {\n            val error = json.decodeFromString<LastFmError>(responseText)\n            throw LastFmException(error.error, error.message)\n        }\n        json.decodeFromString<Authentication>(responseText)\n    }\n\n    class LastFmException(val code: Int, override val message: String) : Exception(message) {\n        override fun toString(): String = \"LastFmException(code=$code, message=$message)\"\n    }\n\n    suspend fun updateNowPlaying(\n        artist: String, track: String,\n        album: String? = null, trackNumber: Int? = null, duration: Int? = null\n    ) = runCatching {\n        client.post {\n            lastfmParams(\n                method = \"track.updateNowPlaying\",\n                apiKey = API_KEY,\n                secret = SECRET,\n                sessionKey = sessionKey!!,\n                extra = buildMap {\n                    put(\"artist\", artist)\n                    put(\"track\", track)\n                    album?.let { put(\"album\", it) }\n                    trackNumber?.let { put(\"trackNumber\", it.toString()) }\n                    duration?.let { put(\"duration\", it.toString()) }\n                }\n            )\n            parameter(\"format\", \"json\")\n        }\n    }\n\n    suspend fun scrobble(\n        artist: String, track: String, timestamp: Long,\n        album: String? = null, trackNumber: Int? = null, duration: Int? = null\n    ) = runCatching {\n        client.post {\n            lastfmParams(\n                method = \"track.scrobble\",\n                apiKey = API_KEY,\n                secret = SECRET,\n                sessionKey = sessionKey!!,\n                extra = buildMap {\n                    put(\"artist[0]\", artist)\n                    put(\"track[0]\", track)\n                    put(\"timestamp[0]\", timestamp.toString())\n                    album?.let { put(\"album[0]\", it) }\n                    trackNumber?.let { put(\"trackNumber[0]\", it.toString()) }\n                    duration?.let { put(\"duration[0]\", it.toString()) }\n                }\n            )\n            parameter(\"format\", \"json\")\n        }\n    }\n\n\n    suspend fun setLoveStatus(\n        artist: String, track: String, love: Boolean\n    ) = runCatching {\n        val method = if (love) \"track.love\" else \"track.unlove\"\n        client.post {\n            lastfmParams(\n                method = method,\n                apiKey = API_KEY,\n                secret = SECRET,\n                sessionKey = sessionKey!!,\n                extra = buildMap {\n                    put(\"artist\", artist)\n                    put(\"track\", track)\n                }\n            )\n            parameter(\"format\", \"json\")\n        }\n    }\n\n    // API keys passed from the app module (loaded from BuildConfig/GitHub Secrets)\n    private var API_KEY = \"\"\n    private var SECRET = \"\"\n\n    /**\n     * Initialize LastFM with API credentials\n     * @param apiKey LastFM API key\n     * @param secret LastFM secret key\n     */\n    fun initialize(apiKey: String, secret: String) {\n        API_KEY = apiKey\n        SECRET = secret\n    }\n\n    fun isInitialized(): Boolean = API_KEY.isNotEmpty() && SECRET.isNotEmpty()\n\n    const val DEFAULT_SCROBBLE_DELAY_PERCENT = 0.5f\n    const val DEFAULT_SCROBBLE_MIN_SONG_DURATION = 30\n    const val DEFAULT_SCROBBLE_DELAY_SECONDS = 180\n}\n"
  },
  {
    "path": "lastfm/src/main/kotlin/com/metrolist/lastfm/models/Authentication.kt",
    "content": "package com.metrolist.lastfm.models\n\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class Authentication(\n    val session: Session\n) {\n    @Serializable\n    data class Session(\n        val name: String,       // Username\n        val key: String,        // Session Key\n        val subscriber: Int,    // Last.fm Pro?\n    )\n}\n\n@Serializable\ndata class TokenResponse(\n    val token: String\n)\n\n@Serializable\ndata class LastFmError(\n    val error: Int,\n    val message: String\n)\n"
  },
  {
    "path": "lint.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<lint>\n    <issue id=\"UnsafeOptInUsageError\">\n        <ignore regexp=\"\\(markerClass = androidx\\.media3\\.common\\.util\\.UnstableApi\\.class\\)\" />\n    </issue>\n</lint>\n"
  },
  {
    "path": "lrclib/.gitignore",
    "content": "/build"
  },
  {
    "path": "lrclib/build.gradle.kts",
    "content": "plugins {\n    id(\"com.android.library\")\n    alias(libs.plugins.kotlin.serialization)\n}\n\nandroid {\n    namespace = \"com.metrolist.lrclib\"\n    compileSdk = 36\n\n    defaultConfig {\n        minSdk = 26\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_21\n        targetCompatibility = JavaVersion.VERSION_21\n    }\n}\n\nkotlin {\n    jvmToolchain(21)\n}\n\ndependencies {\n    implementation(libs.ktor.client.core)\n    implementation(libs.ktor.client.okhttp)\n    implementation(libs.ktor.client.cio)\n    implementation(libs.ktor.client.content.negotiation)\n    implementation(libs.ktor.serialization.json)\n    implementation(libs.ktor.client.encoding)\n    testImplementation(libs.junit)\n\n    coreLibraryDesugaring(libs.desugaring)\n}\n"
  },
  {
    "path": "lrclib/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n</manifest>\n"
  },
  {
    "path": "lrclib/src/main/kotlin/com/metrolist/lrclib/LrcLib.kt",
    "content": "package com.metrolist.lrclib\n\nimport com.metrolist.lrclib.models.Track\nimport com.metrolist.lrclib.models.bestMatchingFor\nimport com.metrolist.lrclib.models.bestMatchingForRelaxed\nimport io.ktor.client.HttpClient\nimport io.ktor.client.call.body\nimport io.ktor.client.engine.cio.CIO\nimport io.ktor.client.plugins.contentnegotiation.ContentNegotiation\nimport io.ktor.client.plugins.defaultRequest\nimport io.ktor.client.request.get\nimport io.ktor.client.request.parameter\nimport io.ktor.serialization.kotlinx.json.json\nimport kotlinx.coroutines.currentCoroutineContext\nimport kotlinx.coroutines.ensureActive\nimport kotlinx.serialization.json.Json\nimport kotlin.math.abs\n\nobject LrcLib {\n    private val client by lazy {\n        HttpClient(CIO) {\n            install(ContentNegotiation) {\n                json(\n                    Json {\n                        isLenient = true\n                        ignoreUnknownKeys = true\n                    },\n                )\n            }\n\n            defaultRequest {\n                url(\"https://lrclib.net\")\n            }\n\n            expectSuccess = true\n        }\n    }\n\n    // Patterns to clean from title\n    private val titleCleanupPatterns = listOf(\n        Regex(\"\"\"\\s*\\(.*?(official|video|audio|lyrics|lyric|visualizer|hd|hq|4k|remaster|remix|live|acoustic|version|edit|extended|radio|clean|explicit).*?\\)\"\"\", RegexOption.IGNORE_CASE),\n        Regex(\"\"\"\\s*\\[.*?(official|video|audio|lyrics|lyric|visualizer|hd|hq|4k|remaster|remix|live|acoustic|version|edit|extended|radio|clean|explicit).*?\\]\"\"\", RegexOption.IGNORE_CASE),\n        Regex(\"\"\"\\s*【.*?】\"\"\"),\n        Regex(\"\"\"\\s*\\|.*$\"\"\"),\n        Regex(\"\"\"\\s*-\\s*(official|video|audio|lyrics|lyric|visualizer).*$\"\"\", RegexOption.IGNORE_CASE),\n        Regex(\"\"\"\\s*\\(feat\\..*?\\)\"\"\", RegexOption.IGNORE_CASE),\n        Regex(\"\"\"\\s*\\(ft\\..*?\\)\"\"\", RegexOption.IGNORE_CASE),\n        Regex(\"\"\"\\s*feat\\..*$\"\"\", RegexOption.IGNORE_CASE),\n        Regex(\"\"\"\\s*ft\\..*$\"\"\", RegexOption.IGNORE_CASE),\n    )\n\n    // Patterns to extract primary artist\n    private val artistSeparators = listOf(\" & \", \" and \", \", \", \" x \", \" X \", \" feat. \", \" feat \", \" ft. \", \" ft \", \" featuring \", \" with \")\n\n    private fun cleanTitle(title: String): String {\n        var cleaned = title.trim()\n        for (pattern in titleCleanupPatterns) {\n            cleaned = cleaned.replace(pattern, \"\")\n        }\n        return cleaned.trim()\n    }\n\n    private fun cleanArtist(artist: String): String {\n        var cleaned = artist.trim()\n        // Get primary artist (first one before any separator)\n        for (separator in artistSeparators) {\n            if (cleaned.contains(separator, ignoreCase = true)) {\n                cleaned = cleaned.split(separator, ignoreCase = true, limit = 2)[0]\n                break\n            }\n        }\n        return cleaned.trim()\n    }\n\n    private suspend fun queryLyricsWithParams(\n        trackName: String? = null,\n        artistName: String? = null,\n        albumName: String? = null,\n        query: String? = null,\n    ): List<Track> = runCatching {\n        client.get(\"/api/search\") {\n            if (query != null) parameter(\"q\", query)\n            if (trackName != null) parameter(\"track_name\", trackName)\n            if (artistName != null) parameter(\"artist_name\", artistName)\n            if (albumName != null) parameter(\"album_name\", albumName)\n        }.body<List<Track>>()\n    }.getOrDefault(emptyList())\n\n    private suspend fun queryLyrics(\n        artist: String,\n        title: String,\n        album: String? = null,\n    ): List<Track> {\n        val cleanedTitle = cleanTitle(title)\n        val cleanedArtist = cleanArtist(artist)\n        \n        // Strategy 1: Search with cleaned title and artist\n        var results = queryLyricsWithParams(\n            trackName = cleanedTitle,\n            artistName = cleanedArtist,\n            albumName = album\n        ).filter { it.syncedLyrics != null || it.plainLyrics != null }\n        \n        if (results.isNotEmpty()) return results\n        \n        // Strategy 2: Search with cleaned title only (artist might be different)\n        results = queryLyricsWithParams(\n            trackName = cleanedTitle\n        ).filter { it.syncedLyrics != null || it.plainLyrics != null }\n        \n        if (results.isNotEmpty()) return results\n        \n        // Strategy 3: Use q parameter with combined search\n        results = queryLyricsWithParams(\n            query = \"$cleanedArtist $cleanedTitle\"\n        ).filter { it.syncedLyrics != null || it.plainLyrics != null }\n        \n        if (results.isNotEmpty()) return results\n        \n        // Strategy 4: Use q parameter with just title\n        results = queryLyricsWithParams(\n            query = cleanedTitle\n        ).filter { it.syncedLyrics != null || it.plainLyrics != null }\n        \n        if (results.isNotEmpty()) return results\n        \n        // Strategy 5: Try original title if different from cleaned\n        if (cleanedTitle != title.trim()) {\n            results = queryLyricsWithParams(\n                trackName = title.trim(),\n                artistName = artist.trim()\n            ).filter { it.syncedLyrics != null || it.plainLyrics != null }\n        }\n        \n        return results\n    }\n\n    suspend fun getLyrics(\n        title: String,\n        artist: String,\n        duration: Int,\n        album: String? = null,\n    ) = runCatching {\n        val tracks = queryLyrics(artist, title, album)\n        val cleanedTitle = cleanTitle(title)\n        val cleanedArtist = cleanArtist(artist)\n\n        val res = when {\n            duration == -1 -> {\n                tracks.bestMatchingFor(duration, cleanedTitle, cleanedArtist)?.let { track ->\n                    track.syncedLyrics ?: track.plainLyrics\n                }?.let(LrcLib::Lyrics)\n            }\n            else -> {\n                // Try with relaxed duration matching (±5 seconds instead of ±2)\n                tracks.bestMatchingForRelaxed(duration)?.let { track ->\n                    track.syncedLyrics ?: track.plainLyrics\n                }?.let(LrcLib::Lyrics)\n            }\n        }\n\n        if (res != null) {\n            return@runCatching res.text\n        } else {\n            throw IllegalStateException(\"Lyrics unavailable\")\n        }\n    }\n\n    suspend fun getAllLyrics(\n        title: String,\n        artist: String,\n        duration: Int,\n        album: String? = null,\n        callback: (String) -> Unit,\n    ) {\n        val tracks = queryLyrics(artist, title, album)\n        val cleanedTitle = cleanTitle(title)\n        val cleanedArtist = cleanArtist(artist)\n        var count = 0\n        var plain = 0\n\n        val sortedTracks = when {\n            duration == -1 -> {\n                tracks.sortedByDescending { track ->\n                    var score = 0.0\n\n                    if (track.syncedLyrics != null) score += 1.0\n\n                    val titleSimilarity = calculateStringSimilarity(cleanedTitle, track.trackName)\n                    val artistSimilarity = calculateStringSimilarity(cleanedArtist, track.artistName)\n                    score += (titleSimilarity + artistSimilarity) / 2.0\n                    \n                    score\n                }\n            }\n            else -> {\n                tracks.sortedBy { abs(it.duration.toInt() - duration) }\n            }\n        }\n\n        sortedTracks.forEach { track ->\n            currentCoroutineContext().ensureActive()\n            if (count <= 4) {\n                if (track.syncedLyrics != null && duration == -1) {\n                    count++\n                    track.syncedLyrics.let(callback)\n                } else {\n                    // Relaxed duration matching (±5 seconds)\n                    if (track.syncedLyrics != null && abs(track.duration.toInt() - duration) <= 5) {\n                        count++\n                        track.syncedLyrics.let(callback)\n                    }\n                    if (track.plainLyrics != null && abs(track.duration.toInt() - duration) <= 5 && plain == 0) {\n                        count++\n                        plain++\n                        track.plainLyrics.let(callback)\n                    }\n                }\n            }\n        }\n    }\n\n    private fun calculateStringSimilarity(str1: String, str2: String): Double {\n        val s1 = str1.trim().lowercase()\n        val s2 = str2.trim().lowercase()\n        \n        if (s1 == s2) return 1.0\n        if (s1.isEmpty() || s2.isEmpty()) return 0.0\n        \n        return when {\n            s1.contains(s2) || s2.contains(s1) -> 0.8\n            else -> {\n                val maxLength = maxOf(s1.length, s2.length)\n                val distance = levenshteinDistance(s1, s2)\n                1.0 - (distance.toDouble() / maxLength)\n            }\n        }\n    }\n\n    private fun levenshteinDistance(str1: String, str2: String): Int {\n        val len1 = str1.length\n        val len2 = str2.length\n        val matrix = Array(len1 + 1) { IntArray(len2 + 1) }\n        \n        for (i in 0..len1) matrix[i][0] = i\n        for (j in 0..len2) matrix[0][j] = j\n        \n        for (i in 1..len1) {\n            for (j in 1..len2) {\n                val cost = if (str1[i - 1] == str2[j - 1]) 0 else 1\n                matrix[i][j] = minOf(\n                    matrix[i - 1][j] + 1,      // deletion\n                    matrix[i][j - 1] + 1,      // insertion\n                    matrix[i - 1][j - 1] + cost // substitution\n                )\n            }\n        }\n        \n        return matrix[len1][len2]\n    }\n\n    suspend fun lyrics(\n        artist: String,\n        title: String,\n    ) = runCatching {\n        queryLyrics(artist = artist, title = title, album = null)\n    }\n\n    @JvmInline\n    value class Lyrics(\n        val text: String,\n    ) {\n        val sentences\n            get() =\n                runCatching {\n                    buildMap {\n                        put(0L, \"\")\n                        text.trim().lines().filter { it.length >= 10 }.forEach {\n                            put(\n                                it[8].digitToInt() * 10L +\n                                    it[7].digitToInt() * 100 +\n                                    it[5].digitToInt() * 1000 +\n                                    it[4].digitToInt() * 10000 +\n                                    it[2].digitToInt() * 60 * 1000 +\n                                    it[1].digitToInt() * 600 * 1000,\n                                it.substring(10),\n                            )\n                        }\n                    }\n                }.getOrNull()\n    }\n}\n\n\n"
  },
  {
    "path": "lrclib/src/main/kotlin/com/metrolist/lrclib/models/Track.kt",
    "content": "package com.metrolist.lrclib.models\n\nimport kotlinx.serialization.Serializable\nimport kotlin.math.abs\n\n@Serializable\ndata class Track(\n    val id: Int,\n    val trackName: String,\n    val artistName: String,\n    val duration: Double,\n    val plainLyrics: String?,\n    val syncedLyrics: String?,\n)\n\ninternal fun List<Track>.bestMatchingFor(duration: Int): Track? {\n    if (isEmpty()) return null\n\n    if (duration == -1) {\n        return firstOrNull { it.syncedLyrics != null } ?: firstOrNull()\n    }\n\n    return minByOrNull { abs(it.duration.toInt() - duration) }\n        ?.takeIf { abs(it.duration.toInt() - duration) <= 2 }\n}\n\n// Relaxed matching with ±5 seconds tolerance\ninternal fun List<Track>.bestMatchingForRelaxed(duration: Int): Track? {\n    if (isEmpty()) return null\n\n    if (duration == -1) {\n        return firstOrNull { it.syncedLyrics != null } ?: firstOrNull()\n    }\n\n    // First try to find synced lyrics within tolerance\n    val syncedMatch = filter { it.syncedLyrics != null }\n        .minByOrNull { abs(it.duration.toInt() - duration) }\n        ?.takeIf { abs(it.duration.toInt() - duration) <= 5 }\n    \n    if (syncedMatch != null) return syncedMatch\n    \n    // Fall back to any lyrics within tolerance\n    return minByOrNull { abs(it.duration.toInt() - duration) }\n        ?.takeIf { abs(it.duration.toInt() - duration) <= 5 }\n}\n\ninternal fun List<Track>.bestMatchingFor(\n    duration: Int,\n    trackName: String? = null,\n    artistName: String? = null\n): Track? {\n    if (isEmpty()) return null\n\n    if (duration == -1) {\n        if (trackName != null && artistName != null) {\n            return findBestMatch(trackName, artistName)\n        }\n        return firstOrNull { it.syncedLyrics != null } ?: firstOrNull()\n    }\n\n    // Use relaxed matching for duration-based search\n    return bestMatchingForRelaxed(duration)\n}\n\nprivate fun List<Track>.findBestMatch(trackName: String, artistName: String): Track? {\n    val normalizedTrackName = trackName.trim().lowercase()\n    val normalizedArtistName = artistName.trim().lowercase()\n    \n    return maxByOrNull { track ->\n        var score = 0.0\n\n        val trackNameSimilarity = calculateSimilarity(\n            normalizedTrackName, \n            track.trackName.trim().lowercase()\n        )\n\n        val artistNameSimilarity = calculateSimilarity(\n            normalizedArtistName, \n            track.artistName.trim().lowercase()\n        )\n        \n        score = (trackNameSimilarity + artistNameSimilarity) / 2.0\n\n        if (track.syncedLyrics != null) score += 0.1\n        \n        score\n    }?.takeIf { track ->\n        val trackNameSimilarity = calculateSimilarity(\n            normalizedTrackName, \n            track.trackName.trim().lowercase()\n        )\n        val artistNameSimilarity = calculateSimilarity(\n            normalizedArtistName, \n            track.artistName.trim().lowercase()\n        )\n\n        (trackNameSimilarity + artistNameSimilarity) / 2.0 > 0.6\n    }\n}\n\nprivate fun calculateSimilarity(str1: String, str2: String): Double {\n    if (str1 == str2) return 1.0\n    if (str1.isEmpty() || str2.isEmpty()) return 0.0\n\n    val containsScore = when {\n        str1.contains(str2) || str2.contains(str1) -> 0.8\n        else -> 0.0\n    }\n\n    val maxLength = maxOf(str1.length, str2.length)\n    val distance = levenshteinDistance(str1, str2)\n    val distanceScore = 1.0 - (distance.toDouble() / maxLength)\n    \n    return maxOf(containsScore, distanceScore)\n}\n\nprivate fun levenshteinDistance(str1: String, str2: String): Int {\n    val len1 = str1.length\n    val len2 = str2.length\n    val matrix = Array(len1 + 1) { IntArray(len2 + 1) }\n    \n    for (i in 0..len1) matrix[i][0] = i\n    for (j in 0..len2) matrix[0][j] = j\n    \n    for (i in 1..len1) {\n        for (j in 1..len2) {\n            val cost = if (str1[i - 1] == str2[j - 1]) 0 else 1\n            matrix[i][j] = minOf(\n                matrix[i - 1][j] + 1,      // deletion\n                matrix[i][j - 1] + 1,      // insertion\n                matrix[i - 1][j - 1] + cost // substitution\n            )\n        }\n    }\n    \n    return matrix[len1][len2]\n}\n"
  },
  {
    "path": "renovate.json",
    "content": "{\n  \"$schema\": \"https://docs.renovatebot.com/renovate-schema.json\",\n  \"extends\": [\n    \"config:recommended\"\n  ],\n  \"ignoreUnstable\": true,\n  \"respectLatest\": true,\n  \"separateMajorMinor\": true,\n  \"packageRules\": [\n    {\n      \"matchPackageNames\": [\n        \"*\"\n      ],\n      \"ignoreUnstable\": true,\n      \"respectLatest\": true\n    },\n    {\n      \"matchManagers\": [\n        \"gradle\"\n      ],\n      \"enabled\": false,\n      \"matchPackageNames\": [\n        \"/^com\\\\.github\\\\.libre\\\\-tube:NewPipeExtractor$/\"\n      ]\n    },\n    {\n      \"description\": \"Ignore all pre-release versions (alpha, beta, rc, dev, snapshot)\",\n      \"matchPackageNames\": [\"*\"],\n      \"allowedVersions\": \"!/-(alpha|beta|rc|dev|snapshot|SNAPSHOT|Alpha|Beta|RC|Dev)/i\"\n    }\n  ]\n}\n"
  },
  {
    "path": "settings.gradle.kts",
    "content": "@file:Suppress(\"UnstableApiUsage\")\n\ndependencyResolutionManagement {\n    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)\n\n    repositories {\n        google()\n        mavenCentral()\n        maven { setUrl(\"https://jitpack.io\") }\n        maven { setUrl(\"https://maven.aliyun.com/repository/public\") }\n    }\n}\n\n// F-Droid doesn't support foojay-resolver plugin\n// plugins {\n//     id(\"org.gradle.toolchains.foojay-resolver-convention\") version(\"1.0.0\")\n// }\n\nrootProject.name = \"Metrolist\"\ninclude(\":app\")\ninclude(\":innertube\")\ninclude(\":kugou\")\ninclude(\":lrclib\")\ninclude(\":kizzy\")\ninclude(\":lastfm\")\ninclude(\":betterlyrics\")\ninclude(\":simpmusic\")\ninclude(\":shazamkit\")\n\n// Use a local copy of NewPipe Extractor by uncommenting the lines below.\n// We assume, that Metrolist and NewPipe Extractor have the same parent directory.\n// If this is not the case, please change the path in includeBuild().\n//\n// For this to work you also need to change the implementation in innertube/build.gradle.kts\n// to one which does not specify a version.\n// From:\n//      implementation(libs.newpipe.extractor)\n// To:\n//      implementation(\"com.github.teamnewpipe:NewPipeExtractor\")\n//includeBuild(\"../NewPipeExtractor\") {\n//    dependencySubstitution {\n//        substitute(module(\"com.github.teamnewpipe:NewPipeExtractor\")).using(project(\":extractor\"))\n//    }\n//}\n"
  },
  {
    "path": "shazamkit/build.gradle.kts",
    "content": "plugins {\n    id(\"com.android.library\")\n    alias(libs.plugins.kotlin.serialization)\n}\n\nandroid {\n    namespace = \"com.metrolist.shazamkit\"\n    compileSdk = 36\n\n    defaultConfig {\n        minSdk = 26\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_21\n        targetCompatibility = JavaVersion.VERSION_21\n    }\n}\n\nkotlin {\n    jvmToolchain(21)\n}\n\ndependencies {\n    implementation(libs.ktor.client.core)\n    implementation(libs.ktor.client.okhttp)\n    implementation(libs.ktor.client.cio)\n    implementation(libs.ktor.client.content.negotiation)\n    implementation(libs.ktor.serialization.json)\n    implementation(libs.ktor.client.encoding)\n    testImplementation(libs.junit)\n\n    coreLibraryDesugaring(libs.desugaring)\n}\n"
  },
  {
    "path": "shazamkit/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n</manifest>\n"
  },
  {
    "path": "shazamkit/src/main/kotlin/com/metrolist/shazamkit/Shazam.kt",
    "content": "package com.metrolist.shazamkit\n\nimport com.metrolist.shazamkit.models.RecognitionResult\nimport com.metrolist.shazamkit.models.ShazamRequestJson\nimport com.metrolist.shazamkit.models.ShazamResponseJson\nimport io.ktor.client.HttpClient\nimport io.ktor.client.call.body\nimport io.ktor.client.engine.cio.CIO\nimport io.ktor.client.plugins.contentnegotiation.ContentNegotiation\nimport io.ktor.client.request.header\nimport io.ktor.client.request.parameter\nimport io.ktor.client.request.post\nimport io.ktor.client.request.setBody\nimport io.ktor.http.ContentType\nimport io.ktor.http.contentType\nimport io.ktor.http.isSuccess\nimport io.ktor.serialization.kotlinx.json.json\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.SupervisorJob\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport kotlinx.serialization.json.Json\nimport java.util.UUID\nimport java.util.concurrent.ConcurrentHashMap\nimport java.util.concurrent.ConcurrentLinkedQueue\nimport java.util.concurrent.atomic.AtomicInteger\nimport kotlin.random.Random\n\n/**\n * Shazam music recognition with built-in rate limiting and queue management\n */\nobject Shazam {\n\n    private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())\n\n    // Configuration\n    private const val MAX_CONCURRENT_REQUESTS = 2\n    \n    private const val MIN_REQUEST_INTERVAL_MS = 1000L\n    \n    private const val MAX_RETRIES = 3\n    \n    private const val INITIAL_RETRY_DELAY_MS = 2000L\n    \n    private const val CACHE_DURATION_MS = 300000L\n    \n    private const val MAX_QUEUE_SIZE = 50\n\n    // Internal State\n    private val activeRequests = AtomicInteger(0)\n    \n    private var lastRequestTime = 0L\n    \n    private val requestMutex = Mutex()\n    \n    private val requestQueue = ConcurrentLinkedQueue<PendingRequest>()\n    \n    private val resultCache = ConcurrentHashMap<String, CachedResult>()\n    \n    private var nextRequestId = 0L\n    \n    private var isProcessingQueue = false\n\n    // HTTP Client Configuration\n    private val client by lazy {\n        HttpClient(CIO) {\n            install(ContentNegotiation) {\n                json(\n                    Json {\n                        isLenient = true\n                        ignoreUnknownKeys = true\n                        encodeDefaults = true\n                    },\n                )\n            }\n            expectSuccess = false\n            \n            engine {\n                requestTimeout = 30000\n            }\n        }\n    }\n\n    private val userAgents = listOf(\n        \"Dalvik/2.1.0 (Linux; U; Android 5.0.2; VS980 4G Build/LRX22G)\",\n        \"Dalvik/1.6.0 (Linux; U; Android 4.4.2; SM-T210 Build/KOT49H)\",\n        \"Dalvik/2.1.0 (Linux; U; Android 5.1.1; SM-P905V Build/LMY47X)\",\n        \"Dalvik/2.1.0 (Linux; U; Android 6.0.1; SM-G920F Build/MMB29K)\",\n        \"Dalvik/2.1.0 (Linux; U; Android 5.0; SM-G900F Build/LRX21T)\"\n    )\n\n    private val timezones = listOf(\n        \"Europe/Paris\", \"Europe/London\", \"America/New_York\",\n        \"America/Los_Angeles\", \"Asia/Tokyo\", \"Asia/Dubai\"\n    )\n\n    /**\n     * Recognize music from audio signature\n     * \n     * @param signature Audio signature in Shazam DejaVu format\n     * @param sampleDurationMs Sample duration in milliseconds\n     * @return Result containing recognition result or error\n     */\n    suspend fun recognize(signature: String, sampleDurationMs: Long): Result<RecognitionResult> {\n        val cacheKey = generateCacheKey(signature)\n        getCachedResult(cacheKey)?.let {\n            return Result.success(it)\n        }\n\n        return enqueueRequest(signature, sampleDurationMs)\n    }\n\n    /**\n     * Get number of pending requests in queue\n     */\n    fun getPendingRequestsCount(): Int = requestQueue.size\n\n    /**\n     * Get number of active requests\n     */\n    fun getActiveRequestsCount(): Int = activeRequests.get()\n\n    /**\n     * Clear cache\n     */\n    fun clearCache() {\n        resultCache.clear()\n    }\n\n    /**\n     * Cancel all pending requests\n     */\n    fun cancelPendingRequests() {\n        requestQueue.clear()\n    }\n\n    /**\n     * Cleanup resources\n     */\n    fun cleanup() {\n        cancelPendingRequests()\n        clearCache()\n        client.close()\n    }\n\n    /**\n     * Enqueue request for processing\n     */\n    private suspend fun enqueueRequest(\n        signature: String,\n        sampleDurationMs: Long\n    ): Result<RecognitionResult> = requestMutex.withLock {\n        if (requestQueue.size >= MAX_QUEUE_SIZE) {\n            return Result.failure(Exception(\"Request queue is full. Please wait.\"))\n        }\n\n        val requestId = nextRequestId++\n        val request = PendingRequest(\n            id = requestId,\n            signature = signature,\n            sampleDurationMs = sampleDurationMs\n        )\n\n        requestQueue.offer(request)\n\n        if (!isProcessingQueue) {\n            isProcessingQueue = true\n            processQueue()\n        }\n\n        return request.awaitResult()\n    }\n\n    /**\n     * Process request queue\n     */\n    private suspend fun processQueue() {\n        while (true) {\n            val request = requestQueue.poll() ?: break\n\n            while (activeRequests.get() >= MAX_CONCURRENT_REQUESTS) {\n                delay(100)\n            }\n\n            activeRequests.incrementAndGet()\n\n            scope.launch {\n                try {\n                    val result = executeRequest(request.signature, request.sampleDurationMs)\n                    request.completeWith(result)\n                } catch (e: Exception) {\n                    request.completeWith(Result.failure(e))\n                } finally {\n                    activeRequests.decrementAndGet()\n                }\n            }\n\n            enforceRateLimit()\n        }\n\n        isProcessingQueue = false\n    }\n\n    /**\n     * Execute recognition request with retry logic\n     */\n    private suspend fun executeRequest(\n        signature: String,\n        sampleDurationMs: Long\n    ): Result<RecognitionResult> {\n        var lastException: Exception? = null\n\n        for (attempt in 0 until MAX_RETRIES) {\n            try {\n                enforceRateLimit()\n                \n                val result = performRecognition(signature, sampleDurationMs)\n                \n                val cacheKey = generateCacheKey(signature)\n                cacheResult(cacheKey, result)\n                \n                return Result.success(result)\n            } catch (e: Exception) {\n                lastException = e\n\n                if (e.message?.contains(\"429\") == true ||\n                    e.message?.contains(\"Too many requests\", ignoreCase = true) == true\n                ) {\n                    if (attempt < MAX_RETRIES - 1) {\n                        val delayTime = calculateBackoffDelay(attempt)\n                        delay(delayTime)\n                        continue\n                    }\n                } else {\n                    throw e\n                }\n            }\n        }\n\n        throw lastException ?: Exception(\"Recognition failed after $MAX_RETRIES attempts\")\n    }\n\n    /**\n     * Perform actual recognition request\n     */\n    private suspend fun performRecognition(\n        signature: String,\n        sampleDurationMs: Long\n    ): RecognitionResult {\n        val timestamp = System.currentTimeMillis() / 1000\n        val uuid1 = UUID.randomUUID().toString().uppercase()\n        val uuid2 = UUID.randomUUID().toString()\n\n        val request = ShazamRequestJson(\n            geolocation = ShazamRequestJson.Geolocation(\n                altitude = Random.nextDouble() * 400 + 100,\n                latitude = Random.nextDouble() * 180 - 90,\n                longitude = Random.nextDouble() * 360 - 180\n            ),\n            signature = ShazamRequestJson.Signature(\n                samplems = sampleDurationMs,\n                timestamp = timestamp,\n                uri = signature\n            ),\n            timestamp = timestamp,\n            timezone = timezones.random()\n        )\n\n        val response = client.post(\"https://amp.shazam.com/discovery/v5/en/US/android/-/tag/$uuid1/$uuid2\") {\n            parameter(\"sync\", \"true\")\n            parameter(\"webv3\", \"true\")\n            parameter(\"sampling\", \"true\")\n            parameter(\"connected\", \"\")\n            parameter(\"shazamapiversion\", \"v3\")\n            parameter(\"sharehub\", \"true\")\n            parameter(\"video\", \"v3\")\n            header(\"User-Agent\", userAgents.random())\n            header(\"Content-Language\", \"en_US\")\n            contentType(ContentType.Application.Json)\n            setBody(request)\n        }\n\n        if (!response.status.isSuccess()) {\n            val statusCode = response.status.value\n            when (statusCode) {\n                429 -> throw Exception(\"Too many requests\")\n                404 -> throw Exception(\"No match found\")\n                in 500..599 -> throw Exception(\"Shazam service temporarily unavailable\")\n                else -> throw Exception(\"Recognition failed (error $statusCode)\")\n            }\n        }\n\n        val shazamResponse = response.body<ShazamResponseJson>()\n        return shazamResponse.toRecognitionResult()\n            ?: throw Exception(\"No match found\")\n    }\n\n    /**\n     * Enforce minimum time between requests\n     */\n    private suspend fun enforceRateLimit() {\n        val currentTime = System.currentTimeMillis()\n        val timeSinceLastRequest = currentTime - lastRequestTime\n\n        if (timeSinceLastRequest < MIN_REQUEST_INTERVAL_MS) {\n            val delayTime = MIN_REQUEST_INTERVAL_MS - timeSinceLastRequest\n            delay(delayTime)\n        }\n\n        lastRequestTime = System.currentTimeMillis()\n    }\n\n    /**\n     * Calculate delay using Exponential Backoff\n     */\n    private fun calculateBackoffDelay(attempt: Int): Long {\n        return INITIAL_RETRY_DELAY_MS * (1 shl attempt)\n    }\n\n    /**\n     * Generate cache key\n     */\n    private fun generateCacheKey(signature: String): String {\n        return signature.hashCode().toString()\n    }\n\n    /**\n     * Get result from cache\n     */\n    private fun getCachedResult(key: String): RecognitionResult? {\n        val cached = resultCache[key] ?: return null\n        val currentTime = System.currentTimeMillis()\n\n        if (currentTime - cached.timestamp > CACHE_DURATION_MS) {\n            resultCache.remove(key)\n            return null\n        }\n\n        return cached.result\n    }\n\n    /**\n     * Cache result\n     */\n    private fun cacheResult(key: String, result: RecognitionResult) {\n        resultCache[key] = CachedResult(\n            timestamp = System.currentTimeMillis(),\n            result = result\n        )\n\n        cleanupCache()\n    }\n\n    /**\n     * Cleanup expired cache entries\n     */\n    private fun cleanupCache() {\n        if (resultCache.size < 100) return\n\n        val currentTime = System.currentTimeMillis()\n        val iterator = resultCache.entries.iterator()\n\n        while (iterator.hasNext()) {\n            val entry = iterator.next()\n            if (currentTime - entry.value.timestamp > CACHE_DURATION_MS) {\n                iterator.remove()\n            }\n        }\n    }\n\n    /**\n     * Convert Shazam response to internal model\n     */\n    private fun ShazamResponseJson.toRecognitionResult(): RecognitionResult? {\n        val track = this.track ?: return null\n\n        val songSection = track.sections?.find { it?.type == \"SONG\" }\n        val metadata = songSection?.metadata\n        val album = metadata?.find { it?.title == \"Album\" }?.text\n        val label = metadata?.find { it?.title == \"Label\" }?.text\n        val releaseDate = metadata?.find { it?.title == \"Released\" }?.text\n\n        val lyricsSection = track.sections?.find { it?.type == \"LYRICS\" }\n        val lyrics = lyricsSection?.text\n\n        val appleAction = track.hub?.options?.firstOrNull {\n            it?.providername?.contains(\"apple\", ignoreCase = true) == true\n        }?.actions?.firstOrNull()\n        \n        val spotifyProvider = track.hub?.providers?.find {\n            it?.caption?.contains(\"spotify\", ignoreCase = true) == true\n        }\n\n        val youtubeAction = track.hub?.options?.find {\n            it?.type?.contains(\"video\", ignoreCase = true) == true\n        }?.actions?.firstOrNull()\n        \n        val youtubeVideoId = youtubeAction?.uri?.let { uri ->\n            uri.substringAfterLast(\"v=\", \"\").takeIf { it.isNotEmpty() }\n                ?: uri.substringAfterLast(\"/\", \"\").takeIf { it.isNotEmpty() && it.length == 11 }\n        }\n\n        return RecognitionResult(\n            trackId = track.key ?: tagid ?: \"\",\n            title = track.title ?: \"\",\n            artist = track.subtitle ?: \"\",\n            album = album,\n            coverArtUrl = track.images?.coverart,\n            coverArtHqUrl = track.images?.coverarthq,\n            genre = track.genres?.primary,\n            releaseDate = releaseDate,\n            label = label,\n            lyrics = lyrics,\n            shazamUrl = track.url,\n            appleMusicUrl = appleAction?.uri,\n            spotifyUrl = spotifyProvider?.actions?.firstOrNull()?.uri,\n            isrc = track.isrc,\n            youtubeVideoId = youtubeVideoId\n        )\n    }\n\n    /**\n     * Pending request in queue\n     */\n    private class PendingRequest(\n        val id: Long,\n        val signature: String,\n        val sampleDurationMs: Long\n    ) {\n        private val mutex = Mutex()\n        private var result: Result<RecognitionResult>? = null\n        private var isCompleted = false\n\n        suspend fun awaitResult(): Result<RecognitionResult> {\n            while (!isCompleted) {\n                delay(50)\n            }\n            return result ?: Result.failure(Exception(\"Result not received\"))\n        }\n\n        fun completeWith(result: Result<RecognitionResult>) {\n            this.result = result\n            this.isCompleted = true\n        }\n    }\n\n    /**\n     * Cached result\n     */\n    private data class CachedResult(\n        val timestamp: Long,\n        val result: RecognitionResult\n    )\n}\n"
  },
  {
    "path": "shazamkit/src/main/kotlin/com/metrolist/shazamkit/models/ShazamModels.kt",
    "content": "package com.metrolist.shazamkit.models\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class ShazamRequestJson(\n    @SerialName(\"geolocation\")\n    val geolocation: Geolocation,\n    @SerialName(\"signature\")\n    val signature: Signature,\n    @SerialName(\"timestamp\")\n    val timestamp: Long,\n    @SerialName(\"timezone\")\n    val timezone: String\n) {\n    @Serializable\n    data class Geolocation(\n        @SerialName(\"altitude\")\n        val altitude: Double,\n        @SerialName(\"latitude\")\n        val latitude: Double,\n        @SerialName(\"longitude\")\n        val longitude: Double\n    )\n\n    @Serializable\n    data class Signature(\n        @SerialName(\"samplems\")\n        val samplems: Long,\n        @SerialName(\"timestamp\")\n        val timestamp: Long,\n        @SerialName(\"uri\")\n        val uri: String\n    )\n}\n\n@Serializable\ndata class ShazamResponseJson(\n    @SerialName(\"matches\")\n    val matches: List<Match?>? = null,\n    @SerialName(\"location\")\n    val location: Location? = null,\n    @SerialName(\"timestamp\")\n    val timestamp: Long? = null,\n    @SerialName(\"timezone\")\n    val timezone: String? = null,\n    @SerialName(\"track\")\n    val track: Track? = null,\n    @SerialName(\"tagid\")\n    val tagid: String? = null\n) {\n    @Serializable\n    data class Match(\n        @SerialName(\"id\")\n        val id: String? = null,\n        @SerialName(\"offset\")\n        val offset: Double? = null,\n        @SerialName(\"timeskew\")\n        val timeskew: Double? = null,\n        @SerialName(\"frequencyskew\")\n        val frequencyskew: Double? = null\n    )\n\n    @Serializable\n    data class Location(\n        @SerialName(\"latitude\")\n        val latitude: Double? = null,\n        @SerialName(\"longitude\")\n        val longitude: Double? = null,\n        @SerialName(\"altitude\")\n        val altitude: Double? = null,\n        @SerialName(\"accuracy\")\n        val accuracy: Double? = null\n    )\n\n    @Serializable\n    data class Track(\n        @SerialName(\"layout\")\n        val layout: String? = null,\n        @SerialName(\"type\")\n        val type: String? = null,\n        @SerialName(\"key\")\n        val key: String? = null,\n        @SerialName(\"title\")\n        val title: String? = null,\n        @SerialName(\"subtitle\")\n        val subtitle: String? = null,\n        @SerialName(\"images\")\n        val images: Images? = null,\n        @SerialName(\"share\")\n        val share: Share? = null,\n        @SerialName(\"hub\")\n        val hub: Hub? = null,\n        @SerialName(\"sections\")\n        val sections: List<Section?>? = null,\n        @SerialName(\"url\")\n        val url: String? = null,\n        @SerialName(\"artists\")\n        val artists: List<Artist?>? = null,\n        @SerialName(\"isrc\")\n        val isrc: String? = null,\n        @SerialName(\"genres\")\n        val genres: Genres? = null,\n        @SerialName(\"relatedtracksurl\")\n        val relatedtracksurl: String? = null,\n        @SerialName(\"albumadamid\")\n        val albumadamid: String? = null\n    ) {\n        @Serializable\n        data class Images(\n            @SerialName(\"background\")\n            val background: String? = null,\n            @SerialName(\"coverart\")\n            val coverart: String? = null,\n            @SerialName(\"coverarthq\")\n            val coverarthq: String? = null,\n            @SerialName(\"joecolor\")\n            val joecolor: String? = null\n        )\n\n        @Serializable\n        data class Share(\n            @SerialName(\"subject\")\n            val subject: String? = null,\n            @SerialName(\"text\")\n            val text: String? = null,\n            @SerialName(\"href\")\n            val href: String? = null,\n            @SerialName(\"image\")\n            val image: String? = null,\n            @SerialName(\"twitter\")\n            val twitter: String? = null,\n            @SerialName(\"html\")\n            val html: String? = null,\n            @SerialName(\"avatar\")\n            val avatar: String? = null,\n            @SerialName(\"snapchat\")\n            val snapchat: String? = null\n        )\n\n        @Serializable\n        data class Hub(\n            @SerialName(\"type\")\n            val type: String? = null,\n            @SerialName(\"image\")\n            val image: String? = null,\n            @SerialName(\"actions\")\n            val actions: List<Action?>? = null,\n            @SerialName(\"options\")\n            val options: List<Option?>? = null,\n            @SerialName(\"providers\")\n            val providers: List<Provider?>? = null,\n            @SerialName(\"explicit\")\n            val explicit: Boolean? = null,\n            @SerialName(\"displayname\")\n            val displayname: String? = null\n        ) {\n            @Serializable\n            data class Action(\n                @SerialName(\"name\")\n                val name: String? = null,\n                @SerialName(\"type\")\n                val type: String? = null,\n                @SerialName(\"id\")\n                val id: String? = null,\n                @SerialName(\"uri\")\n                val uri: String? = null\n            )\n\n            @Serializable\n            data class Option(\n                @SerialName(\"caption\")\n                val caption: String? = null,\n                @SerialName(\"actions\")\n                val actions: List<OptionAction?>? = null,\n                @SerialName(\"beacondata\")\n                val beacondata: Beacondata? = null,\n                @SerialName(\"image\")\n                val image: String? = null,\n                @SerialName(\"type\")\n                val type: String? = null,\n                @SerialName(\"listcaption\")\n                val listcaption: String? = null,\n                @SerialName(\"overflowimage\")\n                val overflowimage: String? = null,\n                @SerialName(\"colouroverflowimage\")\n                val colouroverflowimage: Boolean? = null,\n                @SerialName(\"providername\")\n                val providername: String? = null\n            ) {\n                @Serializable\n                data class OptionAction(\n                    @SerialName(\"name\")\n                    val name: String? = null,\n                    @SerialName(\"type\")\n                    val type: String? = null,\n                    @SerialName(\"uri\")\n                    val uri: String? = null,\n                    @SerialName(\"id\")\n                    val id: String? = null\n                )\n\n                @Serializable\n                data class Beacondata(\n                    @SerialName(\"type\")\n                    val type: String? = null,\n                    @SerialName(\"providername\")\n                    val providername: String? = null\n                )\n            }\n\n            @Serializable\n            data class Provider(\n                @SerialName(\"caption\")\n                val caption: String? = null,\n                @SerialName(\"images\")\n                val images: ProviderImages? = null,\n                @SerialName(\"actions\")\n                val actions: List<ProviderAction?>? = null,\n                @SerialName(\"type\")\n                val type: String? = null\n            ) {\n                @Serializable\n                data class ProviderImages(\n                    @SerialName(\"overflow\")\n                    val overflow: String? = null,\n                    @SerialName(\"default\")\n                    val default: String? = null\n                )\n\n                @Serializable\n                data class ProviderAction(\n                    @SerialName(\"name\")\n                    val name: String? = null,\n                    @SerialName(\"type\")\n                    val type: String? = null,\n                    @SerialName(\"uri\")\n                    val uri: String? = null\n                )\n            }\n        }\n\n        @Serializable\n        data class Section(\n            @SerialName(\"type\")\n            val type: String? = null,\n            @SerialName(\"metapages\")\n            val metapages: List<Metapage?>? = null,\n            @SerialName(\"tabname\")\n            val tabname: String? = null,\n            @SerialName(\"metadata\")\n            val metadata: List<Metadata?>? = null,\n            @SerialName(\"url\")\n            val url: String? = null,\n            @SerialName(\"text\")\n            val text: List<String>? = null,\n        ) {\n            @Serializable\n            data class Metapage(\n                @SerialName(\"image\")\n                val image: String? = null,\n                @SerialName(\"caption\")\n                val caption: String? = null\n            )\n\n            @Serializable\n            data class Metadata(\n                @SerialName(\"title\")\n                val title: String? = null,\n                @SerialName(\"text\")\n                val text: String? = null\n            )\n        }\n\n        @Serializable\n        data class Artist(\n            @SerialName(\"id\")\n            val id: String? = null,\n            @SerialName(\"adamid\")\n            val adamid: String? = null\n        )\n\n        @Serializable\n        data class Genres(\n            @SerialName(\"primary\")\n            val primary: String? = null\n        )\n    }\n}\n\ndata class RecognitionResult(\n    val trackId: String,\n    val title: String,\n    val artist: String,\n    val album: String?,\n    val coverArtUrl: String?,\n    val coverArtHqUrl: String?,\n    val genre: String?,\n    val releaseDate: String?,\n    val label: String?,\n    val lyrics: List<String>?,\n    val shazamUrl: String?,\n    val appleMusicUrl: String?,\n    val spotifyUrl: String?,\n    val isrc: String?,\n    val youtubeVideoId: String? = null\n)\n\nsealed class RecognitionStatus {\n    data object Ready : RecognitionStatus()\n    data object Listening : RecognitionStatus()\n    data object Processing : RecognitionStatus()\n    data class Success(val result: RecognitionResult) : RecognitionStatus()\n    data class NoMatch(val message: String = \"No matches found\") : RecognitionStatus()\n    data class Error(val message: String) : RecognitionStatus()\n}\n"
  },
  {
    "path": "simpmusic/build.gradle.kts",
    "content": "plugins {\n    id(\"com.android.library\")\n    alias(libs.plugins.kotlin.serialization)\n}\n\nandroid {\n    namespace = \"com.metrolist.simpmusic\"\n    compileSdk = 36\n\n    defaultConfig {\n        minSdk = 26\n    }\n\n    compileOptions {\n        isCoreLibraryDesugaringEnabled = true\n        sourceCompatibility = JavaVersion.VERSION_21\n        targetCompatibility = JavaVersion.VERSION_21\n    }\n}\n\nkotlin {\n    jvmToolchain(21)\n}\n\ndependencies {\n    implementation(libs.ktor.client.core)\n    implementation(libs.ktor.client.cio)\n    implementation(libs.ktor.client.content.negotiation)\n    implementation(libs.ktor.serialization.json)\n\n    coreLibraryDesugaring(libs.desugaring)\n}\n"
  },
  {
    "path": "simpmusic/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n</manifest>\n"
  },
  {
    "path": "simpmusic/src/main/kotlin/com/metrolist/simpmusic/SimpMusicLyrics.kt",
    "content": "package com.metrolist.simpmusic\n\nimport com.metrolist.simpmusic.models.LyricsData\nimport com.metrolist.simpmusic.models.SimpMusicApiResponse\nimport io.ktor.client.HttpClient\nimport io.ktor.client.call.body\nimport io.ktor.client.engine.cio.CIO\nimport io.ktor.client.plugins.contentnegotiation.ContentNegotiation\nimport io.ktor.client.plugins.defaultRequest\nimport io.ktor.client.plugins.HttpTimeout\nimport io.ktor.client.request.get\nimport io.ktor.client.request.header\nimport io.ktor.http.HttpHeaders\nimport io.ktor.http.HttpStatusCode\nimport io.ktor.serialization.kotlinx.json.json\nimport kotlinx.serialization.json.Json\nimport kotlin.math.abs\n\nobject SimpMusicLyrics {\n    private const val BASE_URL = \"https://api-lyrics.simpmusic.org/v1/\"\n\n    private val client by lazy {\n        HttpClient(CIO) {\n            install(ContentNegotiation) {\n                json(\n                    Json {\n                        isLenient = true\n                        ignoreUnknownKeys = true\n                        explicitNulls = false\n                    },\n                )\n            }\n\n            install(HttpTimeout) {\n                requestTimeoutMillis = 15000\n                connectTimeoutMillis = 10000\n                socketTimeoutMillis = 15000\n            }\n\n            defaultRequest {\n                url(BASE_URL)\n                header(HttpHeaders.Accept, \"application/json\")\n                header(HttpHeaders.UserAgent, \"SimpMusicLyrics/1.0\")\n                header(HttpHeaders.ContentType, \"application/json\")\n            }\n\n            expectSuccess = false\n        }\n    }\n\n    suspend fun getLyricsByVideoId(videoId: String): List<LyricsData> = runCatching {\n        val response = client.get(BASE_URL + videoId)\n        \n        if (response.status == HttpStatusCode.OK) {\n            val apiResponse = response.body<SimpMusicApiResponse>()\n            if (apiResponse.success) {\n                apiResponse.data\n            } else {\n                emptyList()\n            }\n        } else {\n            emptyList()\n        }\n    }.getOrDefault(emptyList())\n\n    suspend fun getLyrics(\n        videoId: String,\n        duration: Int = 0,\n    ): Result<String> = runCatching {\n        val tracks = getLyricsByVideoId(videoId)\n        \n        if (tracks.isEmpty()) {\n            throw IllegalStateException(\"Lyrics unavailable\")\n        }\n\n        // Filter tracks that match duration within tolerance (10 seconds)\n        val validTracks = if (duration > 0) {\n            tracks.filter { track ->\n                abs((track.duration ?: 0) - duration) <= 10\n            }\n        } else {\n            tracks\n        }\n\n        if (validTracks.isEmpty()) {\n            throw IllegalStateException(\"Lyrics unavailable\")\n        }\n\n        val bestMatch = if (duration > 0 && validTracks.size > 1) {\n            validTracks.minByOrNull { track ->\n                abs((track.duration ?: 0) - duration)\n            }\n        } else {\n            validTracks.firstOrNull()\n        }\n\n        // Prioritize richSyncLyrics for word-by-word sync, then syncedLyrics, then plainLyrics\n        val lyrics = bestMatch?.richSyncLyrics?.takeIf { it.isNotBlank() }\n            ?: bestMatch?.syncedLyrics?.takeIf { it.isNotBlank() }\n            ?: bestMatch?.plainLyrics?.takeIf { it.isNotBlank() }\n            ?: throw IllegalStateException(\"Lyrics unavailable\")\n        \n        lyrics\n    }\n\n    suspend fun getAllLyrics(\n        videoId: String,\n        duration: Int = 0,\n        callback: (String) -> Unit,\n    ) {\n        val tracks = getLyricsByVideoId(videoId)\n        var count = 0\n        var plain = 0\n\n        val sortedTracks = if (duration > 0) {\n            tracks.sortedBy { abs((it.duration ?: 0) - duration) }\n        } else {\n            tracks\n        }\n\n        sortedTracks.forEach { track ->\n            if (count <= 4) {\n                // Check duration match - relaxed to 10 seconds or skip if duration is 0\n                val durationMatch = duration <= 0 || abs((track.duration ?: 0) - duration) <= 10\n\n                // Prioritize richSyncLyrics for word-by-word sync\n                if (track.richSyncLyrics != null && track.richSyncLyrics.isNotBlank() && durationMatch) {\n                    count++\n                    callback(track.richSyncLyrics)\n                } else if (track.syncedLyrics != null && track.syncedLyrics.isNotBlank() && durationMatch) {\n                    count++\n                    callback(track.syncedLyrics)\n                }\n                if (track.plainLyrics != null && track.plainLyrics.isNotBlank() && durationMatch && plain == 0) {\n                    count++\n                    plain++\n                    callback(track.plainLyrics)\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "simpmusic/src/main/kotlin/com/metrolist/simpmusic/models/LyricsResponse.kt",
    "content": "package com.metrolist.simpmusic.models\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class LyricsData(\n    val id: String? = null,\n    val videoId: String? = null,\n    @SerialName(\"songTitle\")\n    val title: String? = null,\n    @SerialName(\"artistName\")\n    val artist: String? = null,\n    @SerialName(\"albumName\")\n    val album: String? = null,\n    @SerialName(\"durationSeconds\")\n    val duration: Int? = null,\n    val syncedLyrics: String? = null,\n    @SerialName(\"plainLyric\")\n    val plainLyrics: String? = null,\n    val richSyncLyrics: String? = null,\n    val vote: Int? = null,\n)\n\n@Serializable\ndata class SimpMusicApiResponse(\n    val type: String? = null,\n    val data: List<LyricsData> = emptyList(),\n) {\n    val success: Boolean\n        get() = type == \"success\"\n}\n"
  }
]